主頁 > 移動端開發 > Android全面決議之由淺及深Handler訊息機制

Android全面決議之由淺及深Handler訊息機制

2020-12-12 08:59:33 移動端開發

前言

很高興遇見你~ 歡迎閱讀我的文章,

關于Handler的博客可謂是俯拾皆是,而這也是一個老生常談的話題,可見的他非常基礎,也非常重要,但很多的博客,卻很少有從入門開始介紹,這在我一開始學習的時候就直接給我講Looper講阻塞,非常難以理解,同時,也很少有系統地講解關于Handler的一切,知識比較零散,我希望寫一篇從入門到深入,系統地全面地講解Handler的文章,幫助大家認識Handler,

這篇文章的講解深度循序漸進,不同程式的讀者可選擇對應的部分查看:

  1. 第一部分是對于Handler的入門概述,了解一個新事物,需要問三個問題:是什么、為什么、怎么用,包括關于Handler的結構等都有介紹,
  2. 第二部分是在對Handler有一定的認知基礎上,對各個類進行詳細的講解和原始碼分析,
  3. 第三部分是整體的流程分析以及常見問題的決議,
  4. 最后一部分是Android對于訊息機制設計的講解以及全文總結,

文章基本涵蓋了關于Handler相關的知識,因而篇幅也比較長
考慮過把文章分割成幾篇小文章,考慮到閱讀的整體性以及方便性,最侄訓是集成了一篇大文章
文章成體系,全面地講解知識點,而不是把知識碎片化,否則很難真正去理解單一的知識,更不易于對整體知識的把握
讀者可自行選擇感興趣的章節閱讀

那么,我們開始吧,

概述

什么是Handler?

準確來說,是Handler機制,Handler只是Handler機制中的一個角色,只是我們對Handler接觸比較多,所以經常以Handler來代稱,

Handler機制是Android中基于單線訊息佇列模式的一套執行緒訊息機制,

他的本質是訊息機制,負責訊息的分發以及處理,這樣講可能有點抽象,不太容易理解,什么是“單線訊息佇列模式”?什么是“訊息”?

通俗點來說,每個執行緒都有一個“流水線”,我們可往這條流水線上放“訊息”,流水線的末端有作業人員會去處理這些訊息,因為流水線是單線的,所有訊息都必須按照先來后到的形式依次處理(在Handler機制中有“加急線”:同步屏障,這個后面講),如下圖:

0073QO.png

放什么訊息以及怎么處理訊息,是需要我們去自定義的,Handler機制相當于提供了這樣的一套模式,我們只需要“放訊息到流水線上”,“撰寫這些訊息的處理邏輯”就可以了,流水線會源源不斷把訊息運送到末端處理,最后注意重點:每個執行緒只有一個“流水線”,他的基本范圍是執行緒,負責執行緒內的通信以及執行緒間的通信,每個執行緒可以看成一個廠房,每個廠房只有一個生產線,

兩個關鍵問題

了解Handler的作用前需要了解Handler背景下的兩個關鍵問題:

  1. 不能在非UI創建執行緒去操作UI
  2. 不能在主執行緒執行耗時任務

我們普遍的認知是:不能在非主執行緒更新UI,但這是不準確的,如果我們在子執行緒更新了UI,看看報錯資訊是什么:

筆者留下了英語渣渣的眼淚,百度翻譯一下:

只有創建視圖層次結構的原始執行緒才能訪問其視圖,但為什么我們一直都說是非主執行緒不能更新ui?這是因為我們的界面一般都是由主執行緒進行繪制的,所以界面的更新也就一般都限制在主執行緒內,這個例外是在viewRootIimpl.checkThread()方法中拋出來的,那可不可以繞過他?當然可以,在他還沒創建出來的時候就可以偷偷更新ui了,閱讀過Activity啟動流程的讀者知道,ViewRootImpl是在onCreate方法之后被創建的,所以我們可以在onCreate方法中創建個子執行緒偷偷更新UI,(Actvity啟動流程決議傳送門)但還是那句話,可以,但沒必要去繞過這個限制,因為這是谷歌為了我們的程式更加安全而設計的,

為什么不能在子執行緒去更新UI?因為這會讓界面產生不可預期的結果,例如主執行緒在繪制一個按鈕,繪制一半另一個執行緒突然過來把按鈕的大小改成兩倍大,這個時候再回去主執行緒繼續執行繪制邏輯,這個繪制的效果就會出現問題,所以UI的訪問是決不能是并發的,但,子執行緒又想更新UI,怎么辦?加鎖,加鎖確實可以解決這個問題,但是會帶來另外的問題:界面卡頓,鎖對于性能是有消耗的,是比較重量級的操作,而ui操作講究快準狠,加鎖會讓ui操作性能大打折扣,那有什么更好的方法?Handler就是解決這個問題的,

第二個問題,不能在主執行緒執行耗時操作,耗時操作包括網路請求、資料庫操作等等,這些操作會導致ANR(Application Not Responding),這個是比較好理解的,沒有什么問題,但是這兩個問題結合起來,就有大問題了,資料請求一般是耗時操作,必須在子執行緒進行請求,而當請求完成之后又必須更新UI,UI又只能在主執行緒更新,這就導致必須切換執行緒執行代碼,上面討論了加鎖是不可取的,那么Handler的重要性就體現出來了,

不用Handler可不可以?可以,但沒必要,Handler是谷歌設計來方便開發者切換執行緒以及處理訊息,然后你說我偏不用,我自己用Java工具類,自己弄個出來不可以嗎?那,,,請收下小的膝蓋,

為什么要有Handler?

先給結論:

  1. 切換代碼執行的執行緒
  2. 按順序規則地處理訊息,避免并發
  3. 阻塞執行緒,避免讓執行緒結束
  4. 延遲處理訊息

第一個作用是最明顯也是最常用的,上一部分已經講了Handler存在的必要性,android限制了不能在非UI創建執行緒去操作UI,同時不能在主執行緒執行耗時任務,所以我們一般是在子執行緒執行網路請求等耗時操作請求資料,然后再切換到主執行緒來更新UI,這個時候就必須用到Handler來切換執行緒了,上面討論過了這里不再贅述,

這里有一個誤區是:我們的activity是執行在主執行緒的,我們在網路請求完成之后回呼主執行緒的方法不就切換到主執行緒了嗎?咳咳,不要笑,不要覺得這種低級錯誤太離譜,很多童鞋剛開始接觸開發的時候都會犯這個思維錯誤,這其實是理解錯了執行緒這個概念,代碼本身并沒有限制運行在哪個執行緒,代碼執行的執行緒環境取決于你的執行邏輯是在哪個執行緒,這樣講可能還是有點抽象,例如現在有一個方法void test(){},然后兩個不同的執行緒去呼叫它:

new Thread(){
    // 第一個執行緒呼叫
    test();
}.start();

new Thread(){
    // 第二個執行緒呼叫
    test();
}

此時雖然都是test這個方法,但是他的執行邏輯是由不同的執行緒呼叫的,所以他是執行在兩個不同的執行緒環境下,而當我們想要把邏輯切換到另一個執行緒去執行的時候,就需要用到Handler來切換邏輯,

