1. 引文
handler 基本定義:先直接看看最權(quán)威的官方定義
- A Handler allows you to send and process {@link Message} and Runnable
- objects associated with a thread's {@link MessageQueue}. Each Handler
- instance is associated with a single thread and that thread's message
- queue. When you create a new Handler it is bound to a {@link Looper}.
- It will deliver messages and runnables to that Looper's message
- queue and execute them on that Looper's thread.
- <p>There are two main uses for a Handler: (1) to schedule messages and
- runnables to be executed at some point in the future; and (2) to enqueue
- an action to be performed on a different thread than your own.
大意就是:
Handler 允許與唯一的線程綁定凰兑,主要作用:
- 允許延期執(zhí)行任務(wù)
- 允許切換線程執(zhí)行任務(wù)
主要API:
post(Runnable)熙暴、
postAtTime(Runnable龙优、long)卫玖、
postDelayed(Runnable绪励、long)庶骄、
sendEmptyMessage(int)、
sendMessage(msg)题暖、
sendMessageAtTime(msg、long)
sendMessageDelayed(msg墩莫、long)方法來完成的芙委。
** 文末會(huì)給簡(jiǎn)單的demo示例 **
2. 先看常見錯(cuò)誤
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);
Log.d(TAG, "1--》" + Thread.currentThread().getId());
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
Log.e(TAG, e.toString());
}
tv.setText("oncreate方法,無耗時(shí)狂秦,改變Tv 文本");
}
}).start();
}
如果手抖寫出上述代碼灌侣,那恭喜你,喜提下列報(bào)錯(cuò):
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
at android.view.View.invalidateInternal(View.java:13656)
at android.view.View.invalidate(View.java:13620)
這個(gè)比較簡(jiǎn)單裂问,子線程不允許更新Ui (really?)
3. 正常打開姿勢(shì)
android更新UI的姿勢(shì)有:
runOnUiThread();
handler.post();
handler.sendMessage();
view.post();
簡(jiǎn)單的demo界面
3.1 runOnUiThread
Activity中的API ,使用簡(jiǎn)單粗暴侧啼,不過必須在Activity的上下文環(huán)境使用
public void runOnUIBtnOnclick(View view) {
runOnUiThread(new Runnable() { // Activity 方法牛柒,只能在Activity中使用
@Override
public void run() {
tv.setText("runOnUiThread 改變Tv 文本");
}
});
}
3.2 handler.post()
使用比較方便,主要用來切換線程痊乾。
基本的使用
Handler handler = new Handler(Looper.myLooper());
handler.post(new Runnable() {
@Override
public void run() {
tv.setText("切換線程皮壁,改變Tv 文本");
}
});
來看下線程是如何切換的
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);
Log.d(TAG, "1--》" + Thread.currentThread().getId());
...
public void changeThreadBtnOnclick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "2---》" + Thread.currentThread().getId());
handler.post(new Runnable() {
@Override
public void run() {
Log.d(TAG, "3---》" + Thread.currentThread().getId());
tv.setText("切換線程,改變Tv 文本");
}
});
}
}).start();
}
測(cè)試的結(jié)果是:
2020-12-31 20:40:19.544 8170-8170/com.gavin.handlerdemo D/MainActivity: 1--》1
2020-12-31 20:42:15.357 8170-8208/com.gavin.handlerdemo D/MainActivity: 2---》346
2020-12-31 20:42:15.365 8170-8170/com.gavin.handlerdemo D/MainActivity: 3---》1
可以看出哪审,2處的線程的確是子線程蛾魄,3處是主線程
總結(jié)一下:
注意不是寫在子線程方法里的就是子線程執(zhí)行....
3.3 handler.sendMessage()
這個(gè)主要用來傳遞消息,消息常見的做法是子線程向主線程發(fā)送來通知更新UI,不過湿滓,主線程一樣可以向子線程發(fā)消息滴须。
下面造一個(gè)死循環(huán),主線程向子線程發(fā)消息叽奥,子線程收到后扔水,回傳主線程,然后循環(huán)下去朝氓。 這樣演示的目的魔市,是為了警示一下,退出時(shí)赵哲,不反注冊(cè)這個(gè)callback 后果可能很嚴(yán)重待德。
private static String TAG = "SecActivity";
TextView tv;
Handler threadHandler;
Handler mainHandler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.obj != null) {
Log.e(TAG, "主線程收到了呼叫, \"" + msg.obj + "\"" );
} else {
Log.e(TAG, "主線程收到了呼叫");
}
Message message = new Message();
message.obj = "這是來自主線程的呼叫!J母汀磅网!";
if (threadHandler != null) {
threadHandler.sendMessageDelayed(message, 1000);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sec);
tv = findViewById(R.id.tv);
HandlerThread thread = new HandlerThread("HandlerThread"); // 使用HandlerThread主要是方便獲取Looper
thread.start();
new Thread(new Runnable() {
@Override
public void run() {
threadHandler = new Handler(thread.getLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
if (msg.obj != null) {
Log.e(TAG, "子線程收到了呼叫, \"" + msg.obj + "\"" );
} else {
Log.e(TAG, "子線程收到了呼叫");
}
Message message = new Message();
message.obj = "這是來自子線程的呼叫L附亍?曷拧!";
mainHandler.sendMessageDelayed(message,1000);
}
};
}
}).start();
}
public void testMsgTransform(View view) {
mainHandler.sendEmptyMessageDelayed(0, 100); // 發(fā)送一個(gè)空消息簸喂,被自身的handleMessage 捕獲毙死。誰發(fā)送 誰接收。
}
得到的結(jié)果:
2020-12-31 20:48:29.521 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫
2020-12-31 20:48:30.523 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫S黯6筇取!"
2020-12-31 20:48:31.526 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫3恰T倬铡!"
2020-12-31 20:48:32.527 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫Q赵>腊巍!"
2020-12-31 20:48:33.529 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫7汉馈3砘濉侦鹏!"
2020-12-31 20:48:34.531 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫!M涡稹略水!"
2020-12-31 20:48:35.533 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫!H坝渊涝!"
2020-12-31 20:48:36.537 8170-8221/com.gavin.handlerdemo E/SecActivity: 子線程收到了呼叫, "這是來自主線程的呼叫!4蚕印驶赏!"
2020-12-31 20:48:37.537 8170-8170/com.gavin.handlerdemo E/SecActivity: 主線程收到了呼叫, "這是來自子線程的呼叫!<染稀煤傍!"
總結(jié)一下:
Handler 可以有很多個(gè),不過每個(gè)Handler 只能綁定一個(gè)線程嘱蛋, 只能有一個(gè)Looper蚯姆,Handler 發(fā)出的消息,最終要由自己的handleMessage 消費(fèi)洒敏,是真正的肥水不流外人田龄恋。
即使你關(guān)閉了頁面,你會(huì)發(fā)現(xiàn)凶伙,這個(gè)死循環(huán)還在郭毕,它的生命周期是app的生命周期,不關(guān)閉函荣,后果真的很嚴(yán)重显押。
關(guān)閉方法:
@Override
protected void onDestroy() {
super.onDestroy();
//如果這里不移除 callback,這個(gè)callback會(huì)一直存在傻挂,容易引發(fā)內(nèi)存泄漏
mainHandler.removeCallbacksAndMessages(null);
threadHandler.removeCallbacksAndMessages(null);
}
3.4 view.post()
public void viewpostTest(View view) {
tv.post(new Runnable() {
@Override
public void run() {
tv.setText("view.post方法乘碑,改變Tv 文本");
}
});
}
view.post與Handler.post 基本類似,不過也有所不同金拒,這里不繼續(xù)展開了兽肤。
4. 子線程真的就不能更新Ui 么?
答案是一般都不能更新绪抛,但是面試時(shí)资铡,你就得回答可以更新。幢码。笤休。
可以先驗(yàn)證下效果。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.tv);
Log.d(TAG, "1--》" + Thread.currentThread().getId());
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("oncreate方法蛤育,無耗時(shí)宛官,改變Tv 文本");
}
}).start();
}
可以看出頁面并未報(bào)錯(cuò)葫松,正常展示。
不過如果你加上延遲底洗,就是這么一段話
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tv.setText("oncreate方法腋么,無耗時(shí),改變Tv 文本");
}
}).start();
就會(huì)喜提上面提到的崩潰了亥揖。
為啥呢珊擂?
看報(bào)錯(cuò)提示:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
這個(gè)報(bào)錯(cuò)是ViewRootImpl 給出的。
流程是這樣:
onreate方法费变,并不會(huì)給出這個(gè)效驗(yàn)摧扇,因此程序正常執(zhí)行,Ui 也確實(shí)更新了挚歧。
但在Activity 的onresume 階段扛稽,會(huì)調(diào)用ViewRootImpl 的方法,然后checkThread去校驗(yàn)這個(gè)更新Ui的線程是否為主線程滑负。
具體的代碼相對(duì)復(fù)雜在张,簡(jiǎn)單講的話,可以看下onresume的注釋
大意是View 變?yōu)榭梢姇r(shí)矮慕,會(huì)調(diào)用ViewRootImpl performTraversals方法帮匾。
因此如果子線程沒有耗時(shí),初始進(jìn)入時(shí)痴鳄,還沒來的及執(zhí)行onresume中的方法瘟斜,因此在ViewRootImpl checkThread 執(zhí)行前已經(jīng)搶先完成了UI 更新,就不會(huì)有問題痪寻。 但一旦執(zhí)行耗時(shí)方法螺句,就會(huì)觸發(fā)checkThread機(jī)制。
5. 內(nèi)存泄漏問題
以上代碼槽华,僅作為測(cè)試demo是沒有問題的壹蔓。但上線的話,會(huì)存在內(nèi)存泄漏問題猫态。
( 這里假設(shè)ondestroy 時(shí),沒有removeCallbacksAndMessages)
原因主要是:
通過匿名內(nèi)部類實(shí)例化的Handler類對(duì)象披摄,隱式的持有外部Activity對(duì)象亲雪。
Activity退出時(shí),Handler 的生命周期與APP 生命周期相同疚膊,因其持有Activity實(shí)例义辕,導(dǎo)致Activity 也不能被GC 回收,引發(fā)內(nèi)存泄漏寓盗。
解決方案:
通過創(chuàng)建繼承Handler的靜態(tài)內(nèi)部類灌砖,并使用弱引用來避免Handler對(duì)象持有外部類對(duì)象的強(qiáng)引用璧函。
public MyHandler mHandler = new MyHandler(this);
//靜態(tài)內(nèi)部類
static class MyHandler extends Handler {
private WeakReference<Context> weakReference;
MyHandler(Context context) {
weakReference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity mainActivity = mWeakReference.get();
if (mainActivity != null) {
tv.setText(msg.obj + "");
}