Android異步消息處理機制(源碼分析+面試題)

參考文獻:

1 概述

主線程不能執行耗時操作,因爲會阻塞,在子線程裏進行耗時操作;子線程不能更新UI,用handler發送一個更新UI的消息,handler分發消息,處理消息

子線程爲何不能訪問UI?

  • 源碼角度:當訪問UI時,ViewRootImpl會調用checkThread()方法檢查當前線程是哪個線程,如果不是UI線程會拋出異常;
  • 線程安全角度:訪問UI不是線程安全的;
    • 訪問UI爲什麼不加鎖:邏輯複雜、效率低;

Handler作用:

  • 線程間通信(例如,子線程通知主線程更新UI);
  • 執行計劃任務;

 下面源碼分析基於Android 8.0;

2 Message 消息

 Message消息,是多線程間通信的實體,是Handler發送和處理的對象。Message對象實現了Parcelable接口,說明Message對象支持序列化/反序列化操作。

2.1 屬性

    //msg ID
    public int what;
    //存儲int類型的數據域
    public int arg1;
    //存儲int類型的數據域
    public int arg2;
    //存儲Object類型數據域
    public Object obj;
    //存儲Bundle類型數據域
    /*package*/ Bundle data;

    /*package*/ static final int FLAG_IN_USE = 1 << 0;
    //消息標識,當消息對象進入消息隊列或回收時設置爲FLAG_IN_USE,msg.obtain時設置爲0
    /*package*/ int flags;
    
    //處理消息的時間
    /*package*/ long when;
    //發送和處理消息的Handler
    /*package*/ Handler target;
    //post的Runnable
    /*package*/ Runnable callback;

    // 鏈式結構,指向下一個Message對象,用於維護鏈表結構的消息池(消息隊列)
    /*package*/ Message next;
    
    //信號量,消息池的加鎖對象
    private static final Object sPoolSync = new Object();
    //消息池的表頭,由它維護了一個鏈式消息池,當消息被回收的時候,會加入到這個消息池中
    private static Message sPool;
    //消息池大小
    private static int sPoolSize = 0;
    //消息池最大容量50,消息隊列的最大容量是50
    private static final int MAX_POOL_SIZE = 50;
  • Message可傳輸int , Object ,Bundle類型的數據
  • 如果你的message只需要攜帶簡單的int,請優先使用Message.arg1Message.arg2來傳遞信息,這比用Bundle更省內存;
  • 擅用message.what來標識信息,以便用不同方式處理message;
  • Message維護了一個全局的消息池(消息隊列),消息隊列最大容量是50;消息被回收後,會放入到消息池中,並將flag字段設置爲FLAG_IN_USE;

2.2 靜態obtain()方法

    public static Message obtain() {
        synchronized (sPoolSync) {//對消息池加鎖
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // flags設爲0
                sPoolSize--;//從鏈表刪除
                return m;
            }
        }
        return new Message();//若消息池爲空,直接new
    }

obtain方法用於獲取一個消息對象,如果當前消息池爲空,直接new,否則從消息池頭部取一個消息對象進行復用;
obtain方法還有好幾個重載方法,但最終都會調用該該無參方法。

