06.Android之消息机制问题

2,307 阅读10分钟

目录介绍

  • 6.0.0.1 谈谈消息机制Hander作用?有哪些要素?流程是怎样的?
  • 6.0.0.2 为什么一个线程只有一个Looper、只有一个MessageQueue,可以有多个Handler?
  • 6.0.0.3 可以在子线程直接new一个Handler吗?会出现什么问题,那该怎么做?
  • 6.0.0.4 Looper.prepare()能否调用两次或者多次,会出现什么情况?
  • 6.0.0.5 为什么系统不建议在子线程访问UI,不对UI控件的访问加上锁机制的原因?
  • 6.0.0.6 如何获取当前线程的Looper?是怎么实现的?(理解ThreadLocal)
  • 6.0.0.7 Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?
  • 6.0.0.8 Handler.sendMessageDelayed()怎么实现延迟的?结合Looper.loop()循环中,Message=messageQueue.next()和MessageQueue.enqueueMessage()分析。
  • 6.0.0.9 Message可以如何创建?哪种效果更好,为什么?
  • 6.0.1.3 使用Hanlder的postDealy()后消息队列会发生什么变化?
  • 6.0.1.4 ThreadLocal有什么作用?

好消息

  • 博客笔记大汇总【15年10月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计500篇[近100万字],将会陆续发表到网上,转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!所有的笔记将会更新到GitHub上,同时保持更新,欢迎同行提出或者push不同的看法或者笔记!

6.0.0.1 谈谈消息机制Hander作用?有哪些要素?流程是怎样的?

  • 作用:
    • 跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
  • 四要素:
    • Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。技术博客大总结
    • MessageQueue(消息队列):用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
    • Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
    • Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。
  • 具体流程
    • Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
    • 通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
    • 调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
    • image

6.0.0.2 为什么一个线程只有一个Looper、只有一个MessageQueue,可以有多个Handler?

  • 注意:一个Thread只能有一个Looper,可以有多个Handler
    • Looper有一个MessageQueue,可以处理来自多个Handler的Message;MessageQueue有一组待处理的Message,这些Message可来自不同的Handler;Message中记录了负责发送和处理消息的Handler;Handler中有Looper和MessageQueue。
  • 为什么一个线程只有一个Looper?技术博客大总结
    • 需使用Looper的prepare方法,Looper.prepare()。可以看下源代码,Android中一个线程最多仅仅能有一个Looper,若在已有Looper的线程中调用Looper.prepare()会抛出RuntimeException(“Only one Looper may be created per thread”)。
    • 所以一个线程只有一个Looper,不知道这样解释是否合理!更多可以查看我的博客汇总:github.com/yangchong21…
    public static void prepare() {
        prepare(true);
    }
    
    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));
    }
    

6.0.0.3 可以在子线程直接new一个Handler吗?会出现什么问题,那该怎么做?

  • 不同于主线程直接new一个Handler,由于子线程的Looper需要手动去创建,在创建Handler时需要多一些方法:
    • Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。因此Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。
    • 正确的使用方法是:技术博客大总结
    handler = null;
    new Thread(new Runnable() {
       private Looper mLooper;
       @Override
       public void run() {
           //必须调用Looper的prepare方法为当前线程创建一个Looper对象,然后启动循环
           //prepare方法中实质是给ThreadLocal对象创建了一个Looper对象
           //如果当前线程已经创建过Looper对象了,那么会报错
           Looper.prepare();
           handler = new Handler();
           //获取Looper对象
           mLooper = Looper.myLooper();
           //启动消息循环
           Looper.loop();
           //在适当的时候退出Looper的消息循环,防止内存泄漏
           mLooper.quit();
       }
    }).start();
    
  • 主线程中默认是创建了Looper并且启动了消息的循环的,因此不会报错:应用程序的入口是ActivityThread的main方法,在这个方法里面会创建Looper,并且执行Looper的loop方法来启动消息的循环,使得应用程序一直运行。

6.0.0.4 Looper.prepare()能否调用两次或者多次,会出现什么情况?

  • Looper.prepare()方法源码分析
    • 可以看到Looper中有一个ThreadLocal成员变量,熟悉JDK的同学应该知道,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    public static void prepare() {
        prepare(true);
    }
    
    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));
    }
    
  • 思考:Looper.prepare()能否调用两次或者多次
    • 如果运行,则会报错,并提示prepare中的Excetion信息。由此可以得出在每个线程中Looper.prepare()能且只能调用一次
    • 技术博客大总结
    //这里Looper.prepare()方法调用了两次
    Looper.prepare();
    Looper.prepare();
    Handler mHandler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           if (msg.what == 1) {
              Log.i(TAG, "在子线程中定义Handler,并接收到消息。。。");
           }
       }
    };
    Looper.loop();
    

