Android_Handler源碼分析

什麼是Handler?

Handler主要用於異步消息的處理:當發出一個消息以後,首先進入一個消息隊列,發送消息的函數即刻返回,而另一個部分在消息隊列中逐一將消息取出,而後對消息進行處理git

相信大部分Android開發者對於Handler都有所瞭解,概念的知識就不作贅述,下面咱們主要是帶着幾個問題去分析(面試中常被問到的問題~)github

  • ① Handler是否存在內存泄漏?
  • ② 爲何不能在子線程建立Handler?
  • ③ textView.setText() 只能在主線程執行??
  • ④ new Handler() 兩種寫法有什麼區別?
  • ⑤ ThreadLocal 用法和原理

①首先第一個問題比較簡單,咱們直接測試下:

代碼也比較簡單,簡單說下,在MainActivity中建立了一個Handler,而且開啓了一個子線程,休眠5s後,handler發送一條消息,handler收到消息跳轉到SecondActivity,,貼下代碼面試

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

    //第一種方式建立handler
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //跳轉另外一個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();
    }

    //內存泄露測試,開啓一個線程,休眠5s後handler發送消息
    private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {
                Message message = new Message();
                message.what=123;//能夠不設置
                message.obj="並無銷燬";
                //休眠五秒鐘,假設是一些耗時操做
                SystemClock.sleep(5000);
                handler.sendMessage(message);
            }
        }).start();
    }

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

咱們的操做是,在休眠過程當中,點擊返回鍵,銷燬MainActivity,看下效果和日誌: api

Handler形成的內存泄漏.gif

日誌:bash

com.frizzle.handler E/HANDLER_TEST: onDestroy
複製代碼

咱們能夠看到,咱們點擊返回按鈕銷燬了,而且MainActivity觸發了onDestroy(),可是休眠結束,仍是跳轉了SecondActivity,因此這裏是存在內存泄漏的,而且很嚴重,看到這裏其實,不少小夥伴會說,在onDestroy()方法中調用handler.removeCallbacksAndMessages(123)不就能夠解決內存泄露的問題了,然而這麼作並無效果,仍是會形成內存泄漏,表現與上面一致,這是爲何呢?緣由是上述代碼的方式,handler會在休眠五秒結束以後以後,纔會sendMessage(),也就是將消息放進隊列queue,在message沒有被放入隊裏中時,調用handler.removeCallbacksAndMessages()是沒有實際意義的。 正確的處理方式舉例:異步

//內存泄露測試,開啓一個線程,休眠5s後handler1發送消息
    private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {
                Message message = new Message();
                message.what=123;//能夠不設置
                message.obj="並無銷燬";
                //休眠五秒鐘,假設是一些耗時操做
                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;
        }
    }
複製代碼

須要注意的是:若是發送消息是採用的是handler.sendMessageDelayed()的方式,在onDestroy()中經過handler.removeCallbacksAndMessages()是能夠已解決內存泄漏的問題的,由於handler.removeCallbacksAndMessages()會將消息放進隊列queue,可是handler.sendMessageDelayed()在開發中並不經常使用,由於耗時操做耗時多久一般是不肯定的,還有一點是Message對象的建立建議使用Message.obtain(),還有就是若是Message被定義爲全局變量的話,使用時也須要注意,好比以下方式會發生異常This message is already in use.:ide

//內存泄露測試,開啓一個線程,休眠5s後handler1發送消息
    private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {
                message = new Message();
                message.what=123;//能夠不設置
                message.obj="並無銷燬";
                //休眠五秒鐘,假設是一些耗時操做
                SystemClock.sleep(5000);
                if (handler1!=null) {
                    handler1.sendMessage(message);
                }
            }
        }).start();
    }

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

和上面內存泄漏的緣由相似~函數

②爲何不能在子線程中建立Handler?

