14

View 的异步 Inflate+ 全局缓存:加速你的页面

 3 years ago
source link: https://www.infoq.cn/article/pWylrFrSh8wcE1LtYb1u
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、背景

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 问题。

  1. 异步 Inflater 的时候,View 中 new Handler 导致的安全问题解决方案

对于直接 new Thread 执行 inflate,或者使用 HandleThread+Handler 的方式,或者使用 android 官方 android.support.v4.view 包的 AsyncLayoutInflater 类。

示例代码:

nYf6jqy.png!web

三种方式的对比:

FZ7NjaZ.png!web

第一种和第三种方式,run 方法中由于没有使用 Looper.loop 机制,这使得 new Handler 即使调用 post 方法发消息,并不会正在执行,导致 UI 不能正常刷新。

第二种,虽然 new Handler 能正常工作,但如刷新 UI,很可能会 crash,比如如下的异常:

IJN7raF.png!web

所以 Handler hander = new Handler(),往 handler 抛的消息需要抛到主线程,比如改写成 Handler hander = new Handler(Looper.getMainLooper())。但是我们无法更改 View 的 Handler 构造代码,下面方案通过了反射的方式,强制把后台的线程的 Looper 设置为 mainLooper,这样后台线程 new Handler() 方式也能把消息抛到主线程消息队列

jQfQvua.png!web

使用示例:

IVNJjmv.png!web

  1. 全局缓存,View 的 context 问题解决方案

在全局缓存时,为了解决创建 view 的 context 不一定是 activity 导致的问题,或者是 activity 导致的内存泄露问题,对 Context 做封装:新建了 ViewContext 代理类:

QNviyeF.png!web

inflate 的时候,将用 ViewContext 传入 View,方式如下:

MFJviyV.png!web

四、基本实施及使用

  1. 基本架构

byuQNv3.png!web

  1. 实施思路

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() 的方式。

  1. 实施

UBf6nav.png!web

基本使用如下,新建单例(如 ViewHelper):

fuUZzaE.png!web

使用 ViewHelper 如下:

bURfEvq.png!web

使用前后的效果:

ma2yEvY.png!web

  1. 注意事项

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 方法获取。

五、总结

uMFzyme.png!web

选择异步 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% 左右的提升。

作者 | 阿里文娱高级无线开发工程师 瑞源


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK