從阻塞隊列聊到AsyncLayoutInflater

一.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為空

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容