06.Android之消息机制问题

作者 : 开心源码 本文共8187个字,预计阅读时间需要21分钟 发布时间: 2022-05-12 共130人阅读

目录详情

  • 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万字],将会陆续发表到网上,转载请注明出处,谢谢!
  • 链接地址: 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,不知道这样解释能否正当!更多可以查看我的博客汇总: 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控件的访问加上锁机制的起因
    • 上锁会让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);    }}@Overridepublic 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.关于博客汇总链接

  • 1.技术博客汇总
  • 2.开源项目汇总
  • 3.生活博客汇总
  • 4.喜马拉雅音频汇总
  • 5.其余汇总

02.关于我的博客

  • 我的个人站点:www.yczbj.org, www.ycbjie.cn
  • github: yangchong211
  • 知乎:https://www.zhihu.com/people/yczbj/activities
  • 简书:http://www.songma.com/u/b7b2c6ed9284
  • csdn:http://my.csdn.net/m0_37700275
  • 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
  • 开源中国:https://my.oschina.net/zbj1618/blog
  • 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
  • 邮箱:yangchong211@163.com
  • 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
  • segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
  • 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 06.Android之消息机制问题

发表回复