2.3 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的方法,looper.loop()在從消息隊列取出並處理消息後調用這個方法
    void recycleUnchecked() {
        flags = FLAG_IN_USE; //修改標記??
        //爲了無差別(handler發送的所有消息)複用消息對象,清空所有域
        what = 0; arg1 = 0; arg2 = 0;
        obj = null; replyTo = null; sendingUid = -1;
				when = 0;target = null;callback = null;data = null;

        synchronized (sPoolSync) {//將使用完的消息回收後放入消息池,頭插法
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

 在Message對象被處理或從消息隊列移除後,可以手動調用recycle()方法回收消息對象;當然recycleUnchecked()方法纔是真正回收消息的方法,looper.loop()在從消息隊列取出並處理消息後調用這個方法進行消息回收;這個方法首先會將flag標記爲FLAG_IN_USE,並把清空所有屬性;並在消息池沒有達到最大限定值的情況下,把這個對象插入消息池的表頭。同樣,在操作消息池的時候需要先對sPoolSync信號量加鎖。

回收消息放入消息池

3 MessageQueue消息隊列

MessageQueue是一個常量類,不允許被繼承;
 消息隊列用來存放Handler發送過來的消息,內部通過單鏈表的數據結構來維護消息列表,等待Looper的抽取。

3.1 消息出隊next()

    Message next() {
				//...
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        //死循環從隊列取Message,直到返回一個Message,或者MessageQueue退出
        for (;;) {
          	//...
            synchronized (this) {//消息隊列加鎖
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //取隊頭消息,若該消息不爲空且者是屏障消息(target爲空),則繼續遍歷,直到取到一個異步消息爲止
                //屏障消息:target爲空時是屏障消息;用於區分同步消息和異步消息;如果設置了屏障消息,只執行異步消息,不執行同步消息,直到移除了屏障;如果沒設置屏障消息,同步消息和異步消息都執行
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {//如果消息執行時間未到,繼續循環,等待時間到
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            //preMsg不空,說明此時隊列頭結點是一個target爲空的屏障消息,同時msg此時是異步消息。
                            prevMsg.next = msg.next;//直接從鏈表取下該消息
                        } else {//此時msg是隊列頭結點,直接刪除隊頭即可
                            mMessages = msg.next;
                        }
                        msg.next = null;//斷開next鏈接
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();//修改標記
                        return msg;//取道非空消息退出
                    }
                } else {//隊列爲空,next方法阻塞,繼續循環,等待新消息到來
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
              	//若消息隊列已退出,返回true退出死循環
                if (mQuitting) {
                    dispose();
                    return null; //返回null後Looper.loop()方法也會結束循環
                }
								//...
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;//當隊列爲空時,next()方法會阻塞,繼續循環,直到有新消息到達隊列
                    continue;
                }
								//...
        }
    }
  • next()方法用於將隊列頭部消息出隊並返回
  • 該方法內部有個死循環,如果消息隊列中沒消息,next方法會阻塞,繼續循環,直到取道新消息;如果消息隊列中有消息,先判斷執行時間是否到了,如果時間沒到則等待,繼續循環如果時間到了就將消息出隊返回
  • 在循環過程中會對消息隊列加鎖,所以該方法是線程安全的;

3.2 消息入隊enqueueMessage()

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {//入隊的消息的target,必須不爲空,否則會拋異常
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {//消息在回收之前,不可以被重複使用
            throw new IllegalStateException(msg + " This message is already in use.");
        }
        synchronized (this) {
            if (mQuitting) {//如果消息隊列在退出狀態 ,則直接回收消息,返回false
                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;
            }
            //把消息標記爲在使用狀態,設置when
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;//此時p是鏈表頭部
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                //如果隊列爲空或者when等於0,或者when小於隊頭Message的when,則直接把消息插入隊頭
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;//prev是p的前驅節點,依次遍歷
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;//當p已經到隊尾或者找到一個節點msg.when < p.when時退出循環
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //鏈表插入操作,把msg插入到p節點前邊,並把p的前驅節點的next改爲msg
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
          	//...
        }
        return true;//插入成功,返回true
    }
  • 該方法用於將消息入隊,其實就是單鏈表的插入操作;
  • 該方法對鏈表隊列操作時,依然是進行了加鎖同步,所以是線程安全的;
  • 隊列是一個(消息執行時間)when升序鏈表,所以插入也必須找到合適的節點進行插入;如果待插入Message不設置when或when=0,直接插入隊列頭部;否則遍歷隊列結點,直到找到第一個大於when的結點,插入到該結點的前面

消息入隊

4 Looper消息泵

 通過Looper.loop()不斷地從MessageQueue中抽取Message,將消息分發給目標處理者(Handler);

4.1 主要屬性和構造器

    //線程本地變量,每個線程有一個獨立的Looper對象,不存在線程安全問題
    //如果不調用prepare()方法,sThreadLocal.get()返回null
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static Looper sMainLooper;  // 主線程的Looper,由Looper.class維護
    final MessageQueue mQueue;//looper的MessageQueue
    final Thread mThread;//(創建)Looper線程
		//私有構造器,不允許外部調用
		private Looper(boolean quitAllowed) {
    	mQueue = new MessageQueue(quitAllowed);
   		mThread = Thread.currentThread();
		}
  • Looper的構造器是私有的,不能在Looper類外部new Looper,所以在Looper類外部必須調用prepare()方法來創建一個Looper對象
  • Looper內部有一個MessageQueue屬性

4.2 創建Looper

Looper.prepare()創建Looper,而不是new

//公有方法,開發者只能調用這個方法爲當前線程創建Looper,允許退出
public static void prepare() {
    prepare(true);
}
//私有方法,開發者無法調用,同一個線程只允許調用一次prepare(),否則會拋出異常
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));//將Looper對象設置爲線程本地變量
}
//主線程的Looper初始化,雖然是公有方法,我們無法調用,
//因爲系統啓動的時候已經調用過了,如果再次調用,會拋異常
public static void prepareMainLooper() {
    prepare(false);
    synchronized(Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        //把得到的主線程Looper賦值給sMainLooper 
        sMainLooper = myLooper();
    }
}
//獲取當前線程的Looper對象
public static@Nullable Looper myLooper() {
    return sThreadLocal.get();//獲取線程本地變量
}
  • 調用**Looper.prepare()爲當前線程創建Looper對象**,並將Looper對象設置爲線程本地變量
  • 調用Looper.myLooper()方法來獲取當前線程的Looper對象;
  • 主線程的Looper允許MessageQueue退出,而其他線程不允許;
  • 一個線程只能調用一次Looper.prepare()方法,否則會拋出異常,所以在prepare()創建Looper對象之前,應該先調用Looper.myLooper()方法判斷是否爲空;同時也說明了一個線程只有一個Looper對象
  • 主線程的Looper在ActivityThread中的main()方法中創建的,所以主線程不需要手動創建Looper
