一、背景
Android 在 View 的使用中,过多的布局文件 inflate 影响性能,尤其在一些滚动列表、样式种类很丰富的场景下,inflate 次数相对较多,整体 inflate 耗时就会增加,导致滚动过程卡顿。
所以,需要 View 的异步 inflate,甚至 View 的全局缓存,通过这些方式,去减少 UI 线程 inflate 的耗时及次数,以便减少卡顿,提升性能。
二、现有的解决方案
要实现 View 的异步 Inflate,最简单粗暴的方式是直接 new Thread 执行 inflate,或者使用 HandleThread+Handler 的方式,或者使用 android 官方 android.support.v4.view 包的 AsyncLayoutInflater 类。但都存在一个问题,就是如果 inflate view 实例的时候 view 的构造方法里面直接有 new Handler()(原本想新建一个主线程 Handler 用于 UI 操作),将会达不到要求。
ListView、RecycleView 等控件,自身实现了“缓存复用”机制,这使得滑动过程性能得以提升,但更多的是控件间、或者页面内的缓存复用,而对于页面之间(activity 与 activity)缓存复用,没一个有效的解决方案。当然,如果把 View 的缓存做成全局单例,是可以做到页面之间的缓存复用,但 View 和 context 强绑定在一起,全局缓存可能会导致 activity 实例不能正常释放,导致内存泄露问题;也可能在 context 使用上存在问题,如用 APPlication 实例传递给 View 的 context,代码中又直接把 context 当成 activity 用,将导致一些问题。
三、异步 Infllate+全局缓存问题分析及解决方案
从现有方案中,提炼出两个核心问题,一、异步 Inflater 的时候,View 中 new Handler 导致的安全问题;二、全局缓存,View 的 context 问题。
异步 Inflater 的时候,View 中 new Handler 导致的安全问题解决方案
对于直接 new Thread 执行 inflate,或者使用 HandleThread+Handler 的方式,或者使用 android 官方 android.support.v4.view 包的 AsyncLayoutInflater 类。
示例代码:
三种方式的对比:
第一种和第三种方式,run 方法中由于没有使用 Looper.loop 机制,这使得 new Handler 即使调用 post 方法发消息,并不会正在执行,导致 UI 不能正常刷新。
第二种,虽然 new Handler 能正常工作,但如刷新 UI,很可能会 crash,比如如下的异常:
所以 Handler hander = new Handler(),往 handler 抛的消息需要抛到主线程,比如改写成 Handler hander = new Handler(Looper.getMainLooper())。但是我们无法更改 View 的 Handler 构造代码,下面方案通过了反射的方式,强制把后台的线程的 Looper 设置为 mainLooper,这样后台线程 new Handler()方式也能把消息抛到主线程消息队列
使用示例:
全局缓存,View 的 context 问题解决方案
在全局缓存时,为了解决创建 view 的 context 不一定是 activity 导致的问题,或者是 activity 导致的内存泄露问题,对 Context 做封装:新建了 ViewContext 代理类:
inflate 的时候,将用 ViewContext 传入 View,方式如下:
四、基本实施及使用
基本架构
实施思路
1)View 的缓存大小应控制,且可动态修改:在 View 的缓存方面,设计一个缓存大小机制,且允许动态的修改对应的缓存大小,这样可以根据具体需要设置,从而更好的控制内存使用;
2)缓存 View 的状态处理,方便管理。异步创建 View,放入缓存池并标记可用,每次从缓存池获取 View 后,标记状态不可用,待回收后在标记可用;
3)异步创建 View,可预加载。提供 View 的异步创建,并放入缓存中,结合预加载,能有效的减少实际创建 View 所需的耗时,提升性能;
4)内存管理策略–应用低内存自动释放缓存。通过 context 取到 APPlication 注册一个 ComponentCallbacks,监听 APP 内存状态,适当的释放缓存;
5)缓存优先级策略–可设置缓存释放优先级。提供设置缓存释放优先级的能力,业务方可以更精准的利用缓存,更好的满足业务所需;
6)View 创建方式。对于 View 的创建,可以设计一个 IViewCreeator,创建 View 的过程由使用方决定。如布局文件中只有 TextView,可以传入 layoutId 选择 new TextView()的方式。
实施
基本使用如下,新建单例(如 ViewHelper):
使用 ViewHelper 如下:
使用前后的效果:
注意事项
1)使用异步 Inflate+全局缓存构建的 View,在使用时需要重新设置 LayoutParams,不然显示上可能不是最终想要的结果;
2)使用异步 Inflate+全局缓存构建的 View,如果 View 的解析过程中,存在 Theme 相关的,可能会导致 View 构建失败。如原生的 TabLayout,解析时会读取 Theme 中的属性,如果 context 传入的是 APPlication 且没有设置相关 Theme,就会报错;
3)使用异步 Inflate+全局缓存构建的 View,需要及时的调用 refreshCurrentActivity 方法,这样尽可能的保持 context 是当前的 activity 实例。在使用 context 的时候,避免直接把 context 强转 activity,而是使用 ViewContext 的 getActivity 方法获取。
五、总结
选择异步 Inflate ,应根据需要,合理的选择。如只需 activity 级别的,选择原生 AsyncLayoutInflater 的方式,就能很好的满足要求,并且没有 context 使用问题。如想更早的准备或者跨页面复用,View 的异步 Inflate+全局缓存是更好的选择,但要注意 context 使用问题,因为 inflate 所需的 context 不一定是 activity,也正是这点使得单例缓存的 View,不用担心内存泄露问题,满足多页面的缓存复用。
目前,优酷在 AsyncView 项目已经实现 View 的异步 Inflate+全局缓存,该项目已经对公司内部开源,是 AIOSO 的子项目之一,也在进一步的落实对外开源。它是“低侵入式”的,没有对 Android 原生 UI 进行改造,Android UI 框架开发的 APP 都可以方便接入。该项目已经在优酷 APP 上大量使用,反馈效果良好,主要体现在:
1)在帧率方面,整体带来了 5%左右提升。尤其在低端机,体感上有明显的提升;
2)在启动方面,结合各业务端提前做一些预加载任务,整体带来了 20%左右的提升。
作者 | 阿里文娱高级无线开发工程师 瑞源
评论 2 条评论