從源碼的角度來談談面試常客Handler的內部原理

 

 

前言

我們都知道,在進行Android應用開發的時候,主線程(又稱爲UI線程)不能進行網絡請求一類的耗時操作,必須開啓一個子線程來處理;但是在子線程裏面又不能進行更新UI的操作,更新UI必須在主線程裏操作。那麼當子線程進行完耗時操作時如何通知主線程更新UI吶?這個時候Handler就孕育而生了。

Handler被稱之爲Android內部消息機制,他的作用是在子線程進行完耗時操作的時發送消息通知主線程來更新UI。

 

使用

private Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        if (msg.what == 1){
            Toast.makeText(JavaDemo.this, "更新UI操作", Toast.LENGTH_SHORT).show();
        }
    }
};
​
@Override
protected void onCreate(Bundle savedInstanceState) {
​
​
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                handler.sendEmptyMessage(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
​
            }
        }
    }).start();
}

這裏採用實例化一個Thread線程並通過線程阻塞sleep()模擬耗時操作。我們可以從上面代碼看到,當線程完成耗時操作之後,我們使用sendEmptyMessage()將消息發送給主線程中的handler,覆寫主線程中handler的handMessage()方法並在這個方法裏進行更新UI的操作。這裏需要注意一點,其實我們這麼寫handler是錯誤的,會引發內存泄露的問題,具體如何引起的我們後面再分析。

 

源碼分析

handler的源碼主要是由LooperMessageQueueHandlerThreadLocal幾個部分組成。下面一個一個來進行分析。

 

Looper:

Looper主要由兩部分東西組成,prepare()和loop()。首先來看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));
}

可以看到,在prepare()中主要做了兩件事。第一,判斷ThreadLocal中是否能夠取出Looper對象,如果不爲空,則拋出"一個線程只能有一個Looper"的異常。這就代表說在一個線程裏有且僅可以創建一個Looper,如果多次調用prepare()方法創建Looper則程序會拋出異常。如果發現線程之中沒有Looper,那麼便會new一個Looper將其set進入ThreadLocal當中去。那麼這個ThreadLocal又是什麼?

 

ThreadLocal:

ThreadLocal被稱爲線程內部存儲類。他有一個特點就是在A線程裏面進行set()存儲的數據,只能在A線程get()取出。

final ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();
threadLocal.set(true);
​
new Thread("thread1"){
    @Override
    public void run() {
        
        Log.i("thread1",threadLocal.get() + "");
    }
}.start();

我們看到上面的例子,在主線程中將true放入了ThreadLocal中,之後在子線程試圖從中取出,結果發現此時報null。可見,在主線程中存儲的數據必須在主線程纔可以取出。那麼我們再從ThreadLocal內部代碼看看爲什麼會出現這種操作。

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}

我們看到set()方法中,首先會去獲取當前線程currentThread,之後通過values從當前線程中獲取數據。判斷這個數據是否爲空“if (values == null)”,爲空則調用initializeValues()方法賦初值,否則將獲取到的value值put()進入values中“values.put(this, value)”

 

接着來看get()方法。

public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }
​
        return (T) values.getAfterMiss(this);
    }

從代碼中可以看到,get()方法的操作其實和set()差不多,都是先獲取當前線程,如果values不爲空則將值返回,如果爲空則先賦初值然後再返回初始值。由於set()和get()方法都涉及到了從currentThread()中獲取數據,這也就解釋了爲什麼在一個線程中存儲數據必須要在相同線程中才能取的到的原因。上述只是對ThreadLocal這個類做簡單的分析,其實這個類內部還有很多東西,由於篇幅原因再加上ThreadLocal並非這篇文章重點,所以這裏我們只是簡單敘述,有機會專門寫一篇來講解ThreadLocal。

上面說了在判斷ThreadLocal中取出來的數據爲空時會去new一個Looper,並把他添加進ThreadLocal中,那我們來看看new出的這個Looper的構造方法。

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

構造方法非常簡單,裏面實例化一個消息隊列MessageQueue,並且還會獲取當前線程。也就是說消息隊列此時已經和當前線程綁定,其作用的區域爲當前實例化Looper的線程 。我們再來看loop()。

 

​
public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
}
​
​
public static void loop() {
    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) {
            // No message indicates that the message queue is quitting.
            return;
        }
​
        
        try {
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
​
       
​
        msg.recycleUnchecked();
    }
}

​
   

這裏對代碼進行了一些刪減。可以看到首先會調用myLooper()去獲取一個Looper對象。而從myLooper()的源碼看到從ThreadLocal裏取出在prepare()中存入的Looper對象。先判斷對象是否爲空,若爲空,則拋出異常告訴程序在調用loop()方法之前必須要有一個Looper。這也就說在使用的時候,prepare()方法必須要在loop()方法之前被調用

 