//主線程中不需要自己創建Looper
public static void main(String[] args) {
        //...
        Looper.prepareMainLooper();//爲主線程創建Looper,該方法內部又調用 Looper.prepare()
        //...
        Looper.loop();//開啓消息輪詢
        //...
    }
  • 另外可以在任何地方調用Looper.getMainLooper();獲取主線程的Looper;

 但是子線程就不一樣了,子線程在創建Handler對象前必須手動調用Looper.prepare()方法創建Looper對象;

//子線程中創建Looper的標準寫法
new Thread(new Runnable() {
    @Override
    public void run() {
        if(Looper.myLooper()==null){//保證一個線程只有一個Looper
            Looper.prepare();//創建Looper
        }
        Handler handler=new Handler();
        Looper.loop();//開啓消息輪詢
    }
}).start();

4.3 開啓消息輪詢loop()

 //代碼省去打印等其他無關邏輯
public static void loop() {
    //獲取當前線程的sThreadLocal變量,即Looper對象
    final Looper me = myLooper();
    //如果當前線程沒有調用過prepare()方法,則me爲null,拋出異常
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    //從me裏獲得本線程的MessageQueue對象
    final MessageQueue queue = me.mQueue;
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    //開啓消息輪詢
    for (;;) {
        //從消息隊列取消息
        Message msg = queue.next();  // 當消息隊列爲空且未退出時,next方法會阻塞
        if (msg == null) {
            //next返回null表明消息隊列已退出
            return;//結束輪詢,loop方法唯一出口
        }
        try {
            //target是Message的Handler,調用它的dispatchMessage()方法來分發消息
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        final long newIdent = Binder.clearCallingIdentity();
        msg.recycleUnchecked();//當消息被處理完後,回收當前消息
    }
}
  • 通過調用Looper.loop()方法開啓消息輪詢;該方法會調用queue.next()方法從隊列中取頭部消息
  • 該方法會循環調用msg.target.dispatchMessage(msg);來進行消息分發,分發給消息的target,也就是消息對應的的Handler對象;
  • 當消息被處理完後,會調用msg.recycleUnchecked();回收消息,當前消息放入消息池,以便以後複用;
  • handler是在它關聯的looper線程(創建Looper對象的線程)中處理消息的;

5 Handler 消息處理器

5.1 主要屬性和構造器

5.1.1 主要屬性

//是否發現潛在的內存泄漏,默認爲false
private static final boolean FIND_POTENTIAL_LEAKS = false;
private static final String TAG = "Handler";
//靜態全局變量,主線程的Handler
private static Handler MAIN_THREAD_HANDLER = null;
//綁定的Looper對象
final Looper mLooper;
//綁定的MessageQueue消息隊列,通過looper獲取
final MessageQueue mQueue;
//回調接口
final Callback mCallback;
//是否是異步的,如果是異步的,在發送消息的時候,
//會調用Message.setAsynchronous(true)把消息設爲異步消息
final boolean mAsynchronous;
  • 由此可見一個Handler持有一個Looper類型的屬性;即一個Handler對應一個唯一的Looper;而對應的MessageQueue消息隊列,通過Looper屬性獲取;

5.1.2 構造器

public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            //如果爲true,如果Handler實現類是匿名類或內部類或非static類,會給出警告,告知開發者存在內存泄漏的風險
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        //獲取當前線程的線程本地變量,即Looper對象;
        mLooper = Looper.myLooper();
        if (mLooper == null) {//創建Handler對象前要調用Looper.prepare
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //mQueue直接拿Looper裏的MessageQueue類型的引用對象
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    
   //該構造器可爲當前Handler指定Looper對象,所以Handler對象和Looper對象不一定是在同一線程創建的
   public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    
  	public Handler() {this(null, false);}
        
   	public Handler(Callback callback) {this(callback, false);}
   	
   	public Handler(Looper looper) {this(looper, null, false);}
  • Handler對應的MessageQueue對象來自其關聯的Looper對象;

  • 其他構造方法都會調用前面倆構造器中一個;早常用的構造器是Handler()Handler(Callback);

  • Handler(Looper looper)用於爲當前Handler指定關聯的Looper對象;

  • Handler關聯的Looper對象既可以來自當前線程(創建Handler實例的線程)的本地變量,也可以在構造器裏指定;前面一種情況,Handler和Looper是在同一線程裏實例化的,後面一種情況不一定;

5.1.3obtainMessage

public final Message obtainMessage() {
    return Message.obtain(this);
}

 使用該方法獲取handler當前處理的Message;Handler中有一系列obtanMessage()重載方法,最後調用的還是該方法;需要注意的是,在Message中,obtain()方法是靜態方法,在Handler中,是非靜態的,需要通過具體的Handler實例對象來獲得,但是禁止子類進行覆寫;

5.2 發送消息

 在Handler中,可以發送一個Runnable對象,也可以發送一個Message對象;通過sendMessage(Message)方式發送一個Message對象;通過post(Runnable)方式發送一個Runnable對象,這個Runnable對象最終也會被包裝成一個Message對象發送;

5.2.1 post方式

//立即post一個Runnable對象到MessageQueue中,此時Runnable對象被包裝成Message後入隊(when == 當前系統時間,可能是隊頭,也可能不是隊頭,隊列中已經有when值小於當前時間的Message)
public final boolean post(Runnable r) {
    return sendMessageDelayed(getPostMessage(r), 0);
}
//Runnable包裝成Message後加入到MessageQueue中,但此時
//Message.when=uptimeMillis,uptimeMills是消息的執行時間,
public final boolean postAtTime(Runnable r, long uptimeMillis) {
    return sendMessageAtTime(getPostMessage(r), uptimeMillis);
}
//同上,只是又傳入了token對象,存儲在Message.obj中
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {
    return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
}
//Runnable包裝成Message後加入到MessageQueue中,
//但是Message.when = now + delayMillis,
//表示延遲delayMills後執行
public final boolean postDelayed(Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}
//Runnable包裝成Message後加入到MessageQueue中,此時when=0,所以一定是在MessageQueue的隊頭
public final boolean postAtFrontOfQueue(Runnable r) {
    return sendMessageAtFrontOfQueue(getPostMessage(r));
}
//把Runnable對象包裝成Message對象,可見只是把Runnable對象賦值給了Message的callback域
private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
//上述方法的重載方法,把token賦值給了Message的obj域,可以用這個方法進行傳Object數據
private static Message getPostMessage(Runnable r, Object token) {
    Message m = Message.obtain();
    m.obj = token;
    m.callback = r;
    return m;
}

5.2.2 sendMessage方式

public final boolean sendMessage(Message msg) {
    return sendMessageDelayed(msg, 0);
}

public final boolean sendEmptyMessage(int what) {
    return sendEmptyMessageDelayed(what, 0);
}

public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
    Message msg = Message.obtain();
    msg.what = what;
    return sendMessageDelayed(msg, delayMillis);
}
//延遲發送,把當前時間加上延遲時間後調用了sendMessageAtTime()方法
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
//發送消息的方法,對queue判空後,調用enqueueMessage進行實際入隊
public boolean sendMessageAtTime(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);
}
//實際對消息入隊的方法,在該方法中,會把Message的target域進行賦值,
//如果mAsynchronous是true,則會調用setter方法把消息設置爲異步消息,
//調用的入隊方法其實是調用的MessageQueue的enqueueMessage方法
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
//...
//省略其他方法,基本上跟post系列方法是一一對應的
  • 幾個時間關係:when = now + delay,when 表示分發消息(dispatchMessage)的時間,now表示當前(相對系統啓動)時間SystemClock.uptimeMillis(),delay表示延遲時間delayMillis;

