-Android 的消息机制读书笔记
Posted willwaywang6
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了-Android 的消息机制读书笔记相关的知识,希望对你有一定的参考价值。
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以上是关于-Android 的消息机制读书笔记的主要内容,如果未能解决你的问题,请参考以下文章
读书笔记: 博弈论导论 - 14 - 不完整信息的静态博弈 机制设计