Handler全解析

Handler相關

1. 用一句話概括Handler,并簡述其原理

Handler是Android系統的根本,在Android應用被啟動的時候,會分配一個單獨的虛擬機,虛擬機會執行ActivityThread中的main方法,在main方法中對主線程Looper進行了初始化,也就是幾乎所有代碼都執行在Handler內部。Handler也可以作為主線程和子線程通訊的橋梁。

Handler通過sendMessage發送消息,將消息放入MessageQueue中,在MessageQueue中通過時間的維度來進行排序,Looper通過調用loop方法不斷的從MessageQueue中獲取消息,執行Handler的dispatchMessage,最后調用handleMessage方法。


2. 為什么系統不建議在子線程訪問UI?【為什么不能在子線程更新UI?】

在某些情況下,在子線程中是可以更新UI的。但是在ViewRootImpl中對UI操作進行了checkThread,但是我們在OnCreateonResume中可以使用子線程更新UI,由于我們在ActivityThread中的performResumeActivity方法中通過addView創建了ViewRootImpl,這個行為是在onResume之后調用的,所以在OnCreate和onResume可以進行更新UI。

但是我們不能在子線程中更新UI,因為客戶端很難控制UI的更新在ViewRootImpl被創建之前執行,也就是checkThread之前,如果添加了耗時操作之后,一旦ViewRootImpl被創建將會拋出異常。一旦在子線程中更新UI,容易產生并發問題。


3. 一個Thread可以有幾個Looper?幾個Handler?

一個線程只能有一個Looper,可以有多個Handler

一個線程中只有一個Looper。Looper的構造方法被聲明為了private,我們無法通過 new關鍵字來實例化 Looper,
唯一開放的可以實例化Looper的地方是prepare,prepare方法源碼

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

我們知道ThreadLocal是一個線程內部的數據存儲類,當某個線程執行prepare方法的時候,會首先通過ThreadLocal檢查這個線程是否已經創建了Looper

  • 如果還沒創建,他會創建一個Looper并且將Looper保存在ThreadLocal中,每個線程都有一個LocalThreadMap【注意LocalThreadMap是在線程中,而不是在ThreadLocal中】,ThreadLocal會利用LocalThreadMap,將Looper保存在對應線程中的LocalThreadMap,keyThreadLocalvalueLooper

  • 而如果 ThreadLocal中已經保存了Looper,則會拋出一個RuntimeException異常。
    那么意味著在一個線程中最多只能調用一次prepare方法,這樣就保證了Looper的唯一性


4. 可以在子線程直接new一個Handler嗎?那該怎么做?

可以在子線程中創建Handler
我們需要調用Looper.perpare和Looper.loop方法,或者通過獲取主線程的looper來創建Handler

//1.直接獲取當前子線程的looper
new Thread(new Runnable() {
   @Override
   public void run() {
       Looper.prepare();
       Handler handler = new Handler(){
           @Override
           public void handleMessage(Message msg) {
               Log.e("handleMessage","handleMessage1");
           }
       };
       handler.sendEmptyMessage(1);
       Looper.loop();
   };
}).start();

//2.通過主線程的looper來實現的
new Thread(new Runnable() {
   @Override
   public void run() {
       Handler handler = new Handler(Looper.getMainLooper()) { //重點在此處
           @Override
           public void handleMessage(Message msg) {
               Log.e("handleMessage","handleMessage2");
           }
      };
      handler.sendEmptyMessage(1);
   }
}).start();


5. Message可以如何創建?哪種效果更好,為什么?

可以new Message,但是更推薦Message.obtain來創建Message。這樣會復用之前的Message的內存,不會頻繁的創建對象,導致內存抖動。


6. 主線程中Looper的輪詢死循環為何沒有阻塞主線程?【重點

本身在沒有任何事件的時候 就是阻塞的,一旦有事件就不阻塞了

阻塞在ActivityThread main方法
app阻塞兩種方式

while(true){
   
}
  1. 隊列的方式阻塞

Looper的阻塞是第二種,是釋放CPU的

Android是以事件驅動型的操作系統

線程即是一段可執行的代碼,當可執行代碼執行完成后,線程生命周期便該終止了,線程退出。而對于主線程,我們是絕不希望會被運行一段時間,自己就退出,那么如何保證能一直存活呢?簡單做法就是可執行代碼是能一直執行下去的,死循環便能保證不會被退出。

例如,binder線程也是采用死循環的方法,通過循環方式不同與Binder驅動進行讀寫操作,當然并非簡單地死循環,無消息時會休眠。但這里可能又引發了另一個問題,既然是死循環又如何去處理其他事務呢?

答:通過創建新線程的方式。舉例ActivityThread#main()

public static void man(String[] args){
    ...
    Looper.prepareMainLooper();
    ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    ...
    Looper.loop();
    ...
}

thread.attach(false, startSeq);便會創建一個Binder線程(具體是指ApplicationThread,Binder的服務端,用于接收系統服務AMS發送來的事件),該Binder線程通過Handler將Message發送給主線程。

真正會卡死主線程的操作是在回調方法onCreate/onStart/onResume等操作時間過長,會導致掉幀,甚至發生ANR,looper.loop本身不會導致應用卡死

主線程的死循環一直運行是不是特別消耗CPU資源呢? 其實不然,這里就涉及到Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loopqueue.next()中的nativePollOnce()方法里,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這里采用的epoll機制,是一種IO多路復用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質是同步I/O,即讀寫是阻塞的

所以說,主線程大多數時候都是處于休眠狀態,并不會消耗大量CPU資源。

簡單回答:
Looper輪詢是死循環,但是當沒有消息的時候他會block,ANR是當我們處理點擊事件的時候5s內沒有響應,我們在處理點擊事件的時候也是用的Handler,所以一定會有消息執行,并且ANR也會發送Handler消息,所以不會阻塞主線程。


7. 使用Handler的postDelayed()后消息隊列會發生什么變化?

