Handler Message源碼分析

Lucky胡IP屬地: 云南
字數 1,670

Handler用于異步消息處理:
當發出一個消息之后,首先進入一個消息隊列,發送消息的函數同步返回,而另一個部分在消息隊列中逐一將消息取出,然后對消息進行處理。

1、Handler內存泄漏問題
2、在子線程創建Handler報錯Looper沒有prepare?
3、textview.setText()只能在主線程執行?有點問題
4、new Handler()的兩種寫法
5、ThreadLocal用法和原理

1、Handler引起的內存泄漏問題

如下代碼,當在子線程中休眠或做了耗時操作后,再用Handler發送消息。此時如果Activity已經destroy了,但是Handler仍然會發送消息到消息隊列里,產生了嚴重的內存泄漏。

        new Thread(new Runnable() {
            @Override
            public void run() {
                //內存泄漏問題
                Message message = new Message();
                message.obj = "胡軍";
                message.what = 2;
                SystemClock.sleep(3000);
                handler1.sendMessage(message);
            }
        }).start();

如何解決:
(1)用handler1.sendMessageDelayed(message,3000);發送延時消息。
在Activity被消耗后,將消息移除handler1.removeMessages(2);

(2)在Activity銷毀后,將Handler置空,這樣就不會在延時結束后調用sendMessage了。

2、為什么不能在子線程創建Handler

會報錯:Can't create handler inside thread that has not called Looper.prepare();

這里看看Handler的構造方法源碼:
public Handler(){}
public Handler(Callback callback){}

mLooper = Looper.myLooper();
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
...
}

也就是默認Looper是從當前Thread里獲取Looper。
但是在新生成的子線程里,并沒有生成Looper。這樣就會報錯。

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

在子線程里,需要先prepare,在當前thread寫入相應Looper到ThreadLocal里。

                Looper.prepare();
                Handler handler = new Handler();

而在主線程里,已經默認生成了一個Looper.
ActivityThread.java里的main()方法中:

        Looper.prepareMainLooper();

而Looper里面有個static的變量sMainLooper用來存儲主線程的Looper,任何時候都能獲取到該主線程Looper。
看Looper.prepareMainLooper();都做了什么:
首先調用了prapare方法,生成了一個Looper放入ThreadLocal里,這是用來存儲和取出和線程相關的變量的,之后會進行詳細說明。這里將生成的主線程Looper放入ThreadLocal后,然后利用myLooper()方法,即從當前線程取出Looper,主線程里就是主線程Looper,然后賦值給sMainLooper。

        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    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));
    }
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

3、textview.setText()只能在主線程執行?有點問題

如下,在onCreate()里調用,生成的子線程里setText()是沒有報錯,且執行成功的了。為什么呢?

        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "run: "+Thread.currentThread().getName());
                textView.setText("胡軍");
            }
        }).start();

分析下setText()的源碼:
setText() --> checkForRelayout() -->requestLayout() --> mParent.requestLayout();
而這個mParent.requestLayout();調用的是父類ViewParent的requestLayout()方法。

接口ViewParent的實現類ViewRootImpl,里面有requestLayout()的實現:
requestLayout() --> checkThread()

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

這里,mThread是在ViewParent初始化時設置的

        mThread = Thread.currentThread();

所以,"Only the original thread that created a view hierarchy can touch its views."錯誤并不是指必須在UI主線程調用setText(),而是需要在創建ViewParent的線程里調用setText()。
我們使用的ViewParent都是在主線程調用的,所以setText()就需要在主線程中調用。

當setText()足夠快,在檢查線程前就更新完成,則不會報錯。

4、new Handler()的兩種寫法

    //Handler有下面兩種構造方式
    private Handler handler1 = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            return false;
        }
    });

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

看源碼:
在啟動一個Thread后,會生成一個Looper循環。
比如主線程里,有

Looper.prepareMainLooper();
...
Looper.loop();

在loop()方法里,啟動了一個無限輪訓的獲取Message隊列的循環。

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
...
}

當取到Message后,會調用

msg.target.dispatchMessage(msg);

這里,Target就是發送這個Message的Handler,在調用Handler發送Message時,會將Message的Target設置為發送的Handler。

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

首先會回調msg自己的Callback,沒有就回調初始化Handler時給的Callback,再不行才回調重寫的Handler的handleMessage()方法。

