Handler用于異步消息處理:
當發出一個消息之后,首先進入一個消息隊列,發送消息的函數同步返回,而另一個部分在消息隊列中逐一將消息取出,然后對消息進行處理。
1、Handler內存泄漏問題
2、在子線程創建Handler報錯Looper沒有prepare?
3、textview.setText()只能在主線程執行?有點問題
4、new Handler()的兩種寫法
5、ThreadLocal用法和原理
1、Handler引起的內存泄漏問題
如下代碼,當在子線程中休眠或做了耗時操作后,再用Handler發送消息。此時如果Activity已經destroy了,但是Handler仍然會發送消息到消息隊列里,產生了嚴重的內存泄漏。
new Thread(new Runnable() {
@Override
public void run() {
//內存泄漏問題
Message message = new Message();
message.obj = "胡軍";
message.what = 2;
SystemClock.sleep(3000);
handler1.sendMessage(message);
}
}).start();
如何解決:
(1)用handler1.sendMessageDelayed(message,3000);發送延時消息。
在Activity被消耗后,將消息移除handler1.removeMessages(2);
(2)在Activity銷毀后,將Handler置空,這樣就不會在延時結束后調用sendMessage了。
2、為什么不能在子線程創建Handler
會報錯:Can't create handler inside thread that has not called Looper.prepare();
這里看看Handler的構造方法源碼:
public Handler(){}
public Handler(Callback callback){}
mLooper = Looper.myLooper();
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
...
}
也就是默認Looper是從當前Thread里獲取Looper。
但是在新生成的子線程里,并沒有生成Looper。這樣就會報錯。
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
在子線程里,需要先prepare,在當前thread寫入相應Looper到ThreadLocal里。
Looper.prepare();
Handler handler = new Handler();
而在主線程里,已經默認生成了一個Looper.
ActivityThread.java里的main()方法中:
Looper.prepareMainLooper();
而Looper里面有個static的變量sMainLooper用來存儲主線程的Looper,任何時候都能獲取到該主線程Looper。
看Looper.prepareMainLooper();都做了什么:
首先調用了prapare方法,生成了一個Looper放入ThreadLocal里,這是用來存儲和取出和線程相關的變量的,之后會進行詳細說明。這里將生成的主線程Looper放入ThreadLocal后,然后利用myLooper()方法,即從當前線程取出Looper,主線程里就是主線程Looper,然后賦值給sMainLooper。
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
3、textview.setText()只能在主線程執行?有點問題
如下,在onCreate()里調用,生成的子線程里setText()是沒有報錯,且執行成功的了。為什么呢?
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "run: "+Thread.currentThread().getName());
textView.setText("胡軍");
}
}).start();
分析下setText()的源碼:
setText() --> checkForRelayout() -->requestLayout() --> mParent.requestLayout();
而這個mParent.requestLayout();調用的是父類ViewParent的requestLayout()方法。
接口ViewParent的實現類ViewRootImpl,里面有requestLayout()的實現:
requestLayout() --> checkThread()
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
這里,mThread是在ViewParent初始化時設置的
mThread = Thread.currentThread();
所以,"Only the original thread that created a view hierarchy can touch its views."錯誤并不是指必須在UI主線程調用setText(),而是需要在創建ViewParent的線程里調用setText()。
我們使用的ViewParent都是在主線程調用的,所以setText()就需要在主線程中調用。
當setText()足夠快,在檢查線程前就更新完成,則不會報錯。
4、new Handler()的兩種寫法
//Handler有下面兩種構造方式
private Handler handler1 = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
return false;
}
});
private Handler handler2 = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
看源碼:
在啟動一個Thread后,會生成一個Looper循環。
比如主線程里,有
Looper.prepareMainLooper();
...
Looper.loop();
在loop()方法里,啟動了一個無限輪訓的獲取Message隊列的循環。
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
}
當取到Message后,會調用
msg.target.dispatchMessage(msg);
這里,Target就是發送這個Message的Handler,在調用Handler發送Message時,會將Message的Target設置為發送的Handler。
/**
* 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);
}
}
首先會回調msg自己的Callback,沒有就回調初始化Handler時給的Callback,再不行才回調重寫的Handler的handleMessage()方法。
其中,msg自己的Callback,是調用Handler的post方法時,設置給Message的。就完成了Handler.post(runnable)里的run回調。
這里有個知識點,調用Activity.runOnUIThread(),其實內部就是用主線程的Handler.post()方法實現的。
5、ThreadLocal用法和原理
作用是把參數存儲在線程相關的map里,在不同的線程里存儲,當在相應的線程里能讀取出當前線程存儲的參數。
下面的例子里,在ThreadLocal里存儲String,則在不同的線程里存儲不同的String,在不同的線程里,就能讀取出這個線程存儲的String。
val threadLocal = object : ThreadLocal<String>() {
override fun initialValue(): String? {
return "默認值"
}
}
threadLocal.set("胡軍")
println("當前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")
Thread(Runnable {
println("當前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")
//當使用完成后,建議remove掉。否則不用的線程越來越多,占用內存越來越多
threadLocal.remove()
},"子線程1").start()
Thread(Runnable {
threadLocal.set("胡軍2")
println("當前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")
//當使用完成后,建議remove掉。否則不用的線程越來越多,占用內存越來越多
threadLocal.remove()
},"子線程2").start()
ThreadLocal源碼分析
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
在Looper里,保存了一個ThreadLocal和主線程的Looper,并且都是全局唯一靜態的。Looper就是用ThreadLocal來保存不同線程里的Looper的。
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper; // guarded by Looper.class
Handler+Message的原理分析
1、首先看主線程:
應用啟動時,在主線程ActivityThread.class里找到main()方法
ActivityThread.main() --> Looper.prepareMainLooper() --> Looper.prepare() --> Looper.sThreadLocal.set(new Looper(quitAllowed)); --> Looper.sMainLooper = myLooper();
這里就完成了主線程的Looper的生成。之后在主線程里都是調用
Looper.sMainLooper作為主線程Looper。
2、看Looper代碼
構造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
初始化Looper的消息隊列mQueue,以及賦值給該Looper運行的線程mThread.
由于主線程Looper只有一個,所以整個主線程只有一個消息隊列mQueue。
3、看Handler代碼
構造方法:
Handler(){}
Handler(Callback callback){}
Handler(Looper looper){}
Handler(boolean async){}
Handler(Callback callback, boolean async){}
Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async){}
有一堆構造方法,如果不指定Looper,則會從Looper.myLooper()獲取。
所以當當前Thread并沒有Looper時,則找不到Looper,會報錯。只有在當前Thread里調用了Looper.prerare()才可以找到Looper。
發送消息(存儲消息):
Handler有很多個發送消息的方法,
sendMessage(@NonNull Message msg)
sendMessageDelayed(@NonNull Message msg, long delayMillis)
sendEmptyMessage(int what)
//post方法,將Runnable賦予Message
post(@NonNull Runnable r)
postDelayed(@NonNull Runnable r, long delayMillis)
實際上,上面所有的發送方法,最后都調用了一個方法:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
最后調用了MessageQueue的enqueueMessage()方法。
boolean enqueueMessage(Message msg, long when) {
//如果是第一條消息,賦值給MessageQueue里的變量mMessages
mMessages = msg;
//如果不是第一條,或者需要插入之前的消息前面,
//利用message的next來形成鏈式的排列。
}
取出消息(消費)
調用Looper.loop()方法后,就開始不斷輪訓消息隊列。
其中,主線程的Looper在ActivityThread里main()方法中,
main(){
...
Looper.prepareMainLooper();
...
Looper.loop();
}
分析loop()方法
public static void loop() {
//拿到該線程里的Looper
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//拿到Looper里的MessageQueue
final MessageQueue queue = me.mQueue;
...
//開始不斷輪訓MessageQueue
for (;;) {
//首先拿到Queue里的Message
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
try {
msg.target.dispatchMessage(msg);
...}
}
}
上面分析得到,所有的消息最后在handleMessage或在Callback的run方法里執行,而執行的線程就是loop()方法被調用時處于的線程。
主程序里調用了loop()方法,所有的主線程消息都在主線程中運行了。
問題
loop()啟動了一個無限死循環,如何避免導致anr呢?
里面有挺好的垃圾回收機制,開始前調用了Binder.clearCallingIdentity();
循環中調用了
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
一旦需要等待時,或還沒有執行到執行的時候,會調用NDK里面的JNI方法,釋放當前時間片,這樣就不會引發ANR異常了。