Handler發送消息到消息隊列,消息隊列是一個時間優先級隊列,內部是一個單向鏈表。發動postDelayed之后會將該消息進行時間排序存放到消息隊列中


8. 點擊頁面上的按鈕后更新TextView的內容,談談你的理解?【同步屏障】

點擊按鈕的時候會發送消息到Handler,但是為了保證優先執行,會加一個標記異步,同時會發送一個target為null的消息,這樣在使用消息隊列的next獲取消息的時候,如果發現消息的target為null,那么會遍歷消息隊列將有異步標記的消息獲取出來優先執行,執行完之后會將target為null的消息移除。(同步屏障)


9. 描述下Handler中的生產者-消費者設計模式?

舉個例子,面包店廚師不斷在制作面包,客人來了之后就購買面包,這就是一個典型的生產者消費者設計模式。但是需要注意的是如果消費者消費能力大于生產者,或者生產者生產能力大于消費者,需要一個限制。

在java里有一個blockingQueue。當目前容器內沒有東西的時候,消費者來消費的時候會被阻塞,當容器滿了的時候也會被阻塞。Handler.sendMessage相當于一個生產者,MessageQueue相當于容器,Looper相當于消費者。


10. Handler是如何完成子線程和主線程通信的?【如何進行線程切換】

在主線程中創建Handler,在子線程中發送消息,放入到MessageQueue中,通過Looper.loop取出消息進行執行handleMessage,由于looper我們是在主線程初始化的,在初始化looper的時候會創建消息隊列,所以消息是在主線程被執行的。

  • 假設現在有一個線程A,在A線程通過Looper.prepare 和 Looper.loop來開啟Looper,并且在A線程中實例化出來一個Handler。
    Looper.prepare()方法被調用時會初始化Looper 并為ThreadLocal設置Looper,此時ThreadLocal中就存儲了A線程的Looper。
    另外MessageQueue也會在Looper中被初始化

  • 接著當調用Looper.loop方法時,loop方法會通過myLooper得到A線程中的Looper,進而拿到Looper中的MessageQueue,接著開啟死循環等待執行MessageQueue中的方法。

  • 此時再開啟一個線程B,并在B線程中通過Handler【A線程中實例化的那個】發送出一個Message,這個Message最終會通過sendMessageAtTime方法,調用到MessageQueue【A線程中實例化的Handler所持有的】的 enqueueMessage方法,將消息插入到隊列【插入之前,還會將Message實例的target賦值為當前Handler】

  • 由于Looper的loop是一個死循環,當MessageQueue中被插入消息的時候,loop方法就會通過queue.next()取出MessageQueue中的消息,并執行msg.target.dispatchMessage(msg)。進而調用Handler的handleCallbackhandleMessage,而這些方法都是執行在A線程中的,因此達到了線程的切換。


11. 關于ThreadLocal,談談你的理解?

ThreadLocal類似于每個線程有一個單獨的內存空間,不共享,ThreadLocal在set的時候會將數據存入對應線程的ThreadLocalMap中,key=ThreadLocal,value=值
詳見: http://www.lxweimin.com/p/f1e71028f4e6

注:ThreadLocal的擴容
結論:在清理過期Entery后如果長度 >= 原長度的2/3時會進行rehash,再次清理過期Entery后如果長度 >= 原長度的1/2時會進行擴容,擴容為原長度的2倍。
詳見:https://blog.csdn.net/u010002184/article/details/82227795


12. 享元設計模式有用到嗎?

享元設計模式就是重復利用內存空間,減少對象的創建。
Message中使用到了享元設計模式。內部維護了一個鏈表,并且最大長度是50,當消息處理完之后會將消息內的屬性設置為空,并且插入到鏈表的頭部,使用obtain創建的Message會從頭部獲取空的Message


13. Handler內存泄漏問題及解決方案

非靜態內部類和匿名內部類都會隱式持有外部類的引用,這會導致內存泄漏。例如Activity退出的時候,MessageQueue中還有一個Message沒有執行,這個Message持有了Handler的引用,而Handler持有了Activity的引用,導致Activity無法被回收,導致內存泄漏。
解決方案:使用static關鍵字修飾,在onDestory的時候將消息清除。

//注:靜態內部類的弱引用包裹Activity,是為了方便使用 activity. 獲取變量方法等
//如果直接使用強引用,顯然會導致activity泄露
private WeakReference<Activity> mWeakReference;


14. 子線程中維護的Looper,消息隊列無消息的時候處理方案是什么?有什么用?

在Looper.loop()方法中,for死循環里,當message為空時,會退出整個循環。所以 子線程無消息的時候要調用Looper.quit()退出

  • 如果在主線程下,不會調用quit方法,但是會無限等待,等到下次Message進來,也就是messageQueue.enqueue方法,會通過底層方法 nativeWake(mPtr) 喚醒
  • 如果在子線程下,會調用quit方法,返回message=null,最后會停止loop 的for循環


15. 既然可以存在多個Handler往MessageQueue中添加數據(發消息是各個handler可能處于不同線程),那他內部是如何確保線程安全的?

在添加數據MessageQueue#enqueueMessage和執行MessageQueue#next的時候都加了this鎖,這樣可以保證添加的位置是正確的,獲取的也會是最前面的。


16. Handler異步消息處理【HandlerThread】

HandlerThread是Android API提供的一個方便、便捷的類,使用它我們可以快速的創建一個帶有Looper的線程。Looper可以用來創建Handler實例。注意:start()仍然必須被調用。

  • HandlerThread本質上是一個線程類,它繼承了Thread;

  • HandlerThread有自己的內部Looper對象,可以進行looper循環;

  • 通過獲取HandlerThread的looper對象傳遞給Handler對象,可以在handleMessage方法中執行異步任務。

  • 創建HandlerThread后必須先調用HandlerThread.start()方法,Thread會先調用run方法,創建Looper對象。

  • 內部使用了Handler + Thread,并且處理了getLooper的并發問題。如果獲取Looper的時候發現Looper還沒創建,則wait,等待looper創建了之后在notify

