Android 页面秒开优化总结

性能优化是一个长期的过程,并非一劳永逸,需要我们去抠细节,找到可以提升的地方。

针对Android平台自身特性的一些优化(例如xml布局优化、方法耗时之类)在这里就不展开了,主要还是从逻辑和业务出发~

数据加载优化

网络请求前置

也许是因为时序的问题,通常情况下 Activity 启动之后有三个步骤:

  1. 加载布局及初始化View
  2. 再进行网络请求等待
  3. 请求结果json解析
  4. 最后再渲染到界面上。

而实际上 步骤1、2、3 这三步是可以并行去做的,假设说 加载布局及初始化View 需要 150ms,整个网络请求耗时 200ms,那么并行之后理想情况就可以节省 150ms 的启动时间。

这时候可能就有疑问了,假设网络请求时间比View初始化来得快,网络请求结束后要去更新UI,就很有可能引起空指针问题。所以针对这种情况,我们需要做一个等待View初始化完的操作。

其实因为 Android 基于消息机制,并且通常情况下View的更新都在主线程,实际上网络请求结束后,post到主线程后更新UI,onCreate 已经执行完了,所以不需要等待也可以。但如果是在子线程去调用非更新View的方法,比如获取一些状态之类的,那就需要做等待操作。

Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class BaseActivity extends AppCompatActivity {

private ReentrantLock mReentrantLock = new ReentrantLock();

protected void onCreate(Bundle savedInstanceState) {
try {
mReentrantLock.lock();
onInitData(savedInstanceState);
} catch (Exception ignored) {
} finally {
super.onCreate(savedInstanceState);
onCreateView(savedInstanceState);
mReentrantLock.unlock();
}
}

protected void waitViewInitialized() {
try {
mReentrantLock.lock();
} catch (Exception ignored) {
} finally {
mReentrantLock.unlock();
}
}

protected abstract void onInitData(Bundle savedInstanceState);
protected abstract void onInitView(Bundle savedInstanceState);
}

Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class BaseFragment extends Fragment {

private ReentrantLock mReentrantLock = new ReentrantLock();

protected View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView;
try {
mReentrantLock.lock();
onInitData(savedInstanceState);
} catch (Exception ignored) {
} finally {
rootView = onInitView(inflater, container, savedInstanceState);
mReentrantLock.unlock();
}
return rootView;
}

protected void waitViewInitialized() {
try {
mReentrantLock.lock();
} catch (Exception ignored) {
} finally {
mReentrantLock.unlock();
}
}

protected abstract void onInitData(Bundle savedInstanceState);
protected abstract View onInitView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
}

json异步解析

在上述步骤1和2并行的情况下,json在子线程解析效率理论上来讲要优于在主线程。View的初始化在主线程,假设网络请求比view初始化来得快,那么view初始化完成还需要等待json解析,那速度可能要更慢一些。我们统计了Android线上搜索结果的fastjson解析时间的平均数据,需要40ms左右。

json子线程解析在加载更多的场景下对滑动帧率也是有帮助的。

缓存&预加载

数据后带

针对一些特殊场景,例如从 搜索结果列表页 跳 商品详情页,可以把商品主图、标题等信息带过去,提前展示,提升白屏体验。

数据预加载

1、空间换时间方案

通过端智能及数据分析(可能需要算法的配合),对高频用户点击或展示的数据,可以在空闲线程做适当的预加载处理。

2、H5等资源内置、离线包或预加载

数据缓存

结合业务场景,针对一些非实时更新但是复用性较高的接口,可以做一层网络数据缓存。

通过 LRUCache 做缓存限制
缓存失效时间策略,降低数据出错的可能性

附:LRUCache 简单实现,可以做一些定制扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class LRUCache<K, V> {

public static class Entry<K, V> {
public K key;
public V value;

public Entry<K, V> pre;
public Entry<K, V> next;

public Entry(K key, V value) {
this.key = key;
this.value = value;
}
}

private static final int DEFAULT_SIZE = 2;

private int size = DEFAULT_SIZE;

private Map<K, Entry<K, V>> values;

private Entry<K, V> first;
private Entry<K, V> last;

public LRUCache(int size) {
if (size > 0) {
this.size = size;
}
// 设定初始容量*扩容因子 避免扩容
values = new HashMap<>((int)Math.ceil(size * 0.75f));
}

public final void put(@NotNull K key, V value) {
Entry<K, V> entry = values.get(key);
if (entry == null) {
if (values.size() >= size) {
removeLastEntry();
}
entry = new Entry<>(key, value);
} else {
entry.value = value;
}
moveEntryToFirst(entry);
}

public final V get(@NotNull K key) {
Entry<K, V> entry = values.get(key);
if (entry == null) {
return null;
}
moveEntryToFirst(entry);
return entry.value;
}

private void moveEntryToFirst(@NotNull Entry<K, V> entry) {
values.put(entry.key, entry);
if (first == null || last == null) {
first = last = entry;
return;
}

if (entry == first) {
return;
}

if (entry.pre != null) {
entry.pre.next = entry.next;
}
if (entry.next != null) {
entry.next.pre = entry.pre;
}

if (entry == last) {
last = last.pre;
}

entry.next = first;
first.pre = entry;
first = entry;
first.pre = null;
}

private void removeLastEntry() {
if (last != null) {
values.remove(last.key);
last = last.pre;

if (last == null) {
first = null;
} else {
last.next = null;
}
}
}
}

数据&View懒加载

首屏不使用到的数据或者view,尽量采用懒加载的方式。

例如针对搜索结果页侧边栏筛选,可以在点击展开之后再添加筛选项。并且针对一些使用频率不高的功能,懒加载也能节约一定的运行内存空间。

布局加载优化

提前异步Inflate

布局 Inflate 过程慢主要有两个原因:

1、xml文件读取io过程
2、反射创建View

AsyncLayoutInflater:support v4包下面提供的类,用于在 work thread 加载布局,最后回调到主线程。

通常在网络请求的过程中,页面会处于一个空闲的状态,假设场景是搜索结果列表页,那么我们可以在数据请求前置的同时,去异步 inflate 一些 recyclerview 的 itemview,那么在渲染阶段就可以节约 recyclerview 的 createViewHolder 的时间。

并发优化

客户端通常情况下需要并发处理的场景比较少,这里举个特殊场景。

搜索结果页采用 Mist 做动态化方案。需要再 view 渲染之前,异步去 build 每个数据对应的节点信息(主要是measure和layout过程),通过测试比较,针对某一款机型,单线程去build 30个数据节点需要300ms以上,多线程并发只需要100ms左右,并发线程数为 CPU核心数-1。

多线程并发对资源有抢占,但整体效果还是可以的。并且要做好任务分配,让并发的几个线程处理的任务数差不多,减少最后的等待时间。

日志治理

大量的打印日志也会影响页面启动性能,需要相应治理。

交互优化 增强体感

骨架图

假设说网络请求的时间要比view初始化慢得多,可以通过骨架图的形式,提前创建好一些itemview,来增强一些用户体感,同事也达到提前创建 view 的效果。

RPC 优化

  • 推动服务端进行rt优化
  • 数据冗余压缩策略,例如接口数据携带大量埋点信息,可以考虑做精简