6.0.0.5 为什么系统不建议在子线程访问UI,不对UI控件的访问加上锁机制的原因?

  • 为什么系统不建议在子线程访问UI
    • 系统不建议在子线程访问UI的原因是,UI控件非线程安全,在多线程中并发访问可能会导致UI控件处于不可预期的状态。
  • 不对UI控件的访问加上锁机制的原因

6.0.0.7 Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

  • 问题描述
    • 在处理消息的时候使用了Looper.loop()方法,并且在该方法中进入了一个死循环,同时Looper.loop()方法是在主线程中调用的,那么为什么没有造成阻塞呢?
  • ActivityThread中main方法
    • ActivityThread类的注释上可以知道这个类管理着我们平常所说的主线程(UI线程)
      • 首先 ActivityThread 并不是一个 Thread,就只是一个 final 类而已。我们常说的主线程就是从这个类的 main 方法开始,main 方法很简短
      public static final void main(String[] args) {
          ...
          //创建Looper和MessageQueue
          Looper.prepareMainLooper();
          ...
          //轮询器开始轮询
          Looper.loop();
          ...
      }
      
  • Looper.loop()方法无限循环
    • 看看Looper.loop()方法无限循环部分的代码
      while (true) {
         //取出消息队列的消息,可能会阻塞
         Message msg = queue.next(); // might block
         ...
         //解析消息,分发消息
         msg.target.dispatchMessage(msg);
         ...
      }
      
  • 为什么这个死循环不会造成ANR异常呢?
    • 因为Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。技术博客大总结
  • 处理消息handleMessage方法
    • 如下所示
      • 可以看见Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施。
      • 如果某个消息处理时间过长,比如你在onCreate(),onResume()里面处理耗时操作,那么下一次的消息比如用户的点击事件不能处理了,整个循环就会产生卡顿,时间一长就成了ANR。
      public void handleMessage(Message msg) {
          if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
          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 RELAUNCH_ACTIVITY: {
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
                  ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                  handleRelaunchActivity(r);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
              }
              break;
              case PAUSE_ACTIVITY:
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
                  handlePauseActivity((IBinder) msg.obj, false, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 2) != 0);
                  maybeSnapshot();
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                  break;
              case PAUSE_ACTIVITY_FINISHING:
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
                  handlePauseActivity((IBinder) msg.obj, true, (msg.arg1 & 1) != 0, msg.arg2, (msg.arg1 & 1) != 0);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                  break;
              ...........
          }
      }
      
  • loop的循环消耗性能吗?
    • 主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
    • 简单的来说:ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。

6.0.0.9 Message可以如何创建?哪种效果更好,为什么?runOnUiThread如何实现子线程更新UI?

  • 创建Message对象的几种方式:技术博客大总结
    • Message msg = new Message();
    • Message msg = Message.obtain();
    • Message msg = handler1.obtainMessage();
  • 后两种方法都是从整个Messge池中返回一个新的Message实例,能有效避免重复Message创建对象,因此更鼓励这种方式创建Message
  • runOnUiThread如何实现子线程更新UI
    • 看看源码,如下所示
    • 如果msg.callback为空的话,会直接调用我们的mCallback.handleMessage(msg),即handler的handlerMessage方法。由于Handler对象是在主线程中创建的,所以handler的handlerMessage方法的执行也会在主线程中。
    • 在runOnUiThread程序首先会判断当前线程是否是UI线程,如果是就直接运行,如果不是则post,这时其实质还是使用的Handler机制来处理线程与UI通讯。
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    
    @Override
    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }
    

6.0.1.3 使用Hanlder的postDealy()后消息队列会发生什么变化?

  • post delay的Message并不是先等待一定时间再放入到MessageQueue中,而是直接进入并阻塞当前线程,然后将其delay的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。此时,如果队头的Message正是被delay的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该Message,否则唤醒后直接执行。

6.0.1.4 ThreadLocal有什么作用?

  • 线程本地存储的功能
    • ThreadLocal类可实现线程本地存储的功能,把共享数据的可见范围限制在同一个线程之内,无须同步就能保证线程之间不出现数据争用的问题,这里可理解为ThreadLocal帮助Handler找到本线程的Looper。
    • 技术博客大总结
  • 怎么存储呢?底层数据结构是啥?
    • 每个线程的Thread对象中都有一个ThreadLocalMap对象,它存储了一组以ThreadLocal.threadLocalHashCode为key、以本地线程变量为value的键值对,而ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,也就包含了一个独一无二的threadLocalHashCode值,通过这个值就可以在线程键值值对中找回对应的本地线程变量。

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客