「性能优化2.4」 AsyncLayoutInflater实现异
「性能优化1.0」启动分类及启动时间的测量
「性能优化1.1」计算方法的执行时间
「性能优化1.2」异步优化
「性能优化1.3」延迟加载方案
「性能优化2.0」布局加载原理
「性能优化2.1」LayoutInflater Hook控件加载耗时
「性能优化2.2」获取布局的加载时间
「性能优化2.3」Choreographer检测丢帧
「性能优化2.4」 AsyncLayoutInflater实现异步加载布局
一、背景
在前面几篇博文中,我们通过 LayoutInflater.Factory 来获取控件的加载时间(参考:「性能优化2.1」LayoutInflater Hook控件加载耗时)以及通过 AOP 的方式获取布局加载时间(参考:「性能优化2.2」获取布局的加载时间),通过分析这两个数据即可初步知道布局加载是否耗时。
正常情况下,给一个 Activity 设置一个布局的代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
//一系列的 findViewById(...)
}
这种方式,内部会通过 LayoutInflater 去加载
R.layout.activity_main
这个资源id对应的布局文件,这里就是我们上面所说的卡顿的地方。
我们来思考一下:在之前的博文中我们分析了一个布局的加载涉及到两个性能关键点,一个是通过 IO 从磁盘中加载布局文件,另一个是从通过反射的方式创建对应的 View。那么这两个步骤在主线程操作就会因为布局层级比较深,而导致画面卡顿问题。
痛点:如何通过这两种方式检测我们页面布局加载是比较耗时的,那么有没有优化的方案呢?
在 Android support-v4 包中提供一个用于异步加载的工具类 AsyncLayoutInflater
,从名字可以看出,相当于 LayoutInflater
多了一个异步操作的功能。
现在异步加载资源布局就变成这样了:
public void onCreate(){
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListe
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
//view:加载得到 view
setContentView(view);
//一系列的 findViewById(...)
}
});
}
二、AsyncLayoutInflater的介绍
2.1、AsyncLayoutInflater工作原理
在分析原理之前,先来看一张草图:
AsyncLayoutInflater 工作原理通过上面这个图,我们将整个过程分为8个步骤,我们来简要地分析一个每一个步骤做了什么事:
-
通过
AsyncLayoutInflater
来加载R.layout.activity_main
资源文件并注入一个OnInflateFinishedListener
接口对象,这个接口用于接受异步加载得到的 VIew 对象。 -
AsyncLayoutInflater
构造一个InflateRequest
来封装
此次加载资源的一些数据,例如需要加载布局文件resid,实际负责加载这个资源的LayoutInflater
和负责回调层上层的OnInflateFinishedListener
接口。 -
将构造好的
InflateRequest
请求放入到队列
中。 -
异步线程
死循环
轮训这个队列,当队列中有数据,取出一个InflateRequest
。 -
通过获取
InflateRequest.LayoutInflater
真正地加载 resid 对应的布局文件,最终得到一个View
对象,并赋值给InflateRequest.view
。 -
通过
UIHandler
将InflateRequest
回调到主线程
中 (ps:这时加载完成的 View 就传到了主线程了) -
UIHanlder
处理消息,通过InflateRequest#callback
将加载得到的View
对象回调给调用层。 -
在第一步注入地接口方法
onInflateFinished()
得到的View
对象并将其传给setContentView(view)
方法,然后再做其他的 findViewById(...)工作。
2.2、AsyncLayoutInflater源码介绍
看完整个操作以及每一个步骤的描述,现在心中对 AsyncLayoutInflater
的整体流程有一个大致的认识。下面来浏览一下源码:
- 通过注释来 AsyncLayoutInflater 的描述
从 AsyncLayoutInflater 源码的注释来看,AsyncLayoutInflater 可以异步
地加载布局,并通过 OnInflateFinishedListener
接口回调到 UI 线程
,返回对应加载成功的 View
对象给调用者。
/**
* Helper class for inflating layouts asynchronously. To use, construct
* an instance of {@link AsyncLayoutInflater} on the UI thread and call
* {@link #inflate(int, ViewGroup, OnInflateFinishedListener)}. The
* {@link OnInflateFinishedListener} will be invoked on the UI thread
* when the inflate request has completed.
*/
public final class AsyncLayoutInflater {
...
}
- InflateThread线程
上面描述了资源是在异步线程去加载,而线程就是 AsyncLayoutInflater.InflateThread。
具体的 AsyncLayoutInflater.InflateThread 源码如下:
private static class InflateThread extends Thread {
private static final InflateThread sInstance;
//①线程是在静态代码块中开启的。
static {
sInstance = new InflateThread();
sInstance.start();
}
public static InflateThread getInstance() {
return sInstance;
}
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
public void runInner() {
InflateRequest request;
try {
//③从队列中得到一个 InflateRequest
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}
try {
//④真正的去加载资源布局
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
// Probably a Looper failure, retry on the UI thread
Log.w(TAG, "Failed to inflate resource in the background! Retrying on the U
+ " thread", ex);
}
//⑤将 request 发送给 UIHandler
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
@Override
public void run() {
//②通过死循环执行 runInner()方法
while (true) {
runInner();
}
}
//从池子中获取一个 InflateRequest
public InflateRequest obtainRequest() {
InflateRequest obj = mRequestPool.acquire();
if (obj == null) {
obj = new InflateRequest();
}
return obj;
}
//回收 InflateRequest 并放入池子中
public void releaseRequest(InflateRequest obj) {
obj.callback = null;
obj.inflater = null;
obj.parent = null;
obj.resid = 0;
obj.view = null;
mRequestPool.release(obj);
}
//将构造好的 InflateRequest 放入到队列中
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
}
- AsyncLayoutInflarer#inflate(...)
主要工作是将传入的参数封装为一个 InflateRequest 对象。
//AsyncLayoutInflater.java
@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
@NonNull OnInflateFinishedListener callback) {
if (callback == null) {
throw new NullPointerException("callback argument may not be null!");
}
InflateRequest request = mInflateThread.obtainRequest();
request.inflater = this;
request.resid = resid;
request.parent = parent;
request.callback = callback;
mInflateThread.enqueue(request);
}
- 将 InflateRequest 放入到队列中
//InflateThread.java
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
- mInflateThread.obtainRequest()
获取从mRequestPool
池子中获取一个 InflateRequest 对象,如果没有获取到,则创建一个。mRequestPool这个东西主要是用来缓存 InflateRequest 对象。
//InflateThread.java
private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
public InflateRequest obtainRequest() {
InflateRequest obj = mRequestPool.acquire();
if (obj == null) {
obj = new InflateRequest();
}
return obj;
}
- mInflateThread.enqueue(request)
将 request 请求放入地队列中。
//InflateThread.java
private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
- 在线程中执行 request
① 从队列中获取一个 InflateRequest。
② 通过 LayoutInflater 来加载对应的资源文件得到 View 对象,并赋值给 request.view。
③ 通过 UIHanler 将 request 发送到主线程中。
public void runInner() {
InflateRequest request;
try {
//①
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
return;
}
try {
//②
request.view = request.inflater.mInflater.inflate(
request.resid, request.parent, false);
} catch (RuntimeException ex) {
// Probably a Looper failure, retry on the UI thread
Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
+ " thread", ex);
}
//③
Message.obtain(request.inflater.mHandler, 0, request)
.sendToTarget();
}
- UIHanler 处理消息
① 取出子线程传递过程的 request 对象。
② 判断 request.view 如果为 null 表示在子线程加载失败,那么这时会交给主线程中重新去加载这个资源布局。
③ 回调 OnInflateFinishedListener 给调用层。
④ 回收 request 请求。
private Callback mHandlerCallback = new Callback() {
@Override
public boolean handleMessage(Message msg) {
//①
InflateRequest request = (InflateRequest) msg.obj;
if (request.view == null) {
//②
request.view = mInflater.inflate(
request.resid, request.parent, false);
}
//③
request.callback.onInflateFinished(
request.view, request.resid, request.parent);
//④
mInflateThread.releaseRequest(request);
return true;
}
};
- mInflateThread.releaseRequest
将已经处理完毕地 InflateRequest 进行回收,并且将其放入到池子中。
public void releaseRequest(InflateRequest obj) {
obj.callback = null;
obj.inflater = null;
obj.parent = null;
obj.resid = 0;
obj.view = null;
mRequestPool.release(obj);
}
- 在 onInflateFinished 中处理
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListe
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
//view:加载得到 view
setContentView(view);
//findViewById(...)
}
});
三、总结
在本文中我们分析如何进行布局文件的异步加载,并绘制了一个草图,描述了AsyncLayoutInflater
的工作原理和对每一个步骤都做了简要地说明。最后我们通过源码的角度验来证这个草图所描述的内容。
通过异步加载资源的方式,可以从侧面缓解
了主线程加载布局卡顿的问题,但是问题的根源还是存在地。
这里留下一个思考:如何根治主线程 IO 加载布局以及反射创建 View 导致加载卡顿的问题呢?
记录于2019年3月21日