美文网首页
从阻塞队列聊到AsyncLayoutInflater

从阻塞队列聊到AsyncLayoutInflater

作者: 墨源为水 | 来源:发表于2021-03-10 23:07 被阅读0次

    一.BlockingQueue阻塞队列

    阻塞队列,Java给出的解释如下:


    在队列的基础上额外支持了这些操作:当取元素的时候会等待队列至不为空的时候;当添加元素的时候会等待队列有可用空间的时候。

    解释完阻塞队列了,思考一下问题:

    1.为什么Java要提供这么一个队列?即阻塞队列的存在性是什么?

    多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

    2.阻塞队列实现原理是什么?

    在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。

    • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
    • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。

    3.BlockingQueue核心方法

    BlockingQueue接口有以下方法:

        boolean add(E e);
    
        boolean offer(E e);
    
        void put(E e) throws InterruptedException;
    
        boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
    
        E take() throws InterruptedException;
    
        E poll(long timeout, TimeUnit unit) throws InterruptedException;
    
        int remainingCapacity();
    
        boolean remove(Object o);
    
        boolean contains(Object o);
    
        int drainTo(Collection<? super E> c);
    
        int drainTo(Collection<? super E> c, int maxElements);
    
    放入数据
    • (1)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法
      的线程);
    • (2)offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
    • (3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
    • (4)add(anObject):它与offer(anObject)功能大致一致,不过对此方法官方有如下备注:When using a capacity-restricted queue, it is generally preferable to use offer(Object),即:当使用容量限制的队列时,通常倾向于选择offer方法,容量限制的队列指的是在初始化队列是必须指定容量大小,eg:ArrayBlockingQueue
    获取数据
    • (1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
    • (2)poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
    • (3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
    • (4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

    4.BlockingQueue实现类

    衍生类

    5.分析ArrayBlockingQueue内部机制

    ArrayBlockingQueue官方解释为: A bounded blocking queue backed by an array,即由数组支持的有界阻塞队列;在构造ArrayBlockingQueue时需要指定容量,也就意味着底层数组一旦创建了,容量就不能改变了。那对此我提出一下问题。

    • 如何用固定容量的数组实现队列的FIFO机制
    • 如何在入栈,出栈的时候,达到阻塞效果,详细的说要做到当取元素的时候会等待队列至不为空的时候;当添加元素的时候会等待队列有可用空间的时候。

    我们看下ArrayBlockingQueue是如何解决第一个问题的,ArrayBlockingQueue出入栈的源码如下:

        final Object[] items;
        int takeIndex;
        int putIndex;
        private void enqueue(E x) {
            // assert lock.getHoldCount() == 1;
            // assert items[putIndex] == null;
            final Object[] items = this.items;
            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notEmpty.signal();
        }
    
        private E dequeue() {
            // assert lock.getHoldCount() == 1;
            // assert items[takeIndex] != null;
            final Object[] items = this.items;
            @SuppressWarnings("unchecked")
            E x = (E) items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            if (itrs != null)
                itrs.elementDequeued();
            notFull.signal();
            return x;
        }
    

    这里ArrayBlockingQueue内部记录两个全局变量,putIndex与takeIndex默认都为0,入栈的时候往putIndex对应下标的位置插入,同时putIndex自增1,当增加到固定容量-1的时候,再将putIndex置为0;当出栈的时候将takeIndex对应下标的位置清空即置为null,同时takeIndex自增1,当增加到固定容量-1的时候,再将takeIndex置为0;这样做的好处是时间复杂度最低,无需在出入栈的时候去遍历数组。

    ArrayBlockingQueue是如何解决第二个问题的呢?即如何在入栈,出栈的时候,达到阻塞效果呢

        int count;
        final ReentrantLock lock;
        private final Condition notEmpty;
        private final Condition notFull;
        public ArrayBlockingQueue(int capacity, boolean fair) {
            if (capacity <= 0)
                throw new IllegalArgumentException();
            this.items = new Object[capacity];
            lock = new ReentrantLock(fair);
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }
    

    这里ArrayBlockingQueue首先声明了以上几个全局变量,并在构造函数中进行初始化。在这里用到java.util.concurrent.locks.ReentrantLock与java.util.concurrent.locks.Condition的使用,这两个类即做到了线程安全的目的,这里先拓展讲一下线程安全的知识点

    5.1 线程安全探索

    在讲这两个locks类之前,先看一下以下例子:

    public class Demo {
    
        private static int count = 0;
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(15);
            for (int i = 0; i < 500; i++){
                executorService.execute(() -> {
                    add();
                });
            }
            Thread.sleep(1000);
            System.out.println(count);
        }
    
        private static int add(){
            return ++count;
        }
    }
    

    该Demo代码最终打印的不是500,而是小于500的数字,为了保证结果和预期一致,则要实现线程安全
    首先提问:如何保证线程安全呢?


    多线程有三个核心点:原子性、可见性、有序性;原子性即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效);可见性即当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到;有序性即程序执行的顺序按照代码的先后顺序执行。
    只要实现以上3个特性,就可以做到线程安全

    1.互斥同步(阻塞同步/悲观的并发策略)

    互斥同步(Mutual Exclusion & Synchronization)是最常见的一种并发正确性保证手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Sempahore)都是主要的互斥实现方式。因此,在这四个字里面,互斥是因,同步是果,互斥是方法,同步是目的。互斥即为阻塞,让其他线程阻塞;其实现了操作的原子性(这个操作没有被中断) 和 可见性(对数据进行更改后,会立马写入到内存中,其他线程在使用到这个数据时,会获取到最新的数据)

    常用Synchronized 关键字;synchronized实现同步的基础就是:Java中的每一个对象都可以作为锁。具体表现为:

    • 1.普通同步方法,锁是当前实例对象 public synchronized void **()
    • 2.静态同步方法,锁是当前类的Class对象 private synchronized static void **()
    • 3.同步方法块,锁是Synchronized括号里匹配的对象 synchronized (lockContent){}

    synchronized同步的原理是

    首先Java代码在编译之后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,JAVA中所使用的并发机制依赖于JVM的实现和CPU的指令。
    synchronized经过编译之后,会在同步块的前后生成 monitorenter 和monitorexit这两个字节码指令。这两个字节码指令之后有一个reference类型(存在于java虚拟机栈的局部变量表中,可以根据reference数据,来操作堆上的具体对象)的参数来指明要锁定和解锁的对象。根据虚拟机规范,在执行monitorenter 指令时,首先会尝试获取对象的锁(堆内存中存储的对象内容中,起对象头带有线程持有的锁),如果该对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加一。若获取对象失败,那当前线程就要阻塞等待,知道对象锁被另一个线程释放。

    锁的可重入性的可重入是指:当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象的锁的。比如

    public class Test01 {
        public synchronized void sm1(){
            System.out.println("同步方法1");
            //线程执行sm1()方法,默认this作为锁对象,在sm1()方法中调用了sm2()方法,注意当前线程还是持有this锁对象的
            //sm2()同步方法默认的锁对象也是this对象, 要执行sm2()必须先获得this锁对象,当前this对象被当前线程持有,可以 再次获得this对象, 这就是锁的可重入性. 假设锁不可重入的话,可能会造成死锁
            sm2();
        }
    
        private synchronized void sm2() {
            System.out.println("同步方法2");
            sm3();
        }
    
        private synchronized void sm3() {
            System.out.println("同步方法3");
        }
    
        public static void main(String[] args) {
            Test01 obj = new Test01();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    obj.sm1();
                }
            }).start();
        }
    }
    

    线程间通信

    多个线程间相互配合工作,需要依靠线程间通信。在 Object对象中 , 提供了wait/notify/notifyall,可以用于控制线程的状态。


    public class WaitNotifyDemo {
    
        private static Object object = new Object();
    
        public static void main(String[] args) throws InterruptedException {
            Thread threadA = new Thread(() -> {
                synchronized (object) {
                    System.out.println("ThreadA---Start");
                    try {
                        // 释放锁等待
                        object.wait();
                        System.out.println("ThreadA---Wait");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
    
            Thread threadB = new Thread(() -> {
                synchronized (object) {
                    System.out.println("ThreadB---Start");
                    // 唤醒ThreadA,需要注意的是(ThreadB需要执行完同步代码)
                    object.notify();
                    System.out.println("ThreadB---Wait");
                }
            });
    
            threadA.start();
            TimeUnit.SECONDS.sleep(2);
            threadB.start();
            threadA.join();
            threadB.join();
        }
    }
    
    打印如下:
    ThreadA---Start
    ThreadB---Start
    ThreadB---Wait
    ThreadA---Wait
    

    在这里要特别强调一点,官方对wait有如下备注:

         * A thread can also wake up without being notified, interrupted, or
         * timing out, a so-called <i>spurious wakeup</i>.  While this will rarely
         * occur in practice, applications must guard against it by testing for
         * the condition that should have caused the thread to be awakened, and
         * continuing to wait if the condition is not satisfied.  In other words,
         * waits should always occur in loops, like this one:
         * <pre>
         *     synchronized (obj) {
         *         while (&lt;condition does not hold&gt;)
         *             obj.wait(timeout);
         *         ... // Perform action appropriate to condition
         *     }
    

    java为了避免所谓的虚假唤醒,在多线程判断中,用while来代替if。所谓的虚假唤醒指的是:当一个条件满足时,很多线程都被唤醒了,但是只有其中部分是有用的唤醒,其它的唤醒都是无用功
    这样在同步方法条件判断时,用while去做判断,避免了notifyall,让那些不满足条件的线程依然还在挂起中

    synchronized可重入锁的实现
    java 在虚拟机中除了线程计数器,java虚拟机栈 是线程私有的,其余的java堆,方法区,和运行时常量池都是线程共享的内存区域。java堆是存储对象和数组的,但是对象在内存中的存储布局可以分为三块区域:对象头,实例数据(对象真正存储的有效信息,程序代码中所定义的各个类型的字段内容),对齐填充。
    每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

    存在的问题
    互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因而这种同步又称为阻塞同步,它属于一种悲观的并发策略,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在 CPU 转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起 CPU 频繁的上下文切换导致效率很低。synchronized 采用的就是这种并发策略。

    随着指令集的发展,我们有了另一种选择:基于冲突检测的乐观并发策略,通俗地讲就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了,如果共享数据被争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断地重拾,直到试成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步被称为非阻塞同步

    2.非阻塞同步(乐观并发策略)

    因为使用synchronized的时候,只能有一个线程可以获取对象的锁,其他线程就会进入阻塞状态,阻塞状态就会引起线程的挂起和唤醒,会带来很大的性能问题,所以就出现了非阻塞同步的实现方法。互斥同步的原理是用同步实现了原子性和可见性。而非阻塞同步的原理就是: 先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,如果共享数据有争用,就采取补偿措施(不断地重试)

    常用类如下:



    我们可以看下AtomicInteger类incrementAndGet方法

    public final int incrementAndGet() {
            for (;;) {
                int current = get();
                int next = current + 1;
                if (compareAndSet(current, next))
                    return next;
            }
    }
    
    public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    由以上源码可知,自增操作,就是不断在for循环,直到比较操作为true,才会跳出循环,这就是补偿措施; 比较操作compareAndSet最终调用了unsafe的compareAndSwapInt方法,即原子操作CAS指令(Compare-And-Swap比较并交换),它有三个操作数:内存位置,旧的预期值,新值,在执行CAS操作时,当且仅当内存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。

    3.无同步方案

    线程本地存储:将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。常用的是ThreadLocal类

      public T get() { }  
      public void set(T value) { }  
      public void remove() { }  
      protected T initialValue() { }  
    

    get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法;
    其实引起线程不安全最根本的原因 就是 :线程对于共享数据的更改会引起程序结果错误。线程安全的解决策略就是:保护共享数据在多线程的情况下,保持正确的取值。


    所以要想解决Demo打印值符合预期值500,既可以让count声明为AtomicInteger类型;也可以对add静态方法,用synchronized修改(当然也可以在调用add方法地方加synchronized修饰)

      executorService.execute(() -> {
                    synchronized (Demo.class){
                        add();
                    }
                });
        或者
        private static int add(){
            synchronized (Demo.class){
                return ++count;
            }
        }
    

    5.2聊聊ReentrantLock与Condition

    以上例子也可以用ReentrantLock类

    public class Demo {
    
        private static int count = 0;
    
        private static Lock lock = new ReentrantLock();
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(15);
            for (int i = 0; i < 500; i++){
                executorService.execute(() -> {
                    add();
                });
            }
            Thread.sleep(1000);
            System.out.println(count);
        }
    
        private static int add(){
            lock.lock();
            try {
                return ++count;
            }finally {
                lock.unlock();
            }
    
        }
    }
    

    ReetrantLock 采用的乐观并发策略,就是非阻塞同步;在基本用法上,ReentranLock与synchronized很相似,他们都具备一样的线程重入特性,synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的,一个表现为API层面的互斥锁(lock和unlock方法配合try/finally语句块来完成),一个表现为原生语法层面的互斥锁;ReentrantLock 可中断,而 synchronized 不行。即ReentrantLock比synchronized增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁,一级锁可以绑定多个条件。

    • 等待可中断是指当持有锁的线程长期不是放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
    • 公平锁是指多个线程在等待同一锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放的时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
    • 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在sychronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

    ReetrantLock 当调用Condition的await方法后,当前线程会释放锁并等待,而其他线程调用condition 对象的 signal 或者 signalall 方法通知被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。
    所以,condition 中两个最重要的方法,一个是 await,一个是 signal 方法await:把当前线程阻塞挂起signal:唤醒阻塞的线程

    我们回来再想想如何实现阻塞队列的代码原理,即:当添加元素的时候会等待队列有可用空间的时候;当取元素的时候会等待队列至不为空的时候;

    • 为了做到插入要等待有空间,在插入时判断count==size,符合就阻塞线程,不符合就执行插入操作
    • 为了做到取出要等待不为空,在取出时判断count==0,符合就阻塞线程,不符合就执行取出操作
      但是这两个操作是彼此影响的,即插入线程和取出线程是要通信的,插入完成之后,通知取出线程继续操作;取出完成之后,通知插入线程继续操作;
     class BoundedBuffer {
       final Lock lock = new ReentrantLock();
       final Condition notFull  = lock.newCondition(); 
       final Condition notEmpty = lock.newCondition(); 
     
       final Object[] items = new Object[100];
       int putptr, takeptr, count;
     
       public void put(Object x) throws InterruptedException {
         lock.lock();
         try {
           while (count == items.length) 
             notFull.await();
           items[putptr] = x; 
           if (++putptr == items.length) putptr = 0;
           ++count;
           notEmpty.signal();
         } finally {
           lock.unlock();
         }
       }
     
       public Object take() throws InterruptedException {
         lock.lock();
         try {
           while (count == 0) 
             notEmpty.await();
           Object x = items[takeptr]; 
           if (++takeptr == items.length) takeptr = 0;
           --count;
           notFull.signal();
           return x;
         } finally {
           lock.unlock();
         }
       } 
     }
    

    这里有两个Condition,一个是notFull,一个是notEmpty。当执行入队列时先判count!=size条件,如果不符合会执行notFull的await操作,会去阻塞线程并释放线程锁,此时出队列操作中,如果有阻塞线程的情况,收到释放线程锁信号会继续判断notEmpty条件,当count!=0时,会执行出队列实际操作,执行完成后执行notFull的signal方法,通知入队列的阻塞线程继续判断notFull条件,如何有多余空间,执行实际入队列操作

    ReentrantLock和Condition具体使用与分析可看以下文章:
    ReentrantLock和condition源码浅析(一)
    Java多线程之ReentrantLock与Condition
    漫谈Java中的互斥同步
    什么是线程安全?如何保证线程安全?

    二.AsyncLayoutInflater分析

    google对此解释如下:Helper class for inflating layouts asynchronously,即异步加载布局的帮助类;传统中,对于布局文件的加载必须要在主线程,为什么要在主线程呢?是因为ViewRootImpl在以下操作前都做了checkThread判断



    checkThread源码如下:

        void checkThread() {
            if (mThread != Thread.currentThread()) {
                throw new CalledFromWrongThreadException(
                        "Only the original thread that created a view hierarchy can touch its views.");
            }
        }
    
        public ViewRootImpl(Context context, Display display) {
            -------------------
            mThread = Thread.currentThread();
            -------------------
        }
    

    有以上源码可知,checkThread方法判断,当前操作线程是否等于当时初始化ViewRootImpl时的线程。如果不相等抛出“Only the original thread that created a view hierarchy can touch its views”异常,Android View 绘制流程之 DecorView 与 ViewRootImpl

    为了加载耗时布局,官方提供了AsyncLayoutInflater异步加载工具。那它内部如何实现异步加载布局的呢?我们阅读一下源码

    public final class AsyncLayoutInflater {
        LayoutInflater mInflater;
        Handler mHandler;
        InflateThread mInflateThread;
    
        public AsyncLayoutInflater(@NonNull Context context) {
            mInflater = new BasicInflater(context);
            mHandler = new Handler(mHandlerCallback);
            mInflateThread = InflateThread.getInstance();
        }
    
        @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);
        }
    }
    

    AsyncLayoutInflater 构造器内初始化了主线程的Handler和单例InflateThread 线程以及一个自定义的布局加载器BasicInflater;当用户调用inflate方法时,就会调用obtainRequest方法从InflateThread 线程内获取一个InflateRequest加载布局请求,之后调用enqueue去入队列到InflateThread线程的队列中;我们再看下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 {
                    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();
            }
    
            @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);
                }
            }
        }
    

    由源码可知,只要第一次初始化InflateThread 类,就会启动一个单例InflateThread 线程,注意run方法内部用了while循环runInner,是为了保证该线程都不会关闭;obtainRequest方法就是从mRequestPool的同步池内获取Request加载布局请求(其实这样的操作等同于Handler的obtainMessage方法去获取消息,而不是直接实例化Message,这样就可以避免内存抖动问题);enqueue方法就是入队列,注意看mQueue是一个ArrayBlockingQueue类型的队列,就是上文讲解的数组阻塞队列,通过调用put方法可达到其内部数组没有空间存放时而去阻塞队列,直到调用take出队列操作等到数组有空间时,才会真正存入数组中;线程run方法不断调用runInner方法,从阻塞队列中取出request加载布局请求,从而异步执行BasicInflater的inflate方法
    我们看下BasicInflater类源码

        private static class BasicInflater extends LayoutInflater {
            private static final String[] sClassPrefixList = {
                "android.widget.",
                "android.webkit.",
                "android.app."
            };
    
            BasicInflater(Context context) {
                super(context);
            }
    
            @Override
            public LayoutInflater cloneInContext(Context newContext) {
                return new BasicInflater(newContext);
            }
    
            @Override
            protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
                for (String prefix : sClassPrefixList) {
                    try {
                        View view = createView(name, prefix, attrs);
                        if (view != null) {
                            return view;
                        }
                    } catch (ClassNotFoundException e) {
                        // In this case we want to let the base class take a crack
                        // at it.
                    }
                }
    
                return super.onCreateView(name, attrs);
            }
        }
    

    可以看下LayoutInflater部分源码,如下:

        public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
            final Resources res = getContext().getResources();
            if (DEBUG) {
                Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                        + Integer.toHexString(resource) + ")");
            }
    
            final XmlResourceParser parser = res.getLayout(resource);
            try {
                return inflate(parser, root, attachToRoot);
            } finally {
                parser.close();
            }
        }
    
        public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
    
                final Context inflaterContext = mContext;
                final AttributeSet attrs = Xml.asAttributeSet(parser);
                Context lastContext = (Context) mConstructorArgs[0];
                mConstructorArgs[0] = inflaterContext;
                View result = root;
    
                try {
                    // Look for the root node.
                    int type;
                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty
                    }
    
                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(parser.getPositionDescription()
                                + ": No start tag found!");
                    }
    
                    final String name = parser.getName();
    
                    if (DEBUG) {
                        System.out.println("**************************");
                        System.out.println("Creating root view: "
                                + name);
                        System.out.println("**************************");
                    }
    
                    if (TAG_MERGE.equals(name)) {
                        if (root == null || !attachToRoot) {
                            throw new InflateException("<merge /> can be used only with a valid "
                                    + "ViewGroup root and attachToRoot=true");
                        }
    
                        rInflate(parser, root, inflaterContext, attrs, false);
                    } else {
                        // Temp is the root view that was found in the xml
                        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
                        ViewGroup.LayoutParams params = null;
    
                        if (root != null) {
                            if (DEBUG) {
                                System.out.println("Creating params from root: " +
                                        root);
                            }
                            // Create layout params that match root, if supplied
                            params = root.generateLayoutParams(attrs);
                            if (!attachToRoot) {
                                // Set the layout params for temp if we are not
                                // attaching. (If we are, we use addView, below)
                                temp.setLayoutParams(params);
                            }
                        }
    
                        if (DEBUG) {
                            System.out.println("-----> start inflating children");
                        }
    
                        // Inflate all children under temp against its context.
                        rInflateChildren(parser, temp, attrs, true);
    
                        if (DEBUG) {
                            System.out.println("-----> done inflating children");
                        }
    
                        // We are supposed to attach all the views we found (int temp)
                        // to root. Do that now.
                        if (root != null && attachToRoot) {
                            root.addView(temp, params);
                        }
    
                        // Decide whether to return the root that was passed in or the
                        // top view found in xml.
                        if (root == null || !attachToRoot) {
                            result = temp;
                        }
                    }
    
                } catch (XmlPullParserException e) {
                    final InflateException ie = new InflateException(e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } catch (Exception e) {
                    final InflateException ie = new InflateException(parser.getPositionDescription()
                            + ": " + e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
    
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
    
                return result;
            }
        }
    

    inflate方法内调用的createViewFromTag方法最终回、会调用onCreateView方法;我们在看下AsyncLayoutInflater的Handler传入的mHandlerCallback

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

    mHandlerCallback 的主线程回调中会去检查异步请求后的view,如果为空,主线程会去加载布局,达到二次检验,避免异步加载后的View为空

    相关文章

      网友评论

          本文标题:从阻塞队列聊到AsyncLayoutInflater

          本文链接:https://www.haomeiwen.com/subject/rlntqltx.html