1. 程式人生 > >Handler、HandlerThread及handler記憶體洩漏

Handler、HandlerThread及handler記憶體洩漏

    在Android開發中,handler機制可以說是使用的最為頻繁的一種機
過,但是這些部落格都是看了就忘,沒有留下太深的印象,而且因為
是別人寫的東西,個人的理解不夠深入,這裡就結合原始碼和官方文
檔,做一個詳細一點的handler機制分析,加深印象,也便於自己以後
翻看。

Handler、Looper、MessageQueue和Message

handler原始碼分析:
建立handler:
handler物件的建立主要是提供4個成員變數:

public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;//這裡的MessageQueue實際為looper的一個屬性
mCallback = callback; mAsynchronous = async; }

從這4個成員變數的定義來看,其均被定義為final型別,即必須在物件建立時賦值:

    final MessageQueue mQueue;//繫結的MessageQueue
    final Looper mLooper;//繫結的Looper物件
    final Callback mCallback;//Callback中定義了handleMessage方法,可以通過實現Callback介面,new Handler(Callback)的方式例項化handler物件,避免寫成內部類,造成程式碼結構混亂,一般很少看到指定這個,直接繼承handler,重寫handleMessage即可
final boolean mAsynchronous;//看起來像一個標誌位,預設指定為false

ok,綜合分析下來,在例項化handler物件時,looper物件是必須指定的,但是,我們平常例項化Handler方式並沒有傳入looper物件,這是為什麼呢?原始碼中一下部分解釋了這個問題:

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()"
); }

從上面程式碼我們可以看出:這裡如果mLooper物件為空,直接丟擲異常。
而我們平常的寫法並不會丟擲異常,就只有一種解釋了:Looper.myLooper創建出了物件,或者將一個已經建立好的looper物件傳了進來。
Looper.myLooper原始碼如下:

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

sThreadLocal是一個ThreadLocal物件,在不同的執行緒中呼叫其get方法時,返回該執行緒對應的一個looper物件(可以暫時理解為一個key為thread,value為Looper的HashMap,實際情況比這個要複雜,後續我會再寫篇部落格對ThreadLocal原始碼進行分析)。
結合先前的結論,我們可以得出sThreadLocal.get()返回了不為null的Looper物件。
這裡我們直接結果論,在Android應用主執行緒啟動時(從原始碼上看是在Application例項化前),會自動建立主執行緒的Looper,這個過程在ActivityThread.main()函式中完成:

public static void main(String[] args) {
        .....
        Looper.prepareMainLooper();
        .....
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
}
public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

如果我們在子執行緒中進行直接新建Handler而不新建Handler物件,則會丟擲異常。
從上面的原始碼中可以看出,主執行緒自動建立handler物件且啟動之,主執行緒中的Looper物件是同步的,而且可以看出,主執行緒只能有一個Looper物件例項與之對應。如果多次新建,則會丟擲異常。其實,對所有執行緒均是如此。
Looper物件建立時(呼叫Looper.prepa()),均是通過 prepare(boolean quitAllowed)方法例項化。

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物件,則會丟擲異常。
OK,接下來我麼看handler例項化過程中的MessageQueue物件mQueue,其為mLooper物件中的一個屬性,在呼叫Looper.loop()方法時被使用,具體使用後續分析,這裡先看MessageQueue的資料結果結構.
MessageQueue用於管理Message物件(對新增的Message物件進行排序和提供下一個被處理的Message物件),雖然從名字上看是一個佇列,但是實際結果為一個連結串列結構,在其初始化方法中我們沒有辦法看出,但是從其next方法和enqueueMessage方法可以看出,這兩個方法中的操作都是典型的列表操作,不向佇列一般有嚴格的先進先出原則(因為訊息本身有延時屬性,後新增的訊息可能比先新增的佇列先出)。

接下來,Message類的結構:

  public int what;//標誌一個message型別
  public int arg1; 
  public int arg2;
  public Object obj;//提供一些屬性,使用者可以通過設定這些屬性值,用於傳遞具體資訊
  long when;//訊息應該發出的時間
  Bundle data;//message也可以用於傳遞bundle物件
  Handler target;//message應該傳遞給哪一個Handler物件,一個執行緒可以有多個handler物件,但是隻有一個looper物件
  Runnable callback;//使用handler.post(Runnable)方法時,會將對應的Runnable物件傳遞過來,用於之後的執行
  Message next;//指向下一條訊息
  private static final Object sPoolSync = new Object();//一個用於同步的物件,在對程序內的訊息池進行操作時使用
  private static Message sPool;//當前訊息池中用於返回的物件
  private static int sPoolSize = 0;//當前訊息池大小
  private static final int MAX_POOL_SIZE = 50;//最大訊息池大小

