-Android 的消息机制读书笔记

Posted willwaywang6

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了-Android 的消息机制读书笔记相关的知识,希望对你有一定的参考价值。

1. android 的消息机制概述

1.1 Android 的消息机制是什么?

Android 的消息机制是通过 Handler 的运行机制来实现将一个任务切换到 Handler 所在的线程中去执行。

但是,完成把一个任务切换到 Handler 所在的线程中去执行这个事情,单靠 Handler 类是不行的;实际上,Handler 的运行需要 MessageQueueLooper 的支撑,Handler 是作为 Android 消息机制的上层接口而已。

换句话说,Android 定义了Handler 直接面向了开发者,屏蔽了 MessageQueueLooper(没有完全屏蔽 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.");
    

只要 mThreadThread.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 来说,它需要获取当前线程的 LooperLooper 的作用域就是线程并且不同线程具有不同的 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 - 不完整信息的静态博弈 机制设计

java线程的等待通知机制读书笔记

《需求工程-软件建模与分析之读书笔记之四》

JavaScript设计模式与开发实践---读书笔记 发布-订阅模式

读书笔记iOS-发布你的促销消息-推动通知