什么是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
,看下效果和日志:
日志:
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
,在ActivityThread
的main()
方法中調(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)用的是ThreadLocalMap
的get()
和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ā)生改變!
對(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ō)就是View
或ViewGroup
在更新UI時(shí)調(diào)用的invalidate()
都會(huì)在ViewRootImpl
中執(zhí)行線程的檢查,如上,如果不是主線程,會(huì)直接拋異常颗味。
注:
TextView
繼承自View
實(shí)現(xiàn)了ViewParent接口
,而ViewRootImpl
是接口實(shí)現(xiàn)類(lèi),在ViewRootImpl
的requestLayout
中調(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的兩種方式示例如下:
在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)作的流程圖
簡(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)了Handler
的handleMessage()
方法
這里有幾點(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()
,ActivityThread
的main()
方法執(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