AsyncLayoutInfalter详解

简介

在Google发布的Supportv4包中,给我们提供了一个异步加载布局的帮助类:AsyncLayoutInflater。官方解释:

AsyncLayoutInflater 是来帮助做异步加载 layout 的,inflate(int, ViewGroup, OnInflateFinishedListener) 方法运行结束之后 OnInflateFinishedListener 会在主线程回调返回 View;这样做旨在 UI 的懒加载或者对用户操作的高响应。

    // 代码1
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new AsyncLayoutInflater(AsyncLayoutActivity.this)
                .inflate(R.layout.async_layout, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
                    @Override
                    public void onInflateFinished(View view, int resid, ViewGroup parent) {
                        setContentView(view);
                    }
                });
        // 别的操作
    }

简单的说我们知道默认情况下setContentView函数是在UI线程执行的,其中一系列的耗时操作:xml的解析,view的反射创建等过程都是在UI线程执行的,AsyncLayoutInflater就是帮助我们把这些过程以异步的方式执行,提高UI线程的响应。

setContentView的疑惑

  • setContentView在非UI线程中调用是否crash?
// 代码2
public class AsyncLayoutActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
            @Override
            public void run() {
                setContentView(R.layout.activity_async_layout);
            }
        }).start();
    }
}

经测试,在魅族Note5(Android6.0)上可能崩溃可能显示正常,而其中谷歌Nexus6P上必崩,crash日志如下:

[站外图片上传中...(image-b26414-1577417250016)]

日志比较好理解,调用setContentView最终会调用AnimatorSet的start方法,该方法判断Looper.myLooper()为空则会抛出此异常,而代码中直接开启线程并未创建Looper,所以会崩溃。

  • 既然没有Looper,那创建Looper执行会是什么现象?代码如下:
// // 代码3
public class AsyncLayoutActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                setContentView(R.layout.activity_async_layout);
                Looper.loop();
            }
        }).start();
    }
}

经测试,也会崩溃,崩溃日志如下:

[站外图片上传中...(image-4189c1-1577417250016)]

崩溃日志告诉我们只能主线程可以更新UI,可Android中子线程真的不能更新UI吗?相信有小伙伴验证过子线程中可以更新UI,如以下代码:

// 代码4
public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                main_tv.setText("子线程中访问");
            }
        }).start();
    }
}

了解以上代码之所以可以更新UI,需要先理解下面三个概念:

  1. ViewRootImpl:ViewRootImpl是View中的最高层级,属于所有View的根(但ViewRootImpl不是View,只是实现了ViewParent接口),实现了View和WindowManager之间的通信协议,而WindowManager负责操作View的添加、更新、删除操作
  2. DecorView:Android视图树的根节点视图
  3. Window:Window是视图的承载器,内部持有一个 DecorView

[站外图片上传中...(image-f1b08c-1577417250016)]

检测线程中是否可以更新UI的类是ViewRootImpl,ViewRootImpl是在ActivityThread的handleResumeActivity方法中与DecorView进行关联的,视图是加载到DecorView上的。上述代码线程中执行setText时会向父布局请求requestLayout方法,一直向上传递到根布局DecorView,而根布局最终传递给ViewRootImpl,而此时并未创建ViewRootImpl,从而无法触发ViewRootImpl的检测方法。而在线程中执行setContentView(代码3)却会报错,推测是setContentView中执行installDecor()方法和inflate()方法操作耗时而导致优先触发了handleResumeActivity中的ViewRootImpl的创建。

先看下这个Window, Window是视图的承载器,setContentView加载的视图也是放在这个Window中的,它内部持有一个DecorView,DecorView本身也是一个ViewGroup,执行setContentView时会创建DecorView,然后将加载的视图就是这个DecorView中
的一个子ViewGroup添加到这个DecorView中,这些View可以理解为setContentView布局里面的子View。在ActivityThread执行handleResumeActivity时,将onCreate中最终生成好的DecorView添加到这个ViewRootImpl中,并且将DecorView的parent指向
了ViewRootImpl,这样DecorView和ViewRootImpl就关联起来了。同时在handleResumeActivity时,通过WindowManager将DecorView添加到视图中显示出来。将TextView看做是一个View,执行setText最后会执行parent的requestLayout,就会一直走到
DecorView的mParent.requestLayout,而DecoreView的mParent是ViewRootImpl,最终会执行ViewRootImpl的requestLayout,而ViewRootImpl的requestLayout方法中会检查是在UI线程。在上面onCreate中线程中执行UI时,此时并未执行到
handleResumeActivity的关联ViewRootImpl,自然无法触发检测机制,所以不会崩溃。而在onCreate中执行setContentView会发生崩溃的推断原因是setContentView优先执行layout的解析操作比较耗时,导致setContentView后面检测UI时,已经完成了
ViewRootImpl和DecorView的关联

源码分析

1. 构造函数

[站外图片上传中...(image-140ddd-1577417250016)]

我们可以看到构造初始化三个成员变量:创建BasicLayoutInflater,Handler,InflateThread.

  • BasicLayoutInflater:继承自LayoutInflater,作用是将XML布局文件实例化为响应的View对象,关键是重写onCreateView,onCreateView是LayoutInfalter的inflate方法中创建View的方法,这么写为了优先加载这三个前缀下的Layout,然后按照默认的流程去加载,因为大多数情况下Layout的View都是这三个包下的。

    [站外图片上传中...(image-1b0115-1577417250016)]

  • Handler:用于线程切换,将inflate创建的view回传给创建handler所在的线程。这里需要注意如果AsyncLayoutInflater在线程中实例化,会因为找不到Looper或不能在非UI线程中加载View而崩溃。

  • InflateThread:单例工作线程,用于做inflate的

2. Inflate方法

