「性能优化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日

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335