從Handler.post(Runnable r)再一次梳理Android的消息機制(以及handler的內存泄露)

Handler

每個初學Android開發的都繞不開Handler這個“坎”,為什么說是個坎呢,首先這是Android架構的精髓之一,其次大部分人都是知其然卻不知其所以然。今天看到Handler.post這個方法之后決定再去翻翻源代碼梳理一下Handler的實現機制。

異步更新UI

先來一個必背口訣“主線程不做耗時操作,子線程不更新UI”,這個規定應該是初學必知的,那要怎么來解決口訣里的問題呢,這時候Handler就出現在我們面前了(AsyncTask也行,不過本質上還是對Handler的封裝),來一段經典常用代碼(這里忽略內存泄露問題,我們后面再說):

首先在Activity中新建一個handler:

private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 0:
                    mTestTV.setText("This is handleMessage");//更新UI
                    break;
            }
        }
    };

然后在子線程里發送消息:

new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);//在子線程有一段耗時操作,比如請求網絡
                    mHandler.sendEmptyMessage(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

至此完成了在子線程的耗時操作完成后在主線程異步更新UI,可是并沒有用上標題的post,我們再來看post的版本:

private Handler mHandler;//全局變量
@Override
protected void onCreate(Bundle savedInstanceState) {
    .......
    mHandler = new Handler();
    new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);//在子線程有一段耗時操作,比如請求網絡
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                mTestTV.setText("This is post");//更新UI
                            }
                        });
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
}

從表面上來看,給post方法傳了個Runnable,像是開了個子線程,可是在子線程里并不能更新UI啊,那么問題來了,這是怎么個情況呢?帶著這個疑惑,來翻翻Handler的源碼:

先來看看普通的sendEmptyMessage是什么樣子:

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

將我們傳入的參數封裝成了一個消息,然后調用sendMessageDelayed:

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

再調用sendMessageAtTime:

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

好了,我們再來看post():

public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);//getPostMessage方法是兩種發送消息的不同之處
    }

方法只有一句,內部實現和普通的sendMessage是一樣的,但是只有一點不同,那就是 getPostMessage(r) 這個方法:

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

這個方法我們發現也是將我們傳入的參數封裝成了一個消息,只是這次是m.callback = r,剛才是msg.what=what,至于Message的這些屬性就不看了

Android消息機制

看到這里,我們只是知道了post和sendMessage原理都是封裝成Message,但是還是不清楚Handler的整個機制是什么樣子,繼續探究下去。

剛才看到那兩個方法到最終都調用了sendMessageAtTime

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,看名字應該是把消息加入隊列的意思,點進去看下:

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

mAsynchronous這個異步有關的先不管,繼續將參數傳給了queue的enqueueMessage方法,至于那個msgtarget的賦值我們后面再看,現在繼續進入MessageQueue類的enqueueMessage方法,方法較長,我們看看關鍵的幾行:

Message prev;
for (;;) {
    prev = p;
    p = p.next;
    if (p == null || when < p.when) {
        break;
    }
    if (needWake && p.isAsynchronous()) {
        needWake = false;
    }
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;

果然像方法名說的一樣,一個無限循環將消息加入到消息隊列中(鏈表的形式),但是有放就有拿,這個消息怎樣把它取出來呢?

翻看MessageQueue的方法,我們找到了next(),代碼太長,不贅述,我們知道它是用來把消息取出來的就行了。不過這個方法是在什么地方調用的呢,不是在Handler中,我們找到了Looper這個關鍵人物,我叫他環形使者,專門負責從消息隊列中拿消息,關鍵代碼如下:

for (;;) {
     Message msg = queue.next(); // might block
     ...
     msg.target.dispatchMessage(msg);
     ...
     msg.recycleUnchecked();
}

簡單明了,我們看到了我們剛才說的msg.target,剛才在Handler中賦值了msg.target=this,所以我們來看Handler中的dispatchMessage:

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
  1. msg的callback不為空,調用handleCallback方法(message.callback.run()
  2. mCallback不為空,調用mCallback.handleMessage(msg)
  3. 最后如果其他都為空,執行Handler自身的 handleMessage(msg) 方法

msg的callback應該已經想到是什么了,就是我們通過Handler.post(Runnable r)傳入的Runnable的run方法,這里就要提提java基礎了,直接調用線程的run方法相當于是在一個普通的類調用方法,還是在當前線程執行,并不會開啟新的線程。

所以到了這里,我們解決了開始的疑惑,為什么在post中傳了個Runnable還是在主線程中可以更新UI

繼續看如果msg.callback為空的情況下的mCallback,這個要看看構造方法:

1.
public Handler() {
        this(null, false);
    }
2.    
public Handler(Callback callback) {
        this(callback, false);
    }
3.
public Handler(Looper looper) {
        this(looper, null, false);
    }
4.
public Handler(Looper looper, Callback callback) {
        this(looper, callback, false);
    }
5.
public Handler(boolean async) {
        this(null, async);
    }
6.
public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            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());
            }
        }

        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;
    }
7.
public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

具體的實現就只有最后兩個,已經知道mCallback是怎么來的了,在構造方法中傳入就行。

最后如果這兩個回調都為空的話就執行Handler自身的handleMessage(msg)方法,也就是我們熟知的新建Handler重寫的那個handleMessage方法。

Looper

看到了這里有一個疑惑,那就是我們在新建Handler的時候并沒有傳入任何參數,也沒有哪里顯示調用了Looper有關方法,那Looper的創建以及方法調用在哪里呢?其實這些東西Android本身已經幫我們做了,在程序入口ActivityThread的main方法里面我們可以找到:

 public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
    ...

總結

已經大概梳理了一下Handler的消息機制,以及post方法和我們常用的sendMessage方法的區別。來總結一下,主要涉及四個類Handler、Message、MessageQueue、Looper

新建Handler,通過sendMessage或者post發送消息,Handler調用sendMessageAtTimeMessage交給MessageQueue


MessageQueue.enqueueMessage方法將Message以鏈表的形式放入隊列中


Looperloop方法循環調用MessageQueue.next()取出消息,并且調用HandlerdispatchMessage來處理消息


dispatchMessage中,分別判斷msg.callback、mCallback也就是post方法或者構造方法傳入的不為空就執行他們的回調,如果都為空就執行我們最常用重寫的handleMessage


最后談談handler的內存泄露問題

再來看看我們的新建Handler的代碼:

private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            ...
        }
    };

當使用內部類(包括匿名類)來創建Handler的時候,Handler對象會隱式地持有Activity的引用。

而Handler通常會伴隨著一個耗時的后臺線程一起出現,這個后臺線程在任務執行完畢后發送消息去更新UI。然而,如果用戶在網絡請求過程中關閉了Activity,正常情況下,Activity不再被使用,它就有可能在GC檢查時被回收掉,但由于這時線程尚未執行完,而該線程持有Handler的引用(不然它怎么發消息給Handler?),這個Handler又持有Activity的引用,就導致該Activity無法被回收(即內存泄露),直到網絡請求結束。

另外,如果執行了Handler的postDelayed()方法,那么在設定的delay到達之前,會有一條MessageQueue -> Message -> Handler -> Activity的鏈,導致你的Activity被持有引用而無法被回收。

解決方法之一,使用弱引用:

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

推薦閱讀更多精彩內容