其中,msg自己的Callback,是調用Handler的post方法時,設置給Message的。就完成了Handler.post(runnable)里的run回調。
這里有個知識點,調用Activity.runOnUIThread(),其實內部就是用主線程的Handler.post()方法實現的。

5、ThreadLocal用法和原理

作用是把參數存儲在線程相關的map里,在不同的線程里存儲,當在相應的線程里能讀取出當前線程存儲的參數。

下面的例子里,在ThreadLocal里存儲String,則在不同的線程里存儲不同的String,在不同的線程里,就能讀取出這個線程存儲的String。

        val threadLocal = object : ThreadLocal<String>() {
            override fun initialValue(): String? {
                return "默認值"
            }
        }
        threadLocal.set("胡軍")
        println("當前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")

        Thread(Runnable {
            println("當前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")
            //當使用完成后,建議remove掉。否則不用的線程越來越多,占用內存越來越多
            threadLocal.remove()
        },"子線程1").start()

        Thread(Runnable {
            threadLocal.set("胡軍2")
            println("當前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")

            //當使用完成后,建議remove掉。否則不用的線程越來越多,占用內存越來越多
            threadLocal.remove()
        },"子線程2").start()

ThreadLocal源碼分析

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

在Looper里,保存了一個ThreadLocal和主線程的Looper,并且都是全局唯一靜態的。Looper就是用ThreadLocal來保存不同線程里的Looper的。

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static Looper sMainLooper;  // guarded by Looper.class

Handler+Message的原理分析

Handler+Message原理圖

1、首先看主線程:
應用啟動時,在主線程ActivityThread.class里找到main()方法
ActivityThread.main() --> Looper.prepareMainLooper() --> Looper.prepare() --> Looper.sThreadLocal.set(new Looper(quitAllowed)); --> Looper.sMainLooper = myLooper();

這里就完成了主線程的Looper的生成。之后在主線程里都是調用
Looper.sMainLooper作為主線程Looper。

2、看Looper代碼
構造方法:

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

初始化Looper的消息隊列mQueue,以及賦值給該Looper運行的線程mThread.
由于主線程Looper只有一個,所以整個主線程只有一個消息隊列mQueue。

3、看Handler代碼
構造方法:

Handler(){}
Handler(Callback callback){}
Handler(Looper looper){}
Handler(boolean async){}
Handler(Callback callback, boolean async){}
Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async){}

有一堆構造方法,如果不指定Looper,則會從Looper.myLooper()獲取。
所以當當前Thread并沒有Looper時,則找不到Looper,會報錯。只有在當前Thread里調用了Looper.prerare()才可以找到Looper。

發送消息(存儲消息):
Handler有很多個發送消息的方法,

sendMessage(@NonNull Message msg)
sendMessageDelayed(@NonNull Message msg, long delayMillis)
sendEmptyMessage(int what)

//post方法,將Runnable賦予Message
post(@NonNull Runnable r)
postDelayed(@NonNull Runnable r, long delayMillis)

實際上,上面所有的發送方法,最后都調用了一個方法:

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

最后調用了MessageQueue的enqueueMessage()方法。

boolean enqueueMessage(Message msg, long when) {

//如果是第一條消息,賦值給MessageQueue里的變量mMessages
mMessages = msg;

//如果不是第一條,或者需要插入之前的消息前面,
//利用message的next來形成鏈式的排列。

}

取出消息(消費)
調用Looper.loop()方法后,就開始不斷輪訓消息隊列。
其中,主線程的Looper在ActivityThread里main()方法中,

main(){
...
        Looper.prepareMainLooper();
...
        Looper.loop();
}

分析loop()方法

public static void loop() {
//拿到該線程里的Looper
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
//拿到Looper里的MessageQueue
        final MessageQueue queue = me.mQueue;

...
//開始不斷輪訓MessageQueue
        for (;;) {
//首先拿到Queue里的Message
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
...
try {
                msg.target.dispatchMessage(msg);
...}
}
}

上面分析得到,所有的消息最后在handleMessage或在Callback的run方法里執行,而執行的線程就是loop()方法被調用時處于的線程。
主程序里調用了loop()方法,所有的主線程消息都在主線程中運行了。

問題
loop()啟動了一個無限死循環,如何避免導致anr呢?
里面有挺好的垃圾回收機制,開始前調用了Binder.clearCallingIdentity();
循環中調用了

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

一旦需要等待時,或還沒有執行到執行的時候,會調用NDK里面的JNI方法,釋放當前時間片,這樣就不會引發ANR異常了。

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