性能优化是一个长期的过程,并非一劳永逸,需要我们去抠细节,找到可以提升的地方。
针对Android平台自身特性的一些优化(例如xml布局优化、方法耗时之类)在这里就不展开了,主要还是从逻辑和业务出发~
数据加载优化
网络请求前置
也许是因为时序的问题,通常情况下 Activity 启动之后有三个步骤:
- 加载布局及初始化View
- 再进行网络请求等待
- 请求结果json解析
- 最后再渲染到界面上。
而实际上 步骤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优化
- 数据冗余压缩策略,例如接口数据携带大量埋点信息,可以考虑做精简