Android_Handler源碼分析

什么是Handler?

Handler主要用于異步消息的處理:當(dāng)發(fā)出一個(gè)消息之后,首先進(jìn)入一個(gè)消息隊(duì)列,發(fā)送消息的函數(shù)即刻返回,而另外一個(gè)部分在消息隊(duì)列中逐一將消息取出,然后對(duì)消息進(jìn)行處理

相信大部分Android開(kāi)發(fā)者對(duì)于Handler都有所了解,概念的知識(shí)就不做贅述,下面我們主要是帶著幾個(gè)問(wèn)題去分析(面試中常被問(wèn)到的問(wèn)題~)

  • ① Handler是否存在內(nèi)存泄漏?
  • ② 為什么不能在子線程創(chuàng)建Handler?
  • ③ textView.setText() 只能在主線程執(zhí)行??
  • ④ new Handler() 兩種寫(xiě)法有什么區(qū)別?
  • ⑤ ThreadLocal 用法和原理

①首先第一個(gè)問(wèn)題比較簡(jiǎn)單,我們直接測(cè)試下:

代碼也比較簡(jiǎn)單,簡(jiǎn)單說(shuō)下,在MainActivity中創(chuàng)建了一個(gè)Handler,并且開(kāi)啟了一個(gè)子線程,休眠5s后,handler發(fā)送一條消息,handler收到消息跳轉(zhuǎn)到SecondActivity,,貼下代碼

  private static final String TAG="HANDLER_TEST";
    private TextView mTextView;

    //第一種方式創(chuàng)建handler
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //跳轉(zhuǎn)另一個(gè)Activity
            startActivity(new Intent(MainActivity.this,SecondActivity.class));
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.tv);
        leakTest();
    }

    //內(nèi)存泄露測(cè)試,開(kāi)啟一個(gè)線程,休眠5s后handler發(fā)送消息
    private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {
                Message message = new Message();
                message.what=123;//可以不設(shè)置
                message.obj="并沒(méi)有銷(xiāo)毀";
                //休眠五秒鐘,假設(shè)是一些耗時(shí)操作
                SystemClock.sleep(5000);
                handler.sendMessage(message);
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e(TAG,"onDestroy");
    }

我們的操作是,在休眠過(guò)程中,點(diǎn)擊返回鍵,銷(xiāo)毀MainActivity,看下效果和日志:

Handler造成的內(nèi)存泄漏.gif

日志:

com.frizzle.handler E/HANDLER_TEST: onDestroy

我們可以看到,我們點(diǎn)擊返回按鈕銷(xiāo)毀了,并且MainActivity觸發(fā)了onDestroy(),但是休眠結(jié)束,還是跳轉(zhuǎn)了SecondActivity,所以這里是存在內(nèi)存泄漏的,并且很?chē)?yán)重,看到這里其實(shí),很多小伙伴會(huì)說(shuō),在onDestroy()方法中調(diào)用handler.removeCallbacksAndMessages(123)不就可以解決內(nèi)存泄露的問(wèn)題了,然而這么做并沒(méi)有效果,還是會(huì)造成內(nèi)存泄漏,表現(xiàn)與上面一致,這是為什么呢?原因是上述代碼的方式,handler會(huì)在休眠五秒結(jié)束之后之后,才會(huì)sendMessage(),也就是將消息放進(jìn)隊(duì)列queue,在message沒(méi)有被放入隊(duì)里中時(shí),調(diào)用handler.removeCallbacksAndMessages()是沒(méi)有實(shí)際意義的三妈。
正確的處理方式舉例:

 //內(nèi)存泄露測(cè)試,開(kāi)啟一個(gè)線程,休眠5s后handler1發(fā)送消息
    private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {
                Message message = new Message();
                message.what=123;//可以不設(shè)置
                message.obj="并沒(méi)有銷(xiāo)毀";
                //休眠五秒鐘,假設(shè)是一些耗時(shí)操作
                SystemClock.sleep(5000);
                if (handler!=null) {
                    handler.sendMessage(message);
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e(TAG,"onDestroy");
        if (handler!=null) {
            handler.removeCallbacksAndMessages(123);
            handler=null;
        }
    }

需要注意的是:如果發(fā)送消息是采用的是handler.sendMessageDelayed()的方式,在onDestroy()中通過(guò)handler.removeCallbacksAndMessages()是可以已解決內(nèi)存泄漏的問(wèn)題的,因?yàn)?code>handler.removeCallbacksAndMessages()會(huì)將消息放進(jìn)隊(duì)列queue,但是handler.sendMessageDelayed()在開(kāi)發(fā)中并不常用,因?yàn)楹臅r(shí)操作耗時(shí)多久通常是不確定的,還有一點(diǎn)是Message對(duì)象的創(chuàng)建建議使用Message.obtain(),還有就是如果Message被定義為全局變量的話,使用時(shí)也需要注意,比如如下方式會(huì)發(fā)生異常This message is already in use.:

  //內(nèi)存泄露測(cè)試,開(kāi)啟一個(gè)線程,休眠5s后handler1發(fā)送消息
    private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {
                message = new Message();
                message.what=123;//可以不設(shè)置
                message.obj="并沒(méi)有銷(xiāo)毀";
                //休眠五秒鐘,假設(shè)是一些耗時(shí)操作
                SystemClock.sleep(5000);
                if (handler1!=null) {
                    handler1.sendMessage(message);
                }
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e(TAG,"onDestroy");
        message.recycle();
    }