第二個作用可能看著有點懵,但其實他解決了另一個問題:并發操作,雖然切換執行緒解決了,如果主執行緒正在繪制一個按鈕,剛測量好按鈕的長寬,突然子執行緒一個新的請求過來打斷了,先停下這邊的繪制操作,把按鈕改成了兩倍大,然后邏輯切回來繼續繪制,這個時候之前的測量的長寬已經是不準確的了,繪制的結果肯定也不準確,怎么解決?單線訊息佇列模型,在講什么是Handler那部分簡單介紹過,就是相當于一個流水線一樣的模型,子執行緒的請求會變成一個個的訊息,然后主執行緒依次處理,那么就不會出現繪制一半被打斷的問題了,

同時這種模型也不止用于解決ui并發問題,在ActivityThread中有一個H類,他其實就是個Handler,在ActivityThread中定義了一百多中訊息型別以及對應的處理邏輯,這樣,當需要讓ActivityThread處理某一個邏輯的時候,只需要發送對應的訊息給他即可,而且可以保證訊息按順序執行,例如先呼叫onCreate再呼叫onResume,而如果沒有Hanlder的話,就需要讓ActivityThread有一百多個介面對外開放,同時還需要不斷進行回呼保證任務按順序執行,這顯然復雜了非常多,

我們執行一個Java程式的時候,從main方法入口,執行完成之后,馬上就退出了,但是我們android應用程式肯定是不可以的,他需要一直等待用戶的操作,而Handler機制就解決了這個問題,但訊息佇列中沒有任務的時候,他就會把執行緒阻塞,等到有新的任務的時候,再重新啟動處理訊息,

第四個作用讓延遲處理訊息得到了最佳解決方案,假如你想讓應用啟動5秒后界面彈出一個對話框,沒有handler的情況下,會如何處理?開一個Thread然后使用Thread.sleep讓執行緒睡眠一對應的時間對吧,但如果多個延遲任務呢?而開啟執行緒也是個比較重量級的操作且執行緒的數量有限,而可以直接給Handler發送延遲對應時間的訊息,他會在對應時間之后準時處理該訊息(當然有特殊情況,如單件訊息處理時間過長或者同步屏障,后面會講到),而且無論發送多少延遲訊息都不會對性能有任何影響,同時,也是通過這個功能來記錄ANR的時間,

講這些作用可能讀者心中并沒有一個很形象的概念,也可能看完就忘了,但是關于Handler的定義不能忘:Handler機制是Android中基于單線訊息佇列模式的一套執行緒訊息機制,,上述四個作用是為了讓讀者更好地理解Handler機制,

如何使用Handler

我們平常使用Handler有兩種不同的創建方式,但總體流程是相同的:

  1. 創建Looper
  2. 使用Looper創建Handler
  3. 啟動Looper
  4. 使用Handler發送資訊

Looper可理解為回圈器,就像“流水線”上的滾帶,后面會詳細講到,每個執行緒只有一個Looper,通常主執行緒已經創建好了,追溯應用程式啟動流程可以知道啟動程序中呼叫了Looper.prepareMainLooper,而在子執行緒就必須使用如下方法來初始化Looper:

Looper.prepare();

第二步是創建Handler,也是最熟悉的一步,我們有兩種方法來創建Handler:傳入callBack物件和繼承,如下:

public class MainActivity extends AppComposeActivity{
    ...;
    // 第一種方法:使用callBack創建handler
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        Handler handler = Handler(Looper.myLooper(),new CallBack(){
            public Boolean handleMessage(Message msg) {
                TODO("Not yet implemented")
            }
        });
    }
    
    // 第二種方法:繼承Handler并重寫handlerMessage方法
    static MyHandler extends Hanlder{
        public MyHandler(Looper looper){
            super(looper);
        }
        @Override
        public void handleMessage(Message msg){
            super.handleMessage(msg);
            // TODO(重寫這個方法)
        }
    }
}

注意第二種方法,要使用靜態內部類,不然可能會造成記憶體泄露,原因是非靜態內部類會持有外部類的參考,而Handler發出的Message會持有Handler的參考,如果這個Message是個延遲的訊息,此時activity被退出了,但Message依然在“流水線”上,Message->handler->activity,那么activity就無法被回收,導致記憶體泄露,

兩種Handler的寫法各有千秋,繼承法可以寫比較復雜的邏輯,callback法適合比價簡單的邏輯,看具體的業務來選擇,

然后再呼叫Looper的loope方法來啟動Looper:

Looper.loop();

最后就是使用Handler來發送資訊了,當我們獲得handler的實體之后,就可以通過他的sendMessage相方法和post相關方法來發送資訊,如下:

handler.sendMessage(msg);
handler.sendMessageDelayed(msg,delayTime);
handler.post(runnable);
handler.postDelayed(runnable,delayTime);

然后一般情況下是哪個Handler發出的資訊,最終由哪個Handler來處理,這樣,只要我們拿到Handler物件,就可以往對應的執行緒發送資訊了,

Handler內部模式結構

經過前面的介紹對于Looper已經有了一定的認知,但可能對他內部的模式還不太清楚,這一部分先講解Handler的大概內部模式,目的是為下面的詳解做鋪墊,為做整體概念感知,先上圖:

Handler機制內部有三大關鍵角色:Handler,Looper,MessageQueue,其中MessageQueue是Looper內部的一個物件,MessageQueue和Looper每個執行緒有且只有一個,而Handler是可以有很多個的,他們的作業流程是:

  1. 用戶使用執行緒的Looper構建Handler之后,通過Handler的send和post方法發送訊息
  2. 訊息會加入到MessageQueue中,等待Looper獲取處理
  3. Looper會不斷地從MessageQueue中獲取Message然后交付給對應的Handler處理

這就是大名鼎鼎的Handler機制內部模式了,說難,其實也是很簡單,

Handler機制關鍵類

一、ThreadLocal

概述

ThreadLocal是Java中一個用于執行緒內部存盤資料的工具類,

ThreadLocal是用來存盤資料的,但是每個執行緒只能訪問到各自執行緒的資料,我們一般的用法是:

ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
String s = stringLocal.get();

不同的執行緒之間訪問到的資料是不一樣的:

public static void main(String[] args){
    ThreadLocal<String> stringLocal = new ThreadLocal<>();
	stringLocal.set("java");
    
    System.out.println(stringLocal.get());
    new Thread(){
        System.out.println(stringLocal.get());
    }
}

結果:
java
null

執行緒只能訪問到自己執行緒存盤的資料,

ThreadLocal的作用

ThreadLocal的特性適用于同樣的資料型別,不同的執行緒有不同的備份情況,如我們這篇文章一直在講的Looper,每個執行緒都有一個物件,但是不同執行緒的Looper是不一樣的,這個時候就特別適合使用ThreadLocal來存盤資料,這也是為什么這里要講ThreadLocal的原因

ThreadLocal內部結構

ThreadLocal的內部機制結構如下:

每個Thread,也就是每個執行緒內部維護有一個ThreadLocalMap,ThreadLocalMap內部存盤多個Entry,Entry可以理解為鍵值對,他的本質是一個弱參考,內部有一個object型別的內部變數,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = https://www.cnblogs.com/huan89/archive/2020/12/11/v;
    }
}