基礎使用步驟

  1. 創建HandlerThread線程

  2. 運行線程

  3. 獲取HandlerThread線程中的Looper實例

  4. 通過這個Looper實例創建Handler實例,從而使mSubThreadHandler與該線程連接到一起。

mSubThreadHandler = new Handler(loop){
   //實現handlerMessage的處理消息方法
   public void handleMessage(Message msg) {
   }
  1. 使用mSubThreadHandler發送消息,以及在onDestroy中調用mHandlerThread.quit();退出HandlerThread的looper循環


17. 關于IntentService,談談你的理解

HandlerThread + Service實現,可以實現Service在子線程中執行耗時操作,并且執行完耗時操作時候會將自己stop。

18. Glide是如何維護生命周期的?

@NonNull
private RequestManagerFragment getRequestManagerFragment(
       @NonNull final android.app.FragmentManager fm,
       @Nullable android.app.Fragment parentHint,
       boolean isParentVisible) {
   RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
   if (current == null) {
       current = pendingRequestManagerFragments.get(fm);
       if (current == null) {
           current = new RequestManagerFragment();
           current.setParentFragmentHint(parentHint);
           if (isParentVisible) {
               current.getGlideLifecycle().onStart();
           }
           pendingRequestManagerFragments.put(fm, current);
           fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
           handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
       }
   }
   return current;
}
  1. 為什么會判斷兩次null,再多次調用with的時候,commitAllowingStateLoss會被執行兩次,所以我們需要使用一個map集合來判斷,如果map中已經有了證明已經添加過了

  2. handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();我們需要將map里面的記錄刪除。


19. handler中的msg是怎么發送的,發送到哪去了

無論是sendMessage還是sendEmptyMessageDelayed等等方法,都是通過sendMessageAtTime進行發送

然后執行enqueueMessage方法 -- 調用->queue.enqueueMessage方法

handler中的message發送到了MessageQueue中:并且使用單鏈表數據結構進行存儲

單鏈表:

  • 非順序存儲,順序訪問;
  • 不需要內存連續;
  • 非線性,非順序的物理結構。
  • 由n個節點組成,靠next指針關聯起來;

單鏈表這么好,為什么還需要ArrayList
message屬于高頻使用,并且生命周期非常短;
message還使用了對象池;

線程同理:屬于高頻使用,并且生命周期非常短。
頻繁的生成與銷毀會耗費大量系統資源


20. msg是如何被消費的

Looper通過loop方法,不停的從MessageQueue中獲取Message對象,
并調用Message.target.dispatchMessage進行回調


21. handler,Looper,MessageQueue,Message是什么關系

ActivityThreadmain方法中,Looper.prepareMainLooper會創建Looper,并且在Looper的構造方法中會創建MessageQueue的對象【mQueue】

在主線程創建之后會創建一個Looper對象,創建Looper對象的時候會去創建一個MessageQueue,而Looper是一個輪詢器,會不停的輪詢MessageQueue中的消息,在獲取到消息之后就會把這個消息交給相應的handler來進行處理,在主線程中創建一個handler對象,這個handler對象把一個Message放到消息隊列中,然后獲取到消息進行處理。

Looper:是一個消息分發器,在主線程創建的時候就會創建一個Looper對象;
MessageQueue:消息隊列,是由Message組成的一個隊列;
Handler:獲取到Message,然后執行動作,可以在主線程和子線程中互相傳遞數據;


22. linux的 epoll模型

Handler中所使用到的Linux pipe/epoll機制,簡單說就是在主線程的MessageQueue沒有消息時,便阻塞在loopqueue.next()中的nativePollOnce()方法里,此時主線程會釋放CPU資源進入休眠狀態,直到下個消息到達或者有事務發生,通過往pipe管道寫端寫入數據來喚醒主線程工作。這里采用的epoll機制,是一種IO多路復用機制,可以同時監控多個描述符,當某個描述符就緒(讀或寫就緒),則立刻通知相應程序進行讀或寫操作,本質是同步I/O,即讀寫是阻塞的

epoll可以理解為event poll,在本地創建一個文件描述符,然后需要等待的一方則監聽這個文件描述符,喚醒的一方只需修改這個文件,那么等待的一方就會收到文件從而打破喚醒。和Looper監聽MessageQueue,Handler添加message是比較類似的。不同于忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))

epoll支持四類事件,分別是

  • EPOLLIN(句柄可讀)
  • EPOLLOUT(句柄可寫)
  • EPOLLERR(句柄錯誤)
  • EPOLLHUP(句柄斷)

當沒有消息的時候會epoll.wait,等待句柄寫的時候再喚醒,這個時候其實是阻塞的。
所有的ui操作都通過handler來發消息操作
比如屏幕刷新16ms一個消息,你的各種點擊事件,所以就會有句柄寫操作,喚醒上文的wait操作,所以不會被卡死了。

這里涉及線程,先說說說進程/線程:
進程:每個app運行時前首先創建一個進程,該進程是由Zygote fork出來的,用于承載App上運行的各種Activity/Service等組件。進程對于上層應用來說是完全透明的,這也是google有意為之,讓App程序都是運行在Android Runtime。大多數情況一個App就運行在一個進程中,除非在AndroidManifest.xml中配置Android:process屬性,或通過native代碼fork進程。

線程:線程對應用來說非常常見,比如每次new Thread().start都會創建一個新的線程。該線程與App所在進程之間資源共享,從Linux角度來說進程與線程除了是否共享資源外,并沒有本質的區別,都是一個task_struct結構體,在CPU看來進程或線程無非就是一段可執行的代碼,CPU采用CFS調度算法,保證每個task都盡可能公平的享有CPU時間片。


23. 能不能讓一個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,即isAsynchornous返回true,其他的消息會直接忽視,
那么這樣的異步消息,就會提前被執行了。
注釋2的代碼注意一下就可以了。

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

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