post(runnable)方式和sendMessage(msg)方式發送消息的聯繫和區別:

  • 聯繫:調用鏈都是handler.sendMessageAtTime()->messageQueue.enqueueMessage()將Message發送到消息隊列;
  • 區別:
    • 消息內容不同:sendMessage發送的消息側重於傳數據,而handleCallback側重於傳任務(Runnable);
    • 處理消息的方式不同:一般情況,sendMessage發送的消息最終會調用handler或callback的handleMessage方法來處理;而post(runnable) 發送的消息最終會調用handler.handleCallback方法來處理;

5.3 處理消息

		//消息分發
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {//post的Runnable參數
            handleCallback(msg);
        } else {
            if (mCallback != null) {//Handler(Callback)構造器,Handler無需派生子類
                if (mCallback.handleMessage(msg)) {//一般爲true,若爲false,還要執行handleMessage
                    return;
                }
            }
            handleMessage(msg);//優先級最低
        }
    }
    
   	private static void handleCallback(Message message) {//處理消息方式1,優先級最高
        message.callback.run();//執行post的Runnable參數地run回調方法,因此Runnable run裏可以有一些更新UI的操作
    }
    
    public interface Callback {//優先級次之
        public boolean handleMessage(Message msg);//處理消息方式2,在調用Handler(Callback)構造器實例化Handler時實現該方法
    }
    
   //Handler子類必須實現這個空方法來接收消息
    public void handleMessage(Message msg) {//處理消息方式3,優先級最低
    }
  • 有兩類發送消息的方式: sendMessage(msg)方式和post(Runnable r)方式;
  • 處理消息方式有三種:handleCallback方式、callback.handleMessage方式、handler.handleMessage方式;並且優先級遞減;