和上面內(nèi)存泄漏的原因類(lèi)似~

②為什么不能在子線程中創(chuàng)建Handler?

這里需要說(shuō)明下,不是所有Android手機(jī)在子線程中new Handler()都會(huì)拋異常,比如華為的部分手機(jī)改寫(xiě)了源碼,并不會(huì)出現(xiàn)異常,這里我們主要關(guān)注出現(xiàn)異常的原因,那么出現(xiàn)異常的原因是什么?

  • 首先我們要知道應(yīng)用啟動(dòng)時(shí),ActivityThread是創(chuàng)建了一個(gè)主線程的Looper對(duì)象的,過(guò)程大致如下:
    在應(yīng)用啟動(dòng)時(shí)創(chuàng)建開(kāi)啟ActivityThread,在ActivityThreadmain()方法中調(diào)用了Looper.prepareMainLooper()方法,然后創(chuàng)建了一個(gè)Looper對(duì)象,這個(gè)Looper對(duì)象是存在主線程中的,并且調(diào)用了sThreadLocal.set(new Looper(quitAllowed)); sThreadLocal是存在在ThreadLocalMap中的,sThreadLocal在存和取的時(shí)候,調(diào)用的是ThreadLocalMapget()set()方法,并且key就是當(dāng)前線程
  • 然后我們?cè)谑褂?code>new Handler()系統(tǒng)做了什么呢?

api的調(diào)用循序大概是這樣的: mLooper = Looper.myLooper()sThreadLocal.get() 因?yàn)樽泳€程沒(méi)有創(chuàng)建Looper對(duì)象,所以已子線程作為key找到的Looper對(duì)象為null就會(huì)拋出異常

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

注:在子線程創(chuàng)建Looper并開(kāi)啟輪詢(xún),這種方式可以在子線程使用Handler,這種方式這里不做討論~

③textView.setText() 只能在主線程執(zhí)行??

首先我們先寫(xiě)一段測(cè)試代碼:

//開(kāi)啟子線程
 private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {

            }
        }).start();
    }

然后我們?cè)?code>run()方法中寫(xiě)幾行代碼,并記錄現(xiàn)象和日志~

①直接改變TextView的文本內(nèi)容

mTextView.setText("子線程更新文本內(nèi)容");

現(xiàn)象:
華為手機(jī) : 沒(méi)有閃退,文本內(nèi)容發(fā)生改變!
谷歌手機(jī) : 沒(méi)有閃退,文本內(nèi)容發(fā)生改變!

黑人問(wèn)號(hào)臉

對(duì)上述有疑問(wèn)的小伙伴請(qǐng)自行測(cè)試~
在下面會(huì)分析原因 ↓

②休眠一秒鐘,改變TextView的文本內(nèi)容

SystemClock.sleep(1000);
mTextView.setText("子線程更新文本內(nèi)容");

現(xiàn)象:
華為手機(jī) : 閃退
谷歌手機(jī) : 閃退
閃退的日志為:

Only the original thread that created a view hierarchy can touch its views.

③彈Toast提示

Toast.makeText(MainActivity.this,"子線程彈吐司",Toast.LENGTH_SHORT).show();

現(xiàn)象:
華為手機(jī) : 部分閃退,部分沒(méi)有發(fā)生閃退,但是也不顯示Toast內(nèi)容
谷歌手機(jī) : 閃退
閃退的日志為:

Can't toast on a thread that has not called Looper.prepare()

根據(jù)第②點(diǎn)的日志,可以我們可以找到源碼中拋出異常的地方,在ViewRootImpl類(lèi)的checkThread()方法:

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