之後通過Looper對象獲取消息隊列MessageQueue,進入一個死循環for( ; ; ),調用MessageQueue的next()方法,不斷從消息隊列裏獲取消息Message,如果獲取的消息爲空,則return跳出循環,如果不爲空,則將msg消息交給msg.target.dispatchMessage(msg)去處理,那麼這個dispatchMessage()又是什麼,其實這個就是handler,不過我們後面再分析。最後調用recycleUnchecked()方法回收。到此Looper源碼分析完成。

 

Handler:

一般使用handler的時候,我們都會先new實例化一個handler對象,那麼我們就從handler的構造方法講起。

public Handler(Callback callback, boolean async) {
    
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

從構造方法我們可以看到,仍然是先調用myLooper()方法,從ThreadLocal中取出Looper對象,之後判斷對象是否爲空,爲空則拋異常,不爲空則獲取MessageQueue消息隊列,這樣Handler也就和消息隊裏進行了綁定。

之後在使用handler的時候一般都會使用sendMessage()方法去發送消息。看看這個方法內部做了什麼操作。

public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}
​
​
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
​
​
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);
}

可以看到裏面層層遞進,sendMessage()裏面調用sendMessageDelayed(),sendMessageDelayed()又調用了sendMessageAtTime(),最終發現其實所有的發送消息方法最後都會來到sendMessageAtTime()方法裏,於是着重看這個方法。這個方法裏先獲取消息隊列MessageQueue,然後將隊列queue、發送的消息msg以及延時時間uptimeMillis一起傳入到enqueueMessage()裏去。

 

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

在這個方法裏,又調用queue.enqueueMessage()方法將發射的消息傳入到消息隊列MessageQueue當中去。也就是說從handler中發送的消息其實最後全都送到了MessageQueue當中去,而之前在分析loop的時候我們看到,在loop裏面又調用了MessageQueue的next()方法,把裏面的消息全部交給dispatchMessage去處理。所以最後我們來看看dispatchMessage()方法

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

在handler裏面我們找到了這個方法。這個方法會根據是否有callback而去調用不同方法,如果有回調則調用handleCallback(),如果沒有回調則調用handleMessage();

 

public void handleMessage(Message msg) {
}

我們可以看到handleMessage()中是一個空方法,這就代表只要覆寫了這個方法所有的一切就全部由我們自己來寫邏輯了。

 

handler內部整體流程:

我們在使用handler的時候,若在主線程,由於主線程已經有一個Looper了,所以不需要在創建一個Looper(一個線程中有且僅可以有一個Looper,不然會報錯)。若在子線程,則先調用Looper.prepare()創建一個Looper對象,之後再實例化一個Handler對象,這個Handler會和Looper中的MessageQueue進行綁定,並將sendMessage()發送的消息存儲到這個綁定的消息隊列當中去。然後我們調用Looper.loop()方法,不斷的從消息隊列MessageQueue當中取出消息交給dispatchMessage()去處理。dispatchMessage()最後調用handleMessage(),所有的邏輯都交給我們自己去處理。到此,Handler內部原理全部講解完成。

 

內存泄露:

在文章開篇我寫了一個例子來演示handler如何使用,並且在最後說這麼使用會造成內存泄漏。那麼現在來講講爲什麼這麼寫會造成內存泄露。

在java中非靜態內部類和匿名內部類都會隱式持有當前類的外部類,由於Handler是非靜態內部類所以其持有當前Activity的隱式引用,如果Handler沒有被釋放,其所持有的外部引用也就是Activity也不可能被釋放,當一個對象不需要再使用了,本來該被回收時,而有另外一個正在使用的對象持有它的引用從而導致它不能被回收,這導致本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏。

 

解決辦法:

方法一:通過邏輯代碼處理來進行保護。

我們在關閉Activity的時候停掉你的後臺線程。線程停止掉了,就等於切斷了handler與外部的連接,那麼Activity在退出的時候就會被回收了。

如果你在寫一個驗證碼之類的倒數計時器,用到delay方法去發送消息的時候,我們在銷燬Activity的時候,應該在onDestroy()方法裏面調用removeCallbacks()將消息移除掉即可。

 

方法二:將Handler聲明爲靜態類

靜態內部類不會持有外部類的對象,並且爲了避免在靜態內部類中能夠使用到Activity對象,這裏我們採用弱引用的方式來持有Activity對象。這裏順帶說下弱引用(WeakReference),弱引用所持有的對象,不管Java內存是否滿了只要調用了GC就一定會被回收掉。所以我們將最早使用的handler代碼改造一下。

static class MyHandler extends Handler {
    WeakReference<Activity > mActivityReference;
​
    MyHandler(Activity activity) {
        mActivityReference= new WeakReference<Activity>(activity);
    }
​
    @Override
    public void handleMessage(Message msg) {
        final Activity activity = mActivityReference.get();
        if (activity != null) {
            mImageView.setImageBitmap(mBitmap);
        }
    }
}

可見到我們寫了一個靜態類來繼承Handler,並且在構造方法裏面用弱引用持有了傳遞進來的Activity對象,在handleMessage()方法裏面從弱引用中取出activity對象,如果activity對象不爲空,則直接進行更新UI的操作。

到此,handler所有的內容講解完畢!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章