Entry是ThreadLocalMap的一個靜態內部類,這樣每個Entry里面就維護了一個ThreadLocal和ThreadLocal泛型物件,每個執行緒的內部維護有一個Entry陣列,并通過hash演算法使得讀取資料的速度達到O(1),由于不同的執行緒對應的Thread物件不同,所以對應的ThreadLocalMap肯定也不同,這樣只有獲取到Thread物件才能獲取到其內部的資料,資料就被隔離在不同的執行緒內部了,

ThreadLocal作業流程

那ThreadLocal是怎么實作把資料存盤在不同執行緒中的?先從他的set方法入手:

TheadLocal.class
    
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

邏輯不是很復雜,首先獲取當前執行緒的Thread物件,然后再獲取Thread的ThreadLocalMap物件,如果該map物件不存在則創建一個并呼叫他的set方法把資料存盤起來,我們繼續看ThreadLocalMap的set方法:

ThreadLocalMap.class

private void set(ThreadLocal<?> key, Object value) {
    // 每個ThreadLocalMap內部都有一個Entry陣列
    Entry[] tab = table;
    int len = tab.length;
    // 獲取新的ThreadLocal在Entry陣列中的下標
    int i = key.threadLocalHashCode & (len-1);
    // 判斷當前位置是否發生了Hash沖突
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 如果資料存在且相同則直接回傳
        if (k == key) {
            e.value = https://www.cnblogs.com/huan89/archive/2020/12/11/value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 若當前位置沒有其他元素則直接把新的Entry物件放入
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 判斷是否需要對陣列進行擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

這里的邏輯和HashMap是很像的,我們可以直接使用HashMap的思維來理解ThreadLocalMap:ThreadLocalMap的key是ThreadLocal,value是ThreadLocal對應的泛型,他的存盤步驟如下:

  1. 根據自身的threadLocalHashCode與陣列的長度進行相與得到下標
  2. 如果此下標為空,則直接插入
  3. 如果此下標已經有元素,則判斷兩者的ThreadLocal是否相同,相同則更新value后回傳,否則找下一個下標
  4. 直到找到合適的位置把entry物件插入
  5. 最后判斷是否需要對entry陣列進行擴容

是不是和HashMap非常像?和HashMap的不同是:hash演算法不一樣,以及這里使用的是開發地址法,而HashMap使用的是鏈表法,ThreadLocalMap犧牲一定的空間來換取更快的速度,具體的Hash演算法這里就不再深入了,有興趣的讀者可以閱讀這篇文章ThreadLocal傳送門

然后繼續看ThreadLocal的get方法:

ThreadLocal.class

public T get() {
    // 獲取當前執行緒的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 根據ThreadLocal獲取Entry物件
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果沒找到也會執行初始化作業
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 把獲取到的物件進行回傳
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

前面講到ThreadLocalMap其實非常像一個HashMap,他的get方法也是一樣的,使用ThreadLocal作為key獲取到對應的Entry,再把value回傳即可,如果map尚未初始化則會執行初始化操作,下面繼續看下ThreadLocalMap的get方法:

ThreadLocalMap.class

private Entry getEntry(ThreadLocal<?> key) {
    // 根據hash演算法找到下標
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 找到資料則回傳,否則通過開發地址法尋找下一個下標
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

利用ThreadLocal的threadLocalHashCode得到下標,然后根據下標找到資料,沒找到則根據演算法尋找下個下標,

記憶體泄露問題

我們會發現Entry中,ThreadLocal是一個弱參考,而value則是強參考,如果外部沒有對ThreadLocal的任何參考,那么ThreadLocal就會被回收,此時其對應的value也就變得沒有意義了,但是卻無法被回收,這就造成了記憶體泄露,怎么解決?在ThreadLocal回收的時候記得呼叫其remove方法把entry移除,防止記憶體泄露,

ThreadLocal總結

ThreadLocal適合用于在不同執行緒作用域的資料備份

ThreadLocal機制通過在每個執行緒維護一個ThreadLocalMap,其key為ThreadLocal,value為ThreadLocal對應的泛型物件,這樣每個ThreadLocal就可以作為key將不同的value存盤在不同Thread的Map中,當獲取資料的時候,同個ThreadLocal就可以在不同執行緒的Map中得到不同的資料,如下圖:

ThreadLocalMap類似于一個改版的HashMap,內部也是使用陣列和Hash演算法來存盤資料,使得存盤和讀取的速度非常快,

同時使用ThreadLocal需要注意記憶體泄露問題,當ThreadLocal不再使用的時候,需要通過remove方法把value移除,

二、Message

概述

Message是負責承載訊息的類,主要是關注他的內部屬性:

// 用戶自定義,主要用于辨別Message的型別
public int what;
// 用于存盤一些整型資料
public int arg1;
public int arg2;
// 可放入一個可序列化物件
public Object obj;
// Bundle資料
Bundle data;
// Message處理的時間,相對于1970.1.1而言的時間
// 對用戶不可見
public long when;
// 處理這個Message的Handler
// 對用戶不可見
Handler target;
// 當我們使用Handler的post方法時候就是把runnable物件封裝成Message
// 對用戶不可見
Runnable callback;
// MessageQueue是一個鏈表,next表示下一個
// 對用戶不可見
Message next;

回圈利用Message

當我們獲取Message的時候,官方建議是通過Message.obtain()方法來獲取,當使用完之后使用recycle()方法來回識訓圈利用,而不是直接new一個新的物件:

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維護了一個靜態鏈表,鏈表頭是sPool,Message有一個next屬性,Message本身就是鏈表結構,sPoolSync是一個object物件,僅作為解決并發訪問安全設計,當我們呼叫obtain來獲取一個新的Message的時候,首先會檢查鏈表中是否有空閑的Message,如果沒有則新建一個回傳,

當我們使用完成之后,可以呼叫Message的recycle方法進行回收:

public void recycle() {
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    recycleUnchecked();
}

如果這個Message正在使用則會拋出例外,否則則呼叫recycleUnchecked進行回收:

void recycleUnchecked() {
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = UID_NONE;
    workSourceUid = UID_NONE;
    when = 0;
    target = null;
    callback = null;
    data = https://www.cnblogs.com/huan89/archive/2020/12/11/null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

這個方法的邏輯也非常簡單,把Message中的內容清空,然后判斷鏈表是否達到最大值(50),然后插入鏈表中,

Message總結

Message的作用就是承載訊息,他的內部有很多的屬性用于給用戶賦值,同時Message本身也是一個鏈表結構,無論是在MessageQueue還是在Message內部的回識訓制,都是使用這個結構來形成鏈表,同時官方建議不要直接初始化Message,而是通過Message.obtain()方法來獲取一個Message回圈利用,一般來說我們不需要去呼叫recycle進行回收,在Looper中會自動把Message進行回收,后面會講到,

三、MessageQueue

概述

每個執行緒都有且只有一個MessageQueue,他是一個用于承載訊息的佇列,內部使用鏈表作為資料結構,所以待處理的訊息都會在這里排隊,前面講到ThreadLocalMap是一個“修改版的HashMap”,而MessageQueue就是一個“修改版的LinkQueue”,他也有兩個關鍵的方法:入隊(enqueueMessage)和出隊(next),這也是MessageQueue的重點所在,

Message還涉及到一個關鍵概念:執行緒休眠,當MessageQueue中沒有訊息或者都在等待中,則會將執行緒休眠,讓出cpu資源,提高cpu的利用效率,進入休眠后,如果需要繼續執行代碼則需要將執行緒喚醒,當方法暫時無法直接回傳需要等待的時候,則可以將執行緒阻塞,即休眠,等待被喚醒繼續執行邏輯,這部分內容也會在后面詳細講,

關鍵方法

  • 出隊 -- next()

    next方法主要是做訊息出隊作業,

    Message next() {
        // 如果looper已經退出了,這里就回傳null
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
        ...
        // 阻塞時間
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            // 阻塞對應時間 
            nativePollOnce(ptr, nextPollTimeoutMillis);
    		// 對MessageQueue進行加鎖,保證執行緒安全
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...
                if (msg != null) {
                    if (now < msg.when) {
                        // 下一個訊息還沒開始,等待兩者的時間差
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 獲得訊息且現在要執行,標記MessageQueue為非阻塞
                        mBlocked = false;
                        // 鏈表操作
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // 沒有訊息,進入阻塞狀態
                    nextPollTimeoutMillis = -1;
                }
               ...
        }
    }
    

    代碼很長,其中還涉及了同步屏障和IdleHandler,這兩部分內容我放在后面講,這里先講主要的出隊邏輯,代碼中我都加了注釋,這里還是再講一下,next方法目的是獲取MessageQueue中的一個Message,如果佇列中沒有訊息的話,就會把方法阻塞住,等待新的訊息來喚醒,主要步驟如下:

    1. 如果Looper已經退出了,直接回傳null
    2. 進入死回圈,直到獲取到Message或者退出
    3. 回圈中先判斷是否需要進行阻塞,阻塞結束后,對MessageQueue進行加鎖,獲取Message
    4. 如果MessageQueue中沒有訊息,則直接把執行緒無限阻塞等待喚醒;
    5. 如果MessageQueue中有訊息,則判斷是否需要等待,否則則直接回傳對應的message,

    可以看到邏輯就是判斷當前時間Message中是否需要等待,其中nextPollTimeoutMillis表示阻塞的時間,-1表示無限時間,只有通過喚醒才能打破阻塞,

  • 入隊 -- enqueueMessage()

    MessageQueue.class
    
    boolean enqueueMessage(Message msg, long when) {
        // Hanlder不允許為空
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
    
        // 對MessageQueue進行加鎖
        synchronized (this) {
            // 判斷目標thread是否已經死亡
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
            // 標記Message正在被執行,以及需要被執行的時間,這里的when是距離1970.1.1的時間
            msg.markInUse();
            msg.when = when;
            // p是MessageQueue的鏈表頭
            Message p = mMessages;
            boolean needWake;
            // 判斷是否需要喚醒MessageQueue
            // 如果有新的隊頭,同時MessageQueue處于阻塞狀態則需要喚醒佇列
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                ...
                // 根據時間找到插入的位置
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    ...
                }
                msg.next = p; 
                prev.next = msg;
            }
    		
            // 如果需要則喚醒佇列
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    

    這部分的代碼好像也很多,但是邏輯也是不復雜,主要就是鏈表操作以及判斷是否需要喚醒MessageQueue,代碼中我加了一些注釋,下面再總結一下:

    1. 首先判斷message的目標handler不能為空且不能正在使用中

    2. 對MessageQueue進行加鎖

    3. 判斷目標執行緒是否已經死亡,死亡則直接回傳false

    4. 初始化Message的執行時間以及標記正在執行中

    5. 然后根據Message的執行時間,找到在鏈表中的插入位置進行插入

    6. 同時判斷是否需要喚醒MessageQueue,有兩種情況需要喚醒:當新插入的Message在鏈表頭時,如果messageQueue是空的或者正在等待下個任務的延遲時間執行,這個時候就需要喚醒MessageQueue,

MessageQueue總結

Message兩大重點:阻塞休眠和佇列操作,基本都是圍繞著兩點來展開,而原始碼中還涉及到了同步屏障以及IdleHandler,這兩部分內容我分開到了最后一部分的相關問題中講,平時用的比較少,但也是比較重要的內容,

四、Looper

概述

Looper可以說是Handler機制中的一個非常重要的核心,Looper相當于執行緒訊息機制的引擎,驅動整個訊息機制運行,Looper負責從佇列中取出訊息,然后交給對應的Handler去處理,如果佇列中沒有訊息,則MessageQueue的next方法會阻塞執行緒,等待新的訊息的到來,每個執行緒有且只能有一個“引擎”,也就是Looper,如果沒有Looper,那么訊息機制就運行不起來,而如果有多個Looper,則會違背單線操作的概念,造成并發操作,

每個執行緒僅有一個Looper,由不同Looper分發的Message運行在不同的執行緒中,Looper的內部維護一個MessageQueue,當初始化Looper的時候會順帶初始化MessageQueue,

Looper使用ThreadLocal來保證每個執行緒都有且只有一個相同的副本,

關鍵方法

  • prepare : 初始化Looper

    Looper.class
        
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
    public static void prepare() {
        prepare(true);
    }
    
    // 最終呼叫到了這個方法
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    

    每個執行緒使用Handler之前,都必須呼叫Looper.prepare()方法來初始化當前執行緒的Looper,引數quitAllowed表示該Looper是否可以退出,主執行緒的Looper是不能退出的,不然程式就直接終止了,我們在主執行緒使用Handler的時候是不用初始化Looper的,為什么?因為Activiy在啟動的時候就已經幫我們初始化主執行緒Looper了,這點在后面再詳細展開,所以在主執行緒我們可以直接呼叫Looper.myLooper()獲取當前執行緒的Looper,

    prepare方法重點在sThreadLocal.set(new Looper(quitAllowed));,可以看出來這里使用了ThreadLocal來創建當前執行緒的Looper物件副本,如果當前執行緒已經有Looper了,則會拋出例外,sThreadLocal是Looper類的靜態變數,前面我們介紹過了ThreadLocal了,這里每個執行緒呼叫一次prepare方法就可以初始化當前執行緒的Looper了,

    接下來再看到Looper的構造方法:

    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    

    邏輯很簡單,初始化了一個MessageQueue,再把當前的執行緒的Thread物件賦值給mThread,

  • myLooper() : 獲取當前執行緒的Looper物件

    獲取當前執行緒的Looper物件,這個方法就是直接呼叫ThreadLocal的get方法:

    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
    
  • loop() : 回圈獲取訊息

    當Looper初始化完成之后,他是不會自己啟動的,需要我們自己去啟動Looper,呼叫Looper的loop()方法即可:

    public static void loop() {
        // 獲取當前執行緒的Looper
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        ...
        for (;;) {
            // 獲取訊息佇列中的訊息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // 回傳null說明MessageQueue退出了
                return;
            }
            ...
            try {
                // 呼叫Message對應的Handler處理訊息
                msg.target.dispatchMessage(msg);
                if (observer != null) {
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            }
            ...
    		// 回收Message
            msg.recycleUnchecked();
        }
    }
    

    loop()方法就是Looper這個“引擎”的核心所在,首先獲取當前執行緒的Looper物件,沒有則拋出例外,然后進入一個死回圈:不斷呼叫MessageQueue的next方法來獲取訊息,然后呼叫message的目標handler的dispatchMessage方法來處理Message,

    前面我們了解過了MessageQueue,next方法是可能會進行阻塞的:當MessageQueue為慷訓者目前沒有任何訊息需要處理,所以Looper就會一直等待,阻塞在里,執行緒也就不會結束,當我們退出Looper的時候,next方法會回傳null,那么Looper也就會跟著結束了,

    同時,因為Looper是運行在不同執行緒的邏輯,其呼叫的dispatchMessage方法也是運行在不同的執行緒,這就達到了切換執行緒的目的,

  • quit/quitSafely : 退出Looper

    quit是直接將Looper退出,quitSafely是將MessageQueue中的不需要等待的訊息處理完成之后再退出,看一下代碼:

    public void quit() {
        mQueue.quit(false);
    }
    // 最終都是呼叫到了這個方法
    void quit(boolean safe) {
        // 如果不能退出則拋出例外,這個值在初始化Looper的時候被賦值
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
    
        synchronized (this) {
            // 退出一次之后就無法再次運行了
            if (mQuitting) {
                return;
            }
            mQuitting = true;
    		// 執行不同的方法
            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }
            // 喚醒MessageQueue
            nativeWake(mPtr);
        }
    }
    

    我們可以發現最后都呼叫了quitSafely方法,這個方法先判斷是否能退出,然后再執行退出邏輯,如果mQuitting==true,那么這里會直接方法,我們會發現mQuitting這個變數只有在這里被執行了賦值,所以一旦looper退出,則無法再次運行了,之后執行不同的退出邏輯,我們分別看一下:

    private void removeAllMessagesLocked() {
        Message p = mMessages;
        while (p != null) {
            Message n = p.next;
            p.recycleUnchecked();
            p = n;
        }
        mMessages = null;
    }
    

    這個方法很簡單,直接把當前所有的Message全部移除,再看一下另一個方法:

    private void removeAllFutureMessagesLocked() {
        final long now = SystemClock.uptimeMillis();
        Message p = mMessages;
        if (p != null) {
            // 如果都在等待,則全部移除,直接退出
            if (p.when > now) {
                removeAllMessagesLocked();
            } else {
                Message n;
                // 把需要等待的Message全部移除
                for (;;) {
                    n = p.next;
                    if (n == null) {
                        return;
                    }
                    if (n.when > now) {
                        break;
                    }
                    p = n;
                }
                p.next = null;
                do {
                    p = n;
                    n = p.next;
                    p.recycleUnchecked();
                } while (n != null);
            }
        }
    }
    

    這個方法邏輯也不復雜,就是把需要等待的Message全部移除,當前需要執行的Message則保留,最終在MessageQueue的next方法中,會進行判斷后回傳null,表示退出,Looper收到這個回傳值之后也跟著退出了,

Looper總結

Looper作為Handler消息機制的“動力引擎”,不斷從MessageQueue中獲取訊息,然后交給Handler去處理,Looper的使用前需要先初始化當前執行緒的Looper物件,再呼叫loop方法來啟動它,

同時Handler也是實作切換的核心,因為不同的Looper運行在不同的執行緒,他所呼叫的dispatchMessage方法則運行在不同的執行緒,所以Message的處理就被切換到Looper所在的執行緒了,當looper不再使用時,可呼叫不同的退出方法來退出他,注意Looper一旦退出,執行緒則會直接結束,

五、Handler

概述

我們整個訊息機制稱為Handler機制就可以知道Handler我們的使用頻率之高,一般情況下我們的使用也是圍繞著Handler來展開,Handler是作為整個訊息機制的訊息發起者與處理者,訊息在不同的執行緒通過Handler發送到目標執行緒的MessageQueue中,然后目標執行緒的Looper再呼叫Handler的dispatchMessage方法來處理訊息,

創建Handler

一般情況下我們使用Handler有兩種方式: 繼承Handler并重寫handleMessage方法,直接創建Handler物件并傳入callBack,這在前面使用Handler部分講過就不再贅述,

需要注意的一點是:創建Handler必須顯示指明Looper引數,而不能直接使用無參建構式,如:

Handler handler = new Handler(); //1
Handler handler = new Handler(Looper.myLooper())//2

1是錯的,2是對的,避免在Handler創建程序中Looper已經退出的情況,

發送訊息

Handler發送訊息有兩種系列方法 : postxx 和 sendxx,如下:

public final boolean post(@NonNull Runnable r);
public final boolean postDelayed(@NonNull Runnable r, long delayMillis);
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis);
public final boolean postAtFrontOfQueue(@NonNull Runnable r);

public final boolean sendMessage(@NonNull Message msg);
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis);
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis);
public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg)

