查看原文
其他

什么?黑科技?Android 可以异步加载布局?

点击上方 "程序员小乐" ,关注公众号

8点20分,第一时间与你相约

每日英文

I'm so young that everything sad or happy seems very impressive to me. A gentle touch could be a big deal.

只是因为太年轻,所以所有的悲伤和快乐都显得那么深刻,轻轻一碰就惊天动地。


每日掏心话

记忆开始飞速往回旋转,形成了漩涡。心有多软,壳就要有多硬,不然漫漫人生路如何走得下去。


来自:未见哥哥 | 责编:乐乐

链接:jianshu.com/p/d61b513e6814

图片来自网络




   01 背景   



在前面几篇博文中,我们通过 LayoutInflater.Factory 来获取控件的加载时间(参考:「性能优化2.1」LayoutInflater Hook控件加载耗时)


https://www.jianshu.com/p/9a0cc0a5649c


以及通过 AOP 的方式获取布局加载时间(参考:「性能优化2.2」获取布局的加载时间),通过分析这两个数据即可初步知道布局加载是否耗时。


https://blog.csdn.net/lwj_zeal/article/details/88701570


正常情况下,给一个 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, nullnew AsyncLayoutInflater.OnInflateFinishedListe
        @Override
        public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
            //view:加载得到 view
            setContentView(view);
            //一系列的 findViewById(...)
        }
    });
}



   02 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. 通过 UIHandler 将 InflateRequest 回调到主线程中 (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.parentfalse);
        } 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
                    i 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, nullnew AsyncLayoutInflater.OnInflateFinishedListe
    @Override
    public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
        //view:加载得到 view
        setContentView(view);
        //findViewById(...)
    }
});


   03 总结   



在本文中我们分析如何进行布局文件的异步加载,并绘制了一个草图,描述了AsyncLayoutInflater的工作原理和对每一个步骤都做了简要地说明。最后我们通过源码的角度验来证这个草图所描述的内容。


通过异步加载资源的方式,可以从侧面缓解了主线程加载布局卡顿的问题,但是问题的根源还是存在。

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。


欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。


猜你还想看


阿里、腾讯、百度、华为、京东最新面试题汇集

API 网关从入门到放弃

中台到底是个什么鬼?终于有人讲清楚了!

困扰程序员的30种软件开发问题,你是否时曾相识?

百度地震了,也许早晚的事

理解 IntelliJ IDEA 的项目配置和 Web 部署


关注「程序员小乐」,收看更多精彩

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存