boolean enqueueMessage(Message msg, long when){
    ...
    //對MessageQueue進行加鎖
    synchronized(this){
        ...
        if(p == null || when == 0 || when < p.next){
            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;
}

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

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

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


24. 消息隊列MessageQueue(未退出、未被廢棄)在什么情況下,next()會選擇阻塞線程

  1. 隊列中沒有任何消息 – 永久阻塞:這個時候不能返回null,因為next()的目的是取出一個消息,隊列中現在沒有消息并不代表一段時間后也沒有消息。消息隊列還在可用中,隨時都有可能有Handler發布新的消息給它。那么問題來了,為了節省資源準備阻塞線程,但是多少時間后喚醒它呢?臆斷一個時長并不是很好的解決方案。我們知道消息隊列是用來管理消息的,既然確定不了阻塞時長那么不如先永久阻塞,等新消息入隊后主動喚醒線程。

  2. 隊首的消息執行時間未到 – 定時喚醒:每個消息的when字段都給出了希望系統處理該消息的時刻。如果在next()方法取消息時,發現消息隊列的隊首消息處理時間未到,next()同樣需要阻塞。因為消息隊列是按照消息的when字段從小到大排列的,如果隊首消息的處理時間都沒到那么整個隊列中都沒有能夠立即取出的消息。這個時候因為知道下一次處理的具體時間,結合當前時間就可以確定阻塞時長。

  3. 隊首消息是同步障礙器(SyncBarrier),并且隊列中不含有異步消息 – 永久阻塞:因為對消息隊列施加了同步障礙,所有晚于隊首同步障礙器處理時間的同步消息都變得不可用,next()在選取返回消息時會完全忽略這些消息。這和第一種情況相似,所以采取的阻塞方案也是永久阻塞。

  4. 隊首消息是同步障礙器(SyncBarrier),隊列中含有異步消息但執行時間未到 – 定時喚醒:因為對消息隊列施加了同步障礙,所有晚于隊首同步障礙器處理時間的同步消息都變得不可用,next()在選取返回消息時會完全忽略這些消息。也就是說對于next(),它只會考慮隊列中的異步消息。這和第二種情況相似,所以采取的阻塞方案是設置阻塞時長再阻塞。

Message next(){
   final long ptr = mPtr;
   ...【代碼略,如果消息循環正在退出或者已經廢棄,直接返回null】

   int nextPollTimeoutMillis = 0; //阻塞時長
   for(;;){
       ...【不相關內容】
       //nextPollTimeoutMillis為0 立即返回,為-1則無限等待(必須主動喚醒),涉及本地方法暫不深究
       nativePollOnce(ptr, nextPollTimeoutMillis);
       synchronized(this){
           //now 等于自系統啟動以來到此時此刻的非深度睡眠時長
           final long now = SystemClock.uptimeMillis();
           Message prevMsg = null;
           Message msg = mMessages; //隊首消息

           //如果當前隊首的消息是設置的同步障礙器(target == null)
           if(msg != null && msg.target == null){
               //因為同步障礙器的原因進入該分支,找到下一個異步消息之后才會結束while
               do{
                   prevMsg = msg;
                   msg = msg.next;
               } while(msg != null && !msg.isAsychronous());
           }

           //此時msg一定是普通消息或者null,一定不是同步障礙器
           if(msg != null){
               if(now < msg.when){
                   //隊首第一個非障礙器的msg執行時間未到,計算阻塞時長
                   nextPollTimeoutMillis = (int)Math.min(msg.when - now, Integer.MAX_VALUE);
               } else { //一切正常,開始取消息
                   mBlocked = false; //不阻塞線程
                   if(prevMsg != null){ //如果跳過了隊首的同步障礙器取異步消息
                       prevMsg.next = msg.next;
                   } else { //如果當前消息就是隊首消息
                       mMessages = msg.next;
                   }
                   msg.next = null;
                   if(false) Log.v("MessageQueue", "Returning message: " + msg); 
                   return msg;
               }
           } else { //消息隊列為空,或者隊首是SyncBarrier且隊列中無異步消息
               nextPollTimeoutMillis = -1; // -1表示無限等待
           }

           //所有待處理的消息均處理完成,接下來處理閑時任務
           ......【因為當前代碼在無限for循環中,此時再次驗證一下消息隊列是否正在退出。如果是,則廢棄消息隊列并返回null】

           .....【初始化閑時任務】
           //閑時任務列表為空,或者不是第一次執行到這里
           if(pendingIdleHandlerCount <= 0){
               mBlock = true;
               continue;
           }
           ......【初始化閑時任務】
       } //synchronized結束

       ......【如果是本次調用next()過程中第一次到達這里,則執行閑時任務,如果不是第一次,則不會執行到這里】
       //將待處理的IdleHandler個數設置為0,使得本次next()的調用再也不會到達這個for循環
       pendingIdleHandlerCount = 0;

       //因為執行了閑時任務花費了一段時間(迭代開始出的阻塞方法還未執行到所以還未阻塞)
       //此時再根據之前借宿那出的則色市場阻塞線程顯然不合適
       nextPollTimeoutMillis = 0;
   } //for(;;) 結束
}


25. 四種阻塞情況下,各自在什么時候才需要主動喚醒主動喚醒

  1. 隊列中沒有任何消息 – 永久阻塞
    新增普通消息:需要。新消息入隊后便主動喚醒線程,無論新消息是同步消息、異步消息。
    移除普通消息:不考慮
    新增同步障礙器:不需要
    移除同步障礙器:不考慮

  2. 隊首的消息執行時間未到 – 定時喚醒
    新增普通消息:如果在阻塞時長未耗盡時,就新加入早于隊首消息處理時間的消息,則需要主動喚醒
    移除普通消息:不需要
    新增同步障礙器:不需要
    移除同步障礙器:不需要

  3. 隊首消息是同步障礙器(SyncBarrier),并且隊列中不含有異步消息 – 永久阻塞
    新增普通消息:如果新加入的消息仍然是晚于隊首同步障礙器處理時間,那么這次新消息的發布在next()層面上是毫無意義的,則不需要喚醒線程。只有在新加入早于隊首同步障礙器處理時間的同步消息時,或者,新加入異步消息時(不論處理時間),才會主動喚醒被next()阻塞的線程
    移除普通消息:不需要
    新增同步障礙器:不需要
    移除同步障礙器:移除隊首障礙器能夠使本不可取出的同步消息變得可用,需要主動喚醒線程,重新判斷是否能夠取出消息或者是否需要縮短阻塞時長。除非新的 隊首消息還是同步障礙器才不需要喚醒

  4. 隊首消息是同步障礙器(SyncBarrier),隊列中含有異步消息但執行時間未到 – 定時喚醒
    新增普通消息:因為隊首同步障礙器的緣故,無論新加入什么同步消息都不會主動喚醒線程。只有加入的是異步消息,并且其處理時間需要早于設定好喚醒時執行的異步消息,才會主動喚醒
    移除普通消息:不需要
    新增同步障礙器:不需要
    移除同步障礙器:移除隊首障礙器能夠使本不可取出的同步消息變得可用,需要主動喚醒線程,重新判斷是否能夠取出消息或者是否需要縮短阻塞時長。**除非新的



假設我們移除設定好下次被動喚醒時執行的消息,線程被喚醒后就會因為沒有需要處理的消息而再次進入阻塞,并不會錯過消息所以移除普通消息不需要主動喚醒

總結
關于移除普通消息時是否需要主動喚醒:

  • 第一種情況沒有消息跳過。
  • 第二種情況和第四種情況下,假設我們移除設定好下次被動喚醒時執行的消息,線程被喚醒后就會因為沒有需要處理的消息而再次進入阻塞,并不會錯過消息所以不需要主動喚醒。
  • 第三四種情況下移除設定好下次被動喚醒時執行的消息,線程雖然會再次進入阻塞但并不會錯過消息,也不需要主動喚醒。
    所以移除普通消息在任何情況下都不需要主動喚醒線程

關于新增同步障礙器時是否需要主動喚醒:
新增的同步障礙器都會在被動喚醒時發揮同步障礙的作用,不會因為沒有主動喚醒而多處理不該處理的消息,所以新增同步障礙器之后都不需要主動喚醒線程

先從MessageQueue#enqueueMessage來看源碼在新增普通消息時是怎么判斷是否需要主動喚醒的

boolean enqueueMessage(Message msg, long when){
  ......【msg的合法性判斷,不合法會終止入隊】
  
  synchronized(this){
      ......【判斷當前消息隊列是否正在退出或者已經廢棄】
      msg.markInUse();
      msg.when = when;
      Message p = mMessages;
      boolean needWake;
      //如果隊首為null,或者入隊消息需要馬上執行,或者入隊消息執行時間早于隊首消息,且線程已阻塞則都需要喚醒
      //如果p!=null && when!=0 && when > p.when 則不需要喚醒
      if(p == null || when == 0 || when < p.when){
          msg.next = p;
          mMessages = msg;
          needWake  = mBlocked; //mBlocked記錄消息循環是否阻塞
      } else {
          /**
           * 在隊列中間插入一個消息,
           * 一般情況下不需要喚醒隊列(不是加到隊首為什么要喚醒呢?),
           * 除非是一個同步障礙器而且新插入的消息是
           * 1. 異步消息
           * 2. 執行時間是隊列中最早的
           */
           
           //此處mBlock值需要根據情況決定,當線程已經阻塞,且隊首消息是同步障礙器,
           //新加入是異步消息,needWake才可能(!!)為true
           //這還要判斷 1. 消息隊列中是否有異步消息,
           //2. 以及異步消息的處理時間早于還是晚于新加入的異步消息
           needWake = mBlocked && p.target == null && msg.isAsynchronous(); //如果是true也是暫時的,還要考驗在等著呢
           //尋找位置
           Message prev;
           for(;;){
              prev = p;
              p = p.next;
              if(p == null || when < p.when){
                  break;
              }
              
              if(neekWake && p.isAsynchonous()){
                  //能到達這里,說明msg.when > p.when,既然needWake是true,
                  //毫無疑問此時消息隊列是處于阻塞的。這里只有一種可能,
                  //p這個異步消息的執行時間還沒到(情況4)!
                  //msg的執行時間還更晚(不更晚早就break了)
                  //那就沒有必要喚醒消息隊列了
                  needWake = false;
              }
           } //for結束
           
           //插入新消息
           msg.next = p; //invariant: p == prev.next
           prev.next = msg;
      }
      
      if(needWake){
          nativeWake(mPtr); //喚醒消息隊列
      }
      
  }
}

在這個方法中,出現了一種我們前文都沒提到的一種主動喚醒情況 —— when==0(立即執行),實際上這也是毫無疑問需要主動喚醒的一種情況。對于第一種情況,新加入消息肯定需要主動喚醒;對于第二種,不主動喚醒會錯過;對于第三、第四種情況,隊首的同步障礙器不能影響早于它執行的消息,所以新加入when為0的消息無論如何都能夠執行,如果不主動喚醒也會錯過!所以,無論什么情況,只要新入隊的消息when字段為0,都要主動喚醒線程!
移除消息的MessageQueue.removeMessages()系列和MessageQueue.removeCallbacksAndMessages()方法,雖然可能導致線程下次被動喚醒時沒有消息執行,但是都不會錯過消息所以不需要主動喚醒。
接下來目光轉到同步障礙器上,MessageQueue.removeSyncBarrier()代碼

void removeSyncBarrier(int token){
  synchronized(this){
      Message prev = null;
      Message p = mMessages;
      //找到指定的障礙器
      while(p != null && (p.target != null || p.arg1 != token)){
          prev = p;
          p = p.next;
      }
      
      if(p == null){
          throw new IllegalStateException("The specified message queue synchronization "
              + "barrier token has not been posted or has already been removed");
      }
      final boolean needWake;
      //如果找到障礙器時,它有前驅消息,說明這個障礙器還沒發揮作用,
      //此時無論消息隊列是否阻塞,都不需要改變其(即消息隊列)狀態
      if(prev != null){
          prev.next = p.next;
          needWake = false;
      } else { //如果障礙器是隊首第一個消息
          mMessages = p.next;
          //消息隊列為空或者新隊首消息不是障礙器時,則喚醒消息隊列
          needWake = mMessages == null || mMessag.target != null;
      }
      p.recycleUnchecked();
      
      //If the loop is quitting when it is already awake.
      //we can assume mPtr != 0 when mQuitting is false
      if(needWake && !mQuitting){
          nativeWake(mPtr);
      }
  }
}

needWake = mMessages == null || mMessages.target != null,這個語句含有一種有趣的情況。當消息隊列中不含普通消息只含一個同步障礙器時,移除這個障礙器后整個消息隊列都空了。按理說,移除之前next()線程已經處于無限阻塞中,移除后再喚醒結果還是無線阻塞。從消息處理上來講,這是一個可以輕松避免且毫無意義的喚醒。從空閑任務的管理上來講,next()方法在阻塞線程之前都會執行空閑任務然后再迭代一次判斷是否阻塞,阻塞后再喚醒也不可能在本次next()中再執行一次空閑任務,依然是一個可以輕松避免且毫無意義的喚醒。
另外一點是,這段代碼并沒有融合mBlocked(記錄當前線程是否阻塞)變量的值。可能出現線程未阻塞時主動喚醒線程的無謂舉動。


26. 消息隊列的退出與廢棄

當Looper對象退出循環處理時,會調用MessageQueue的同包成員方法quit(safe)通知消息隊列開始退出操作。如果boolean型的參數safe是true,消息隊列會清除when晚于當前時間的所有同步/異步消息與同步障礙器,留下本應處理完的消息繼續處理;如果safe是false,則完全不顧慮,清除消息隊列中的所有消息。

next()方法執行過程中,如果處理完隊列中全部消息后發現該消息隊列的quit()方法被調用過,則直接調用dispose()廢棄消息隊列并返回null給Looper。當GC回收消息隊列之前,會調用消息隊列重載的finalize()方法,在這個方法中同樣能夠執行廢棄消息隊列的操作(如果還未廢棄)


27. IdleHandler【閑時任務】

IdelHandler允許我們在消息隊列空閑時執行一些不耗時的簡單任務

/**
* 回調接口,當線程準備阻塞以等待更多的消息時調用
* 開發者可以實現自己的IdelHandler類,然后通過{@link #addIdleHandler}方法將其添加到消息隊列中
* 一旦消息隊列的循環空閑下來,就會執行這些Handler的
* {@link IdleHandler#queueIdle IdleHandler.queueIdle()}方法
* 你可以在這個方法中添加一次操作
*/
public static interface IdleHandler{
  /**
   * 方法在一下兩種情況下會被調用:
   * 1. 當消息隊列處理完消息開始等待消息時,此時隊列為空
   * 2. 當隊列中依然有待處理的消息,但這些消息的交付(delivery)時刻要晚于當前時刻時
   * 
   * @return 
   * true 下次執行{@link #next() next()}如果遇到空閑,依然執行這個IdleHandler
   * false 這次IdleHandler執行完之后就把它從限時任務列表中刪除
   */ 
   boolean queueIdle();
}

我們來看看消息隊列是怎么使用IdleHandler的,首先目光轉到成員變量上,我們可以發現有這樣兩個成員:

/**IdleHandler列表**/
private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
/**保存等待處理的IdleHandler**/
private IdleHandler[] mPendingIdleHandlers;

mIdleHandlers可以理解成是一個IdleHandler的總列表,每次next()將要執行IdleHandler時都會從這個總列表中取出所有的IdleHandler
mPendingIdleHandlers指定哪些IdleHandler需要在本次執行中完成,每次next()將要執行IdleHandler時都會從mIdleHandlers拷貝。

下面這一段代碼是MessageQueue.next()中用于處理IdleHandler的代碼段

for(int i = 0; i < pendingIdleHandlerCount; i++){
   final IdleHandler idler = mPendingIdleHandlers[i];
   mPendingIdleHandlers[i] = null; //等待處理任務即將被處理,將其從待處理數組中刪除【置空引用】
   
   boolean keep = false;
   try{
      keep = idler.queueIdle();
   } catch (Throwable t){
      Log.wrf("MessageQueue", "IdleHandler threw exception", t);
   }
   
   if(!keep){
      synchronized(this){
          mIdleHandlers.remove(idler);
      }
   }
}
//Reset the idle handler count to 0 so we do not run them again
pendingIdleHandlerCount = 0;

執行IdleHandler其實就是調用其queueIdle()方法,queueIdle()如果返回false,next()方法會將該IdleHandlermIdleHandlers中刪除。這樣的話,下一次next()方法再執行IdleHandler時就不會再重復執行它了。

需要特別提醒的是,雖然next()方法是一個無限for循環,但是每次調用next()只會執行一次mIdleHandlers中的閑時任務。因為在上面的代碼段之前有這樣一段

if(pendingIdleHandlerCount <= 0){
   mBlocked = true;
   continue; 
}


28. Activity的生命周期是怎么實現在死循環體外能夠執行起來的?

ActivityThread的內部類H繼承于Handler,通過handler消息機制,簡單說Handler機制用于同一個進程的線程間通信。

Activity的生命周期都是依靠主線程的Looper.loop,當收到不同Message時則采用相應措施:

在H.handleMessage(msg)方法中,根據接收到不同的msg,執行相應的生命周期。

比如收到msg=H.LAUNCH_ACTIVITY,則調用ActivityThread.handleLaunchActivity()方法,最終會通過反射機制,創建Activity實例,然后再執行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,則調用ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。 上述過程,我只挑核心邏輯講,真正該過程遠比這復雜。

主線程的消息又是哪來的呢?
當然是App進程中的其他線程通過Handler發送給主線程

    App進程                           system_server進程

     主線程
ActivityThread
       ↑
       | handler方式
       |
線程4(Binder服務端)    binder方式     線程2(Binder客戶端)
ApplicationThread    <-----------  ApplicationThreadProxy

線程3(Binder客戶端)    binder方式     線程1(Binder服務端)
ActivityManagerProxy  -----------> ActivityManagerService

system_server進程是系統進程,java framework框架的核心載體,里面運行了大量的系統服務,比如這里提供ApplicationThreadProxy(簡稱ATP),ActivityManagerService(簡稱AMS),這個兩個服務都運行在system_server進程的不同線程中,由于ATP和AMS都是基于IBinder接口,都是binder線程,binder線程的創建與銷毀都是由binder驅動來決定的。

App進程 則是我們常說的應用程序,主線程主要負責Activity/Service等組件的生命周期以及UI相關操作都運行在這個線程; 另外,每個App進程中至少會有兩個binder線程: ApplicationThread(簡稱AT)和ActivityManagerProxy(簡稱AMP),除了圖中畫的線程,其中還有很多線程,比如signal catcher線程等,這里就不一一列舉。

Binder用于不同進程之間通信,由一個進程的Binder客戶端向另一個進程的服務端發送事務,比如圖中線程2向線程4發送事務;而handler用于同一個進程中不同線程的通信,比如圖中線程4向主線程發送消息。

結合圖說說Activity生命周期,比如暫停Activity,流程如下:

  1. 線程1的AMS中調用線程2的ATP;(由于同一個進程的線程間資源共享,可以相互直接調用,但需要注意多線程并發問題)

  2. 線程2通過binder傳輸到App進程的線程4;

  3. 線程4通過handler消息機制,將暫停Activity的消息發送給主線程;

  4. 主線程在looper.loop()中循環遍歷消息,當收到暫停Activity的消息時,便將消息分發給ActivityThread.H.handleMessage()方法,再經過方法的調用,最后便會調用到Activity.onPause(),當onPause()處理完后,繼續循環loop下去。


29. 子線程中toast、showDialog的方法

new Thread(new Runnable() {
       @Override
       public void run() {
           Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩潰無疑
      }
}).start();

一種解決方式

new Thread(new Runnable() {
        @Override
        public void run() {
 
            Looper.prepare();
            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
            Looper.loop();
        }
    }).start();  

來看一下Toast源碼


public void show() {
    ......
 
    INotificationManager service = getService();//從SMgr中獲取名為notification的服務
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
 
    try {
        service.enqueueToast(pkg, tn, mDuration);//enqueue? 難不成和Handler的隊列有關?
    } catch (RemoteException e) {
        // Empty
    }

在show方法中,我們看到Toast的show方法和 普通UI控件不太一樣,并且也是通過Binder進程間通訊方法執行Toast繪制。這其中的過程就不在多討論了,有興趣的可以在NotificationManagerService類中分析。

現在把目光放在TN這個類上(難道越重要的類命名就越簡潔,如H類),通過TN類,可以了解到它是Binder的本地類。在Toast的show方法中,將這個TN對象傳給NotificationManagerService就是為了通訊!并且我們也在TN中發現了它的show方法。

private static class TN extends ITransientNotification.Stub {//Binder服務端的具體實現類
 
    /**
     * schedule handleShow into the right thread
     */
    @Override
    public void show(IBinder windowToken) {
        mHandler.obtainMessage(0, windowToken).sendToTarget();
    }
 
    final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            IBinder token = (IBinder) msg.obj;
            handleShow(token);
        }
    };
 
}

看完上面代碼,就知道子線程中Toast報錯的原因,因為在TN中使用Handler,所以需要創建Looper對象。 那么既然用Handler來發送消息,就可以在handleMessage中找到更新Toast的方法。 在handleMessage看到由handleShow處理。

//Toast的TN類
public void handleShow(IBinder windowToken) {

    ``````
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
 
    mParams.x = mX;
    mParams.y = mY;
    mParams.verticalMargin = mVerticalMargin;
    mParams.horizontalMargin = mHorizontalMargin;
    mParams.packageName = packageName;
    mParams.hideTimeoutMilliseconds = mDuration ==
    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
    mParams.token = windowToken;
    if (mView.getParent() != null) {
        mWM.removeView(mView);
    }
    mWM.addView(mView, mParams);//使用WindowManager的addView方法
        trySendAccessibilityEvent();
    }
}