這里我只列出了比較常用的兩類方法,除了插在佇列頭的兩個方法,其他方法最終都呼叫到了sendMessageAtTime,我們從post方法跟原始碼分析一下:

public final boolean post(@NonNull Runnable r) {
    return  sendMessageDelayed(getPostMessage(r), 0);
}

post方法把runnable物件封裝成一個Message,再呼叫sendMessageDelayed方法,我們看看他是如何封裝的:

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

可以看到邏輯很簡單,把runnable物件直接賦值給callBack屬性,接下來回去繼續看sendMessageDelayed

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

sendMessageDelayed把小于0的延遲時間改成0,然后呼叫sendMessageAtTime,這個方法主要是判斷MessageQueue是否已經初始化了,然后再呼叫enqueueMessage方法進行入隊操作:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    // 這里把target設定成自己
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
	// 異步handler設定標志位true,后面會講到同步屏障
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    // 最后呼叫MessageQueue的方法入隊
    return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到Handler的入隊操作也是很簡單,把Message的target設定成本身,這樣這個Message最后就是由自己來處理,最后呼叫MessageQueue的入隊方法來入隊,這在前面講過就不再贅述,

其他的發送訊息方法都是大同小異,讀者感興趣可以自己去跟蹤一下原始碼,

處理訊息

