目录
- 1. Android 的消息机制概述
- 2. Android 的消息机制分析
- 2.1 ThreadLocal 的使用场景有哪些?
- 2.2 为什么 ThreadLocal 可以在多个线程中互不干扰地存储和修改数据?
- 2.3 ThreadLocal 在消息机制中是如何使用的?
- 2.4 消息队列是如何完成消息的插入和读取操作的?
- 2.5 Looper 在消息机制中的作用是什么?
- 2.6 Looper.prepare() 方法与 Looper.prepareMainLooper() 方法有什么区别?
- 2.7 Looper 的 quit() 方法和 quitSafely() 方法都用来退出消息循环,它们的区别是什么?
- 2.8 Looper.loop() 方法在消息队列中没有消息时就会退出,这种说法对吗?
- 2.9 为什么主线程不会因为 Looper.loop() 里的死循环卡死?
- 2.10 Handler 什么情况下会导致内存泄漏?为什么?怎么办?
- 2.11 为什么 Looper.loop() 方法和 MessageQueue.next() 方法里都有一个 for(;;) 循环?
- 2.12 消息的分类以及它们的作用分别是什么?
- 2.13 如何获取 `Message`? `Message` 使用完后如何处理?为什么这样设计?
- 3. 主线程的消息循环
- 参考
1. Android 的消息机制概述
1.1 Android 的消息机制是什么?
Android 的消息机制是通过 Handler
的运行机制来实现将一个任务切换到 Handler
所在的线程中去执行。
但是,完成把一个任务切换到 Handler
所在的线程中去执行这个事情,单靠 Handler
类是不行的;实际上,Handler
的运行需要 MessageQueue
和 Looper
的支撑,Handler
是作为 Android 消息机制的上层接口而已。
换句话说,Android 定义了Handler
直接面向了开发者,屏蔽了 MessageQueue
和 Looper
(没有完全屏蔽 Looper
),开发者只需要和 Handler
打交道就可以运用 Android 的消息机制了。
1.2 Handler 就是专门用来更新 UI 的,这种说法对吗?为什么?
不对。
在开发过程中,我们在子线程中执行一些耗时的操作,比如读取文件,读取数据库,访问网络等,拿到我们需要的数据,然后把这些数据显示在 UI 上。这时,直接在子线程中操作 UI 控件来显示数据,Android 是不允许的,会抛出异常给我们的;正确的做法是,在 UI 线程创建一个 Handler
对象,在子线程中使用这个 Handler
对象将要显示的数据切换到 Handler
所在的 UI 线程,再操作 UI 控件来显示数据。这就是 Handler
用来更新 UI 的场景了。
来看看实际的代码吧:
// 在主线程创建 Handler 对象
private Handler mainThreadHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 3) {
Log.d(TAG, "handleMessage: msg.what=" + msg.what + ",msg.obj=" +
msg.obj + ",threadName=" + Thread.currentThread().getName());
// 这里是主线程,可以放心更新 UI 了。
}
}
};
// 点击按钮从子线程发送消息到主线程
public void sendMessage2UIThread(View view) {
// 开启一个子线程
new Thread(new Runnable() {
@Override
public void run() {
int what = 3;
String obj = "hello, ui thread!";
Log.d(TAG, "sendMessage2UIThread: what="+ what +",obj=" +
obj + ",threadName=" + Thread.currentThread().getName());
mainThreadHandler.obtainMessage(what, obj).sendToTarget();
}
}, "work-thread").start();
}
打印日志如下:
D/MainActivity: sendMessage2UIThread: what=3,obj=hello, ui thread!,threadName=work-thread
D/MainActivity: handleMessage: msg.what=3,msg.obj=hello, ui thread!,threadName=main
但是,我们还可以把数据从主线程切换到子线程中去执行。这里使用实际的例子来进行说明:
private Handler workThreadHandler;
private void startWorkThread() {
// 开启一个子线程
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
// 在子线程中创建 Handler 对象
workThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 2) {
Log.d(TAG, "handleMessage: msg.what=" + msg.what + ",msg.obj=" +
msg.obj + ",threadName=" + Thread.currentThread().getName());
}
}
};
Looper.loop();
}
}, "work-thread").start();
}
// 点击按钮从主线程发送消息到子线程
public void sendMessage2WorkThread(View view) {
int what = 2;
String obj = "hello, work thread!";
Log.d(TAG, "sendMessage2WorkThread: what="+ what +",obj=" +
obj + ",threadName=" + Thread.currentThread().getName());
workThreadHandler.sendMessage(workThreadHandler.obtainMessage(what, obj));
}
点击按钮,打印日志如下:
D/MainActivity: sendMessage2WorkThread: what=2,obj=hello, work thread!,threadName=main
D/MainActivity: handleMessage: msg.what=2,msg.obj=hello, work thread!,threadName=work-thread
可以看到,这里确实实现了把数据从主线程切换到子线程中了。
因此,我们说,Handler
并非是专门用来更新 UI 的,只是常被开发者用来更新 UI 而已。
1.3 在子线程真的不能更新 UI 吗?
我们知道,Android 规定访问 UI 要在主线程中进行,如果在子线程中更新 UI,程序就会抛出异常。这是因为在 ViewRootImpl
类中会对 UI 做验证,具体来说是由 checkThread
方法来完成的。
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
现在我们在子线程里去给 TextView
控件设置文本:
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_check_thread_not_working);
tv = (TextView) findViewById(R.id.tv);
new Thread(() -> {
SystemClock.sleep(1000L);
tv.setText("I am text set in " + Thread.currentThread().getName());
},"work-thread").start();
}
运行程序,会报错:
2022-01-08 05:47:15.391 9225-9252/com.wzc.chapter_10 E/AndroidRuntime: FATAL EXCEPTION: work-thread
Process: com.wzc.chapter_10, PID: 9225
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)
at android.view.View.requestLayout(View.java:17476)
at android.view.View.requestLayout(View.java:17476)
at android.view.View.requestLayout(View.java:17476)
at android.view.View.requestLayout(View.java:17476)
at android.view.View.requestLayout(View.java:17476)
at android.widget.TextView.checkForRelayout(TextView.java:6871)
at android.widget.TextView.setText(TextView.java:4057)
at android.widget.TextView.setText(TextView.java:3915)
at android.widget.TextView.setText(TextView.java:3890)
at com.wzc.chapter_10.CheckThreadNotWorkingActivity.lambda$onCreate$0$CheckThreadNotWorkingActivity(CheckThreadNotWorkingActivity.java:19)
at com.wzc.chapter_10.-$$Lambda$CheckThreadNotWorkingActivity$Thy_KGiEr_duYPMycxt-0lYIEGo.run(lambda)
at java.lang.Thread.run(Thread.java:818)
可以看到正是在 ViewRootImpl
类的 checkThread
方法里面抛出了异常:
Only the original thread that created a view hierarchy can touch its views.
checkThread
方法里的 mThread
就是 UI 线程,现在我们在子线程里面调用了 checkThread
方法,则 Thread.currentThread()
就是子线程,这样 mThread != Thread.currentThread()
判断就为 true
,会进入 if
分支,抛出 CalledFromWrongThreadException
异常。
但是,如果我把 SystemClock.sleep(1000L);
这行代码注释掉会怎么样呢?
运行程序,效果如下:
是的,这不是幻觉,在子线程更新 UI 成功了。
那么,问题又来了,为什么有休眠时在子线程更新 UI 报错,而不休眠时在子线程更新 UI 成功呢?
这是因为有休眠时,在执行更新 UI 操作时,ViewRootImpl
对象已经创建成功了,就会执行到 checkThread
方法了;没有休眠时,在执行更新 UI 操作时, ViewRootImpl
对象还未创建,就没有执行到 checkThread
方法了。
实际上,我们这里不加休眠的情况下,只是在子线程设置文本时没有走 checkThread
方法而已,等到真正把文本绘制到屏幕上,仍然是在 UI 线程进行的。
再看一下这个方法,
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
只要 mThread
与 Thread.currentThread()
相同就不会报异常,并且异常的中文含义:只有原来创建了视图体系的线程才可以操作它的 View。这根本没有说不让子线程更新 UI。这里真正想说明的意思是:哪个线程创建了视图体系,就要由那个线程来操作它的 View;换句话说,如果某个线程去操作另外一个线程创建的 View,那是不允许的。
那么,如果我们就在子线程中去完成视图的添加,这会有问题吗?
我们在子线程里面去添加一个 Window,代码如下:
public void createUIInWorkThread(View view) {
new Thread(() -> {
// 这里要由 Looper 对象,因为在 ViewRootImpl 里面会创建 ViewRootHandler 对象。
Looper.prepare();
TextView tv = new TextView(MainActivity.this);
tv.setBackgroundColor(Color.GRAY);
tv.setTextColor(Color.RED);
tv.setTextSize(40);
tv.setText("i am text created in " + Thread.currentThread().getName());
WindowManager windowManager = MainActivity.this.getWindowManager();
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
0, 0, PixelFormat.TRANSPARENT);
layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
layoutParams.gravity = Gravity.CENTER;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
windowManager.addView(tv, layoutParams);
Looper.loop();
}, "work-thread").start();
}
点击按钮,查看效果:
可以看到,在子线程里里面操作 UI 是可以的。
这里我们总结一下:
-
在线程 A 里面一般不能操作线程 B 的 UI;但是如果线程 B 的
ViewRootImpl
还没有创建,这时就不会走checkThread
方法,也不会抛出异常,最终仍是由线程 B 完成了 UI 的操作。 -
在一个线程里操作由这个线程自己创建的视图体系是可以的,也可以说,一个线程只可以操作它自己的 UI。
1.4 Android 系统为什么使用单线程模型来访问 UI?
Android 的 UI 控件不是线程安全的,如果在多线程中并发访问可能会导致 UI 控件处于不可预期的状态;而如果对 UI 控件的访问加上锁机制,会让 UI 访问的逻辑变得复杂,也会降低 UI 访问的效率。
所以,Android 采用单线程模型才处理 UI 操作。
1.5 为什么说 Handler 类是 Android 消息机制的上层接口?
从构造方法来看
分为两大类:可以传递 Looper
对象的和不可以传递 Looper
对象的。
我们重点看不传递 Looper
对象的 Handler(Callback callback, boolean async)
方法,因为这个方法非常具有代表性。
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
这个方法的主要作用:
- 通过
Looper.myLooper()
方法来获取并通过mLooper
持有Looper
对象。Looper.myLooper()
方法会从线程本地变量ThreadLocal
里面取出与当前线程对应的Looper
对象。 - 如果
mLooper
对象仍为null
,就会抛出异常:“Can't create handler inside thread that has not called Looper.prepare()”
,这是告诉当前线程没有调用Looper.prepare()
,所以不能创建Handler
。 - 通过
Looper
对象获取MessageQueue
对象并赋值给mQueue
成员变量。
在构造 Handler
对象时,就一定要持有 Looper
对象和 MessageQueue
对象,也就是说,Handler
类组合了 Looper
对象和 MessageQueue
对象。
从发送消息方法来看
发送消息的方法分为两大类:postXXX 方法和 sendXXX 方法。
postXXX 方法用于发送一个 Runnable
对象,它会包装成一个 Message
对象,再发送到消息队列中;
sendXXX 方法用于发送一个 Message
对象到消息队列中。
从调用图可以看到,不管是 postXXX 方法和 sendXXX 方法,最终调用的都是 enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)
方法:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
这个方法的主要作用:
- 把
Handler
对象赋值给Message
对象的target
字段; - 调用
MessageQueue
对象的enqueueMessage
方法把消息加入到消息队列。
从处理消息方法来看
当消息从消息队列中取出时,会调用 Handler
对象的 dispatchMessage
方法来分发消息
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
public interface Callback {
public boolean handleMessage(Message msg);
}
private static void handleCallback(Message message) {
message.callback.run();
}
public void handleMessage(Message msg) {
}
该方法的主要作用
对于 Message
来说,就是回调接口的作用:Message
对象持有Handler
对象,通过调用这个 Handler
对象的 dispatchMessage
方法,把 Message
对象回调给了 Handler
。
对于 Handler
来说,就是分发消息的作用:
- 如果
Message
对象的callback
字段不为空,那么这个消息内部持有了一个Runnable
对象,就调用handleCallback
方法来运行那个Runnable
对象封装的代码; - 如果
Message
对象的callback
字段为空,而mCallback
对象不为null
,就使用Callback
类的handleMessage
方法来处理消息;这个mCallback
是在Handler
的构造方法里面完成赋值的,使用这个回调的好处是不必要为了重写Handler
类的handleMessage
方法而去子类化Handler
; - 如果
Message
对象的callback
字段为空,且mCallback
对象为null
,就使用Handler
类的handleMessage
方法来处理消息了。
从获取消息来看
Handler
封装了一系列的 obtainMessage
工具方法,方便我们拿到 Message
对象。
从移除消息来看
Handler
封装了 removeXXX 方法,内部委托给 MessageQueue
对象去做真正的工作。
public final void removeMessages(int what) {
mQueue.removeMessages(this, what, null);
}
总结一下:使用 Handler
可以组合 MessageQueue
对象和 Looper
对象,可以发送消息,可以处理消息,可以获取消息对象,可以移除消息,所以说Handler
是 Android 消息机制的上层接口。
1.6 Android 消息机制的整体流程是什么?
图解:
- 在主线程创建
Handler
对象handler
,默认使用的是主线程的Looper
对象以及对应的MessageQueue
对象; - 在工作线程通过
Handler
对象handler
的发送消息方法发送消息,最终通过MessageQueue
对象的enqueueMessage
方法把消息加入到消息队列中; Looper.loop()
方法运行在创建Handler
里的线程,在这里就是运行在主线程,Loop.loop()
方法不断从消息队列中获取符合条件的Message
对象;- 获取到符合条件的
Message
对象后,通过Message
对象持有的target
字段(实际就是发送该消息的那个Handler
对象)的dispatchMessage
方法把消息回调给发送消息的那个Handler
,这样消息就在主线程接收到了。
2. Android 的消息机制分析
2.1 ThreadLocal 的使用场景有哪些?
场景一:当某些数据是以线程为作用域并且不同线程具有不同的线程副本的时候,考虑使用 ThreadLocal
。
对于 Handler
来说,它需要获取当前线程的 Looper
(Looper
的作用域就是线程并且不同线程具有不同的 Looper
),这时候使用 ThreadLocal
就可以轻松实现 Looper
在线程中的存取。
对于 SimpleDateFormat
来说,它不是线程安全的,也就是说在多线程并发操作时,会抛出异常。看演示代码如下:
public class SimpleDateFormatDemo1 {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
5, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));
List<String> data = Arrays.asList(
"2021-03-01 00:00:00",
"2020-01-01 12:11:40",
"2019-07-02 23:11:23",
"2010-12-03 08:22:33",
"2013-11-29 10:10:10",
"2017-09-01 14:14:14",
"2021-04-01 15:15:15"
);
for (String date : data) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(sdf.parse(date));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
threadPool.shutdown();
}
}
运行这段程序,会出现这样的异常:
java.lang.NumberFormatException: For input string: ".103E2103E2"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.java.advanced.features.concurrent.threadlocal.SimpleDateFormatDemo1$1.run(SimpleDateFormatDemo1.java:45)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
为了解决 SimpleDateFormat
线程不安全的问题,我们可以使用一个 synchronized
修饰的方法封装其 parse
方法,但是这样多线程下会竞争锁,效率不高。使用 ThreadLocal
来为每个线程创建一个专属的 SimpleDateFormat
对象副本,当一个线程下需要获取 SimpleDateFormat
对象进行操作时,它获取的是它自己的那个副本,对其他线程的 SimpleDateFormat
对象副本没有影响,这样就不会发生线程不安全的问题了。
public class SimpleDateFormatDemo2 {
private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(new Supplier<SimpleDateFormat>() {
@Override
public SimpleDateFormat get() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
});
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
5, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));
List<String> data = Arrays.asList(
"2021-03-01 00:00:00",
"2020-01-01 12:11:40",
"2019-07-02 23:11:23",
"2010-12-03 08:22:33",
"2013-11-29 10:10:10",
"2017-09-01 14:14:14",
"2021-04-01 15:15:15"
);
for (String date : data) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(threadLocal.get().parse(date));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
threadPool.shutdown();
}
}
多次运行程序,都可以正常解析了。
实际上,JDK1.8 提供了线程安全的 DateTimeFormatter
来替代线程不安全的 SimpleDateFormat
。这里就不详细说明了。
场景二:使用 ThreadLocal
来进行复杂逻辑下的对象传递。
比如一个线程中的任务,它的函数调用栈比较深,或者说调用链有不能修改的第三方库,这时我们想要传递一个监听器参数进去,该怎么办呢?
如果都是自己的代码,可以修改给调用链上的每一个函数增加监听器参数,但是这样改动的地方很多,容易出错,也很麻烦;
如果调用链有不可以改动的第三方库,可以将监听器作为静态变量供线程访问,但是在多线程下每个线程都要有自己的监听器对象,我们就需要用一个集合(以线程名为键,以监听器为值)来管理这些监听器了。这样多线程下在获取指定的监听器的时候,还是会存在就集合的竞争。所以不好。
使用 ThreadLocal
就可以解决这个问题。代码如下:
private static ThreadLocal<Runnable> runnableThreadLocal = new ThreadLocal<>();
public void threadLocalargs(View view) {
new Thread("thread1") {
@Override
public void run() {
task();
}
}.start();
new Thread("thread2") {
@Override
public void run() {
task();
}
}.start();
}
private void task() {
Runnable runnable = () -> Log.d(TAG, "run: " + Thread.currentThread().getName());
runnableThreadLocal.set(runnable);
method1();
}
private void method1() {
method2();
}
private void method2() {
method3();
}
private void method3() {
runnableThreadLocal.get().run();
}
打印日志如下:
D/MainActivity: run: thread1
D/MainActivity: run: thread2
2.2 为什么 ThreadLocal 可以在多个线程中互不干扰地存储和修改数据?
先来看下 ThreadLocal
的基本使用:
private static ThreadLocal<Boolean> sBooleanThreadLocal = new ThreadLocal<>();
private static ThreadLocal<String> sStringThreadLocal = new ThreadLocal<>();
public void threadLocal_basic(View view) {
sBooleanThreadLocal.set(true);
log();
new Thread("Thread#1") {
@Override
public void run() {
super.run();
sBooleanThreadLocal.set(false);
sStringThreadLocal.set(Thread.currentThread().getName());
log();
}
}.start();
new Thread("Thread#2"){
@Override
public void run() {
super.run();
sStringThreadLocal.set(Thread.currentThread().getName());
log();
}
}.start();
}
private void log() {
Log.d(TAG, "["+ Thread.currentThread().getName() +"]"+ "sBooleanThreadLocal.get()=" + sBooleanThreadLocal.get());
Log.d(TAG, "["+ Thread.currentThread().getName() +"]"+ "sStringThreadLocal.get()=" + sStringThreadLocal.get());
}
打印日志如下:
D/MainActivity: [main]sBooleanThreadLocal.get()=true
D/MainActivity: [main]sStringThreadLocal.get()=null
D/MainActivity: [Thread#1]sBooleanThreadLocal.get()=false
D/MainActivity: [Thread#1]sStringThreadLocal.get()=Thread#1
D/MainActivity: [Thread#2]sBooleanThreadLocal.get()=null
D/MainActivity: [Thread#2]sStringThreadLocal.get()=Thread#2
可以看到,虽然我们在不同的线程中访问的都是同样的 ThreadLocal
对象,但是它们从 ThreadLocal
对象中获取的值,正好就是设置的值或者是默认值。
这里我们看的是 JDK1.8 的源码。
我们先来看一下相关的类关系吧:
-
每个
Thread
对象都持有一个ThreadLocal.ThreadLocalMap
对象threadLocals
; -
ThreadLocalMap
是ThreadLocal
的静态内部类,ThreadLocalMap里面维护了一个
Entry数组
table`; -
Entry
是ThreadLocal
的静态内部类,封装了一个键值对,key 是ThreadLoal
对象,value 是ThreadLocal
的泛型对应的值。
绘制类关系图如下:
核心代码如下:
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
}
}
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
接着我们去看 ThreadLocal
的 get()
和 set()
方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
该方法的主要作用:
- 获取当前线程持有的
ThreadLocalMap
变量; - 以当前
ThreadLocal
对象为 key,以泛型对应的值为 value,存入到ThreadLocalMap
中。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
该方法的主要作用:
- 获取当前线程持有的
ThreadLocalMap
变量; - 从
ThreadLocalMap
变量中,以ThreadLocal
对象为 key,查找对应的Entry
对象; - 获取
Entry
对象的 value 部分,并返回。
从 ThreadLocal
的 set
和 get
方法可以看出,ThreadLocal
对象操作的 ThreadLocalMap
对象都是当前线程的 ThreadLocalMap
对象;而每个 ThreadLocalMap
内部会持有一个 Entry
数组,所以 ThreadLocal
对象操作的 Entry
数组都是当前线程的 Entry
数组,也就是说,读/写操作都是针对当前线程的 Entry
数组,不涉及其他线程,不会影响其他线程,所以 ThreadLocal
可以在多个线程中互不干扰地存储和修改数据。
为了更好地理解 ThreadLocal
的线程隔离,把演示 ThreadLocal
的小例子绘制成内存图,如下:
2.3 ThreadLocal 在消息机制中是如何使用的?
public final class Looper {
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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 Looper myLooper() {
return sThreadLocal.get();
}
}
可以看到:
Looper
类中声明了一个ThreadLocal
变量 sThreadLocal
,它被 static
关键字和 final
关键字修饰:被 static
修饰,则无论创建多少个 Looper
对象, sThreadLocal
变量都只占用一份存储区域;被 final
修饰,则变量 sThreadLocal
不可以指向另一个对象。你可能会想:线程可能有很多个,每个线程都可能需要一个 Looper
对象,所有的线程都使用一个 ThreadLocal
对象来管理,这够吗?够了。请看内存图:
prepare()
方法:首先尝试从 sThreadLocal
获取对应线程的 Looper
对象,如果存在就抛出异常,这样就保证了一个线程只能对应一个 Looper
对象;如果不存在,则会创建一个 Looper
对象,并保存在 sThreadLocal
中,实际上是保存在了对应线程的 threadLocals
这个 ThreadLocalMap
里面。
myLooper()
方法:获取对应线程的 Looper
对象,已给外部(如 Handler
、Looper
等)使用。
2.4 消息队列是如何完成消息的插入和读取操作的?
消息队列就是 MessageQueue
,它使用 boolean enqueueMessage(Message msg, long when)
方法完成消息的插入,使用 Message next()
完成消息的读取(在获取到时会先把该消息从消息队列中移除)。
MessageQueue
是通过一个基于时间排序的单链表数据结构来维护消息列表。
下面看一下它的两个主要方法吧。
public final class MessageQueue {
// 链表的头节点
Message mMessages;
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 当队列中没有任何消息,或者新的 Message 的触发时间是最早的,则新的 Message 应该作为头节点
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 新的 Message 应该插入到队列中
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
// 遍历链表,找到应该插入的位置
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
// 插入新的 Message
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
}
public final class MessageQueue {
// 链表的头节点
Message mMessages;
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 当有同步屏障消息时,会进入此分支
if (msg != null && msg.target == null) {
// 通过 do while 循环来获取下一个异步消息。
// 如果没有获取到异步消息,不会结束 do while 循环。
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 下一条消息的触发时间还没有到,则设置一个超时时间以在触发时间到达时唤醒队列
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获取一条消息
mBlocked = false;
// 从链表中移除获取到的消息
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
// 返回获取的消息
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
if (mQuitting) {
dispose();
return null;
}
...// 省略了 IdleHandler 部分
}
...// 省略了 IdleHandler 部分
}
}
}
2.5 Looper 在消息机制中的作用是什么?
Looper
在 Android 的消息机制中扮演着消息循环的角色,它通过 loop()
方法不停地从 MessageQueue
中查看是否有获取到消息:如果获取到消息,就立即交给 Handler
处理,如果获取不到消息,就一直阻塞,如果获取到 null
,就退出消息循环了。
2.6 Looper.prepare() 方法与 Looper.prepareMainLooper() 方法有什么区别?
-
prepare()
方法创建的Looper
消息循环可以退出,prepareMainLooper()
方法创建的消息循环不可以退出。prepare()
方法内部调用prepare(boolean quitAllowed)
传参的参数是true
,而和prepareMainLooper()
内部调用prepare(boolean quitAllowed)
传参的参数是false
,static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); 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)); }
该方法的作用:
-
检查从线程本地变量
sThreadLocal
中获取的Looper
对象是否为null
,不为null
,则抛出异常:每个线程只能创建一个Looper
对象; -
调用
Looper
的构造方法,创建Looper
对象:构造方法里面创建了MessageQueue
对象,并持有了创建Looper
的当前线程private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
quitAllowed
传入到消息队列中,如果这个值为true
,则表示允许退出消息循环;反之,不允许。主线程的消息循环不可以退出。 -
将
Looper
对象保存在线程本地变量sThreadLocal
中。
-
-
prepare()
方法是用于子线程创建Looper
对象的,prepareMainLooper()
是用于主线程创建Looper
对象的。
2.7 Looper 的 quit() 方法和 quitSafely() 方法都用来退出消息循环,它们的区别是什么?
quit()
会设置一个退出标记,并直接退出消息循环;
quitSafely()
只是设置了一个退出标记,Looper
仍会处理完消息队列中的非延时消息后才会退出。
public final class Looper {
final MessageQueue mQueue;
public void quit() {
mQueue.quit(false);
}
public void quitSafely() {
mQueue.quit(true);
}
}
public final class MessageQueue {
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
// 设置退出标记为 true
mQuitting = true;
if (safe) {
// 移除所有需要延时处理的消息,保留需要触发的消息让消息队列继续处理
removeAllFutureMessagesLocked();
} else {
// 移除所有的消息
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
}
不管是调用了 quit()
还是 quitSafely()
方法,如果再向消息队列添加消息,都会返回 false
。
public final class MessageQueue {
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
if (mQuitting) { // 退出标记为 true,进入分支,会返回 false。
...
return false;
}
...
}
return true;
}
}
需要说明的是,在子线程中手动创建 Looper
,并调用Looper
的 loop()
方法,如果不主动调用 quit
方法来退出消息循环,那么这个子线程就一直处于等待的状态,具体来说就是消息循环等待下一个消息。所以,在不需要消息循环时,一定要主动终止 Looper
。
2.8 Looper.loop() 方法在消息队列中没有消息时就会退出,这种说法对吗?
这种说法不对。我们用反证法来说明,假设Looper.loop() 方法在消息队列中没有消息时就会退出的说法正确,那么现在我们在一个子线程里,这样写代码:
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}
然后现在消息队列肯定没有消息,这样 Looper.loop()
方法就会退出了。
但是,我们在主线程使用 mHandler
发送消息,仍然可以发送成功,并且可以在子线程里面接收到。与假设矛盾,所以假设不成立,这种说法是错误的。
我们去看看 loop()
方法在那里 return
了:
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// 如果获取到的 Message 对象为 null,就退出消息循环。
return;
}
msg.target.dispatchMessage(msg);
final long newIdent = Binder.clearCallingIdentity();
msg.recycleUnchecked();
}
}
那么,什么时候 MessageQueue
的 next
方法会返回 null
呢?
当 Looper
的 quit
方法被调用时,消息队列被标记为退出状态,它的 next
方法就会返回 null
;
当 Looper
的 quitSafely
方法被调用时,消息队列被标记为退出状态,等处理完非延时消息后,它的 next
方法才会返回 null
。
实际上,当消息队列中没有消息时,调用 next
方法时不会返回的,它会一直阻塞,这样 loop
方法也阻塞在那里。
2.9 为什么主线程不会因为 Looper.loop() 里的死循环卡死?
首先,需要说明的是 Looper.loop()
里的 for
循环并不是一个死循环,有些同学可能会说,是个死循环啊,我看过源码的。好吧,我们先看一下死循环的定义:死循环(endless loop)是指无法靠自身的控制终止的循环。再看一下,Looper.loop()
方法的源码:
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
// 当取到的消息为 null 时,进入 if 分支,结束掉循环。
if (msg == null) {
return;
}
...// 省略与分析无关的代码
}
}
可以看到,for(;;)
是一个可以考自身的控制终止的循环,因此它不是一个死循环。
但是,在应用运行的时候,这个 for
循环是没有机会结束循环的,所以这里我们称之为无限循环。
其次,主线程需要这样一个无限循环。我们知道,线程里面是一段可执行的代码,当可执行代码执行完成后,线程生命周期就结束了,线程就退出了。但是,对于主线程,它负责控制UI界面的显示、更新和控件交互,所以它的生命周期必须与应用的生命周期一样长才可以。那么,怎么办呢?就需要主线程里的可执行代码执行不完,也就是说主线程里要一直有可执行代码。很容易想到,流程控制中的循环可以达到这一目的吧。Android 正是采用了无限循环的。
最后,说一下卡死。卡死是什么呢?卡死就是程序没有继续执行,而是停在了某个地方。Android 里的 Looper.loop()
方法是循环执行的,所以这里说的卡死应该理解为阻塞才对。阻塞确实是存在的,也是有意义的。这是因为当Android的线程 A 消息队列在空闲时,会调用 nativePollOnce
(这是一个 native 方法,具体来说是通过 Linux 系统的 Epoll 机制中的 epoll_wait
函数进行的),使线程处于休眠状态,释放CPU资源;当在线程 B 里面向线程 A 的消息队列插入消息时,会调用 nativeWake
,唤醒线程 A。
这里的阻塞不会导致应用界面ANR吗?不会的。ANR 是 application not responding,即程序未响应。现在这里发生阻塞是因为程序没有事件需要处理,也就谈不上响应了。
2.10 Handler 什么情况下会导致内存泄漏?为什么?怎么办?
参考:一次性讲清楚 Handler 可能导致的内存泄漏和解决办法
这篇文章比较详细地讲解了 Handler 内存泄漏的问题。
2.11 为什么 Looper.loop() 方法和 MessageQueue.next() 方法里都有一个 for(;😉 循环?
Looper.loop()
方法中的 for(;;)
循环的作用是形成线程里的无限循环,在循环里执行要处理的消息;MessageQueue.next()
方法中的 for(;;)
循环的作用是为了可以在没有消息或者消息执行时刻未到达时执行 nativePollOnce
方法,使线程进入休眠状态,释放 CPU 资源。
2.12 消息的分类以及它们的作用分别是什么?
消息分为同步消息,异步消息和同步屏障消息三种。
消息类型 | isAsynchronous | target |
---|---|---|
同步消息 | false | 不为 null |
异步消息 | true | 不为 null |
同步屏障消息 | false(这点只是默认值,不重要) | 为 null |
直观地查看同步屏障的作用,可以查看笔者的Android筑基——可视化方式理解 Handler 的同步屏障机制。
当消息队列中没有同步屏障消息时,同步消息和异步消息会同等处理,即按照时间优先级顺序处理;
当消息队列中有同步屏障消息时,会只处理异步消息,不会处理同步消息。
对应的代码如下:
Message next() {
...
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 判断是否有同步屏障消息
if (msg != null && msg.target == null) {
// 被同步屏障挡住了.
// 通过 do while 循环去查找到一个异步消息:
// 如果查找到异步消息,就退出 do while 循环;
// 如果查找不到异步消息,就一直处于 do while 循环中。
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
// 下面是准备取出消息的代码
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}
if (mQuitting) {
dispose();
return null;
}
}
nextPollTimeoutMillis = 0;
}
}
2.13 如何获取 Message
? Message
使用完后如何处理?为什么这样设计?
通过 Message
的一系列 obtain
工具类方法来获取 Message
对象,内部都调用了:
private static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
这个方法的作用是:获取 Message
对象的时候,尝试从 sPool
单链表里面取出一个 Message
实例,如果取到就直接返回;否则,只能创建一个新的 Message
对象返回。
需要注意的是,
sPool
单链表消息缓存池的大小是有上限的,即MAX_POOL_SIZE = 50
;sPool
是静态的,这就说明在整个进程中只有一个sPool
,整个进程共用一个消息缓存池。
Message
使用完毕后,会尝试将它放回消息缓存池里面,具体看下面的 recycleUnchecked
方法:
void recycleUnchecked() {
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
在 Looper
和 MessageQueue
中会调用此方法来回收消息。
这个方法的主要作用:先清除 Message
对象的字段值,再尝试把它放到消息缓存池里面。
为什么要这么设计呢?
这是因为在 Android 中,使用 Message
的场景非常多,如果都是通过 new
的方式来创建会带来性能消耗;而采用消息缓存池,就可以复用 Message
实例,一定程度上减少 new
的方式来创建,更加高效。这其实就是享元设计模式的应用了。
3. 主线程的消息循环
3.1 主线程的消息循环模型是如何建立的?
构建主线程消息循环,需要调用 Looper.prepareMainLooper()
方法,来创建 MessageQueue
对象和 Looper
对象。我们查看调用这个方法的地方有哪些:
可以看到在三个地方调用了这个方法:ActivityThread.java
、Bridge.java
和 SystemServer.java
,
其中 ActivityThread
的 main
函数里开启了 App 进程的主线程消息循环,SystemServer
的 run
方法开启了 system_server 进程的主线程消息循环。
这里以 App 进程的主线程消息循环来做说明:
ActivityThread
类是 App 进程的初始类,它的 main
函数是 App 进程的入口。Looper.prepareMainLooper()
方法就是在它的 main
函数中调用的。
我们以 scheduleLaunchActivity
为例,来说明主线程的消息循环,关键代码如下:
public final class ActivityThread {
final ApplicationThread mAppThread = new ApplicationThread();
final H mH = new H();
private class ApplicationThread extends ApplicationThreadNative {
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, CompatibilityInfo compatInfo,
IVoiceInteractor voiceInteractor, int procState, Bundle state,
PersistableBundle persistentState, List<ResultInfo> pendingResults,
List<Intent> pendingNewIntents, boolean notResumed, boolean isForward,
ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
... // 省略一系列赋值操作
sendMessage(H.LAUNCH_ACTIVITY, r);
}
...// 省略了其他的跨进程调用方法,只保留 scheduleLaunchActivity 方法
}
private class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
... // 省略了其他的常量定义,只保留 LAUNCH_ACTIVITY
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
... // 省略了其他的 case 分支,只保留 LAUNCH_ACTIVITY 分支
}
}
}
private void sendMessage(int what, Object obj) {
sendMessage(what, obj, 0, 0, false);
}
private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
Message msg = Message.obtain();
msg.what = what;
msg.obj = obj;
msg.arg1 = arg1;
msg.arg2 = arg2;
if (async) {
msg.setAsynchronous(true);
}
mH.sendMessage(msg);
}
private void attach(boolean system) {
if (!system) {
final IActivityManager mgr = ActivityManagerNative.getDefault();
mgr.attachApplication(mAppThread);
} else {
...
}
}
public static void main(String[] args) {
...
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
AsyncTask.init();
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}
下面对关键代码进行说明:
-
main
方法是 App 进程的入口,在这里面:-
通过
Looper.prepareMainLooper()
创建了主线程的Looper
以及MessageQueue
; -
实例化了
ActivityThread
对象,这样也就在主线程初始化了ActivityThread
对象的成员变量mAppThread
和mH
。final ApplicationThread mAppThread = new ApplicationThread(); final H mH = new H();
ApplicationThread
和H
都是ActivityThread
的内部类,其中ApplicationThread
是作为IApplicationThread
binder 接口的服务端对象存在的,IApplicationThread
是 system_server 进程向 App 进程发起通信的桥梁;H
是一个Handler
类的子类。 -
调用
ActivityThread
对象的attach(false)
方法,内部把mAppThread
设置给了 AMS,相当于 App 进程设置了一个回调接口对象给 AMS,这样 AMS 需要向 App 进程发起通信时就可以通过mAppThread
了。 -
调用
Looper.loop()
方法,开启主线程的消息循环。
-
-
当 AMS 需要向客户端进程发起
scheduleLaunchActivity
请求时,就会通过它持有的IApplicationThread
客户端对象来发起这个请求,经过 binder 驱动,会调用到ApplicationThread
的scheduleLaunchActivity
方法中,需要特别说明的是,因为是跨进程通信,此时ApplicationThread
的scheduleLaunchActivity
方法是运行在客户端进程的 binder 线程池中。 -
接着调用
ActivityThread
的sendMessage
方法,将需要发送的数据封装成一个Message
对象,接着调用了mH
的sendMessage
方法。 -
在
H
类的hanldeMessage
方法中接收到消息,这样就完成了将数据从binder线程池切换到了主线程。
参考
-
Android中子线程真的不能更新UI吗?;
说明了子线程不能操作主线程 UI 的原因,以及演示子线程可以操作主线程 UI 的例子,主要就是有没有走checkThread
方法。 -
Android子线程真的不能更新UI么;
说明了在子线程里面更新 UI 是可以的,只要 View 视图体系是它自己创建的。 -
Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
这是知乎上对于这个问题的讨论。
-
这里面回答了20个关于 Handler 的问题,真的很好。
-
这篇文章比较详细地讲解了 Handler 内存泄漏的问题。