6 四要素之間的關係

6.1 四要素之間的關係

四要素ER圖

  • 一個線程中可創建多個Handler對象;但是一個線程中只能創建一個Looper對象(因爲一個線程中只能調用一次Looper.preapre(),否則會報異常);
  • 一個Looper對象可以對應多個線程,比如主線程的mainLooper,供主線程和所屬子線程(Looper.getMainLooper())共同使用;
  • Looper類中有一個final MessageQueue mQueue屬性;
  • Handler類中有一個屬性final Looper mLooper屬性,Handler關聯的消息隊列通過Looper獲取;
  • 一個消息隊列中有多個Message對象,不同消息的target可以不同,所以消息隊列中的消息可以來自不同的Handler對象;
  • Message類有一個 Handler target屬性,這是消息對象關聯的Handler對象;

6.2 異步消息處理機制的原理(圖非常重要)

 以下面應用場景爲例:在主線程裏實例化Looper和Handler;在子線程(工作線程)處理耗時任務,由於要將任務執行結果在UI上展示,需要更新UI;在子線程中創建一個更新UI的Message對象,並使用Handler對象的引用發送該消息;最後在主線程裏處理消息,更新UI;下面是sample代碼:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.textView) TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.button)
    public void getData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //執行耗時操作...
                Message msg=new Message();msg.what=7;msg.obj= "網絡數據";
                //處理消息時回調handler.handleMessage
//                handler1.sendMessage(msg);
                //處理消息時回調callback.handleMessage
//                handler2.sendMessage(msg);
                //此處使用handler1,2,3 post消息都可以,但是不會執行handleMessage
                handler3.post(new Runnable() {
                    @Override
                    public void run() {
                        //不會開啓新線程執行,handleCallback執行run裏的代碼,所以不會報錯
                        textView.setText("耗時操作處理結果");
                    }
                });
            }
        }).start();
    }

    Handler handler3=new Handler();

    //匿名內部類向上轉型Handler()方式,派生子類
    Handler handler1=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 7:
                    textView.setText(msg.obj.toString());//更新UI
                    break;
            }
        }
    };
    
    //Handler(Callback)方式,不派生子類
    Handler handler2=new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            textView.setText(msg.obj.toString());//更新UI
            return true;//修改爲true
        }
    });
}

異步消息處理機制原理圖

7 常見面試問題

(1) 爲什麼創建 Message 對象推薦使用 Message.obtain()獲取而不是new方式?

 Handler 機制在 Android 系統中使用太頻繁,爲了提神效率,爲Message設置了一個靜態的消息池,當消息被處理完或移除後,會放入到消息池;下次需要使用Message時從消息池中取出消息進行複用

