安卓 Handler面试(1)

概览

在这里插入图片描述

一、简述下安卓的Handler 机制

平时开发时常用Handler做UI更新操作,所以大家可能误解为Handler就是用来进行UI更新的。其实Handler的功能不止这些

每个app进程被创建后会通过反射调用走ActivityThread类的main方法,这个方法是应用程序主进程的入口,在这个方法中会开启Looper轮循,不断从MessageQueue 中取消息。然后让H这个Handler类来处理消息。从消息中可以看出、四大组件的创建、生命周期的处理、等其他操作系统都会发出相应的消息。所以Handler的功能不仅仅被用来更新UI。

上面所说的Looper、MessageQueue就是Handler机制相关类。其实Handler机制一共提供了四个相关类:Handler、Looper、MessageQueue、Message。这四个类分工协作共同完成安卓中的消息机制。

  • Handler用来发送、处理消息。
  • Looper可以理解为轮循器,他是Handler消息机制的核心。他不断从MessageQueue中取消息,然后调用Handler(msg.target)的dispatchMessage让Handler处理消息。
  • MessageQueue是一个消息队列,Handler发送的消息都先存储到这个队列中。
  • Message是消息的包装对象,其中Message还携带了target、next、calback等字段。这些字段在整个消息机制中起着重要作用。

二、每个线程中最多有多少个Looper对象 、多少个MessageQueue对象、多少个Handler对象?

1、每个线程中最多只能创建一个Looper对象。这个可从ActivityThread的main方法中来验证:

ActivityThread#main

 public static void main(String[] args) {
  ...
   Looper.prepareMainLooper();
}

Looper#prepareMainLooper

 public static void prepareMainLooper() {
        prepare(false); // 核心
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper(); // 保存下生成的Looper对象
        }
    }

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

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

prepare方法中首先会从ThreadLocal中查询当前的主线程是否有Looper对象绑定,已经绑定时直接抛异常。即提示一个线程中只能创建1个Looper对象。

2、每个线程中最多创建1个MessageQueue对象,这里可以接着上述的Looper源码分析:

当 Looper对象为空时也即Looper未与当前线程绑定过,这时需要调用Looper的构造创建Looper对象,并绑定到当前线程。

Looper的构造中创建了MessageQueue对象并赋值给自己的成员变量mQueue。因为Looper的绑定只能成功绑定1次。所以MessageQueue也只能被创建1次。即每个线程最多存在1个MessageQueue。


 sThreadLocal.set(new Looper(quitAllowed));

  private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
3、每个线程中的handler数量可以存在多个。

这个要从Handler的sendMessage中分析,方法最终会调用handler的enqueueMessage方法。入队时会把当前的handler也一并作为消息的属性添加到队列中。这样每条message便会与目标Handler绑定。

除此之外在handler对象被创建时还会通过Looper的myLooper方法获得Looper对象,通过looper对象又获得mQueue对象。这样消息就会被放到唯一的MessageQueue中。

以常见的无参构造为栗子:

 public Handler() {
       this(null, false);
   }
   
public Handler(@Nullable Callback callback, boolean async) {
   ...
       mLooper = Looper.myLooper();
       if (mLooper == null) {
           throw new RuntimeException(
               "Can't create handler inside thread " + Thread.currentThread()
                       + " that has not called Looper.prepare()");
       }
       mQueue = mLooper.mQueue;
       mCallback = callback;
       mAsynchronous = async;
   }

从消息发送到入队来看系统并未限制Handler的创建个数。

三、Handler是如何实现线程的切换的?

当创建Handler时:会拿到当前线程所属的Looper对象。

当发送消息时:会把当前的handler作为消息的属性一并发送到消息队列。

当处理消息时,Looper不断从消息队列中取消息,从消息中找到指定的handler 让handler进行处理。

发送消息逻辑在子线程、处理消息逻辑在主线程中,这样就实现了线程的切换。

四、Handler机制一般都是用子线程发送消息、主线程处理消息。那么可以反过来吗?

Looper的创建是给线程有关的。Handler对象创建时要求必须先有Looper对象。Looper的prepare方法创建Looper对象时会绑定当前线程。

所以当在子线程中创建Handler时需要先创建Looper对象,这样Looper就绑定到子线程了。当子线程中创建Looper对象、开启loop轮询,这样在主线程中通过handler对象发送消息时,子线程中轮训处理消息也就可以实现了。

五、Handler A发送的 Message 为啥不会跑到 Handler B的 HandleMessage中被处理?

这道题考查的是Message的target属性。当handler 发送消息时(enqueueMessage)会把自身作为Message的一个属性值(msg.target)封装到Message对象汇总一并发送到消息队列中。 Looper轮循出消息会拿到消息的target属性这样就可以知道每条消息是属于哪个hadler对象的。然后让相应对象的handler进行消息处理。

