Handler主要用於異步消息的處理:當發出一個消息以後,首先進入一個消息隊列,發送消息的函數即刻返回,而另一個部分在消息隊列中逐一將消息取出,而後對消息進行處理git
相信大部分Android開發者對於Handler都有所瞭解,概念的知識就不作贅述,下面咱們主要是帶着幾個問題去分析(面試中常被問到的問題~)github
代碼也比較簡單,簡單說下,在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
日誌: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();
}
複製代碼
和上面內存泄漏的緣由相似~函數
這裏須要說明下,不是全部Android手機在子線程中new Handler()
都會拋異常,好比華爲的部分手機改寫了源碼,並不會出現異常,這裏咱們主要關注出現異常的緣由,那麼出現異常的緣由是什麼?oop
ActivityThread
是建立了一個主線程的Looper
對象的,過程大體以下: 在應用啓動時建立開啓ActivityThread
,在ActivityThread
的main()
方法中調用了Looper.prepareMainLooper()
方法,而後建立了一個Looper
對象,這個Looper對象是存在主線程
中的,而且調用了sThreadLocal.set(new Looper(quitAllowed));
sThreadLocal
是存在在ThreadLocalMap
中的,sThreadLocal
在存和取的時候,調用的是ThreadLocalMap
的get()
和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,這種方式這裏不作討論~
首先咱們先寫一段測試代碼:
//開啓子線程
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,小夥伴們應該都是比較瞭解的,這裏不作過多贅述,簡單說就是View
或ViewGroup
在更新UI時調用的invalidate()
都會在ViewRootImpl
中執行線程的檢查,如上,若是不是主線程,會直接拋異常。 注:
TextView
繼承自View
實現了ViewParent接口
,而ViewRootImpl
是接口實現類,在ViewRootImpl
的requestLayout
中調用checkThread()
校驗線程 因此爲何第一種寫法不會拋異常呢? 緣由是: ViewRootImpl
是在 Activity 建立對象完畢以後再建立對象的,若是咱們調用setText()
等api的速度快於 ViewRootImpl
對象的建立,就不會拋出異常!因此咱們直接調用不會異常,而子線程休眠一秒鐘以後就會拋出異常,對於第三種方式使用Toast
的狀況,首先這種方式最終會調用,setText()
的api,與上面兩種狀況相似,可是在這中間還有不少代碼要執行,至關於延遲了一段時間,更新UI的方法是在ViewRootImpl
對象建立以後作的,因此會發生異常。 因此textView.setText() 只能在主線程執行
這種說法太過絕對
建立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 的使用key
是線程,因此不一樣的線程調用set方法是互不影響的 ② 線程中使用ThreadLocal .set()
方法使用完畢記得remove()
,避免沒必要要的內存浪費~
對於Handler + Message原理分析,網上有不少不少文章了,這裏主要就主要用流程圖來簡單介紹吧~ 咱們都知道要分析Handler + Message,離不開四個對象: Handler
、 Message
、Looper
、 MessageQueue
先看下運做的流程圖
簡單來講:就是Handler發送消息
和處理消息
(知識最少原則)
大體流程就是: 應用在啓動時,ActivityThread
建立了一個主線程惟一的Looper
對象,調用了Looper.loop()
開啓了消息輪詢(死循環),而後Handler對象就能夠調用sendMessage()
方法將消息壓入消息隊列,壓入的過程調用的就是equeueMessage()
方法,Looper
經過輪詢取出隊首的message
(先進先出),而且調用message.target.dispatchMessage()
方法分發消息,而message.target
對象就是Handler
,也就是回調了Handler
的handleMessage()
方法
這裏有幾點要說明:
sendMessage()
、post()
、sendEmptyMessageAttime()
等這些發送消息的api都會經過equeueMessage()
將消息壓入消息隊列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()
,ActivityThread
的main()
方法執行完畢就退出了,這顯然是不符合實際狀況的
其實在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機制的代碼在單元測試test包下 舒適提示:直接右鍵test包下的ActivityThread執行便可看到日誌 github