【Handler面试知识点】

问题1:Handler有哪些部件,这些部件的作用是什么?

Handler:是处理者的意思,它用于发送和处理消息。发送消息使用 handler 的 sendMessage()方法,post 方法等,发出的消息经过一系列地辗转处理后,会到达 Handler 的 handleMessage()方法中。

Looper:Looper 是 MessageQueue 的管家,当调用了 Looper 的 loop()方法后,就会进入一个无限循环中,每当发现 MessageQueue 中存在一条消息,就把它取出,并传递到 Handler 的handleMessage()方法中。每个线程只有一个 Looper 对象

MessageQueue:一个优先级队列,接收到的Message都是即时消息那就按照先进先出原则,如果带有延迟时间的Message会根据时间来插入到队列里每个线程中只会有一个 MessageQueue 对象

Message:消息的载体

问题2:一个线程有几个Handler?

有多个,在线程中可以new多个Handler.

问题3:一个线程有几个Looper?,如何保证?

一个线程只有一个Looper,通过ThreadLocal类中的内部类ThreadLocalMap进行维持

Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
ThreadLocalMap 与当前线程是一一对应的

ThreadLocalMap是ThreadLocal的静态内部类
ThreadLocalMap: <key, value>,key = this,即唯一的 ThreadLocal,value = Looper

ThreadLocal是如何保证的?如何保证唯一?》只让set一次

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();  // 表示sThreadLocal 是唯一的

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {  //sThreadLocal.get()获得的值就是Looper对象
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

线程如何跟Lopper绑定的?
ThreadLocalMap是Map的存储结构,每次存的时候都会根据key(ThreadLocal)进行判断,当前的ThreadLocal是否有对应的Looper,这样就可以表示一个ThreadLocal对应唯一的一个Looper对象.

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

问题4:Handler内存泄漏的原因?为什么其他内部类没有说过有这个问题?

为什么持有外部类/Activity引用?
内部类持有外部类的引用,java内部类特征:默认会持有外部的this的引用

Handler内存泄漏的原因?

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

enqueueMessage会把this(Handler)赋值给msg.target
Message–>包含Handler–>持有Activity
如果你的Message处理时间是1分钟之后、5小时之后,意味着Message会一直存在MessageQueue里面
MessageQueue–>Message–>包含Handler–>持有Activity
说明了MessageQueue会在1分钟之后、5小时之后再处理,MessageQueue等同于内存里面,当Activity调用了onDestroy(),但是由于你的Activity在MessageQueue中(内存中)持有着,那么Activity仍旧不会被释放,这个时候内存就泄漏了.

总结:Handler造成内存泄露的原因。非静态内部类,或者匿名内部类。使得Handler默认持有外部类的引用。在Activity销毁时,由于Handler可能有未执行完/正在执行的Message。导致Handler持有Activity的引用。进而导致GC无法回收Activity。

常见的内存泄漏写法

//匿名内部类  
Handler handler=new Handler(){
  @Override
  public void handleMessage(Message msg) {
    super.handleMessage(msg);
  }
};
  //非静态内部类
  protected class AppHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
        // TODO: 2019/4/30 
      }
    }
  }

怎么解决:

//静态内部类+弱引用
private static class MyHandler extends Handler{
	private final WeakReference<MineActivity> mMineActivityWeak;
	public MyHandler(MineActivity mineActivity){
		mMineActivityWeak = new WeakReference<>(mineActivity);
	}
	@Override
	public void handleMessage(@NonNull Message msg) {
		super.handleMessage(msg);
		MineActivity mineActivity = mMineActivityWeak.get();
		if(mineActivity != null){
			mineActivity.number = 5;
        }
    }
}

Activity销毁时,清空Handler中,未执行或正在执行的Callback以及Message。

    // 清空消息队列,移除对外部类的引用
  @Override
  protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);

  }

 
  //Handler源码中removeCallbacksAndMessages()注释含义
  /**
   * Remove any pending posts of callbacks and sent messages whose
   * <var>obj</var> is <var>token</var>. If <var>token</var> is null,
   * all callbacks and messages will be removed.
   */
  public final void removeCallbacksAndMessages(Object token) {
    mQueue.removeCallbacksAndMessages(this, token);
  }

为什么其他内部类没有说过有这个问题?
JVM定义的,Java语法,没有为什么

内部类为什么会持有外部类的引用?
这是因为内部类虽然和外部类写在同一个文件中,但是编译后还是会生成不同的class文件,其中内部类的构造函数中会传入外部类的实例,然后就可以通过this$0访问外部类的成员。