六、Handler的sendMessage与post的区别?

1、首先看下handler的sendMessage方法

消息最终被调用Handler的enqueueMessage入队。然后在ActivityThread#main#Looper轮询中拿到消息,获得msg.target对象调用Handler的dispatchMessage进行分发消息。

handler#sendMessage


  public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0); // 延迟设置为0
    }
    
    // 注意这个方法的作用:
    //不是说延迟某个时间后吧消息插入消息队列中,代表的意思是到达延迟时间后开始分发消息。
  public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        // 注意这里的时间为:开机时间+延迟时间
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }


   public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
      // 这里有个重要操作,吧当前Handler对象赋值个Message的target属性
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        // 最终调用消息队列的enqueueMessage方法。
        // 注意这里的时间:开机时间+延迟时间
        return queue.enqueueMessage(msg, uptimeMillis);
    }


      public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
     // 直接new handler然后简单发送个消息最终会调用调用用户重写的handleMessage方法。
            handleMessage(msg); 
        }
    }

Looper#loop


     msg.target.dispatchMessage(msg)

如源码,默认情况下我们创建handler对象时直接new了一下。不创建mCallback 对象,sendMessage时也未给Message的callback属性赋值。所以sendMessage的消息最终会被handleMessage(msg)来处理。

2、再次看下handler的post方法,post方法会调用sendMessageDelayed方法最终也是调用的enqueueMessage方法。而且Runnable参数也会被封装成message信息:
    public final boolean post(@NonNull Runnable r) {
     // 把Runnable 封装成message信息。
       return  sendMessageDelayed(getPostMessage(r), 0);//延迟设置为0
    }
  // 具体的封装过程
 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        // 特别注意下这里,把runnable对象赋值给message的callback属性
        //(后续dispatchMessage消息处理会用到)
        m.callback = r; 
        return m;
    }

包装后最终也会调用enqueueMessage方法消息入队。后面就是取消息的流程了。具体源码如上(六-1)这时msg的callback属性是不为null的,这时会走:

private static void handleCallback(Message message) {
 // 这里其实就是执行用户post runnable 的run方法。这里代码是运行在主线程中。
        message.callback.run();
    }

总结:

  • post相对SendMessage比较方便些,不用用户手动创建Message、然后写handleMessage进行处理。其他没啥区别。最终都是被包装成Message入队。
  • post内的代码是运行在主线程的。所以子线程内handler.post 可以进行UI更新操作。
  • post的方式用户也不用定义Handler内部类了,直接new Handler对象就可给主线程发送消息,处理runnable内的任务。这样还可避免非静态Handler内部类的内存泄漏。

ps:

这里需要注意的是,这两种方式其实最后都是调用 sendMessageAtTime 方法然后再enqueueMessage入队。

sendMessageAtTime 方法被调用时是根据传入的延迟时间需要再加上SystemClock.uptimeMillis() 再往下传。

SystemClock.uptimeMillis() 表示的是系统开机到当前的时间总数,单位是毫秒,并且当系统进入休眠时时间就会停止,但是不受时钟缩放或者其他节能机制的影响。

这个时间总和是用来给 Message 排序的,至于为什么是从开机到当前时间呢?这个也很好理解,毕竟只有开机才会有消息的分发;那为什么不用系统时间呢(System.currentTimeMilis),因为这个可能因为用户调整了系统时间而改变,该值并不可靠。

七、Handle 消息处理的优先级策略

       public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) { //1、handler post runnable 方式优先级最高
            handleCallback(msg);
        } else {
            if (mCallback != null) { // 2、new handler 时传入callback 对象优先级次之
                if (mCallback.handleMessage(msg)) { // 有返回值的,当返回true 后续的代码就不会执行了。直接return。
                    return;
                }
            }
            handleMessage(msg); // 3、内存泄漏方式优先级最低(自定义Handler对象重写handleMessage)
        }
    }

如上代码msg#callBack>Handler#callBack>Handler#handleMessage

八、Message的消息池设计原理?

Message 核心源码

1、其实Message内部维护了一个消息池,消息池的具体实现就是采用的链表。
2、取消相关的方法在obtain中实现具体的做法就是取出链表的头部。然后删除头部。
3、消息池添加的操作在recycleUnchecked中,当消息池中未达到最大数量时,采取头插法插入链表头部。

    Message next; //next 指针,指向下一条消息。

    private static final Object sPoolSync = new Object();
    private static Message sPool; // 消息池中的头消息(head 节点)
    private static int sPoolSize = 0; // 消息池中的消息数量,没有被使用的消息。
    private static final int MAX_POOL_SIZE = 50

        // 消息池中取消息,无消息时再new 对象。
    
        public static Message obtain() { 
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) { // 当前节点插入消息池的头部。  注意这里消息池的最大容量为50
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }
1、消息池中的消息哪来的?

