Handler全解析

Handler相关

1. 用一句话概括Handler,并简述其原理

Handler是Android系统的根本,在Android应用被启动的时候,会分配一个单独的虚拟机,虚拟机会执行ActivityThread中的main方法,在main方法中对主线程Looper进行了初始化,也就是几乎所有代码都执行在Handler内部。Handler也可以作为主线程和子线程通讯的桥梁。

Handler通过sendMessage发送消息,将消息放入MessageQueue中,在MessageQueue中通过时间的维度来进行排序,Looper通过调用loop方法不断的从MessageQueue中获取消息,执行Handler的dispatchMessage,最后调用handleMessage方法。


2. 为什么系统不建议在子线程访问UI?【为什么不能在子线程更新UI?】

在某些情况下,在子线程中是可以更新UI的。但是在ViewRootImpl中对UI操作进行了checkThread,但是我们在OnCreateonResume中可以使用子线程更新UI,由于我们在ActivityThread中的performResumeActivity方法中通过addView创建了ViewRootImpl,这个行为是在onResume之后调用的,所以在OnCreate和onResume可以进行更新UI。

但是我们不能在子线程中更新UI,因为客户端很难控制UI的更新在ViewRootImpl被创建之前执行,也就是checkThread之前,如果添加了耗时操作之后,一旦ViewRootImpl被创建将会抛出异常。一旦在子线程中更新UI,容易产生并发问题。


3. 一个Thread可以有几个Looper?几个Handler?

一个线程只能有一个Looper,可以有多个Handler

一个线程中只有一个Looper。Looper的构造方法被声明为了private,我们无法通过 new关键字来实例化 Looper,
唯一开放的可以实例化Looper的地方是prepare,prepare方法源码

   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));
   }

我们知道ThreadLocal是一个线程内部的数据存储类,当某个线程执行prepare方法的时候,会首先通过ThreadLocal检查这个线程是否已经创建了Looper

  • 如果还没创建,他会创建一个Looper并且将Looper保存在ThreadLocal中,每个线程都有一个LocalThreadMap【注意LocalThreadMap是在线程中,而不是在ThreadLocal中】,ThreadLocal会利用LocalThreadMap,将Looper保存在对应线程中的LocalThreadMap,keyThreadLocalvalueLooper

  • 而如果 ThreadLocal中已经保存了Looper,则会抛出一个RuntimeException异常。
    那么意味着在一个线程中最多只能调用一次prepare方法,这样就保证了Looper的唯一性


4. 可以在子线程直接new一个Handler吗?那该怎么做?

可以在子线程中创建Handler
我们需要调用Looper.perpare和Looper.loop方法,或者通过获取主线程的looper来创建Handler

//1.直接获取当前子线程的looper
new Thread(new Runnable() {
   @Override
   public void run() {
       Looper.prepare();
       Handler handler = new Handler(){
           @Override
           public void handleMessage(Message msg) {
               Log.e("handleMessage","handleMessage1");
           }
       };
       handler.sendEmptyMessage(1);
       Looper.loop();
   };
}).start();

//2.通过主线程的looper来实现的
new Thread(new Runnable() {
   @Override
   public void run() {
       Handler handler = new Handler(Looper.getMainLooper()) { //重点在此处
           @Override
           public void handleMessage(Message msg) {
               Log.e("handleMessage","handleMessage2");
           }
      };
      handler.sendEmptyMessage(1);
   }
}).start();


5. Message可以如何创建?哪种效果更好,为什么?

可以new Message,但是更推荐Message.obtain来创建Message。这样会复用之前的Message的内存,不会频繁的创建对象,导致内存抖动。


6. 主线程中Looper的轮询死循环为何没有阻塞主线程?【重点

本身在没有任何事件的时候 就是阻塞的,一旦有事件就不阻塞了

阻塞在ActivityThread main方法
app阻塞两种方式

while(true){
   
}
  1. 队列的方式阻塞

Looper的阻塞是第二种,是释放CPU的

Android是以事件驱动型的操作系统

线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。

例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?

答:通过创建新线程的方式。举例ActivityThread#main()

public static void man(String[] args){
    ...
    Looper.prepareMainLooper();
    ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
    ...
    Looper.loop();
    ...
}

thread.attach(false, startSeq);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。

真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死

主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loopqueue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质是同步I/O,即读写是阻塞的

所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

简单回答:
Looper轮询是死循环,但是当没有消息的时候他会block,ANR是当我们处理点击事件的时候5s内没有响应,我们在处理点击事件的时候也是用的Handler,所以一定会有消息执行,并且ANR也会发送Handler消息,所以不会阻塞主线程。


7. 使用Handler的postDelayed()后消息队列会发生什么变化?

Handler发送消息到消息队列,消息队列是一个时间优先级队列,内部是一个单向链表。发动postDelayed之后会将该消息进行时间排序存放到消息队列中


8. 点击页面上的按钮后更新TextView的内容,谈谈你的理解?【同步屏障】

点击按钮的时候会发送消息到Handler,但是为了保证优先执行,会加一个标记异步,同时会发送一个target为null的消息,这样在使用消息队列的next获取消息的时候,如果发现消息的target为null,那么会遍历消息队列将有异步标记的消息获取出来优先执行,执行完之后会将target为null的消息移除。(同步屏障)


9. 描述下Handler中的生产者-消费者设计模式?

举个例子,面包店厨师不断在制作面包,客人来了之后就购买面包,这就是一个典型的生产者消费者设计模式。但是需要注意的是如果消费者消费能力大于生产者,或者生产者生产能力大于消费者,需要一个限制。

在java里有一个blockingQueue。当目前容器内没有东西的时候,消费者来消费的时候会被阻塞,当容器满了的时候也会被阻塞。Handler.sendMessage相当于一个生产者,MessageQueue相当于容器,Looper相当于消费者。