因为在内部类中可以调用外部类的方法,变量等等,所以肯定会持有外部类的引用的。

问题5:为何主线程可以new Handler?如果想要在子线程中new Handler要做些什么准备?

为何主线程可以new Handler?

//ActivityThread.class
public static void main(String[] args) {
	...
    Process.setArgV0("<pre-initialized>");
	// 1. 创建消息循环Looper
    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
	...
	//2. 执行消息循环
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

在ActivityThread类中的main()方法中,有对主函数的Looper进行prepare()Looper.prepareMainLooper()和loop()Looper.loop()操作。主线程中的所有代码全部都运行在Looper.prepareMainLooper()和Looper.loop()两个函数之间。
总结:主线程已经默认初始化好了一个Looper

如果想要在子线程中new Handler要做些什么准备?,必须准备一下两个方法
Looper.prepare()
Looper.loop()

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

消息队列无消息会一直处于等待状态.
(执行到next()方法中的nativePollOnce(ptr, nextPollTimeoutMillis);语句,处于linux等待状态。直到enqueueMessage()方法中执行了nativeWake(mPtr)语句才会唤醒nativePollOnce之后的操作。)

messageQueue消息处理机制:
① 如果消息队列不为空,当前时间还没到,会执行nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞等待,等待nextPollTimeoutMillis时间,时间到了会自己唤醒。
② 如果消息队列为空,当messageQueue执行enqueueMessage()流程的时候,会执行nativeWake(mPtr)进行唤醒。

解决方案:调用Looper的quitSafely()或quit(),
作用:1)释放内存,2)释放线程。
quitSafely()---->安全的
quit()---->不安全的

子线程调用quit()流程:
① 执行quit()流程,会将mQuitting 变量赋值为 true,并执行nativeWake(mPtr)进行线程唤醒操作;
MessageQueue.java

    void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;  // 进行mQuitting 设置为true

            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr); // 进行唤醒
        }
    }

② 在MessageQueue类中的next()方法会对阻塞流程nativePollOnce(ptr, nextPollTimeoutMillis)进行唤醒。
MessageQueue.java

    Message next() {
	...
	for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis); //进行阻塞
            ...
     }
     ...
     if (mQuitting) {  //此时mQuitting为true
         dispose();
         return null;  //返回为null
     }
	... 
}

③ Looper.loop流程中进行next()取消息流程
Looper.java

    public static void loop() {
    final Looper me = myLooper();
    ...
    for (;;) {
        if (!loopOnce(me, ident, thresholdOverride)) {  // 往下执行loopOnce() 流程
            return; //loopOnce()返回为false的时候直接return
        }
    }
}


private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;  //此处message为null时返回false
    }
    ...
 }

当message为空时,loopOnce()返回为false的时候直接return,表现子线程退出,

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

MessageQueue是一个优先级队列,根据时间先后顺序排队的单链表,MessageQueue没有设置限制,可以无限加入message,但是受手机内存的限制。

在enqueueMessage()加消息过程中会进行synchronized操作,并且在next()取消息过程中也会进行synchronized操作。从而保证了线程安全性。

MessageQueue.java
往消息队列中存消息

    boolean enqueueMessage(Message msg, long when) {
   if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
    ... ...
}

从消息队列中取消息
MessageQueue.java

    @UnsupportedAppUsage
