Handler初探(一)

156 阅读6分钟

Handler源码解析:

  • Android源码:

    • Android11--->30的源码
  • 第二期Handler源码学习:

  • Handler作用

    • 依托消息管理机制,进行线程切换;

    • 共享内存池,MessageQueue:避免内存抖动

    1. next函数:将消息传出去
    1. Looper.loop():轮询,取出消息

      • 采用for循环不断进行轮询(死循环),取出消息

      • 此时,ActivityThread中的main就会在死循环中不断执行

        • 因为APP启动后,一直可见(loop中的死循环就保证了代码不会停止)
        • 程序不会停下的好处:类似于后台服务器一直保证活(loop是心跳机制)
        • 但是当没有消息了,loop就会被阻塞(block)
      • 由ActivityThread调用

    2. 调用dispatch函数进行分发:

  • APP中的所有事务处理都是Handler

    • 每一个APP事务都是Message;
    • 服务/Fragment中的每一个生命周期都是一个Message
    • 相当于所有代码都是运行在Message之上了
    • 证明了所有的异常的log信息的起点都是Looper.loop
  • 在处理APP卡顿:定位卡顿位置,判断每个生命周期的执行时间

    • 基础:

      • Handler对外开放了接口
      • 所有主线程中的代码都是Message
    • 思路:BlockCanary

      • 搞一个日志管理系统:通过日志打印时间不同得出每段生命周期的执行时间,就行了
      • 找每个生命周期的执行时间--->找对应的消息的时间
    • 执行:BlockCanary的实现思路

      • 在Looper中有一个msg.target.dispatchMessage(msg)代码
      • 在前后分别整一个开始时间和结束时间--->对logging赋值
      • Looper中有一个叫setMessageLogging的接口,设置一个类,就是接口的参数,就可以打印各个生命周期执行的时间
      • 最后再在msg.target.dispatchMessage(msg)这行代码后面的还有一个logging对应就行了
  • 从handler.sendMessage到Handler处理发生了什么?

    • 概述:

      • sendMessage:向消息队列中添加消息,生产者
      • next():从消息队列中取出消息,消费者
      • 消息队列本身可以看作仓库,整体为生产者消费者模型
    • 生产者--消费者是一个带阻塞的系统

      • 当仓库满了--->入队列被阻塞

      • 当仓库空了--->出队列被阻塞

      • 仓库问题:本身没有设置上限,但是当系统没有内存,进程没有内存时就是满了

        • 为什么不设置上限:达到上限APP就挂掉了

          • 因为Message本身就是一个心跳机制并且对于60HZ的手机,一秒刷新60次,平均16.7ms刷新一次UI,一次刷新带来3条Message;直接干没了。直接就挂掉了
      • 仓库什么时候醒来?入队列就醒了

        • 会调用Native方法
    • 什么是阻塞,为什么仓库空了要阻塞,为什么这个阻塞不会导致ANR?

      • 什么是阻塞:非得等,并且不能干其他的,阻塞就是睡觉,将CPU的资源放出去;这个时候线程都挂起来了

      • 这个时候可以睡觉,当可以了,就会通知你

      • 因为安卓中的每一个都可以看做消息,当这个仓库都会空了,那么就让它睡觉,把CPU的资源交出去;

      • 这个时候为什么不会ANR,明明线程都挂起来了,而且挂的时间还比较久

        • Handler的阻塞与Message的阻塞两码事并且ANR也算一个消息

        • ANR可以看作一个定时任务没有在定时范围内完成,就像埋了一个定时炸弹;

          没有完成就爆炸,并且弹个ANR

    • Android阻塞是如何实现的:

      • 消息队列入队列是如何排序的:for循环轮询,按照执行时间排序,时间早在前面

      • 但是当我拿消息(加了锁的Synchronized)的时候,还没有等到它的执行时间,那么就阻塞一秒钟;但这个是不可能的;因为至少系统会不断刷新UI

      • 具体实现

        1. 取出对头消息,拿到时间,计算出需要等的时间

        2. 退出,将这个时间通过nativePollOnce传到native代码中:

        3. java去调用底层代码--->Linux 层;java虚拟机是由C++实现的,

          java调用底层代码(JNI 层),此时将这个java代码打上native标记

      • 什么是静态调用:

        • java层中调用的跟Native层中调用的是一样的
  • Handler中的阻塞:最终是epoll_wait()

    • 调用流程:

      1. 当消息需要等待的时候调用nativePollOnce(this,等待时间)

      2. 调用与之同名的JNI层的函数:android_os_MessageQueue_nativePollOnce

      3. 然后转交到NativeMessageQueue::pollOnce

        • 调用C++层中对应的mLooer->pollOnce
      4. 再到pollInner

        • 最后到这个epoll_wait里面来(在这里线程才被阻塞,上层调用者也因此被阻塞)
  • 非阻塞忙轮询

    • 一分钟干一个电话,问到没有
  • 场景:设计到I/O:(当有多个网络请求,从数据库中拿数据)

    • 阻塞:读数据的时候用的

      • 因为在读取数据的时候,由于读取的速度不同(缓存,阻塞)

      • 一个线程中如何处理多个I/O?最节约时间的

        • 一个线程里面用while循环,遍历所有的流;有就拿,拿完所有的流
        • 当所有的流都没有数据了,这个时候还是在for循环里面但是没有读取数据的动作,CPU就空转--->CPU资源浪费
        • 所以当流都读完了,就阻塞,避免CPU一直在for循环中挣扎
    • 引入select:处理的是一系列的流,是一把

      • 还有一种解决办法:在for循环之前使用select选择流(Linux 进行实现的),当都没有需要处理的流了,CPU在select里面阻塞;
      • select只知道这个一系列流中有I/O需要处理但是它不知道要处理谁,要处理多少个;这个是硬件层面的了
      • 一般情况下都是,先使用select判断一下;这一系列中有需要处理的,那么就搞个循环去处理所有的流;select就是先对一大把过滤一下,最后细化
    • 细节

      • 即使采用多个线程处理多个I/O :由于CPU底层决定了,每一次I/O,都会涉及到硬盘的读取,每一次读取都会降低效率;
    • 引入epoll:epoll是Linux内核中的一种可扩展IO事件处理机制。大量应用程序请求时能够获得较好的性能。

  • 引入epoll

    • 工作原理:

      • 使用epoll_wait()处理所有的流,当这个流有I/O需要处理的话,就把这个流加到activie_stream[]去--->后面只需要使用一个for循环去遍历activie_stream[]就行了
    • 好处:

      • CPU底层是需要对所有的流都进行一次遍历O(n),但是引入epoll机制后,将时间复杂度降到O(1)了
    • 重要函数:

      • int epoll_create(int size); 创建一个epoll的句柄, size用来告诉内核需要监听的数目一共有多大

        在创建MessageQueue的时候,会调用nativeInit,在这个里面完成对epoll进行初始化

      • int epoll_ctl(int epfd, int op, int fd, structepoll_event *event); epoll的事件注册函数,因为MessageQueue本身就可以看做是一个事件

      • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)

      • 线程与epoll之间是没有关系的,是在MessageQueue搞了一个出来就会带上一个epoll