(2) 簡述MessageQueue 如何入隊和出隊?

  • 消息入隊:調用enquueMessage(msg,when);如果消息沒設置when或者when是0,直接將消息放到隊列頭部;否則遍歷隊列鏈表,找到第一個大於當前消息when的消息結點,插入到該節點前面;最後會形成一個按when升序的單鏈表
  • 消息出隊:調用next()方法,直接取出隊列頭部消息並返回;

(3) 發送消息兩種主要方式 sendMessage(msg)方式和post(Runnable r)方式的區別?

post(runnable)方式和sendMessage(msg)方式發送消息的聯繫和區別:

  • 聯繫:調用鏈都是handler.sendMessageAtTime()->messageQueue.enqueueMessage()將Message發送到消息隊列;
  • 區別:
    • 消息內容不同:sendMessage發送的消息側重於傳數據,而handleCallback側重於傳任務(Runnable);
    • 處理消息的方式不同:一般情況,sendMessage發送的消息最終會調用handler或callback的**handleMessage方法來處理;而post(runnable) 發送的消息最終會調用handler.handleCallback**方法來處理;

(4) 處理消息有哪幾種方式,他們之間優先級?

  • 處理消息方式有三種:handler.handleCallback方式、callback.handleMessage方式、handler.handleMessage方式;並且優先級遞減;

(5) Handler發送、處理消息有哪幾種方式?

  • 結合有以下三種常見的 Handler發送、處理消息 的方式:
		//方式1:send+派生方式
		//發送消息sendMessage;構造器Handler();處理消息handler.handleMessage;
		//注意這種方式由於存在Handler子類內部類,可能存在內存泄漏的情況,需要處理這種情況
		handler1.sendMessage(msg);
		//匿名內部類向上轉型Handler()方式,需要派生子類
    Handler handler1=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 1:
                    textView.setText(msg.obj.toString());//更新UI
                    break;
            }
        }
    };
    
    //方式2:send+Callback方式
    //發送消息sendMessage;構造器Handler(Callback);處理消息callback.handleMessage;
    handler2.sendMessage(msg);
    //Handler(Callback)方式,不派生子類
    Handler handler2=new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            textView.setText(msg.obj.toString());//更新UI
            return true;//修改爲true
        }
    });
    
    //方式3:post(Runnable r)方式
    //發送消息post(Runnable r);構造器Handler();處理消息handleCallback;
    //此處使用handler1,2,3 post消息都可以,但是不會執行handleMessage
    handler3.post(new Runnable() {
    	@Override
   		public void run() {
      //不會開啓新線程,handleCallback執行run裏的代碼,所以不會報錯
				textView.setText("耗時操作處理結果");
     	}
		});
    Handler handler3=new Handler();

(6) 異步消息處理機制的原理(Handler發送消息、處理消息的流程)?(高頻題也是本章核心和概要,很重要)

異步消息處理機制原理圖

  • Handler消息處理器 作用:發送消息(任務),處理消息;
  • Looper消息泵 作用:調用Looper.loop()方法進行消息輪詢;
  • MessageQueue消息隊列 作用:存放消息的場所,是一個按when值遞增的單鏈表;queue.enqueue(msg,when)用於消息入隊;queue.next()方法用於消息出隊,取隊首消息;

(7) post(Runnable r)方式是否會開啓新線程?

  這種方式不會開啓新線程;

  • Runnable對象會包裝成Message對象,r作爲Message對象的callback屬性;
  • 然後調用handler.sendMessageDelay()->handler.sendMessageAtTime()->messageQueue.enqueueMessage()將Message對象發送到消息隊列;
  • Looper在開啓消息輪詢後,到一定時間會從消息隊列中取出該消息對象,交給對應的target(Handler對象)進行消息分發;
  • 然後調用handler.handleCallback()方法處理消息,在這個方法裏Runnable任務會得到執行;

(8) 區分兩個callback?區分兩個handleMessage

 兩個callback:

  • MessageRunnable callback屬性:使用post(Runnable r)裏的Runnable對象初始化;
  • Handlerfinal Callback mCallback屬性:在Handler(Callback callabck)構造器進行初始化;

 兩個handleMessage方法,對應處理消息的兩種方式:

  • handler.handleMessage:send+派生方式調用該方法來處理消息;
  • callback.handleMessage:send+Callback方式調用該方法來處理消息;