10. Handler是如何完成子线程和主线程通信的?【如何进行线程切换】

在主线程中创建Handler,在子线程中发送消息,放入到MessageQueue中,通过Looper.loop取出消息进行执行handleMessage,由于looper我们是在主线程初始化的,在初始化looper的时候会创建消息队列,所以消息是在主线程被执行的。

  • 假设现在有一个线程A,在A线程通过Looper.prepare 和 Looper.loop来开启Looper,并且在A线程中实例化出来一个Handler。
    Looper.prepare()方法被调用时会初始化Looper 并为ThreadLocal设置Looper,此时ThreadLocal中就存储了A线程的Looper。
    另外MessageQueue也会在Looper中被初始化

  • 接着当调用Looper.loop方法时,loop方法会通过myLooper得到A线程中的Looper,进而拿到Looper中的MessageQueue,接着开启死循环等待执行MessageQueue中的方法。

  • 此时再开启一个线程B,并在B线程中通过Handler【A线程中实例化的那个】发送出一个Message,这个Message最终会通过sendMessageAtTime方法,调用到MessageQueue【A线程中实例化的Handler所持有的】的 enqueueMessage方法,将消息插入到队列【插入之前,还会将Message实例的target赋值为当前Handler】

  • 由于Looper的loop是一个死循环,当MessageQueue中被插入消息的时候,loop方法就会通过queue.next()取出MessageQueue中的消息,并执行msg.target.dispatchMessage(msg)。进而调用Handler的handleCallbackhandleMessage,而这些方法都是执行在A线程中的,因此达到了线程的切换。


11. 关于ThreadLocal,谈谈你的理解?

ThreadLocal类似于每个线程有一个单独的内存空间,不共享,ThreadLocal在set的时候会将数据存入对应线程的ThreadLocalMap中,key=ThreadLocal,value=值
详见: https://www.jianshu.com/p/f1e71028f4e6

注:ThreadLocal的扩容
结论:在清理过期Entery后如果长度 >= 原长度的2/3时会进行rehash,再次清理过期Entery后如果长度 >= 原长度的1/2时会进行扩容,扩容为原长度的2倍。
详见:https://blog.csdn.net/u010002184/article/details/82227795


12. 享元设计模式有用到吗?

享元设计模式就是重复利用内存空间,减少对象的创建。
Message中使用到了享元设计模式。内部维护了一个链表,并且最大长度是50,当消息处理完之后会将消息内的属性设置为空,并且插入到链表的头部,使用obtain创建的Message会从头部获取空的Message


13. Handler内存泄漏问题及解决方案

非静态内部类和匿名内部类都会隐式持有外部类的引用,这会导致内存泄漏。例如Activity退出的时候,MessageQueue中还有一个Message没有执行,这个Message持有了Handler的引用,而Handler持有了Activity的引用,导致Activity无法被回收,导致内存泄漏。
解决方案:使用static关键字修饰,在onDestory的时候将消息清除。

//注:静态内部类的弱引用包裹Activity,是为了方便使用 activity. 获取变量方法等
//如果直接使用强引用,显然会导致activity泄露
private WeakReference<Activity> mWeakReference;


14. 子线程中维护的Looper,消息队列无消息的时候处理方案是什么?有什么用?

在Looper.loop()方法中,for死循环里,当message为空时,会退出整个循环。所以 子线程无消息的时候要调用Looper.quit()退出

  • 如果在主线程下,不会调用quit方法,但是会无限等待,等到下次Message进来,也就是messageQueue.enqueue方法,会通过底层方法 nativeWake(mPtr) 唤醒
  • 如果在子线程下,会调用quit方法,返回message=null,最后会停止loop 的for循环


15. 既然可以存在多个Handler往MessageQueue中添加数据(发消息是各个handler可能处于不同线程),那他内部是如何确保线程安全的?

在添加数据MessageQueue#enqueueMessage和执行MessageQueue#next的时候都加了this锁,这样可以保证添加的位置是正确的,获取的也会是最前面的。


16. Handler异步消息处理【HandlerThread】

HandlerThread是Android API提供的一个方便、便捷的类,使用它我们可以快速的创建一个带有Looper的线程。Looper可以用来创建Handler实例。注意:start()仍然必须被调用。

  • HandlerThread本质上是一个线程类,它继承了Thread;

  • HandlerThread有自己的内部Looper对象,可以进行looper循环;

  • 通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务。

  • 创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,创建Looper对象。

  • 内部使用了Handler + Thread,并且处理了getLooper的并发问题。如果获取Looper的时候发现Looper还没创建,则wait,等待looper创建了之后在notify

基础使用步骤

  1. 创建HandlerThread线程

  2. 运行线程

  3. 获取HandlerThread线程中的Looper实例

  4. 通过这个Looper实例创建Handler实例,从而使mSubThreadHandler与该线程连接到一起。