上面講Looper處理訊息的時候,最后就是呼叫handler的dispatchMessage方法來處理,我們來看一下這個方法:

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}

他的邏輯也不復雜,首先判斷Message是否有callBack,有的話就直接執行callBack的邏輯,這個callBack就是我們呼叫handler的post系列方法傳進去的Runnable物件,否則判斷Handler是否有callBack,有的話執行他的方法,如果回傳true則結束,如果回傳false則直接Handler本身的handleMessage方法,這個程序可以用下面的圖表示一下:

dispatchMessage邏輯

記憶體泄露問題

當我們使用繼承Handler方法來使用Handler的時候,要注意使用靜態內部類,而不要用非靜態內部類,因為非靜態內部類會持有外部類的參考,而從上面的分析我們知道Message在被入隊之后他的target屬性是指向了Handler,如果這個Message是一個延遲的訊息,那么這一條參考鏈的物件就遲遲無法被釋放,造成記憶體泄露,

一般這種泄露現象在于:我們在Activity中發送了一個延遲訊息,然后退出了activity,但是由于無法釋放,這樣activity就無法被回收,造成記憶體泄露,

Handler總結

Handler作為訊息的處理和發送者,是整個訊息機制的起點和終點,也是我們接觸最多的一個類,因為我們稱此訊息機制為Handler機制,Handler最重要的就是發送和處理訊息,只要熟練掌握這兩方面的內容就可以了,同時注意記憶體泄露問題,不要使用非靜態內部類去繼承Handler,

