Handler的前世今生——面試篇

言不必當,極口稱是,
未交此人,故意底毀;
卑庸可恥,不足與論事。
——《冰鑑》


1. 背景

2020年註定是不平凡的一年,“金三銀四”怕是被疫情給變了性質,希望面試者都能進入自己心儀的公司。

今天面試了一個5年左右的Android開發者,感覺java基礎和Android知識都比較不錯。在面試Android崗位時,Handler總是繞不開的一個話題 (PS:如果一切順利,就不存在這篇文章啦)。

下面是我提問的問題:

  1. 大致講一下Handler的工作原理;
  2. 一個線程可以有多個Looper嗎? 怎麼保證的?
  3. send 和 post 這兩種發送消息的方式(剛纔面試者說到本質上post還是使用的send方式),你認爲兩者在使用場景上有什麼不同?

晴天霹靂…

原來還有人認爲Handler.post(Runnable) 這種方式是處理異步的,這是典型的形式主義錯誤。


2.再談post方式

Handler的post發送消息不是處理異步消息的。

2.1 post方式不是異步
 	/**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
    	// post 方式發送的消息
        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();
    }

通過上述代碼,我們發現此處並沒有出現Thread的身影,更沒有出現線程啓動的start()這足以證明post方式是同步方式,其Runnable中的代碼執行和Handler所在的線程一致。(PS:可以聯想Thread 不啓動start,直接調用run()的邏輯)


2.1 send 和 post方式的使用場景
2.1.1 send方式

結合我們在開發中的使用場景,如果我們發送消息的類型比較多,我們一般更傾向於使用send方式 ,然後結合下面的代碼根據情況判斷執行邏輯。(PS : 如果閱讀過源碼,我們會發現源碼中很多使用該種方式的)

private static Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 0:
                    //TODO 
                    break;
                case 1:
                	// TODO
                	break;
                ...
            }
        }
    };
2.1.2 post方式

與其認爲Post Runnable,倒不如直接理解爲Post 出 代碼塊

如果消息類型只有1個,使用上述代碼就顯得不太合理啦。這時候就體現出post的快捷方便啦。

new Handler().post(new Runnable() {
        @Override
        public void run() {
              //TODO 
        }
    });

相信大家在實際的開發中,也能體會到兩者用法上的稍許區別。

一定不要理解錯誤,即:不要在Handler.post() 或者 Handler.postDelay()中做耗時操作。

3. Handler 的內存泄漏

3.1 什麼是內存泄漏?

簡而言之,申請的內存,用完不能如數歸還甚至(無法歸還)

假如整個內存就是一家銀行(這裏的 “借” 指的是無息貸款啊)

  1. 正常現象: 借多少及時還多少;
  2. 內存泄漏:借多少還一部分(甚至不還) [不及時還款照樣是內存泄漏],(銀行倒閉了你纔有償還能力那還有個屁用)
  3. 內存溢出:沒有人能從銀行借出來錢,即 銀行倒閉

內存泄漏的最終結果就是內存溢出(Out of memory)

點擊至:知乎高贊

3.2 Handler的內存泄漏

在面試過程中,我發現有很大一部分的面試者,對於Handler的內存泄漏存在認識不足的情況,這裏特地總結一下,希望能夠幫助大家。
這裏不再對GC相關知識展開介紹。

Handler的內存泄漏問題主要發生在兩個場景:

  1. (匿名)內部類默認持有外部類的引用;
  2. 邏輯漏洞導致;

這裏簡要分析一下:

場景1: 在開發中存在這兩種寫法(PS:AS會自動提示我們可能發生內存泄漏)

This Handler class should be static or leaks might occur

a.匿名內部類的形式
	private Handler handler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            // TODO 
        }
    };

對於a形式,我的同事曾經直接在變量前加上static關鍵字,然後底氣十足的告訴我這樣可以吧… (有此想法的童鞋,麻煩再翻譯一下上面那段英文,關鍵是AS的提示依然健在)

b.內部類的形式
  class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // TODO 
        }
    }

相信大家總會遇見各種奇葩隊友,他知道將內部類聲明爲靜態的,所以出現了下面的局面(場面一度失控)

public class TestHandlerActivity extends AppCompatActivity {
	// 這裏應該是不得已而爲之,將其聲明爲靜態變量
    private static TextView tvHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_handler);
        tvHello = findViewById(R.id.tv_hello);
    }
	// 注意這裏:確實是靜態內部類
    static class TestHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            tvHello.setText("Hello");
        }
    }
    
 	@Override
    protected void onDestroy() {
        super.onDestroy();
        // 補救措施
        tvHello = null;
    }   
}

這種寫法就會導致所有的View全部要聲明爲static類型。最可恨的是:內存泄漏的問題將會更加嚴重。

所有的View在初始化的時候,都會有Context,即當前Activity。
此時將View設置爲static,若Activity生命週期結束後,該靜態變量沒有被回收,則持有Activity,將導致Activity無法被回收。所以內存泄漏就出現啦。
當然有解決辦法:在onDestory()中將該變量置爲空即可。

如果View過多,鬼見愁啊…

建議大家深入瞭解static關鍵字的含義

關於更多的內存泄漏場景:Android 內存優化


公佈最佳答案:

弱引用(WeakReference) : GC執行則必被回收。

關於Java的四種引用類型:強、軟、弱、虛

 static class MyHandler extends Handler {
        WeakReference<Activity> weakReference;

        public MyHandler(Activity activity) {
            weakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            
            TestHandlerActivity activity = (TestHandlerActivity) weakReference.get();
            // 有人對此有疑問
            if(activity != null){
                activity.tvHello.setText("Hello");
            }

        }
    }

可能有的同學會存在疑惑?如果GC回收了Activity,那更新UI的操作是不是就出問題啦。
這裏我們要明確一點,大家要搞明白在什麼情況下該Activity纔會被回收,自然就明白啦。(PS : 就好比銀行讓你還款也是在還款日讓你還而不是隨便的讓你還啊)

上面這種場景的引用鏈

執行耗時任務的線程 ----> Handler ----> Activity
該線程一定持有Handler的引用,否則無法發消息。


場景2: 邏輯漏洞問題

想象一下,銀行已經下班啦(Activity被銷燬),
有些人還在趕往銀行的路上——Handler的延時功能 sendMessageDelayed()postDelayed()
另一些人還在銀行門口的排隊呢—— MessageQueue中的待處理消息

引用鏈:

MessageQueue —> Message —> Handler —> Activity

解決辦法:

  @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除所有消息
        if(handler != null){
            handler.removeCallbacksAndMessages(null);
            handler = null;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章