對(duì)于子線程不能更新UI,小伙伴們應(yīng)該都是比較了解的,這里不做過(guò)多贅述,簡(jiǎn)單說(shuō)就是ViewViewGroup在更新UI時(shí)調(diào)用的invalidate()都會(huì)在ViewRootImpl中執(zhí)行線程的檢查,如上,如果不是主線程,會(huì)直接拋異常颗味。
注: TextView繼承自View實(shí)現(xiàn)了ViewParent接口,而ViewRootImpl是接口實(shí)現(xiàn)類(lèi),在ViewRootImplrequestLayout中調(diào)用checkThread()校驗(yàn)線程
所以為什么第一種寫(xiě)法不會(huì)拋異常呢?
原因是: ViewRootImpl是在 Activity 創(chuàng)建對(duì)象完畢之后再創(chuàng)建對(duì)象的,如果我們調(diào)用setText()等api的速度快于 ViewRootImpl對(duì)象的創(chuàng)建,就不會(huì)拋出異常!所以我們直接調(diào)用不會(huì)異常,而子線程休眠一秒鐘之后就會(huì)拋出異常,對(duì)于第三種方式使用Toast的情況,首先這種方式最終會(huì)調(diào)用,setText()的api,與上面兩種情況類(lèi)似,但是在這中間還有很多代碼要執(zhí)行,相當(dāng)于延遲了一段時(shí)間,更新UI的方法是在ViewRootImpl對(duì)象創(chuàng)建之后做的,所以會(huì)發(fā)生異常稿黄。
所以textView.setText() 只能在主線程執(zhí)行這種說(shuō)法太過(guò)絕對(duì)

④ new Handler() 兩種寫(xiě)法有什么區(qū)別?

創(chuàng)建Handler的兩種方式示例如下:


創(chuàng)建Handler的兩種方式

在Android Studio中使用第一種方式的話會(huì)自動(dòng)加淺黃色背景,如上圖,因?yàn)檫@種方式并不推薦使用,我們直接看下源碼中是如何使用的:

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

兩者的區(qū)別:

第一種重寫(xiě)的handleMessage()方法是Handler對(duì)外提供可重寫(xiě)的方法
第二種重寫(xiě)的handleMessage()方法是Handler.ClaaBack接口的重寫(xiě)方法

使用Hander切換主線程的實(shí)現(xiàn)方式:
message.callback是主線程的Runnable對(duì)象,使用切換主線程其實(shí)就會(huì)調(diào)用了調(diào)用了主線程的Runnable的run()方法
這里說(shuō)的run()方法是Thread必須實(shí)現(xiàn)的run()方法,源碼如下:

  private static void handleCallback(Message message) {
        message.callback.run();
    }

⑤ ThreadLocal 用法和原理

這個(gè)問(wèn)題網(wǎng)上有很多文章是講解ThreadLocal 的用法和原理,有興趣的可以去搜一下,這里主要說(shuō)下在使用的時(shí)候注意的問(wèn)題:

① ThreadLocal 的使用key是線程,所以不同的線程調(diào)用set方法是互不影響的
② 線程中使用ThreadLocal .set()方法使用完畢記得remove(),避免不必要的內(nèi)存浪費(fèi)~

Handler + Message原理

對(duì)于Handler + Message原理分析,網(wǎng)上有很多很多文章了,這里主要就主要用流程圖來(lái)簡(jiǎn)單介紹吧~
我們都知道要分析Handler + Message,離不開(kāi)四個(gè)對(duì)象:
Handler 党觅、 Message 黄娘、Looper 尔苦、 MessageQueue

先看下運(yùn)作的流程圖


運(yùn)作流程

簡(jiǎn)單來(lái)說(shuō):就是Handler發(fā)送消息處理消息(知識(shí)最少原則)

大致流程就是: 應(yīng)用在啟動(dòng)時(shí),ActivityThread創(chuàng)建了一個(gè)主線程唯一的Looper對(duì)象,調(diào)用了Looper.loop()開(kāi)啟了消息輪詢(xún)(死循環(huán)),然后Handler對(duì)象就可以調(diào)用sendMessage()方法將消息壓入消息隊(duì)列,壓入的過(guò)程調(diào)用的就是equeueMessage()方法,Looper通過(guò)輪詢(xún)?nèi)〕鲫?duì)首的message(先進(jìn)先出),并且調(diào)用message.target.dispatchMessage()方法分發(fā)消息,而message.target對(duì)象就是Handler,也就是回調(diào)了HandlerhandleMessage()方法

這里有幾點(diǎn)要說(shuō)明:

  • ① Handler的sendMessage()慎皱、post()铡买、sendEmptyMessageAttime()等這些發(fā)送消息的api都會(huì)通過(guò)equeueMessage()將消息壓入消息隊(duì)列
  • ② 利用Handler的可以切換主線程的原因是 Message中有個(gè)變量callback是一個(gè)Runnable對(duì)象并且這個(gè)Runnable是在主線程當(dāng)中的代碼如下,我們可以看到如果msg.callback != null最終就調(diào)用了它的run()方法,所以post()能實(shí)現(xiàn)線程的調(diào)度的原因就在這里
   public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

 private static void handleCallback(Message message) {
        message.callback.run();
    }