關於Message物件的結構大約就這麼多,一個需要注意的地方是訊息池。
呼叫recycle方法時,會將一個Message物件新增到訊息池中。

 public void recycle() {
        //清除物件的所有屬性值
        clearForRecycle();

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;//將當前物件新增到訊息池連結串列頭部,這個訊息池的結構應該是一個棧結構,後進先出
                sPoolSize++;
            }
        }
    }

呼叫obtain方法時,會從訊息池中取出一個Message物件,也即當前的sPool物件。

public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;//取出當前連結串列最前面的訊息
                sPool = m.next;
                m.next = null;//重置指標
                sPoolSize--;
                return m;
            }
        }
        return new Message();//如果當前訊息池中沒有訊息,則新建一個訊息物件
    }

主要的類已經分析的差不多了,這裡做一個基本的總結:
1. 建立handler物件需要Looper物件
2. 每個執行緒有且只有一個Looper物件,主執行緒自身已經建立了一個Looper物件,可以直接使用
3. 每個Looper物件中有一個MessageQueue物件,負責管理和提供Message物件,MessageQueue使用的是連結串列結構
4. Message物件指定了目標Handler物件,傳送時間和用於傳遞的資訊物件,Message管理著一個訊息池,用於回收利用Message物件,這個訊息池為棧結構

接下來,我們開始分析handler的工作流程:
使用handler傳送訊息有如下幾種方式:

//1.handler.sendMessage(Message msg)
public final boolean sendMessage(Message msg)\{
    return sendMessageDelayed(msg, 0);
}
// 2.handler.post(Runnable r)
public final boolean post(Runnable r){
       return  sendMessageDelayed(getPostMessage(r), 0);
}
// 3.Activity.runOnUiThread(Runnable action)
public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }

繼續點選原始碼檢視,所有的方式執行的語句為:

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

故,傳送訊息的方式為 enqueueMessage(queue, msg, uptimeMillis),其原始碼如下:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

均是通過呼叫mQueue.enqueueMessage(msg, uptimeMillis)實現.
我們再來看MessageQueue.enqueueMessage方法的實現:

boolean enqueueMessage(Message msg, long when) {
        //不能重複新增正在使用的msg
        if (msg.isInUse()) {
            throw new AndroidRuntimeException(msg + " This message is already in use.");
        }
        //msg必須有handler來接收
        if (msg.target == null) {
            throw new AndroidRuntimeException("Message must have a target.");
        }

        synchronized (this) {
            //正在退出是時不能新增訊息
            if (mQuitting) {
                RuntimeException e = new RuntimeException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                return false;
            }

            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                //連結串列中沒有訊息||新加入的訊息需要在連結串列最前方的訊息之前發出
                //將當前訊息物件新增到訊息池連結串列頭部
                msg.next = p;
                mMessages = msg;
                //如果當前looper被阻塞(之前沒訊息需要傳送),需要喚醒
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.

                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                // 根據時間,將當前訊息物件新增到連結串列中
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; //
                prev.next = msg;
            }
            // 需要喚醒時,將本線looper喚醒
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

ok,訊息已經發送出去了,接下來需要在handler.handleMessage()方法中獲取訊息並處理,那麼,訊息怎麼傳送過來呢,
這個就需要通過Looper的運行了,在ActivityThread的main方法中我們找到looper方法的執行方式如下:

    Looper.loop();

那麼玄機一定就在這了,其原始碼如下:

public static void loop() {
        //獲取當前執行緒的的Looper物件
        final Looper me = myLooper();
        //在呼叫之前,Looper物件必須存在
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        ......
        //死迴圈,通過queue.next()方法獲取訊息物件,並呼叫msg.target.dispatchMessage(msg)方法
        for (;;) {
            ......
            Message msg = queue.next();
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            msg.target.dispatchMessage(msg);
           ......
            //回收已經處理的訊息
            msg.recycle();
        }
    }

ok,這裡的核心就是queue.next()和msg.target.dispatchMessage(msg)了

    Message next() {
        ......
        //死迴圈,獲取連結串列中的訊息
        for (;;) {
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ......
                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) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    ......
                }
                ......
            }
            .....
    }

queue.next()就是從連結串列中取出已經已經可以傳送的訊息。
接下來看msg.target.dispatchMessage(msg),msg.target就是msg對應的handler物件。

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