(9) 爲什麼Handler會造成內存泄漏?如何解決?

(i) 內存泄漏的原因?
  • 一般造成內存泄漏的原因:長生命週期對象引用短生命週期對象
  • Handler造成Activity內存泄漏的原因:Handler生命週期比Activity長,非靜態內部類默認持有外部類的引用,導致Activity對象無法回收(Activity對象先回收時,Handler對象可能還在處理消息,此時Handler對象還持有Activity對象的引用,導致Activity對象無法回收)。
(i) 解決辦法?

 把Handler子類定義爲靜態(static)內部類;同時用WeakReference包裝外部類的對象activity;

  • 爲什麼Handler子類要定義爲靜態(static)內部類?  
     因爲靜態內部類不持有外部類的引用,所以使用靜態的Handler不會導致Activity內存泄露。
  • 爲什麼Handler子類定義爲靜態(static)內部類同時,還要用WeakReference包裝外部類的對象activity ?
     因爲我們需要訪問外部類的非靜態成員,可以通過強引用"activity. "訪問,如果直接使用強引用訪問,顯然會導致activity泄露。
    MyHandler mHandler=new MyHandler(this);
    
    private static class  MyHandler extends Handler{
        //static和WeakReference是爲了解決內存泄漏
        private WeakReference<MainActivity> weakReference;
        public MyHandler(MainActivity activity) {
           weakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 1:
                MainActivity activity=weakReference.get();
                if(activity != null){//判空是爲了避免空引用異常
                     activity.textView.setText(msg.obj.toString());
                }
                break;
            }
        }
    }

(10) 爲何Looper.loop()死循環不會造成應用卡死?

Looper.loop()不會造成應用卡死,因爲裏面使用了Linux 的epoll機制

(11) 創建Handler前要注意什麼?

 創建Handler對象前必須調用Looper.prepare()方法創建一個Looper對象;值得一體的是,子線程中必須手動調用Looper.prepare()方法,而主線程中可以不調用;因爲主線程ActivityThread的main方法中默認調用了Looper.prepareMainLooper()方法,這個方法會調用Looper.prepare()方法創建一個主線程Looper對象;

(12) 異步消息處理機制是如何保證消息處理器的唯一性(即某條消息的發送者和處理者是同一Handler對象)?

 在Handler的enqueueMessage方法中會把自引用賦值給被髮送的Message的target屬性;而在Looper的loop方法中會調用msg.target.dispatchMessage(msg)來分發、處理消息;

		//Handler.java
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
				//...
    }
    //Looper.java
    public static void loop() {
   			//...
    		msg.target.dispatchMessage(msg);
    		//...
    } 

(13) 子線程中是否可以創建Handler對象?

 子線程中可以創建Handler對象,但是子線程在創建Handler對象前必須手動調用Looper.prepare()方法創建Looper對象;

new Thread(new Runnable() {
    @Override
    public void run() {
        if(Looper.myLooper()==null){//保證一個線程只有一個Looper
            Looper.prepare();//創建Looper
        }
        Handler handler1=new Handler();
        Looper.loop();//開啓消息輪詢
        
        //主線程Looepr對象早已創建,並早已開啓消息輪詢
        Handler handler2=new Handler(Looper.prepareMainLooper());
    }
}).start();

ps:如果在主線程中使用子線程中創建的Handler對象的引用發送消息,最後消息是在子線程中處理的;這樣就實現了主線程向子線程發送消息,而在本文6.2節中的sample代碼中實現了子線程向主線程發送消息;所以兩個線程可以通過Handler進行雙向通信

(14) Handler 與 Looper 是如何關聯的?

  • 對於有Looper參數的構造器Handler(looper):直接通過構造器參數設置關聯的Looepr對象;
  • 對於無Looper參數的Handler構造器:無論是Handler()還是Handler(callback)都會調用下面的構造器,在這個構造其中會爲當前Handler關聯當前線程的Looper對象;
    public Handler(Callback callback, boolean async) {
				//...
        mLooper = Looper.myLooper();//獲取當前線程的Looepr對象
				//...
    }
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();//獲取當前線程的本地變量
    }

(15) Thread 與 Looper 是如何關聯的?

 Looper 與 Thread 之間是通過 ThreadLocal 關聯的,這個可以看 Looper.prepare(quitAllowed)方法:

// Looper.java:93
private static void prepare(boolean quitAllowed) {
		//...
    sThreadLocal.set(new Looper(quitAllowed));//設置當前線程本地變量
}

 Looper 類有一個 ThreadLocal 類型的 sThreadLocal靜態屬性,Looper通過它的 get 和 set 方法來賦值和取值;
 由於 ThreadLocal是與當前線程是綁定的,所以我們只要把 Looper 與 ThreadLocal 綁定了,那 Looper 和 Thread 也就關聯上了;

(16) 如何在子線程中獲取當前線程的 Looper?

Looper.myLooper();//獲取當前線程的Looepr對象

 內部原理就是sThreadLocal.get()

// Looper.java:203
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

(16) 如何在任意線程獲取主線程的 Looper?

Looper.getMainLooper();//獲取主線程的Looper對象

 這個在我們開發 API時特別有用,畢竟你不知道開發者在使用你的API時會在哪個線程初始化Looper,所以我們在創建 Handler 時每次都通過指定主線程的 Looper 的方式保證API正常運行。所以一般使用主線程Looper來進行異步消息處理;

(17) 如何判斷當前線程是不是主線程?

//方式1
Looper.myLooper() == Looper.getMainLooper();
//方式2
Looper.getMainLooper().getThread() == Thread.currentThread();
//方式3:方式2簡化版
Looper.getMainLooper().isCurrentThread();

(18) Looper.loop() 方法會退出嗎?

 不會自動退出,但是我們可以手動調用 looper.quit()looper.quitSafely()方法退出消息輪詢
 這兩個方法都會調用MessageQueue#quit(boolean) 方法MessageQueue#mQuitting屬性置爲true,標記消息隊列已退出;消息隊列退出後,MessageQueue#next() 方法發現已經調用過 MessageQueue#quit(boolean) 時會 return null ;然後Looper.loop() 方法退出消息輪詢;

 如果looper.quit()looper.quitSafely(),MessageQueue#quit(boolean)都不手動調用並且消息隊列爲空,消息隊列不會退出;next()方法會一直死循環(有的說法稱爲next方法阻塞),loop()方法會在Message msg = queue.next();處阻塞等待新消息到達消息隊列,繼續消息輪詢。所以建議當所有Message都被處理完之後手動調用looper.quit()looper.quitSafely()方法退出消息輪詢,避免loop()方法一直阻塞等待

		//Looper.java#322
    public void quit() {//Looper退出
        mQueue.quit(false);
    }
    //Looper.java#338
    public void quitSafely() {
        mQueue.quit(true);
    }
    //Looper.java#137
    public static void loop() {//開啓消息輪詢
    		//...
        for (;;) {
            Message msg = queue.next(); // 當消息隊列爲空且未退出時,next方法會阻塞
            if (msg == null) {
                //next返回null表明消息隊列已退出
                return;//結束輪詢,loop方法唯一出口
            }
						//...
            msg.target.dispatchMessage(msg);
       			//...
        }
    }
    
    //MessageQueue.java#416
    void quit(boolean safe) {
    		//...
        mQuitting = true;
        //...
    }
    //MessageQueue.java#310
    Message next() {
        //...
        for (;;) {
       		 synchronized (this) {
								//...
                Message msg = mMessages;
								//...
                if (msg != null) {
                    if (now < msg.when) {
                    	//...
                    }else{
                    	//...
                    	return msg;//取到非空消息退出
                    }
                }else{
                   	// No more messages.
                    nextPollTimeoutMillis = -1;
                }
                //若消息隊列已退出,返回true退出死循環
                if (mQuitting) {
                    dispose();
                    return null;
                }
        }

(19) MessageQueue#next()方法在消息隊列爲空時會阻塞,如何恢復?

 使用Handler的sendMessage、post 等一系列方法發送消息,這些發送消息的方法會調用MessageQueue#enqueueMessage將新消息入隊,從而使得next()方法不再阻塞;

(20) IdleHandler作用和使用場景?

 把頁面啓動時的複雜邏輯交給IdleHandler去處理,這樣可以讓主線程Handler先處理完相關UI邏輯後再去處理複雜邏輯,可以減少頁面啓動白屏時間,從而優化頁面啓動

(21) 子線程爲何不能訪問UI?

  • 源碼角度:當訪問UI時,ViewRootImpl會調用checkThread()方法檢查當前線程是哪個線程,如果不是UI線程會拋出異常;
  • 線程安全角度:訪問UI不是線程安全的;
    • 訪問UI爲什麼不加鎖:邏輯複雜、效率低;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章