言不必當,極口稱是,
未交此人,故意底毀;
卑庸可恥,不足與論事。
——《冰鑑》
1. 背景
2020年註定是不平凡的一年,“金三銀四”怕是被疫情給變了性質,希望面試者都能進入自己心儀的公司。
今天面試了一個5年左右的Android開發者,感覺java基礎和Android知識都比較不錯。在面試Android崗位時,Handler總是繞不開的一個話題 (PS:如果一切順利,就不存在這篇文章啦)。
下面是我提問的問題:
- 大致講一下Handler的工作原理;
- 一個線程可以有多個Looper嗎? 怎麼保證的?
- 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 什麼是內存泄漏?
簡而言之,申請的內存,用完不能如數歸還甚至(無法歸還)
假如整個內存就是一家銀行(這裏的 “借” 指的是無息貸款啊)
- 正常現象: 借多少及時還多少;
- 內存泄漏:借多少還一部分(甚至不還) [不及時還款照樣是內存泄漏],(銀行倒閉了你纔有償還能力那還有個屁用)
- 內存溢出:沒有人能從銀行借出來錢,即 銀行倒閉
內存泄漏的最終結果就是內存溢出(Out of memory)
點擊至:知乎高贊
3.2 Handler的內存泄漏
在面試過程中,我發現有很大一部分的面試者,對於Handler的內存泄漏存在認識不足的情況,這裏特地總結一下,希望能夠幫助大家。
這裏不再對GC相關知識展開介紹。
Handler的內存泄漏問題主要發生在兩個場景:
- (匿名)內部類默認持有外部類的引用;
- 邏輯漏洞導致;
這裏簡要分析一下:
場景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;
}
}