在ActivityThread#main方法中,Looper的loop方法中取出消息使用完毕后就会通过msg.recycleUnchecked 来调用此方法,清空消息信息。把消息对象放入消息池。

2、消息池设计的好处

这里推荐使用 Message.obtain() 方法来实例化一个 Message,好处在于它会从消息池中取,而避免了重复创建的开销。虽然直接实例化一个 Message 其实并没有多大开销,但是我们知道 Android 是消息驱动的,这也就说明 Message 的使用量是很大的,所以当基数很大时,最好是复用存在的对象,避免创建的开销。这时消息池就显得非常有必要了。

3、上述讲到了消息对象的获取是头部获取的,消息的添加也是头部插入的。那么问题来了,链表插入、删除操作的时间复杂度都是O(1),安卓为啥采用头部操作,不采用尾部操作?

这点是考察你数据结构掌握的是否扎实,虽然链表删除、插入的时间复杂度是O(1)但是这是在不考虑遍历的操作情况下。如果算上遍历操作那么尾部操作时,删除、插入总体操作时间复杂度就是O(n)了。而头部操作不受遍历影响不用遍历。

九、安卓主线程的Looper与子线程的Looper的区别

1、看先安卓主线程中的Looper操作首先调用prepareMainLooper,最终再开启loop循环。
    public static void main(String[] args) {
    ...

        Looper.prepareMainLooper();
    ...
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

    ...
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

Looper#prepareMainLooper

    public static void prepareMainLooper() {
        prepare(false); // 调用了带参数的prepare方法,false代表不允许loop退出循环。
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper(); // 吧looper对象用静态变量保存一份
        }
    }

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

  // 首先去ThreadLocal中取Looper对象,无对象才创建。
    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));
    }
2、再看下平时我们想要在子线程创建handler的流程
        Thread {
            Looper.prepare() // 步骤1:prepare

            Handler().post { // 步骤2:发消息
      
            }

            Looper.loop() // 步骤3:开启循环
            
        }.start()

Looper#prepare

    public static void prepare() {
        prepare(true);
    }
3、总结

1、可见prepare()为public权限,任何人都可以使用,内部调用有参数的prepare(true)消息处理完后可以退出looper。
2、prepare(xxx)为私有访问权限,只能被系统使用。系统使用时传参false,代表消息处理完不允许退出looper,否则app主线程就退出了。主线程直接抛出异常。

十、如何发送个立即执行的消息

Message#when

  /**
     * The targeted delivery time of this message. The time-base is
     * {@link SystemClock#uptimeMillis}.
     * @hide Only for use within the tests.
     */
    public long when;

Handle#sendMessage


  public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0); // 延迟设置为0
    }
    
    // 注意这个方法的作用:
    //不是说延迟某个时间后吧消息插入消息队列中,代表的意思是到达延迟时间后开始分发消息。
  public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        // 注意这里的时间为:开机时间+延迟时间
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }


   public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
      // 这里有个重要操作,吧当前Handler对象赋值个Message的target属性
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        // 最终调用消息队列的enqueueMessage方法。
        // 注意这里的时间:开机时间+延迟时间
        return queue.enqueueMessage(msg, uptimeMillis);
    }
  boolean enqueueMessage(Message msg, long when) {
  ...
            msg.markInUse();
             // 这里对Message的when进行赋值
             // 这里对Message的when进行赋值
             // 这里对Message的when进行赋值
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) { //  插入头部
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else { // 需要插入后面

                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
            ...
}

handler#SendMessage发送消息时消息的时间when = SystemClock.uptimeMillis() + delayMillis,这个时间在消息入队时会被使用到。这个时间决定消息在队列的相对顺序。

即使用sendMessageDelayed发送消息时delayMillis肯定不为0,SystemClock.uptimeMillis()是手机开机相对时间二者加起来肯定不为0

handler#SendMessage发送消息delayMillis为0但是SystemClock.uptimeMillis()是手机开机相对时间二者加起来肯定不为0

所以系统提供了Handler().sendMessageAtFrontOfQueue(xxx)来解决这个问题,可以发送when=0的消息。

    public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
          //when = 0
          //when = 0
           //when = 0
        return enqueueMessage(queue, msg, 0);
    }

收获:所谓的延迟并不是延迟插入消息,而是直接就插入到消息队列了。只是插入时进行了排序,处理时按照时间相对顺序处理。

The end

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值