六、HandlerThread

概述

有時候我們需要開辟一個執行緒來執行一些耗時的任務,一般情況下可以通過新建一個Thread,然后再在他的run方法里初始化該執行緒的Looper,這樣就可以用他的Looper來切執行緒處理訊息了,如下(這里是kotlin代碼,和java差不多相信可以看得懂的):

val thread = object : Thread(){
    lateinit var mHandler: Handler
    override fun run() {
        super.run()
        Looper.prepare()
        mHandler = Handler(Looper.myLooper()!!)
        Looper.loop()
    }
}
thread.start()
thread.mHandler.sendMessage(Message.obtain())

但是,運行一下,炸了:

00tQns.png

Handler還未初始化,Looper初始化是需要一定的時間,就導致了這個問題,那簡單,等待一下就可以了,上代碼:

val thread = object : Thread(){
    lateinit var mHandler: Handler
    override fun run() {
        super.run()
        Looper.prepare()
        mHandler = Handler(Looper.myLooper()!!)
        Looper.loop()
    }
}
thread.start()
Thread(){
    Thread.sleep(10000)
    thread.mHandler.sendMessage(Message.obtain())
}.start()

執行一下,誒,沒有報錯了果然可以,但是!!! ,這樣的代碼顯得特別的難堪和臃腫,還要再開啟一個執行緒來延遲處理,那有沒有更好的解決方案?有,HandlerThread,

HandlerThread本身是一個Thread,他繼承自Thread,他的代碼并不復雜,看一下(代碼還是有點多,可以選擇看或者不看,我下面會講重點方法):

public class HandlerThread extends Thread {
    // 依次是:執行緒優先級、執行緒id、執行緒looper、以及內部handler
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    // 兩個構造器,name是執行緒名字,priority是執行緒優先級
    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
    
    // 在Looper開始運行前的方法
    protected void onLooperPrepared() {
    }

    // 初始化Looper
    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            // 通知初始化完成
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
    
    // 獲取當前執行緒的Looper
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        // 如果尚未初始化則會一直阻塞知道初始化完成
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    // 利用Object物件的wait方法
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

    // 獲取handler,該方法被標記為hide,用戶無法獲取
    @NonNull
    public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }

    // 兩種不同型別的退出,前面講過不再贅述
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }
    public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }

    // 獲取執行緒id
    public int getThreadId() {
        return mTid;
    }
}

整個類的代碼不是很多,重點在run()getLooper()方法,首先看到getLooper方法:

public Looper getLooper() {
    if (!isAlive()) {
        return null;
    }
    // 如果尚未初始化則會一直阻塞知道初始化完成
    synchronized (this) {
        while (isAlive() && mLooper == null) {
            try {
                // 利用Object物件的wait方法
                wait();
            } catch (InterruptedException e) {
            }
        }
    }
    return mLooper;
}

和我們前面自己寫的不同,他有一個wait(),這個是Java中Object類提供的一個方法,類似于我們前面講的MessageQueue阻塞,等到Looper初始化完成之后就會喚醒他,就可以順利回傳了,不會造成Looper尚未初始化完成的情況,然后再看到run方法:

// 初始化Looper
@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        // 通知初始化完成
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

常規的Looper初始化,完成之后呼叫了notifyAll()方法進行喚醒,對應了上面的getLooper方法,

HandlerThread的使用

HandlerThread的使用范圍很有限,開個子執行緒不斷接受訊息處理耗時任務,所以他的使用方法也是比較固定:

HandlerThread ht = new HandlerThread("handler");
Handler handler = new Hander(ht.getLooper());
handler.sendMessage(msg);

獲取到他的Looper,外部自定義Handler來使用即可,

七、總結

Handler,MessageQueue,Looper三者共同構成了android訊息機制,各司其職,其中Handler主要負責發送和處理訊息,MessageQueue主要負責訊息的排序以及在沒有需要處理的訊息的時候阻塞代碼,Looper負責從MessageQueue中取出訊息給Handler處理,同時達到切換執行緒的目的,通過原始碼分析,希望讀者可以對這些概念有更加清晰的認知,

作業流程

這一部分主要講整體的流程,前面零零散散講了各個組件的功能以及原始碼,現在就統一來講一下他們的整體流程,先看圖:

0wcEX6.png
  1. Handler設定一系列的api供給開發者可以使用Handler發送各種型別的資訊,最終都呼叫到了enqueueMessage方法來入隊
  2. 呼叫MessageQueue的enqueueMessage方法把訊息插入到MessageQueue的鏈表中,等待被Looper獲取處理
  3. Looper獲取到Message之后,呼叫Message對應的Handler處理Message

這樣整理的流程就清晰了,細節的原始碼分析我就不再贅述了,如果有讀者哪個部分不夠清晰,可以回到上面對應部分再看一遍,

相關問題

主執行緒為什么不用初始化Looper?

答:因為應用在啟動的程序中就已經初始化主執行緒Looper了,

每個java應用程式都是有一個main方法入口,Android是基于Java的程式也不例外,Android程式的入口在ActivityThread的main方法中:

public static void main(String[] args) {
    ...
	// 初始化主執行緒Looper
    Looper.prepareMainLooper();
    ...
    // 新建一個ActivityThread物件
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    // 獲取ActivityThread的Handler,也是他的內部類H
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    ...
    Looper.loop();
	// 如果loop方法結束則拋出例外,程式結束
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

main方法中先初始化主執行緒Looper,新建ActivityThread物件,然后再啟動Looper,這樣主執行緒的Looper在程式啟動的時候就跑起來了,我們不需要再去初始化主執行緒Looper,

為什么主執行緒的Looper是一個死回圈,但是卻不會ANR?

答: 因為當Looper處理完所有訊息的時候會進入阻塞狀態,當有新的Message進來的時候會打破阻塞繼續執行,

這其實沒理解好ANR這個概念,ANR,全名Application Not Responding,當我發送一個繪制UI 的訊息到主執行緒Handler之后,經過一定的時間沒有被執行,則拋出ANR例外,Looper的死回圈,是回圈執行各種事務,包括UI繪制事務,Looper死回圈說明執行緒沒有死亡,如果Looper停止回圈,執行緒則結束退出了,Looper的死回圈本身就是保證UI繪制任務可以被執行的原因之一,同時UI繪制任務有同步屏障,可以更加快速地保證繪制更快執行,同步屏障下面會講,

Handler如何保證MessageQueue并發訪問安全?

答:回圈加鎖,配合阻塞喚醒機制,

我們可以發現MessageQueue其實是“生產者-消費者”模型,Handler不斷地放入訊息,Looper不斷地取出,這就涉及到死鎖問題,如果Looper拿到鎖,但是佇列中沒有訊息,就會一直等待,而Handler需要把訊息放進去,鎖卻被Looper拿著無法入隊,這就造成了死鎖,Handler機制的解決方法是回圈加鎖,在MessageQueue的next方法中:

Message next() {
   ...
    for (;;) {
		...
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            ...
        }
    }
}