總結一下:

Toast本質是通過window顯示和繪制的(操作的是window),而主線程不能更新UI 是因為ViewRootImpl的checkThread方法在Activity維護的View樹的行為。 Toast中TN類使用Handler是為了用隊列和時間控制排隊顯示Toast,所以為了防止在創建TN時拋出異常,需要在子線程中使用Looper.prepare();和Looper.loop();(但是不建議這么做,因為它會使線程無法執行結束,導致內存泄露)
Dialog亦是如此

在子線程中,如果手動為其創建Looper,是非常危險的操作,需要在所有的事情完成以后,調用quit方法來終止消息循環,否則這個子線程就會一直處于等待的狀態,而如果退出Looper以后,這個線程就會立刻終止,因此建議不需要的時候終止Looper。【 Looper.myLooper().quit();

30. 消息處理:EventBus、BroadCast 和 Handler 優缺點比較

  1. BroadCast
    廣播是相對消耗時間、空間最多的一種方式,但是大家都知道,廣播是四大組件之一,許多系統級的事件都是通過廣播來通知的,比如說網絡的變化、電量的變化,短信發送和接收的狀態,所以,如果與android系統進行相關的通知,還是要選擇本地廣播;在BroadcastReceiver的 onReceive方法中,可以獲得Context 、intent參數,這兩個參數可以調用許多的sdk中的方法,而eventbus獲得這兩個參數相對比較困難;


    因此廣播相對于其他的方式而言,廣播是重量級的,消耗資源較多的方式。他的優勢體現在與sdk連接緊密,如果需要同 android 交互的時候,廣播的便捷性會抵消掉它過多的資源消耗,但是如果不同android交互,或者說,只做很少的交互,使用廣播是一種浪費;


    廣播作為Android組件間的通信方式,可以使用的場景如下:
    ?????1.同一app內部的同一組件內的消息通信(單個或多個線程之間);
    ??????2.同一app內部的不同組件之間的消息通信(單個進程);
    ??????3.同一app具有多個進程的不同組件之間的消息通信;
    ??????4.不同app之間的組件之間消息通信;
    ??????5.Android系統在特定情況下與App之間的消息通信。


    廣播的不可替代性在于它可以跨進程進行通信,也就是不同APP之間可以通過廣播進行傳遞數據,并且在OnReceiver中更容易使用Context和Intent對象來執行必要的操作。單就同一app內部的消息通信而言,使用廣播是較為消耗資源和笨重的。

  2. Handler
    ???handler一般用于線程間通信,它可以分發Message對象和Runnable對象到主線程中, 每個Handler實例,都會綁定到創建他的線程中(一般是位于主線程),它有兩個作用:
    ????????1.安排消息或Runnable 在某個主線程中某個地方執行;
    ????????2.安排一個動作在不同的線程中執行。


    本篇只討論handler中與Message相關的的消息通信,一般Handler的使用方法即在調用線程內創建Handler的內部類,并重寫handlerMessage(Message msg)方法,而在發布消息時使用sendMessage方法進行發布,在處理時通過switch(msg.what)進行消息分發并進行相應的處理。這里,Hander內部類和其定義類是綁定的,這就造成了事件發布者和接受者之間的高耦合。而Handler的最大好處是發生問題時,可以非常明確、快速的進行定位,通過msg.what很容易就可以理清每一條消息流的邏輯。

  3. EventBus
    ??EventBus的優勢在于調度靈活。不依賴于 Context,使用時無需像廣播一樣關注 Context 的注入與傳遞,也解除了Handler所帶來的耦合,父類對于通知的監聽和處理可以繼承給子類,這對于簡化代碼至關重要;通知的優先級,能夠保證 Subscriber 關注最重要的通知;粘滯事件(sticky events)能夠保證通知不會因 Subscriber 的不在場而忽略。可繼承、優先級、粘滯,是 EventBus 比之于廣播、觀察者等方式最大的優點,它們使得創建結構良好組織緊密的通知系統成為可能。


    ??但EventBus也有很明顯的缺陷,在EventBus中事件的分發是通過注解函數的參數類型確定的,因此在事件發布遭到大量濫用時,特別有多個訂閱者、多個相同參數時,很難從事件發布者開始理清消息流,無法快速的找出是哪個訂閱者接受并處理了消息導致的問題,這就要求了參與者必須對整個通知過程有著良好的理解。當程序代碼適量時,這是一個合理的要求,然而當程序太大時,這將成為一種負擔。在EventBus中一定要寫好必要的注釋信息,否則在后續工作交接中會產生很多不必要的麻煩。

