「性能优化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个步骤,我们来简要地分析一个每一个步骤做了什么事:

  1. 通过 AsyncLayoutInflater 来加载 R.layout.activity_main 资源文件并注入一个 OnInflateFinishedListener 接口对象,这个接口用于接受异步加载得到的 VIew 对象。

  2. AsyncLayoutInflater 构造一个 InflateRequest封装此次加载资源的一些数据,例如需要加载布局文件resid,实际负责加载这个资源的 LayoutInflater 和负责回调层上层的OnInflateFinishedListener接口。

  3. 将构造好的 InflateRequest 请求放入到队列中。

  4. 异步线程死循环轮训这个队列,当队列中有数据,取出一个 InflateRequest

  5. 通过获取 InflateRequest.LayoutInflater 真正地加载 resid 对应的布局文件,最终得到一个 View 对象,并赋值给 InflateRequest.view

  6. 通过 UIHandlerInflateRequest 回调到主线程中 (ps:这时加载完成的 View 就传到了主线程了)

  7. UIHanlder 处理消息,通过 InflateRequest#callback 将加载得到的 View 对象回调给调用层。

  8. 在第一步注入地接口方法 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日