我們可以看到他的等待是在鎖外的,當佇列中沒有訊息的時候,他會先釋放鎖,再進行等待,直到被喚醒,這樣就不會造成死鎖問題了,

那在入隊的時候會不會因為佇列已經滿了然后一邊在等待訊息處理一邊拿著鎖呢?這一點不同的是MessageQueue的訊息沒有上限,或者說他的上限就是JVM給程式分配的記憶體,如果超出記憶體會拋出例外,但一般情況下是不會的,

Looper退出后是否可以重新運行?

答: 不可以,

執行緒的存活是靠Looper呼叫的next方法進行阻塞實作的,如果Looper退出后,那么執行緒會馬上結束,也不會再有第二次運行的機會了,即使執行緒還沒結束再一次呼叫loop(),Looper內部有一個mQuitting變數,當他被賦值為false之后就無法再被賦值為true,所以就無法再重新運行了,

Handler是如何切換執行緒的?

答: 使用不同執行緒的Looper處理訊息,

前面我們聊到,代碼的執行執行緒,并不是代碼本身決定,而是執行這段代碼的邏輯是在哪個執行緒,或者說是哪個執行緒的邏輯呼叫的,每個Looper都運行在對應的執行緒,所以不同的Looper呼叫的dispatchMessage方法就運行在其所在的執行緒了,

Handler的阻塞喚醒機制是怎么回事?

答: Handler的阻塞喚醒機制是基于Linux的阻塞喚醒機制,

這個機制也是類似于handler機制的模式,在本地創建一個檔案描述符,然后需要等待的一方則監聽這個檔案描述符,喚醒的一方只需要修改這個檔案,那么等待的一方就會收到檔案從而打破喚醒,和Looper監聽MessageQueue,Handler添加message是比較類似的,具體的Linux層知識讀者可通過這篇文章詳細了解(傳送門)

能不能讓一個Message加急被處理?/ 什么是Handler同步屏障?

答:可以 / 一種使得異步訊息可以被更快處理的機制

如果向主執行緒發送了一個UI更新的操作Message,而此時訊息佇列中的訊息非常多,那么這個Message的處理就會變得緩慢,造成界面卡頓,所以通過同步屏障,可以使得UI繪制的Message更快被執行,

什么是同步屏障?這個“屏障”其實是一個Message,插入在MessageQueue的鏈表頭,且其target==null,Message入隊的時候不是判斷了target不能為null嗎?不不不,添加同步屏障是另一個方法:

public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        // 把當前需要執行的Message全部執行
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 插入同步屏障
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

可以看到同步屏障就是一個特殊的target,哪里特殊呢?target==null,我們可以看到他并沒有給target屬性賦值,那這個target有什么用呢?看next方法:

Message next() {
    ...

    // 阻塞時間
    int nextPollTimeoutMillis = 0;
    for (;;) {
        ...
        // 阻塞對應時間 
        nativePollOnce(ptr, nextPollTimeoutMillis);
		// 對MessageQueue進行加鎖,保證執行緒安全
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            /**
            *  1
            */
            if (msg != null && msg.target == null) {
                // 同步屏障,找到下一個異步訊息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 下一個訊息還沒開始,等待兩者的時間差
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 獲得訊息且現在要執行,標記MessageQueue為非阻塞
                    mBlocked = false;
                    /**
            		*  2
            		*/
                    // 一般只有異步訊息才會從中間拿走訊息,同步訊息都是從鏈表頭獲取
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 沒有訊息,進入阻塞狀態
                nextPollTimeoutMillis = -1;
            }

            // 當呼叫Looper.quitSafely()時候執行完所有的訊息后就會退出
            if (mQuitting) {
                dispose();
                return null;
            }
            ...
        }
        ...
    }
}

這個方法我在前面講過,我們重點看一下關于同步屏障的部分,看注釋1的地方的代碼:

if (msg != null && msg.target == null) {
    // 同步屏障,找到下一個異步訊息
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}

如果遇到同步屏障,那么會回圈遍歷整個鏈表找到標記為異步訊息的Message,即isAsynchronous回傳true,其他的訊息會直接忽視,那么這樣異步訊息,就會提前被執行了,注釋2的代碼注意一下就可以了,

注意,同步屏障不會自動移除,使用完成之后需要手動進行移除,不然會造成同步訊息無法被處理,從原始碼中可以看到如果不移除同步屏障,那么他會一直在那里,這樣同步訊息就永遠無法被執行了,

有了同步屏障,那么喚醒的判斷條件就必須再加一個:MessageQueue中有同步屏障且處于阻塞中,此時插入在所有異步訊息前插入新的異步訊息,這個也很好理解,跟同步訊息是一樣的,如果把所有的同步訊息先忽視,就是插入新的鏈表頭且佇列處于阻塞狀態,這個時候就需要被喚醒了,看一下原始碼:

boolean enqueueMessage(Message msg, long when) {
    ...

    // 對MessageQueue進行加鎖
    synchronized (this) {
        ...
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            /**
            *	1
            */
            // 當執行緒被阻塞,且目前有同步屏障,且入隊的訊息是異步訊息
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                /**
                *	2
                */
                // 如果找到一個異步訊息,說明前面有延遲的異步訊息需要被處理,不需要被喚醒
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; 
            prev.next = msg;
        }
		
        // 如果需要則喚醒佇列
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

同樣,這個方法我之前講過,把無關同步屏障的代碼忽視,看到注釋1處的代碼,如果插入的訊息是異步訊息,且有同步屏障,同時MessageQueue正處于阻塞狀態,那么就需要喚醒,而如果這個異步訊息的插入位置不是所有異步訊息之前,那么不需要喚醒,如注釋2,

那我們如何發送一個異步型別的訊息呢?有兩種辦法:

  • 使用異步型別的Handler發送的全部Message都是異步的
  • 給Message標志異步

Handler有一系列帶Boolean型別的引數的構造器,這個引數就是決定是否是異步Handler:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    // 這里賦值
    mAsynchronous = async;
}

在發送訊息的時候就會給Message賦值:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
	// 賦值
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

但是異步型別的Handler構造器是標記為hide,我們無法使用,所以我們使用異步訊息只有通過給Message設定異步標志:

public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

但是!!!!,其實同步屏障對于我們的日常使用的話其實是沒有多大用處,因為設定同步屏障和創建異步Handler的方法都是標志為hide,說明谷歌不想要我們去使用他,所以這里同步屏障也作為一個了解,可以更加全面地理解原始碼中的內容,

什么是IdleHandler?

答: 當MessageQueue為慷訓者目前沒有需要執行的Message時會回呼的介面物件,

IdleHandler看起來好像是個Handler,但他其實只是一個有單方法的介面,也稱為函式型介面:

public static interface IdleHandler {
    boolean queueIdle();
}

在MessageQueue中有一個List存盤了IdleHandler物件,當MessageQueue沒有需要被執行的MessageQueue時就會遍歷回呼所有的IdleHandler,所以IdleHandler主要用于在訊息佇列空閑的時候處理一些輕量級的作業,

IdleHandler的呼叫是在next方法中:

Message next() {
    // 如果looper已經退出了,這里就回傳null
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    // IdleHandler的數量
    int pendingIdleHandlerCount = -1; 
    // 阻塞時間
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // 阻塞對應時間 
        nativePollOnce(ptr, nextPollTimeoutMillis);
		// 對MessageQueue進行加鎖,保證執行緒安全
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // 同步屏障,找到下一個異步訊息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 下一個訊息還沒開始,等待兩者的時間差
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 獲得訊息且現在要執行,標記MessageQueue為非阻塞
                    mBlocked = false;
                    // 一般只有異步訊息才會從中間拿走訊息,同步訊息都是從鏈表頭獲取
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 沒有訊息,進入阻塞狀態
                nextPollTimeoutMillis = -1;
            }

            // 當呼叫Looper.quitSafely()時候執行完所有的訊息后就會退出
            if (mQuitting) {
                dispose();
                return null;
            }

            // 當佇列中的訊息用完了或者都在等待時間延遲執行同時給pendingIdleHandlerCount<0
           	// 給pendingIdleHandlerCount賦值MessageQueue中IdleHandler的數量
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            // 沒有需要執行的IdleHanlder直接continue
            if (pendingIdleHandlerCount <= 0) {
                // 執行IdleHandler,標記MessageQueue進入阻塞狀態
                mBlocked = true;
                continue;
            }

            // 把List轉化成陣列型別
            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // 執行IdleHandler
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // 釋放IdleHandler的參考
            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }
            // 如果回傳false,則把IdleHanlder移除
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // 最后設定pendingIdleHandlerCount為0,防止再執行一次
        pendingIdleHandlerCount = 0;

        // 當在執行IdleHandler的時候,可能有新的訊息已經進來了
        // 所以這個時候不能阻塞,要回去回圈一次看一下
        nextPollTimeoutMillis = 0;
    }
}

代碼很多,可能看著有點亂,我梳理一下邏輯,然后再回去看原始碼就會很清晰了:

  1. 當呼叫next方法的時候,會給pendingIdleHandlerCount賦值為-1
  2. 如果佇列中沒有需要處理的訊息的時候,就會判斷pendingIdleHandlerCount是否為<0,如果是則把存盤IdleHandler的list的長度賦值給pendingIdleHandlerCount
  3. 把list中的所有IdleHandler放到陣列中,這一步是為了不讓在執行IdleHandler的時候List被插入新的IdleHandler,造成邏輯混亂
  4. 然后遍歷整個陣列執行所有的IdleHandler
  5. 最后給pendingIdleHandlerCount賦值為0,然后再回去看一下這個期間有沒有新的訊息插入,因為pendingIdleHandlerCount的值為0不是-1,所以IdleHandler只會在空閑的時候執行一次
  6. 同時注意,如果IdleHandler回傳了false,那么執行一次之后就被丟棄了,

建議讀者再回去把原始碼看一遍,這樣邏輯會清晰很多,

Handler訊息機制的再認識

到這里關于Handler機制該講的已經講得差不多了,但不知讀者和我一樣是否有同樣的疑惑:

Handler機制為什么叫做Android中的訊息機制?Handler真的就只是用來切換執行緒更新UI 的嗎?怎么樣從原始碼設計的角度來更好地理解Handler訊息機制?

每次學習關于Android中的機制問題時,我都喜歡從研究他在android原始碼設計中體現的作用,或者說思想,這有助于讓我的理解提高一個層次,這里就簡單談談我對Handler機制的理解,

Handler機制,之所以叫handler,我覺得只是因為我們接觸的都是Handler,所以叫做Handler機制,如果我們接觸Looper比較多可能他的名字就是Looper機制了,更準確來說,他應該是Android訊息機制

我們知道,每個java程式都有一個入口:main方法,然后我們從這里開始進入我們的應用程式,相信每個讀者都有使用c語言寫學生管理系統的經歷,我們是如何讓程式暫停下來不要直接結束的?通過回圈+輸入等待,我們會在最外層寫一個死回圈,然后不斷地監聽輸入,再根據輸入執行命令,當用戶無輸入的時候,就會一直等待,這其實和Handler機制是類似的,Handler機制使用的是多執行緒的思路,主執行緒不斷等待訊息,然后從別的執行緒發送訊息讓主執行緒執行邏輯,這也稱為事務驅動型設計,主執行緒的邏輯都是通過message來驅動的,

我們直接來看一下Android應用程式的main方法:

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    AndroidOs.install();
    CloseGuard.setEnabled(false);
    Environment.initForCurrentUser();
    final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
    TrustedCertificateStore.setDefaultUserDirectory(configDir);
    Process.setArgV0("<pre-initialized>");
    // 初始化Looper
    Looper.prepareMainLooper();
    long startSeq = 0;
    if (args != null) {
        for (int i = args.length - 1; i >= 0; --i) {
            if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                startSeq = Long.parseLong(
                        args[i].substring(PROC_START_SEQ_IDENT.length()));
            }
        }
    }
    // 創建ActivityThread
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    // 啟動Looper
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

但是我們可以看到他的代碼其實并不多,啟動了ActivityThread和Looper之后就沒有再執行其他邏輯了,那我們的Activity是如何被呼叫并執行邏輯的?通過Handler,Android是事務驅動型的設計,通過不斷地分發事務來讓整個程式運行起來,熟悉Activity啟動流程的讀者應該可以聯想到,AMS通過binder機制和程式聯系,然后binder執行緒再發送一個訊息給到主執行緒,主執行緒再執行相對應的邏輯,他們的關系可以用下面的圖來表示:

00IgT1.png

當應用行程被創建的時候,只是創建了主執行緒的Looper和handler,以及其他的binder執行緒等,之后AMS通過Binder與應用程式通信,給主執行緒發送message,讓程式執行創建Activity等的操作,這樣的設計我們不用去寫死回圈和等待用戶輸入等邏輯,應用程式就能跑起來且不會結束,關于Activity的啟動相關我這里就不展開講了,讀者可以去看筆者的另一篇文章(Activity啟動流程詳解),之后程式會開啟其他的執行緒來接收用戶的觸摸輸入等,然后把這些包裝成一個message發送到主執行緒去更新UI,

可以說,“無訊息,無安卓”,整個安卓的程式運行都是基于這套訊息機制來跑的,他不僅僅只是切換執行緒這么簡單,他涉及到整個android程式的根基,

總結

這篇文章從一開始的入門講解,到深入講解各個類的原始碼和作用,最后再升華一下整個訊息機制的設計思想,相信讀者關于Handler訊息機制的認識已經非常深刻了,

訊息機制我們日常使用得并不多,雖然他非常重要,但我們的使用也是主要用戶切換執行緒更新UI這一塊,而我們有很多成熟且非常方便的框架可以使用:RxJava、kotlin協程等等,但由于Handler機制對于android程式實在是非常重要,對于深入學習android還是非常有必要去學習、去理解,

希望文章對你有幫助,

全文到此,原創不易,覺得有幫助可以點贊收藏評論轉發,
筆者能力有限,有任何想法歡迎評論區交流指正,
如需轉載請私信交流,

另外歡迎光臨筆者的個人博客:傳送門

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/233509.html

標籤:其他

上一篇:Android的ScrollView簡單使用實體(附Demo)

下一篇:XML布局嵌套多少層會導致OOM

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more