4.31 手寫Handler

public class Handler{
    Looper mLooper;

    public Handler(){
        mLooper = Looper.myLooper();
    }


    //發送
    public void sendMessage(Message msg){
        mLooper.enqueueMessage(msg);
    }

    //處理
    public void handleMessage(){

    }

    private void enqueueMessage(Message msg){
        //把Message 和 handler 綁定
        msg.target = this;

        //下一步,想要將msg發送到messageQueue中
        //但是是發送到哪一個MessageQueue中呢?應該是Looper持有的那個MessageQueue中
        //所以Handler應該持有一個Looper,然后利用Looper,將Message插入MessageQueue
        mLooper.enqueueMessage(msg);
    }
}





public class Message{
    Object  obj;
    Handler target; //為了在loop時能用Message獲取到handler,在發送時綁定,在取出后調用handleMessage

    public Message(String object){
        obj = object;
    }

    public String toString(){
        return obj.toString();
    }
}





public class MessageQueue{
    BlockingQueue<Message> queue = new ArrayBlockingQueue<>(100);

    //存
    public void enqueueMessage(Message msg){
        try{
            queue.put(msg);
        }catch(InterruptedExcepttion e){
            e.printStackTrace();
        }       
    }

    //取
    public Message next(){
        Message msg = null;
        //阻塞
        try{
            msg = queue.take();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        return msg;
    }
}




public class Looper(){
    MessageQueue mQueue;

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();