這裏須要說明下,不是全部Android手機在子線程中new Handler()都會拋異常,好比華爲的部分手機改寫了源碼,並不會出現異常,這裏咱們主要關注出現異常的緣由,那麼出現異常的緣由是什麼?oop

  • 首先咱們要知道應用啓動時,ActivityThread是建立了一個主線程的Looper對象的,過程大體以下: 在應用啓動時建立開啓ActivityThread,在ActivityThreadmain()方法中調用了Looper.prepareMainLooper()方法,而後建立了一個Looper對象,這個Looper對象是存在主線程中的,而且調用了sThreadLocal.set(new Looper(quitAllowed)); sThreadLocal是存在在ThreadLocalMap中的,sThreadLocal在存和取的時候,調用的是ThreadLocalMapget()set()方法,而且key就是當前線程
  • 而後咱們在使用new Handler()系統作了什麼呢?

api的調用循序大概是這樣的: mLooper = Looper.myLooper()sThreadLocal.get() 由於子線程沒有建立Looper對象,因此已子線程做爲key找到的Looper對象爲null就會拋出異常post

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

注:在子線程建立Looper並開啓輪詢,這種方式能夠在子線程使用Handler,這種方式這裏不作討論~

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

首先咱們先寫一段測試代碼:

//開啓子線程
 private void leakTest() {
        new Thread(new Runnable(){
            @Override
            public void run() {

            }
        }).start();
    }
複製代碼

而後咱們在run()方法中寫幾行代碼,並記錄現象和日誌~

①直接改變TextView的文本內容

mTextView.setText("子線程更新文本內容");
複製代碼

現象: 華爲手機 : 沒有閃退,文本內容發生改變! 谷歌手機 : 沒有閃退,文本內容發生改變!

黑人問號臉
對上述有疑問的小夥伴請自行測試~ 在下面會分析緣由 ↓

②休眠一秒鐘,改變TextView的文本內容

SystemClock.sleep(1000);
mTextView.setText("子線程更新文本內容");
複製代碼

現象: 華爲手機 : 閃退 谷歌手機 : 閃退 閃退的日誌爲:

Only the original thread that created a view hierarchy can touch its views.
複製代碼

③彈Toast提示

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

現象: 華爲手機 : 部分閃退,部分沒有發生閃退,可是也不顯示Toast內容 谷歌手機 : 閃退 閃退的日誌爲:

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

根據第②點的日誌,能夠咱們能夠找到源碼中拋出異常的地方,在ViewRootImpl類的checkThread()方法:

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

對於子線程不能更新UI,小夥伴們應該都是比較瞭解的,這裏不作過多贅述,簡單說就是ViewViewGroup在更新UI時調用的invalidate()都會在ViewRootImpl中執行線程的檢查,如上,若是不是主線程,會直接拋異常。 注: TextView繼承自View實現了ViewParent接口,而ViewRootImpl是接口實現類,在ViewRootImplrequestLayout中調用checkThread()校驗線程 因此爲何第一種寫法不會拋異常呢? 緣由是: ViewRootImpl是在 Activity 建立對象完畢以後再建立對象的,若是咱們調用setText()等api的速度快於 ViewRootImpl對象的建立,就不會拋出異常!因此咱們直接調用不會異常,而子線程休眠一秒鐘以後就會拋出異常,對於第三種方式使用Toast的狀況,首先這種方式最終會調用,setText()的api,與上面兩種狀況相似,可是在這中間還有不少代碼要執行,至關於延遲了一段時間,更新UI的方法是在ViewRootImpl對象建立以後作的,因此會發生異常。 因此textView.setText() 只能在主線程執行這種說法太過絕對

④ new Handler() 兩種寫法有什麼區別?

建立Handler的兩種方式示例以下:

建立Handler的兩種方式

在Android Studio中使用第一種方式的話會自動加淺黃色背景,如上圖,由於這種方式並不推薦使用,咱們直接看下源碼中是如何使用的:

/**
     * 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);
        }
    }
複製代碼

二者的區別:

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

使用Hander切換主線程的實現方式: message.callback是主線程的Runnable對象,使用切換主線程其實就會調用了調用了主線程的Runnable的run()方法 這裏說的run()方法是Thread必須實現的run()方法,源碼以下:

private static void handleCallback(Message message) {
        message.callback.run();
    }
複製代碼

⑤ ThreadLocal 用法和原理

這個問題網上有不少文章是講解ThreadLocal 的用法和原理,有興趣的能夠去搜一下,這裏主要說下在使用的時候注意的問題:

① ThreadLocal 的使用key是線程,因此不一樣的線程調用set方法是互不影響的 ② 線程中使用ThreadLocal .set()方法使用完畢記得remove(),避免沒必要要的內存浪費~

Handler + Message原理

對於Handler + Message原理分析,網上有不少不少文章了,這裏主要就主要用流程圖來簡單介紹吧~ 咱們都知道要分析Handler + Message,離不開四個對象: HandlerMessageLooperMessageQueue

先看下運做的流程圖

運做流程

簡單來講:就是Handler發送消息處理消息(知識最少原則)

大體流程就是: 應用在啓動時,ActivityThread建立了一個主線程惟一的Looper對象,調用了Looper.loop()開啓了消息輪詢(死循環),而後Handler對象就能夠調用sendMessage()方法將消息壓入消息隊列,壓入的過程調用的就是equeueMessage()方法,Looper經過輪詢取出隊首的message(先進先出),而且調用message.target.dispatchMessage()方法分發消息,而message.target對象就是Handler,也就是回調了HandlerhandleMessage()方法

這裏有幾點要說明:

  • ① Handler的sendMessage()post()sendEmptyMessageAttime()等這些發送消息的api都會經過equeueMessage()將消息壓入消息隊列
  • ② 利用Handler的能夠切換主線程的緣由是 Message中有個變量callback是一個Runnable對象而且這個Runnable是在主線程當中的代碼以下,咱們能夠看到若是msg.callback != null最終就調用了它的run()方法,因此post()能實現線程的調度的緣由就在這裏
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();
    }
複製代碼

若是以爲上面的圖有點抽象的話,結合下面這種詳細的流程圖,可能更容易理解:

流程圖

到這裏差很少就分析完了,可是還有一個疑問沒有說明,既然在Looper.loop()中是一個死循環,爲何主線程不會ANR?

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

首先要明確一點,若是ActivityThread沒有在主線程調用Looper.loop(),ActivityThreadmain()方法執行完畢就退出了,這顯然是不符合實際狀況的

其實在Looper.next()開啓死循環的時候,一旦須要等待時或尚未執行到執行的時候, 會調用NDK裏面的JNI方法,釋放當前時間片,這樣就不會引起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));
            }
複製代碼

最後總結幾個相對重要的問題:

  • Q :爲何主線程用Looper死循環不會引起ANR異常? A : 由於在Looper.next()開啓死循環的時候,一旦須要等待時或尚未執行到執行的時候, 會調用NDK裏面的JNI方法釋放當前時間片,這樣就不會引起ANR異常了,同上~

  • Q :爲何Handler構造方法裏面的Looper不是直接new? A : 若是在Handler構造方法裏面new Looper,怕是沒法保證保證Looper惟一,只有用 Looper.prepare()才能保證惟一性, 具體去看prepare方法

  • Q : MessageQueue爲何要放在Looper私有構造方法初始化? A : 由於一個線程只綁定一個Looper, 因此在Looper構造方法裏面初始化就能夠保證mQueue也是 惟的Thread對應一個Looper 對應一個mQueue

  • Q :主線程裏面的Looper.prepare/Looper.loop, 是一直在無限循環裏面的嗎? A : yes

最最後附一下簡單實現Handler+Message機制代碼

注: 簡單模擬實現Handler機制的代碼在單元測試test包下 舒適提示:直接右鍵test包下的ActivityThread執行便可看到日誌 github

相關文章
相關標籤/搜索