简介
我们知道setContentView()、layoutinflater.inflate()等传统的布局加载方式都是在UI线程中同步加载布局的。当layout.xml过于复杂繁重,加载就会造成UI卡顿甚至ANR。在Google最近发布的Supportv4包中,给我们提供了一个异步加载布局的帮助类:AsyncLayoutInflater。这个类可以帮助你在非UI线程中加载layout,然后将加载好的布局通过接口回调的形式同步给UI线程。这个帮助类将会允许你的UI线程在执行繁重的inflate时继续保持响应。
使用方式
new AsyncLayoutInflater(this).inflate(
R.layout.activity_main,
null,
new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(View view, int resid, ViewGroup parent) {
//Do something with view
}});
对比一般的加载方式
View result = LayoutInflater.from(this).inflate(R.layout.activity_main, null);
可以看出,二者在用法上几乎唯一的区别就是AsyncLayoutInflater比LayoutInflater多了一个用于通知加载完成的回调。
限制条件
1、 parent的 generateLayoutParams()
函数必须是线程安全的。
2、 所有正在构建的views一定不能创建任何 Handlers 或者调用 Looper.myLooper 函数。
3、 不支持设置LayoutInflater.Factory也不支持LayoutInflater.Factory2
4、 不支持包含Fragments的inflatinglayouts
如果我们尝试异步的方式去inflate的layout不支持这种方式,那么inflation处理将会自动回退到主线程中。
使用场景
繁重复杂的布局文件
源码分析
private LayoutInflater mInflater;
private Handler mHandler;
private InflateThread mInflateThread;
mInflater便是最终真正执行加载布局的类,Handler的唯一作用是从非UI线程切换到UI线程。mInflateThread继承至一个Thread,真正的异步加载便是在它里面发生的,这是一个一new出来就开始run的线程。一句话概括就是layout布局在mInflateThread中交由mInflater进行加载,加载完成之后再由mHandler抛给UI线程。下面来看看具体代码:
@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);
}
在inflate方法中,先生成一个InflateRequest数据结构用来存储外界传递过来的布局文件id、父容器(Parent Group)和回调函数等数据,再将生成好的对象传入enqueue方法,接着我们进入mInflateThread线程看看enqueue方法做了什么:
public void enqueue(InflateRequest request) {
try {
mQueue.put(request);
} catch (InterruptedException e) {
throw new RuntimeException(
"Failed to enqueue async inflate request", e);
}
}
很简单,就一行代码。mQueue是一个ArrayBlockingQueue,可以抽象层一个队列。这行代码的作用无非就是把InflateRequest对象加入队列。
核心代码发生在InflateThread的run()方法中。如下图:
@Override
public void run() {
while (true) {
InflateRequest request;
try {
request = mQueue.take();
} catch (InterruptedException ex) {
// Odd, just continue
Log.w(TAG, ex);
continue;
}
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();
}
}
方法里面是一个while死循环,在每次循环中从mQueue队列取出一个包含了布局资源id、parent、回调函数等所有需要的数据的InflateRequest对象。然后调用request.view = request.inflater.mInflater.inflate(request.resid,request.parent, false);加载布局,这行代码就是我们平时用的方式。加载完成之后在最后一行将结果抛给UI线程。我们来看看mHandler的回调:
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;
}
};
回调里面调用了request.callback.onInflateFinished(request.view, request.resid,request.parent);将最终结果传回给了UI线程。至此全部结束!
几个注意的细节:
1、 InflateRequest其实是一个单例模式,单例模式在这里的使用是一个很恰当的做法,因为线程本来就是一个十分消耗资源的对象,一个线程足够应付布局加载。
2、 为什么要用ArrayBlockingQueue做队列?其实这是一个“生产者-消费者”模型,当队列为空的时候InflateThread线程会被阻塞,有新的数据的时候会被唤醒。从而不会占用过多资源,如CPU。
3、 成员变量mInflater实际上是一个被重写的LayoutInflater类——BasicInflater,在这个类中对onCreateView进行了优化,优先加载android.widget.,android.webkit.,android.app.这三个package中的对象,因为绝大多数UI文件都包含在这三个类中。在一定程度上节省了时间。
4、 一些人的疑问:不是说View的绘制只能在UI线程里面吗?为何还能在非UI线程里加载布局?这里需要弄清楚一点,LayoutInflater只是进行布局树的解析,并没有真正走UI的绘制流程,UI的绘制需要被addView到确定的content里才会绘制的。
最后,留下一段思考空间:在前文我们提到了AsyncLayoutInflater的四个局限,你能够结合代码分析一下为什么会有这四条限制么?
参考资料
https://developer.android.com/reference/android/support/v4/view/AsyncLayoutInflater.html