    public static Looper myLooper(){
        return sThreadLocal.get();
    }

    private Looper(){
        mQueue = new MessageQueue();
    }

    //打開電機電源,要保證一個線程只有一個looper
    public void prepare(){  
        //先取一下
        Looper looper = sThreadLocal.get();
        if(looper != null){
            throw new RuntimeException("Only one Looper may be created per thread");
        }

        //存looper, 存到線程所在的map里面
        //借助 ThreadLocal 這個工具去存
        sThreadLocal.set(new Looper());

        
    }

    public void enqueueMessage(Message msg){
        mQueue.enqueueMessage(msg);
    }

    //開啟電機,利用隊列的阻塞機制
    public void loop(){
        //拿到線程對應的looper

        final Looper me = sThreadLocal.get();
        final MessageQueue mQueue = me.queue;
        for(;;){
            Message msg = mQueue.next();
            if(msg != null){
                //這里怎么獲得handle,然后調用handler.handleMessage呢?
                //在Message的數據結構設計時,加上Handler,將兩者綁定
                //這也是handler會產生內存泄露的一大原因

                msg.target.handleMessage(msg); //用Message自己保存的handler發送自己
            }
        }
    }
}






public class HandlerMain{

    public staitc void main(String[] args){

        Looper.prepare();

        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg){
                System.out.println(msg.toString());
            }
        };

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

推薦閱讀更多精彩內容