[站外图片上传中...(image-3adf9e-1577417250016)]

可以看到首先通过InflateThread的obtainRequest方法拿到一个request,然后将参数传入,最后加入到InflateThread中的队列中。首先看下InflateRequest类:

[站外图片上传中...(image-27012-1577417250016)]

可以看到仅仅是将请求参数包装到一个类中。接着看InflateThread的obtainRequest方法:

        // 创建同步对象池,容量是10个
    private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
    
        public InflateRequest obtainRequest() {
            InflateRequest obj = mRequestPool.acquire(); // 从池中拿去对象,如果有则返回,没有则创建
            if (obj == null) {
                obj = new InflateRequest();
            }
            return obj;
        }

这里为什么要用到对象池?我们知道Java对象的生命周期大致包括三个阶段:对象的创建,对象的使用,对象的清除。有数据表明:新建一个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍。同时对象的清除是采用GC机制,而GC机制运行时会暂停应用程序支持,独占CPU。采用对象池的技术是为了就是为了减少对象的创建和清除所带来的开销。但是创建对象池的开销大于一个对象的开销,所以如果多次调用AsyncLayoutInflater创建布局,会使用对象池中的InflateRequest达到了效率的提升,如果项目中仅使用一次AsyncLayoutInflater,内存开销反而更大。

继续跟踪代码InflateThread的enqueue方法:

        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);
            }
        }

上述代码中采用了带有长度的阻塞队列ArrayBlockingQueue。这里延伸下ArrayBlockingQueue的概念:ArrayBlockingQueue继承BlockingQueue,BlockQueue是Java提供的一个接口,用来线程安全的往里面放数据或者从里面取出数据。它的一个典型应用就是生产者/消费者模型,也就是一个线程生成数据,一个线程消费数据。如下图:

[站外图片上传中...(image-63c814-1577417250016)]

在AsyncLayoutInflater中的InflateThread就是一个消费线程,不断从队列中取出InflateRequest来解析xml,而创建AsyncLayoutInflater的线程是生成线程,不断往InflateThread中塞入请求。ArrayBlockingQueue是阻塞的,就是InflateThread在解析xml时,另外的塞入的请求会阻塞在任务队列中,当任务队列满时,再执行InflateThread.enqueue方法对造成阻塞,因为enqueue内部调用的是put方法,put方法在塞不进入时会造成阻塞。

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);

        // Extracted to its own method to ensure locals have a constrained liveness
        // scope by the GC. This is needed to avoid keeping previous request references
        // alive for an indeterminate amount of time, see b/33158143 for details
        public void runInner() {
            InflateRequest request;
            try {
                request = mQueue.take(); // 虽然是死循环,但队列中没有数据会阻塞,不占用cpu
            } catch (InterruptedException ex) {
                // Odd, just continue
                Log.w(TAG, ex);
                return;
            }

            try {
            //解析xml的位置在这
                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();
        }

        @Override
        public void run() {
            while (true) {
                runInner();
            }
        }

        public InflateRequest obtainRequest() {
            InflateRequest obj = mRequestPool.acquire();
            if (obj == null) {
                obj = new InflateRequest();
            }
            return obj;
        }

      // 请求充值
        public void releaseRequest(InflateRequest obj) {
            obj.callback = null;
            obj.inflater = null;
            obj.parent = null;
            obj.resid = 0;
            obj.view = null;
            mRequestPool.release(obj);
        }

        public void enqueue(InflateRequest request) {
            try {
                mQueue.put(request);
            } catch (InterruptedException e) {
                throw new RuntimeException(
                        "Failed to enqueue async inflate request", e);
            }
        }
    }

回调

[站外图片上传中...(image-618574-1577417250016)]

可以看到handler收到回调时,判断如果解析生成的view是空的,就在当前线程重新解析一遍,否则直接回调给onInflateFinished。

收获

有界生产者-消费者模型

生产者持续生产,直到缓冲区满,阻塞;缓冲区不满后,继续生产

消费者持续消费,直到缓冲区空,阻塞;缓冲区不空后,继续消费

AsyncLayoutInflater使用的是ArrayBlockingQueue来实现此模型,所以如果连续大量的调用AsyncLayoutInflater创建布局,可能会造成缓冲区阻塞。由于使用场景的限制,应用很难遇到这种临界情况。适用场景??无界?其他案例

阻塞队列

说明
ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列
PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列
SynchronousQueue 一个不存储元素的阻塞队列
LinkedBlockingDeque 一个由链表结构组成的双向阻塞队列

对象池

当创建对象的成本比较大,并且创建比较频繁时使用,比如线程的创建代价比较大,于是就有了常用的线程池。
例如创建Handler的Message对象有两种方式:new Message和Message.obtain()。Message.obtain()的源码如下:

    private static final Object sPoolSync = new Object();
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

所以建议使用Message.obtain()创建对象。

类设计严谨性

[站外图片上传中...(image-f6caeb-1577417250016)]

可以看到OnInflateFinishedListener, InflateRequest, BasicInflater, InflateThreads全部都是静态内部类,由于这么类都是AsyncLayoutInflater私有的,设计为私有静态内部类做到了高内聚

单例线程的设计思路

private static class InflateThread extends Thread {
    private static final InflateThread sInstance;
    static {
        sInstance = new InflateThread();
        sInstance.start();
    }

    public static InflateThread getInstance() {
        return sInstance;
    }

    @Override
    public void run() {
        // 执行线程任务
    }
}

在static静态代码块中创建静态单例对象,并开启线程。

参考文章

《Android 带你彻底理解 Window 和 WindowManager》

《深入理解setContentView过程和View绘制过程》

《ViewRootImpl的独白,我不是一个View(布局篇)》

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

推荐阅读更多精彩内容