zoukankan      html  css  js  c++  java
  • 06.Android之消息机制问题

    目录介绍

    • 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万字],将会陆续发表到网上,转载请注明出处,谢谢!
    • 链接地址:https://github.com/yangchong211/YCBlogs
    • 如果觉得好,可以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,不知道这样解释是否合理!更多可以查看我的博客汇总:https://github.com/yangchong211/YCBlogs
      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.关于我的博客

  • 相关阅读:
    对应于 算法原型 中的“纸条”
    Hive 实际上对于所存储的文件的完整性以及数据内容是否和表结构一致无支配力
    kettle
    regularexpression_action
    Consumer Group Example
    Neural Task Programming: Learning to Generalize Across Hierarchical Tasks
    dm层 集市层 四层 Build a multi-level data strategy
    图片服务器 sns 加爬虫 免上云
    不等式放缩 维时间戳是数据仓库无时间维度的事实表的灾难 度表 事实表
    MaLoc: a practical magnetic fingerprinting approach to indoor localization using smartphones
  • 原文地址:https://www.cnblogs.com/yc211/p/10254600.html
Copyright © 2011-2022 走看看