Message next() {
	... ...
	for (;;) {
       if (nextPollTimeoutMillis != 0) {
           Binder.flushPendingCommands();
       }

       nativePollOnce(ptr, nextPollTimeoutMillis);

       synchronized (this) {
           // Try to retrieve the next message.  Return if found.
           final long now = SystemClock.uptimeMillis();
           Message prevMsg = null;
           Message msg = mMessages;
           if (msg != null && msg.target == null) {
           ... ...
          
}

为了保证安全性,加入synchronized,从而导致Handler delay的消息中的时间不完全准确。

synchronized可以修饰:方法,静态方法、代码块(object)、代码块(this)

问题8:我们使用Message时应该如何创建它?

采用obtain()方法进行创建。采用享元设计模式,内存复用。
当MessageQueue进行释放消息的时候,其实并没有把消息释放掉,在执行recycleunchecked()方法时只是把消息里面的内容置空,然后把置空的消息放到池子中,往队列头中进行添加。
Obtain()是从池子队列头取一个点
如果采用new()进行创建,可能会出现内存抖动现象。

问题9:使用Handler的postDelay后消息队列会有什么变化?

如果消息队列为空,该消息不会执行,计算等待时间,让消息进行wait。

问题10:Looper死循环为什么不会导致应用卡死?

每个app都有一个自己的虚拟机。
App启动方式:Launcher—>application—>zygote—>虚拟机—>ActivityThread
所有的生命周期都发运行在Loop里面,都是以消息的方式存在

主线程的唤醒方式有哪些?
① 输入的事件
② Looper添加消息

ANR产生原因:
①在5秒内没有响应输入的事件(例如:按键按下,屏幕触摸)
②BroadcastReceiver在10秒内没有执行完毕

每一个事件就相对于一个message。

为什么不会卡死:
疑惑点:Message msg = queue.next();
此时主线程处于休眠状态,卡死是ANR,休眠和ANR是两码事互不干扰

问题11:Message、Handler、MessageQueue、Looper 的之间的关系?

首先,是这个 MessageQueue,MessageQueue是一个消息队列,它的数据结构形式是有单链表实现的优先级队列,它可以存储 Handler 发送过来的消息,其内部提供了进队和出队的方法来管理这个消息队列,其出队和进队的原理是采用单链表的数据结构进行插入和删除的,即enqueueMessage()方法和 next()方法。这里提到的 Message,其实就是一个Bean 对象,里面的属性用来记录 Message 的各种信息。

然后,Looper 是一个循环器,它可以循环的取出MessageQueue 中的 Message,其内部提供了 Looper 的初始化和循环出去Message 的方法,即 prepare()方法和loop()方法。在 prepare()方法中,Looper会关联一个MessageQueue,而且将 Looper 存进一个 ThreadLocal 中,在loop()方法中,通过 ThreadLocal 取出 Looper,使用MessageQueue的next()方法取出 Message 后,判断 Message 是否为空,如果是则 Looper 阻塞,如果不是,则通过 dispatchMessage()方法分发该 Message 到 Handler 中,而Handler 执行 handlerMessage()方法,由于 handlerMessage()方法是个空方法,这也是为什么需要在 Handler 中重写 handlerMessage()方法的原因。这里要注意的是Looper 只能在一个线程中只能存在一个。这里提到的ThreadLocal,其实就是一个对象,用来在不同线程中存放对应线程的 Looper。

最后,Handler 是Looper 和MessageQueue的桥梁,Handler内部提供了发送 Message 的一系列方法,最终会通过 MessageQueue的enqueueMessage()方法将 Message 存进 MessageQueue 中。我们平时可以直接在主线程中使用 Handler,那是因为在应用程序启动时,在入口的 main 方法中已经默认为我们创建好了 Looper。

问题12:Handler同步屏障

@TestApi
public int postSyncBarrier() {
    // 这里传入的时间是从开机到现在的时间戳
    return postSyncBarrier(SystemClock.uptimeMillis());
}
/**
 * 这就是创建的同步屏障的方法
 * 同步屏障就是一个同步消息,只不过这个消息的target为null
 */
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        // 从消息池中获取Message
        final Message msg = Message.obtain();
        msg.markInUse();
        // 初始化Message对象的时候,并没有给Message.target赋值,
        // 因此Message.target==null
        msg.when = when;
        msg.arg1 = token;
 
        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            // 这里的when是要加入的Message的时间
            // 这里遍历是找到Message要加入的位置
            while (p != null && p.when <= when) {
                // 如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步
                // 消息里有时间小于T,则prev也不为null
                prev = p;
                p = p.next;
            }
        }
        // 根据prev是否为null,将msg按照时间顺序插入到消息队列的合适位置
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

在这里可以看到,Message对象初始化的时候并没有给target赋值,因此target== null的来源就找得到了。这样就可以插入一条target== null的消息,这个消息就是一个同步屏障。
总结:同步屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,因此可以认为,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。

问题13:Handler怎么做到的线程间切换,子线程如何切换到主线程更新UI?

Handler怎么做到的线程间切换?
在创建Handler的时候需要传入目标线程的Looper。(没有传入Looper默认拿当前线程的Looper,如果当前线程也没有准备好Looper会抛异常)而当sendMessage的时候,会将当前的Handler对象赋值给Message中的target变量,并将该Message存到传入目标线程Looper的MessageQueue中。
当Looper消费Message的时候便会拿到Message中的target执行dispatchMessage()方法,从而实现线程切换。

子线程如何切换到主线程更新UI?
在主线程创建的Handler默认会传入主线程的Looper,Looper和线程是相互绑定的,意味着主线程的Looper对应的线程就是主线程,所以在子线程中发送Message,Message.target 绑定的是主线程的Handler,最终会回到主线程Handler的HandleMessage里去处理。

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红桃三呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值