mSubThreadHandler = new Handler(loop){
   //实现handlerMessage的处理消息方法
   public void handleMessage(Message msg) {
   }
  1. 使用mSubThreadHandler发送消息,以及在onDestroy中调用mHandlerThread.quit();退出HandlerThread的looper循环


17. 关于IntentService,谈谈你的理解

HandlerThread + Service实现,可以实现Service在子线程中执行耗时操作,并且执行完耗时操作时候会将自己stop。

18. Glide是如何维护生命周期的?

@NonNull
private RequestManagerFragment getRequestManagerFragment(
       @NonNull final android.app.FragmentManager fm,
       @Nullable android.app.Fragment parentHint,
       boolean isParentVisible) {
   RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
   if (current == null) {
       current = pendingRequestManagerFragments.get(fm);
       if (current == null) {
           current = new RequestManagerFragment();
           current.setParentFragmentHint(parentHint);
           if (isParentVisible) {
               current.getGlideLifecycle().onStart();
           }
           pendingRequestManagerFragments.put(fm, current);
           fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
           handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
       }
   }
   return current;
}
  1. 为什么会判断两次null,再多次调用with的时候,commitAllowingStateLoss会被执行两次,所以我们需要使用一个map集合来判断,如果map中已经有了证明已经添加过了

  2. handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();我们需要将map里面的记录删除。


19. handler中的msg是怎么发送的,发送到哪去了

无论是sendMessage还是sendEmptyMessageDelayed等等方法,都是通过sendMessageAtTime进行发送

然后执行enqueueMessage方法 -- 调用->queue.enqueueMessage方法

handler中的message发送到了MessageQueue中:并且使用单链表数据结构进行存储

单链表:

  • 非顺序存储,顺序访问;
  • 不需要内存连续;
  • 非线性,非顺序的物理结构。
  • 由n个节点组成,靠next指针关联起来;

单链表这么好,为什么还需要ArrayList
message属于高频使用,并且生命周期非常短;
message还使用了对象池;

线程同理:属于高频使用,并且生命周期非常短。
频繁的生成与销毁会耗费大量系统资源


20. msg是如何被消费的

Looper通过loop方法,不停的从MessageQueue中获取Message对象,
并调用Message.target.dispatchMessage进行回调


21. handler,Looper,MessageQueue,Message是什么关系

ActivityThreadmain方法中,Looper.prepareMainLooper会创建Looper,并且在Looper的构造方法中会创建MessageQueue的对象【mQueue】

在主线程创建之后会创建一个Looper对象,创建Looper对象的时候会去创建一个MessageQueue,而Looper是一个轮询器,会不停的轮询MessageQueue中的消息,在获取到消息之后就会把这个消息交给相应的handler来进行处理,在主线程中创建一个handler对象,这个handler对象把一个Message放到消息队列中,然后获取到消息进行处理。

Looper:是一个消息分发器,在主线程创建的时候就会创建一个Looper对象;
MessageQueue:消息队列,是由Message组成的一个队列;
Handler:获取到Message,然后执行动作,可以在主线程和子线程中互相传递数据;


22. linux的 epoll模型

Handler中所使用到的Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loopqueue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质是同步I/O,即读写是阻塞的

epoll可以理解为event poll,在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需修改这个文件,那么等待的一方就会收到文件从而打破唤醒。和Looper监听MessageQueue,Handler添加message是比较类似的。不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

epoll支持四类事件,分别是

  • EPOLLIN(句柄可读)
  • EPOLLOUT(句柄可写)
  • EPOLLERR(句柄错误)
  • EPOLLHUP(句柄断)

当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。
所有的ui操作都通过handler来发消息操作
比如屏幕刷新16ms一个消息,你的各种点击事件,所以就会有句柄写操作,唤醒上文的wait操作,所以不会被卡死了。

这里涉及线程,先说说说进程/线程:
进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。

线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。


23. 能不能让一个Message加急被处理?/什么是Handler是同步屏障

可以 /一种使得异步消息可以被更快处理的机制

如果向主线程发送了一个UI更新操作 Message,而此时消息队列中的消息非常多,那么这个Message的吹就会变的非常慢,造成界面卡顿,所以通过同步屏障,可以使得UI绘制的 Message更快的被执行

什么是同步屏障?
这个”屏障“其实是一个Message,插入在MessageQueue的链表头,且其target==null。Message入队的时候不是判断了target不能为null吗?
不不不,添加同步屏障是另一个方法:

public int postSyncBarrier(){
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when){
    synchronized(this){
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        //把当前需要执行的Message全部执行
        if(when != 0){
            while(p != null && p.when <= when){
                prev = p;
                p = p.next;
            }
        }

        //插入同步屏障
        if(prev != null){ //invariant : p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

可以看到同步屏障就是一个特殊的target,哪里特殊呢?
target==null, 我们可以看到他并没有给target属性赋值,那么这个target有什么用呢?
next方法

Message next(){
    ...
    //阻塞时间
    int nextPollTimeoutMillis = 0;
    for(;;){
        ...
        //阻塞对应时间
        nativePollOnce(ptr, nextPollTimeoutMillis);
        //对MessageQueue进行加锁,保证线程安全
        synchronized(this){
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            /**
             * 1
             */
            if(msg != null && msg.target == null){
                //同步屏障,找到下一个异步消息
                do{
                    prevMsg = msg;
                    msg = msg.next;
                } while(msg != null && !msg.isAsynchronous());
            }
            if(msg != null){
                if(now < msg.when){
                    //下一个消息还没开始,等待两者的时间差
                    nextPollTimeoutMillis = (int)Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    //获得消息且现在要执行,标记MessageQueue为非阻塞
                    mBlocked = false;
                    /**
                     * 2
                     */
                    //一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取
                    if(prevMsg != null){
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else{
                //没有消息,进入阻塞状态
                nextPollTimeoutMillis = -1;
            }

            //当调用Looper.quitSafely()时候执行完所有的消息后就会退出
            if(mQuitting){
                dispose();
                return null;
            }
            ...
        }
        ...
    }
}

这个方法我在前面讲过,我们重点看一下关于同步屏障的部分,看注释1地方的代码

if(msg != null && msg.target == null){
    //同步屏障,找到下一个异步消息
    do{
        prevMsg = msg;
        msg = msg.next;
    } while(msg != null && !msg.isAsynchronous());
}

如果遇到同步屏障,那么会循环遍历整个链表找到标记为异步消息的Message,即isAsynchornous返回true,其他的消息会直接忽视,
那么这样的异步消息,就会提前被执行了。
注释2的代码注意一下就可以了。

注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理!!
从源码中可以看到如果不移除同步屏障,那么他会一直在那里,这样同步消息就永远无法被执行了。

有了同步屏障,那么唤醒的判断条件就必须再加一个:
MessageQueue中有同步屏障且处于阻塞中,此时插入 在所有异步消息前插入新的异步消息。
这个也很好理解,跟同步消息是一样的。如果把所有的同步消息先忽视,就是插入新的链表头且队列处于阻塞状态,这个时候就需要被唤醒了,看源码:

boolean enqueueMessage(Message msg, long when){
    ...
    //对MessageQueue进行加锁
    synchronized(this){
        ...
        if(p == null || when == 0 || when < p.next){
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            /**
             * 1
             */
            //当线程被阻塞,且目前有同步屏障,且入队的消息是异步消息
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for(;;){
                prev = p;
                p = p.next;
                if(p == null || when < p.when){
                    break;
                }
                /**
                 * 2
                 */
                //如果找到一个异步消息,说明前面有延迟的异步消息需要被处理,不需要被唤醒
                if(needWake && p.isAsynchronous()){
                    needWake = false;
                }
            }
            msg.next = p;
            prev.next = msg;
        }

        //如果需要则唤醒队列
        if(needWake){
            nativeWake(mPtr);
        }
    }
    return true;
}

同样,这个方法我之前讲过,把五官同步屏障的代码忽视,看到注释1处的代码。如果插入的消息是异步消息,且有同步屏障,
同时MessageQueue正处于阻塞状态,那么就需要唤醒。而如果这个异步消息的插入位置不是所有异步消息之前,那么不需要唤醒,如注释2

那我们如何发送一个异步类型的消息呢?有两种办法:

  • 使用异步类型的 Handler发送的全部 Message都是异步的
  • 给 Message标记异步

Handler有一系列带Boolean类型的参数的构造器,这个参数就是决定是否是异步 Handler

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async){
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    //这里赋值
    mAsynchronous = async;
}

但是异步类型的 Handler构造器是标记为hide,我们无法使用,所以我们使用异步消息只有通过给Message设置异步标志

public void setAsynchrounous(boolean async){
    if(async){
        flag |= FLAG_ASYNCHRONOUS; 
    } else {
        flag &= ~FLAG_ASYNCHRONOUS;
    }
}

但是!!!其实同步屏障对于我们的日常使用的话,其实也没有多大用处。因为设置同步屏障和创建异步Handler的方法都是标记为hide,说明google不想要我们去使用他。所以这里同步屏障也是作为一个了解,可以更加全面的理解源码中的内容。


24. 消息队列MessageQueue(未退出、未被废弃)在什么情况下,next()会选择阻塞线程

  1. 队列中没有任何消息 – 永久阻塞:这个时候不能返回null,因为next()的目的是取出一个消息,队列中现在没有消息并不代表一段时间后也没有消息。消息队列还在可用中,随时都有可能有Handler发布新的消息给它。那么问题来了,为了节省资源准备阻塞线程,但是多少时间后唤醒它呢?臆断一个时长并不是很好的解决方案。我们知道消息队列是用来管理消息的,既然确定不了阻塞时长那么不如先永久阻塞,等新消息入队后主动唤醒线程。

  2. 队首的消息执行时间未到 – 定时唤醒:每个消息的when字段都给出了希望系统处理该消息的时刻。如果在next()方法取消息时,发现消息队列的队首消息处理时间未到,next()同样需要阻塞。因为消息队列是按照消息的when字段从小到大排列的,如果队首消息的处理时间都没到那么整个队列中都没有能够立即取出的消息。这个时候因为知道下一次处理的具体时间,结合当前时间就可以确定阻塞时长。

  3. 队首消息是同步障碍器(SyncBarrier),并且队列中不含有异步消息 – 永久阻塞:因为对消息队列施加了同步障碍,所有晚于队首同步障碍器处理时间的同步消息都变得不可用,next()在选取返回消息时会完全忽略这些消息。这和第一种情况相似,所以采取的阻塞方案也是永久阻塞。

  4. 队首消息是同步障碍器(SyncBarrier),队列中含有异步消息但执行时间未到 – 定时唤醒:因为对消息队列施加了同步障碍,所有晚于队首同步障碍器处理时间的同步消息都变得不可用,next()在选取返回消息时会完全忽略这些消息。也就是说对于next(),它只会考虑队列中的异步消息。这和第二种情况相似,所以采取的阻塞方案是设置阻塞时长再阻塞。

Message next(){
   final long ptr = mPtr;
   ...【代码略,如果消息循环正在退出或者已经废弃,直接返回null】

   int nextPollTimeoutMillis = 0; //阻塞时长
   for(;;){
       ...【不相关内容】
       //nextPollTimeoutMillis为0 立即返回,为-1则无限等待(必须主动唤醒),涉及本地方法暂不深究
       nativePollOnce(ptr, nextPollTimeoutMillis);
       synchronized(this){
           //now 等于自系统启动以来到此时此刻的非深度睡眠时长
           final long now = SystemClock.uptimeMillis();
           Message prevMsg = null;
           Message msg = mMessages; //队首消息

           //如果当前队首的消息是设置的同步障碍器(target == null)
           if(msg != null && msg.target == null){
               //因为同步障碍器的原因进入该分支,找到下一个异步消息之后才会结束while
               do{
                   prevMsg = msg;
                   msg = msg.next;
               } while(msg != null && !msg.isAsychronous());
           }

           //此时msg一定是普通消息或者null,一定不是同步障碍器
           if(msg != null){
               if(now < msg.when){
                   //队首第一个非障碍器的msg执行时间未到,计算阻塞时长
                   nextPollTimeoutMillis = (int)Math.min(msg.when - now, Integer.MAX_VALUE);
               } else { //一切正常,开始取消息
                   mBlocked = false; //不阻塞线程
                   if(prevMsg != null){ //如果跳过了队首的同步障碍器取异步消息
                       prevMsg.next = msg.next;
                   } else { //如果当前消息就是队首消息
                       mMessages = msg.next;
                   }
                   msg.next = null;
                   if(false) Log.v("MessageQueue", "Returning message: " + msg); 
                   return msg;
               }
           } else { //消息队列为空,或者队首是SyncBarrier且队列中无异步消息
               nextPollTimeoutMillis = -1; // -1表示无限等待
           }

           //所有待处理的消息均处理完成,接下来处理闲时任务
           ......【因为当前代码在无限for循环中,此时再次验证一下消息队列是否正在退出。如果是,则废弃消息队列并返回null】

           .....【初始化闲时任务】
           //闲时任务列表为空,或者不是第一次执行到这里
           if(pendingIdleHandlerCount <= 0){
               mBlock = true;
               continue;
           }
           ......【初始化闲时任务】
       } //synchronized结束

       ......【如果是本次调用next()过程中第一次到达这里,则执行闲时任务,如果不是第一次,则不会执行到这里】
       //将待处理的IdleHandler个数设置为0,使得本次next()的调用再也不会到达这个for循环
       pendingIdleHandlerCount = 0;

       //因为执行了闲时任务花费了一段时间(迭代开始出的阻塞方法还未执行到所以还未阻塞)
       //此时再根据之前借宿那出的则色市场阻塞线程显然不合适
       nextPollTimeoutMillis = 0;
   } //for(;;) 结束
}


25. 四种阻塞情况下,各自在什么时候才需要主动唤醒主动唤醒

  1. 队列中没有任何消息 – 永久阻塞
    新增普通消息:需要。新消息入队后便主动唤醒线程,无论新消息是同步消息、异步消息。
    移除普通消息:不考虑
    新增同步障碍器:不需要
    移除同步障碍器:不考虑

  2. 队首的消息执行时间未到 – 定时唤醒
    新增普通消息:如果在阻塞时长未耗尽时,就新加入早于队首消息处理时间的消息,则需要主动唤醒
    移除普通消息:不需要
    新增同步障碍器:不需要
    移除同步障碍器:不需要

  3. 队首消息是同步障碍器(SyncBarrier),并且队列中不含有异步消息 – 永久阻塞
    新增普通消息:如果新加入的消息仍然是晚于队首同步障碍器处理时间,那么这次新消息的发布在next()层面上是毫无意义的,则不需要唤醒线程。只有在新加入早于队首同步障碍器处理时间的同步消息时,或者,新加入异步消息时(不论处理时间),才会主动唤醒被next()阻塞的线程
    移除普通消息:不需要
    新增同步障碍器:不需要
    移除同步障碍器:移除队首障碍器能够使本不可取出的同步消息变得可用,需要主动唤醒线程,重新判断是否能够取出消息或者是否需要缩短阻塞时长。除非新的 队首消息还是同步障碍器才不需要唤醒

  4. 队首消息是同步障碍器(SyncBarrier),队列中含有异步消息但执行时间未到 – 定时唤醒
    新增普通消息:因为队首同步障碍器的缘故,无论新加入什么同步消息都不会主动唤醒线程。只有加入的是异步消息,并且其处理时间需要早于设定好唤醒时执行的异步消息,才会主动唤醒
    移除普通消息:不需要
    新增同步障碍器:不需要
    移除同步障碍器:移除队首障碍器能够使本不可取出的同步消息变得可用,需要主动唤醒线程,重新判断是否能够取出消息或者是否需要缩短阻塞时长。**除非新的



假设我们移除设定好下次被动唤醒时执行的消息,线程被唤醒后就会因为没有需要处理的消息而再次进入阻塞,并不会错过消息所以移除普通消息不需要主动唤醒

总结
关于移除普通消息时是否需要主动唤醒:

  • 第一种情况没有消息跳过。
  • 第二种情况和第四种情况下,假设我们移除设定好下次被动唤醒时执行的消息,线程被唤醒后就会因为没有需要处理的消息而再次进入阻塞,并不会错过消息所以不需要主动唤醒。
  • 第三四种情况下移除设定好下次被动唤醒时执行的消息,线程虽然会再次进入阻塞但并不会错过消息,也不需要主动唤醒。
    所以移除普通消息在任何情况下都不需要主动唤醒线程

关于新增同步障碍器时是否需要主动唤醒:
新增的同步障碍器都会在被动唤醒时发挥同步障碍的作用,不会因为没有主动唤醒而多处理不该处理的消息,所以新增同步障碍器之后都不需要主动唤醒线程

先从MessageQueue#enqueueMessage来看源码在新增普通消息时是怎么判断是否需要主动唤醒的

boolean enqueueMessage(Message msg, long when){
  ......【msg的合法性判断,不合法会终止入队】
  
  synchronized(this){
      ......【判断当前消息队列是否正在退出或者已经废弃】
      msg.markInUse();
      msg.when = when;
      Message p = mMessages;
      boolean needWake;
      //如果队首为null,或者入队消息需要马上执行,或者入队消息执行时间早于队首消息,且线程已阻塞则都需要唤醒
      //如果p!=null && when!=0 && when > p.when 则不需要唤醒
      if(p == null || when == 0 || when < p.when){
          msg.next = p;
          mMessages = msg;
          needWake  = mBlocked; //mBlocked记录消息循环是否阻塞
      } else {
          /**
           * 在队列中间插入一个消息,
           * 一般情况下不需要唤醒队列(不是加到队首为什么要唤醒呢?),
           * 除非是一个同步障碍器而且新插入的消息是
           * 1. 异步消息
           * 2. 执行时间是队列中最早的
           */
           
           //此处mBlock值需要根据情况决定,当线程已经阻塞,且队首消息是同步障碍器,
           //新加入是异步消息,needWake才可能(!!)为true
           //这还要判断 1. 消息队列中是否有异步消息,
           //2. 以及异步消息的处理时间早于还是晚于新加入的异步消息
           needWake = mBlocked && p.target == null && msg.isAsynchronous(); //如果是true也是暂时的,还要考验在等着呢
           //寻找位置
           Message prev;
           for(;;){
              prev = p;
              p = p.next;
              if(p == null || when < p.when){
                  break;
              }
              
              if(neekWake && p.isAsynchonous()){
                  //能到达这里,说明msg.when > p.when,既然needWake是true,
                  //毫无疑问此时消息队列是处于阻塞的。这里只有一种可能,
                  //p这个异步消息的执行时间还没到(情况4)!
                  //msg的执行时间还更晚(不更晚早就break了)
                  //那就没有必要唤醒消息队列了
                  needWake = false;
              }
           } //for结束
           
           //插入新消息
           msg.next = p; //invariant: p == prev.next
           prev.next = msg;
      }
      
      if(needWake){
          nativeWake(mPtr); //唤醒消息队列
      }
      
  }
}

在这个方法中,出现了一种我们前文都没提到的一种主动唤醒情况 —— when==0(立即执行),实际上这也是毫无疑问需要主动唤醒的一种情况。对于第一种情况,新加入消息肯定需要主动唤醒;对于第二种,不主动唤醒会错过;对于第三、第四种情况,队首的同步障碍器不能影响早于它执行的消息,所以新加入when为0的消息无论如何都能够执行,如果不主动唤醒也会错过!所以,无论什么情况,只要新入队的消息when字段为0,都要主动唤醒线程!
移除消息的MessageQueue.removeMessages()系列和MessageQueue.removeCallbacksAndMessages()方法,虽然可能导致线程下次被动唤醒时没有消息执行,但是都不会错过消息所以不需要主动唤醒。
接下来目光转到同步障碍器上,MessageQueue.removeSyncBarrier()代码

void removeSyncBarrier(int token){
  synchronized(this){
      Message prev = null;
      Message p = mMessages;
      //找到指定的障碍器
      while(p != null && (p.target != null || p.arg1 != token)){
          prev = p;
          p = p.next;
      }
      
      if(p == null){
          throw new IllegalStateException("The specified message queue synchronization "
              + "barrier token has not been posted or has already been removed");
      }
      final boolean needWake;
      //如果找到障碍器时,它有前驱消息,说明这个障碍器还没发挥作用,
      //此时无论消息队列是否阻塞,都不需要改变其(即消息队列)状态
      if(prev != null){
          prev.next = p.next;
          needWake = false;
      } else { //如果障碍器是队首第一个消息
          mMessages = p.next;
          //消息队列为空或者新队首消息不是障碍器时,则唤醒消息队列
          needWake = mMessages == null || mMessag.target != null;
      }
      p.recycleUnchecked();
      
      //If the loop is quitting when it is already awake.
      //we can assume mPtr != 0 when mQuitting is false
      if(needWake && !mQuitting){
          nativeWake(mPtr);
      }
  }
}

needWake = mMessages == null || mMessages.target != null,这个语句含有一种有趣的情况。当消息队列中不含普通消息只含一个同步障碍器时,移除这个障碍器后整个消息队列都空了。按理说,移除之前next()线程已经处于无限阻塞中,移除后再唤醒结果还是无线阻塞。从消息处理上来讲,这是一个可以轻松避免且毫无意义的唤醒。从空闲任务的管理上来讲,next()方法在阻塞线程之前都会执行空闲任务然后再迭代一次判断是否阻塞,阻塞后再唤醒也不可能在本次next()中再执行一次空闲任务,依然是一个可以轻松避免且毫无意义的唤醒。
另外一点是,这段代码并没有融合mBlocked(记录当前线程是否阻塞)变量的值。可能出现线程未阻塞时主动唤醒线程的无谓举动。


26. 消息队列的退出与废弃

当Looper对象退出循环处理时,会调用MessageQueue的同包成员方法quit(safe)通知消息队列开始退出操作。如果boolean型的参数safe是true,消息队列会清除when晚于当前时间的所有同步/异步消息与同步障碍器,留下本应处理完的消息继续处理;如果safe是false,则完全不顾虑,清除消息队列中的所有消息。

next()方法执行过程中,如果处理完队列中全部消息后发现该消息队列的quit()方法被调用过,则直接调用dispose()废弃消息队列并返回null给Looper。当GC回收消息队列之前,会调用消息队列重载的finalize()方法,在这个方法中同样能够执行废弃消息队列的操作(如果还未废弃)


27. IdleHandler【闲时任务】

IdelHandler允许我们在消息队列空闲时执行一些不耗时的简单任务

/**
* 回调接口,当线程准备阻塞以等待更多的消息时调用
* 开发者可以实现自己的IdelHandler类,然后通过{@link #addIdleHandler}方法将其添加到消息队列中
* 一旦消息队列的循环空闲下来,就会执行这些Handler的
* {@link IdleHandler#queueIdle IdleHandler.queueIdle()}方法
* 你可以在这个方法中添加一次操作
*/
public static interface IdleHandler{
  /**
   * 方法在一下两种情况下会被调用:
   * 1. 当消息队列处理完消息开始等待消息时,此时队列为空
   * 2. 当队列中依然有待处理的消息,但这些消息的交付(delivery)时刻要晚于当前时刻时
   * 
   * @return 
   * true 下次执行{@link #next() next()}如果遇到空闲,依然执行这个IdleHandler
   * false 这次IdleHandler执行完之后就把它从限时任务列表中删除
   */ 
   boolean queueIdle();
}

我们来看看消息队列是怎么使用IdleHandler的,首先目光转到成员变量上,我们可以发现有这样两个成员:

/**IdleHandler列表**/
private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
/**保存等待处理的IdleHandler**/
private IdleHandler[] mPendingIdleHandlers;

mIdleHandlers可以理解成是一个IdleHandler的总列表,每次next()将要执行IdleHandler时都会从这个总列表中取出所有的IdleHandler
mPendingIdleHandlers指定哪些IdleHandler需要在本次执行中完成,每次next()将要执行IdleHandler时都会从mIdleHandlers拷贝。

下面这一段代码是MessageQueue.next()中用于处理IdleHandler的代码段

for(int i = 0; i < pendingIdleHandlerCount; i++){
   final IdleHandler idler = mPendingIdleHandlers[i];
   mPendingIdleHandlers[i] = null; //等待处理任务即将被处理,将其从待处理数组中删除【置空引用】
   
   boolean keep = false;
   try{
      keep = idler.queueIdle();
   } catch (Throwable t){
      Log.wrf("MessageQueue", "IdleHandler threw exception", t);
   }
   
   if(!keep){
      synchronized(this){
          mIdleHandlers.remove(idler);
      }
   }
}
//Reset the idle handler count to 0 so we do not run them again
pendingIdleHandlerCount = 0;

执行IdleHandler其实就是调用其queueIdle()方法,queueIdle()如果返回false,next()方法会将该IdleHandlermIdleHandlers中删除。这样的话,下一次next()方法再执行IdleHandler时就不会再重复执行它了。

需要特别提醒的是,虽然next()方法是一个无限for循环,但是每次调用next()只会执行一次mIdleHandlers中的闲时任务。因为在上面的代码段之前有这样一段

if(pendingIdleHandlerCount <= 0){
   mBlocked = true;
   continue; 
}


28. Activity的生命周期是怎么实现在死循环体外能够执行起来的?

ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:

在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。

比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。

主线程的消息又是哪来的呢?
当然是App进程中的其他线程通过Handler发送给主线程

    App进程                           system_server进程

     主线程
ActivityThread
       ↑
       | handler方式
       |
线程4(Binder服务端)    binder方式     线程2(Binder客户端)
ApplicationThread    <-----------  ApplicationThreadProxy

线程3(Binder客户端)    binder方式     线程1(Binder服务端)
ActivityManagerProxy  -----------> ActivityManagerService

system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。

App进程 则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程: ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程,比如signal catcher线程等,这里就不一一列举。

Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。

结合图说说Activity生命周期,比如暂停Activity,流程如下:

  1. 线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)

  2. 线程2通过binder传输到App进程的线程4;

  3. 线程4通过handler消息机制,将暂停Activity的消息发送给主线程;

  4. 主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。


29. 子线程中toast、showDialog的方法

new Thread(new Runnable() {
       @Override
       public void run() {
           Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();//崩溃无疑
      }
}).start();

一种解决方式

new Thread(new Runnable() {
        @Override
        public void run() {
 
            Looper.prepare();
            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
            Looper.loop();
        }
    }).start();  

来看一下Toast源码


public void show() {
    ......
 
    INotificationManager service = getService();//从SMgr中获取名为notification的服务
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
 
    try {
        service.enqueueToast(pkg, tn, mDuration);//enqueue? 难不成和Handler的队列有关?
    } catch (RemoteException e) {
        // Empty
    }

在show方法中,我们看到Toast的show方法和 普通UI控件不太一样,并且也是通过Binder进程间通讯方法执行Toast绘制。这其中的过程就不在多讨论了,有兴趣的可以在NotificationManagerService类中分析。

现在把目光放在TN这个类上(难道越重要的类命名就越简洁,如H类),通过TN类,可以了解到它是Binder的本地类。在Toast的show方法中,将这个TN对象传给NotificationManagerService就是为了通讯!并且我们也在TN中发现了它的show方法。

private static class TN extends ITransientNotification.Stub {//Binder服务端的具体实现类
 
    /**
     * schedule handleShow into the right thread
     */
    @Override
    public void show(IBinder windowToken) {
        mHandler.obtainMessage(0, windowToken).sendToTarget();
    }
 
    final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            IBinder token = (IBinder) msg.obj;
            handleShow(token);
        }
    };
 
}

看完上面代码,就知道子线程中Toast报错的原因,因为在TN中使用Handler,所以需要创建Looper对象。 那么既然用Handler来发送消息,就可以在handleMessage中找到更新Toast的方法。 在handleMessage看到由handleShow处理。

//Toast的TN类
public void handleShow(IBinder windowToken) {

    ``````
    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
 
    mParams.x = mX;
    mParams.y = mY;
    mParams.verticalMargin = mVerticalMargin;
    mParams.horizontalMargin = mHorizontalMargin;
    mParams.packageName = packageName;
    mParams.hideTimeoutMilliseconds = mDuration ==
    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
    mParams.token = windowToken;
    if (mView.getParent() != null) {
        mWM.removeView(mView);
    }
    mWM.addView(mView, mParams);//使用WindowManager的addView方法
        trySendAccessibilityEvent();
    }
}

总结一下:

Toast本质是通过window显示和绘制的(操作的是window),而主线程不能更新UI 是因为ViewRootImpl的checkThread方法在Activity维护的View树的行为。 Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();(但是不建议这么做,因为它会使线程无法执行结束,导致内存泄露)
Dialog亦是如此

在子线程中,如果手动为其创建Looper,是非常危险的操作,需要在所有的事情完成以后,调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。【 Looper.myLooper().quit();

30. 消息处理:EventBus、BroadCast 和 Handler 优缺点比较

  1. BroadCast
    广播是相对消耗时间、空间最多的一种方式,但是大家都知道,广播是四大组件之一,许多系统级的事件都是通过广播来通知的,比如说网络的变化、电量的变化,短信发送和接收的状态,所以,如果与android系统进行相关的通知,还是要选择本地广播;在BroadcastReceiver的 onReceive方法中,可以获得Context 、intent参数,这两个参数可以调用许多的sdk中的方法,而eventbus获得这两个参数相对比较困难;


    因此广播相对于其他的方式而言,广播是重量级的,消耗资源较多的方式。他的优势体现在与sdk连接紧密,如果需要同 android 交互的时候,广播的便捷性会抵消掉它过多的资源消耗,但是如果不同android交互,或者说,只做很少的交互,使用广播是一种浪费;


    广播作为Android组件间的通信方式,可以使用的场景如下:
         1.同一app内部的同一组件内的消息通信(单个或多个线程之间);
          2.同一app内部的不同组件之间的消息通信(单个进程);
          3.同一app具有多个进程的不同组件之间的消息通信;
          4.不同app之间的组件之间消息通信;
          5.Android系统在特定情况下与App之间的消息通信。


    广播的不可替代性在于它可以跨进程进行通信,也就是不同APP之间可以通过广播进行传递数据,并且在OnReceiver中更容易使用Context和Intent对象来执行必要的操作。单就同一app内部的消息通信而言,使用广播是较为消耗资源和笨重的。

  2. Handler
       handler一般用于线程间通信,它可以分发Message对象和Runnable对象到主线程中, 每个Handler实例,都会绑定到创建他的线程中(一般是位于主线程),它有两个作用:
            1.安排消息或Runnable 在某个主线程中某个地方执行;
            2.安排一个动作在不同的线程中执行。


    本篇只讨论handler中与Message相关的的消息通信,一般Handler的使用方法即在调用线程内创建Handler的内部类,并重写handlerMessage(Message msg)方法,而在发布消息时使用sendMessage方法进行发布,在处理时通过switch(msg.what)进行消息分发并进行相应的处理。这里,Hander内部类和其定义类是绑定的,这就造成了事件发布者和接受者之间的高耦合。而Handler的最大好处是发生问题时,可以非常明确、快速的进行定位,通过msg.what很容易就可以理清每一条消息流的逻辑。

  3. EventBus
      EventBus的优势在于调度灵活。不依赖于 Context,使用时无需像广播一样关注 Context 的注入与传递,也解除了Handler所带来的耦合,父类对于通知的监听和处理可以继承给子类,这对于简化代码至关重要;通知的优先级,能够保证 Subscriber 关注最重要的通知;粘滞事件(sticky events)能够保证通知不会因 Subscriber 的不在场而忽略。可继承、优先级、粘滞,是 EventBus 比之于广播、观察者等方式最大的优点,它们使得创建结构良好组织紧密的通知系统成为可能。


      但EventBus也有很明显的缺陷,在EventBus中事件的分发是通过注解函数的参数类型确定的,因此在事件发布遭到大量滥用时,特别有多个订阅者、多个相同参数时,很难从事件发布者开始理清消息流,无法快速的找出是哪个订阅者接受并处理了消息导致的问题,这就要求了参与者必须对整个通知过程有着良好的理解。当程序代码适量时,这是一个合理的要求,然而当程序太大时,这将成为一种负担。在EventBus中一定要写好必要的注释信息,否则在后续工作交接中会产生很多不必要的麻烦。

4.31 手写Handler

public class Handler{
    Looper mLooper;

    public Handler(){
        mLooper = Looper.myLooper();
    }


    //发送
    public void sendMessage(Message msg){
        mLooper.enqueueMessage(msg);
    }

    //处理
    public void handleMessage(){

    }

    private void enqueueMessage(Message msg){
        //把Message 和 handler 绑定
        msg.target = this;

        //下一步,想要将msg发送到messageQueue中
        //但是是发送到哪一个MessageQueue中呢?应该是Looper持有的那个MessageQueue中
        //所以Handler应该持有一个Looper,然后利用Looper,将Message插入MessageQueue
        mLooper.enqueueMessage(msg);
    }
}





public class Message{
    Object  obj;
    Handler target; //为了在loop时能用Message获取到handler,在发送时绑定,在取出后调用handleMessage

    public Message(String object){
        obj = object;
    }

    public String toString(){
        return obj.toString();
    }
}





public class MessageQueue{
    BlockingQueue<Message> queue = new ArrayBlockingQueue<>(100);

    //存
    public void enqueueMessage(Message msg){
        try{
            queue.put(msg);
        }catch(InterruptedExcepttion e){
            e.printStackTrace();
        }       
    }

    //取
    public Message next(){
        Message msg = null;
        //阻塞
        try{
            msg = queue.take();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        return msg;
    }
}




public class Looper(){
    MessageQueue mQueue;

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();

    public static Looper myLooper(){
        return sThreadLocal.get();
    }

    private Looper(){
        mQueue = new MessageQueue();
    }

    //打开电机电源,要保证一个线程只有一个looper
    public void prepare(){  
        //先取一下
        Looper looper = sThreadLocal.get();
        if(looper != null){
            throw new RuntimeException("Only one Looper may be created per thread");
        }

        //存looper, 存到线程所在的map里面
        //借助 ThreadLocal 这个工具去存
        sThreadLocal.set(new Looper());

        
    }

    public void enqueueMessage(Message msg){
        mQueue.enqueueMessage(msg);
    }

    //开启电机,利用队列的阻塞机制
    public void loop(){
        //拿到线程对应的looper

        final Looper me = sThreadLocal.get();
        final MessageQueue mQueue = me.queue;
        for(;;){
            Message msg = mQueue.next();
            if(msg != null){
                //这里怎么获得handle,然后调用handler.handleMessage呢?
                //在Message的数据结构设计时,加上Handler,将两者绑定
                //这也是handler会产生内存泄露的一大原因

                msg.target.handleMessage(msg); //用Message自己保存的handler发送自己
            }
        }
    }
}






public class HandlerMain{

    public staitc void main(String[] args){

        Looper.prepare();

        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg){
                System.out.println(msg.toString());
            }
        };

        new Thread(){
            @Override
            public void run(){
                System.out.println("发送消息 线程"+Thread.currentThread().getName);
                handler.sendMessage(new Messge("ABCD"));        
            }
        }.start();
        
        Looper.loop();
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容