這裡的訊息分發機制:
1. 如果msg有Runnable需要執行,呼叫其run方法執行(注意這裡只是呼叫run方法,不是啟動一個執行緒)
2. 如果指定了CallBack,呼叫其handleMessage方法
3. 呼叫handler的handleMessage方法
上述過程可以用如圖所示表示出來:

圖片引用自 Android<我所理解的Handler機制>

在上述分析中,我們得出下列結論:
1. Looper.loop()方法會一直迴圈取其對應的MessageQueue中的訊息物件並分發給其對應的handler
2. handleMessage方法在Looper對應的執行緒中呼叫
3. 這個訊息機制是一個命令模式的使用案例,message作為一個命令,其中儲存了命令型別(what)和內容(arg1,arg2,obj),以及命令接收者(target),handler把命令發出去,將命令的執行者設定為自己,追蹤在接收到命令後,做出相應反應

HandlerThread相關

HandlerThread的使用很簡單,建立HandlerThread物件,並將之與handler物件關聯,之後就不需要他了

HandlerThread handlerThread = new HandlerThread("handlerThread");
handlerThread.start(); 
Handler handler = new Handler(handlerThread.getLooper()){
    @Override
    public void handleMessage(Message msg){
    }
};
handler.sendEmptyMessage(0x01);

其實,handlerthread的意義就是建立了一個執行緒,使用這個執行緒的looper物件建立handler物件,之後該handler的handleMessage方法在HandlerThread物件中執行。
比如我有一個需要往磁碟寫log的程式,而且我不希望其在主執行緒中執行,這時我可以新建一個HandlerThread物件,使用其looper物件,將主執行緒中需要列印的訊息傳送到子執行緒中去執行(這與我們新建一個子執行緒,在其run方法中呼叫looper.perpare()方法後,再在run方法最後一句新增looper.loop()是一致的,事實上HandlerThread也是這麼實現的)。
其核心方法為run(繼承自Thread類,本身為一個執行緒)和getLooper方法:

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

run方法就是一個準備looper和執行looper的過程,getLooper就是返回執行緒Looper物件而已,這裡不做多述。

handler導致Activity記憶體洩漏

不想解釋得過於複雜,就這麼說吧,從上面的原始碼可以看出來,message應用了handler物件(通過target屬性),而handler為Activity的屬性,所以如果Activity被finish的時候,如果handler不能被回收,這Activity就不能被回收,在還有訊息沒有被處理(比如訊息指定在10分鐘後發出)的時候會出現這種情況。這就是handler導致Activity記憶體洩漏的問題原因。
為了避免這個問題,Android推薦的方式是將handler寫成靜態內部類,handler使用弱應用對Activity進行引用。
程式碼如下:
程式碼引用自:Android App 記憶體洩露之Handler


/**
 * 
 * 實現的主要功能。
 * 
 * @version 1.0.0
 * @author Abay Zhuang <br/>
 *         Create at 2014-7-28
 */
public class HandlerActivity2 extends Activity {

    private static final int MESSAGE_1 = 1;
    private static final int MESSAGE_2 = 2;
    private static final int MESSAGE_3 = 3;
    private final Handler mHandler = new MyHandler(this);

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler.sendMessageDelayed(Message.obtain(), 60000);
        // just finish this activity
        finish();
    }

    public void todo() {
    };

    private static class MyHandler extends Handler {
        private final WeakReference<HandlerActivity2> mActivity;

        public MyHandler(HandlerActivity2 activity) {
            mActivity = new WeakReference<HandlerActivity2>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            System.out.println(msg);
            if (mActivity.get() == null) {
                return;
            }
            mActivity.get().todo();
        }
    }

HandlerActivity2在finish後,因為mHandler對其為弱引用,所以可以正常回收,但是這是mHandler依然存在,所以在handleMessage中需要判斷其引用的HandlerActivity2是否還存在。
mHandler在離開HandlerActivity2就不需要了,但是作為靜態物件依然會存在,但是又不能將其nullfy掉(否則訊息到的時候就是空指標異常了),所以其會一直存活到應用結束。

@Override  
public void onDestroy() {  
    //  If null, all callbacks and messages will be removed.  
    mHandler.removeCallbacksAndMessages(null);  
}  

這樣會把target為mHandler的物件的訊息從佇列中移除掉,個人覺得這個方法更好
我不知道Google為什麼不推薦下面這種方式,而推薦實用靜態類,但是為了避免煩人的handlerleak警告,還是從了吧。