如果覺(jué)得上面的圖有點(diǎn)抽象的話,結(jié)合下面這種詳細(xì)的流程圖,可能更容易理解:


流程圖

到這里差不多就分析完了,但是還有一個(gè)疑問(wèn)沒(méi)有說(shuō)明,既然在Looper.loop()中是一個(gè)死循環(huán),為什么主線程不會(huì)ANR?

//這里就貼了幾行代碼,相信大部分小伙伴都看過(guò)~
for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
        .....
}

首先要明確一點(diǎn),如果ActivityThread沒(méi)有在主線程調(diào)用Looper.loop(),ActivityThreadmain()方法執(zhí)行完畢就退出了,這顯然是不符合實(shí)際情況的

其實(shí)在Looper.next()開(kāi)啟死循環(huán)的時(shí)候,一旦需要等待時(shí)或還沒(méi)有執(zhí)行到執(zhí)行的時(shí)候搬素,
會(huì)調(diào)用NDK里面的JNI方法呵晨,釋放當(dāng)前時(shí)間片,這樣就不會(huì)引發(fā)ANR異常了代碼大致如下:

  • Binder.clearCallingIdentity()
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
  • Trace.traceBegin(traceTag, msg.target.getTraceName(msg))
  if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

最后總結(jié)幾個(gè)相對(duì)重要的問(wèn)題:

  • Q :為什么主線程用Looper死循環(huán)不會(huì)引發(fā)ANR異常?
    A : 因?yàn)樵贚ooper.next()開(kāi)啟死循環(huán)的時(shí)候熬尺,一旦需要等待時(shí)或還沒(méi)有執(zhí)行到執(zhí)行的時(shí)候摸屠,
    會(huì)調(diào)用NDK里面的JNI方法釋放當(dāng)前時(shí)間片,這樣就不會(huì)引發(fā)ANR異常了,同上~
  • Q :為什么Handler構(gòu)造方法里面的Looper不是直接new?
    A : 如果在Handler構(gòu)造方法里面new Looper,怕是無(wú)法保證保證Looper唯一,只有用
    Looper.prepare()才能保證唯一性粱哼, 具體去看prepare方法

  • Q : MessageQueue為什么要放在Looper私有構(gòu)造方法初始化?
    A : 因?yàn)橐粋€(gè)線程只綁定一個(gè)Looper, 所以在Looper構(gòu)造方法里面初始化就可以保證mQueue也是
    唯的Thread對(duì)應(yīng)一個(gè)Looper 對(duì)應(yīng)一個(gè)mQueue

  • Q :主線程里面的Looper.prepare/Looper.loop, 是一直在無(wú)限循環(huán)里面的嗎?
    A : yes

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末季二,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子皂吮,更是在濱河造成了極大的恐慌戒傻,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜂筹,死亡現(xiàn)場(chǎng)離奇詭異需纳,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)艺挪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)不翩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)兵扬,“玉大人,你說(shuō)我怎么就攤上這事口蝠∑髦樱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵妙蔗,是天一觀的道長(zhǎng)傲霸。 經(jīng)常有香客問(wèn)我,道長(zhǎng)眉反,這世上最難降的妖魔是什么昙啄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮寸五,結(jié)果婚禮上梳凛,老公的妹妹穿的比我還像新娘。我一直安慰自己梳杏,他們只是感情好韧拒,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著十性,像睡著了一般叛溢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烁试,一...
    開(kāi)封第一講書(shū)人閱讀 52,696評(píng)論 1 312
  • 那天雇初,我揣著相機(jī)與錄音,去河邊找鬼减响。 笑死,一個(gè)胖子當(dāng)著我的面吹牛郭怪,可吹牛的內(nèi)容都是我干的支示。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鄙才,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼颂鸿!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起攒庵,我...
    開(kāi)封第一講書(shū)人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嘴纺,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后浓冒,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體栽渴,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年稳懒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闲擦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墅冷,靈堂內(nèi)的尸體忽然破棺而出纯路,到底是詐尸還是另有隱情,我是刑警寧澤寞忿,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布驰唬,位于F島的核電站,受9級(jí)特大地震影響腔彰,放射性物質(zhì)發(fā)生泄漏定嗓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一萍桌、第九天 我趴在偏房一處隱蔽的房頂上張望宵溅。 院中可真熱鬧,春花似錦上炎、人聲如沸恃逻。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寇损。三九已至,卻和暖如春裳食,著一層夾襖步出監(jiān)牢的瞬間矛市,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工诲祸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浊吏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓救氯,卻偏偏與公主長(zhǎng)得像找田,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子着憨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361