关于Handler面试题常问问题

1. 一个线程可以创建几个Handler?创建Looper的两种方式?

一个线程可以创建多个Handler。
一般是在主线程中实现一个Handler,然后在子线程中使用它。

class HandlerActivity: AppCompatActivity() {

   private val mHandler = MyHandler()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       // 在子线程中通过自定义的 Handler 发消息
       thread {
           mHandler.sendEmptyMessageDelayed(1, 1000)
       }
   }

   // 自定义一个 Handler
   class MyHandler: Handler() {
       override fun handleMessage(msg: Message) {
           Log.i("HandlerActivity", "主线程:handleMessage: ${msg.what}")
       }
   }
}

或者有时候需要在子线程中创建运行在主线程中的Handler

class HandlerActivity: AppCompatActivity() {
    private var mHandler: Handler? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        thread {
            //获得main looper 运行在主线程
            mHandler = MyHandler(Looper.getMainLooper())
            mHandler!!.sendEmptyMessageDelayed(1, 1000)
        }
    }
     // 自定义一个 Handler
    class MyHandler(): Handler() {
        override fun handleMessage(msg: Message) {
            Log.i("HandlerActivity", "子线程:handleMessage: ${msg.what}")
        }
    }
}
2. 一个线程有几个looper,几个message,几个messageQueue怎么保证?Looper的工作流程。

一个线程只有一个looper,一个message。
looper在Handler初始化的时候获取looper:mLooper = Looper.myLooper();在这一句中Handler通过Looper.myLooper方法获取到了Looper对象,那我们看看这个Looper.myLooper()方法做了什么事情呢。它是如何返回一个Looper对象的呢?

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

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

ThreadLocal提供了线程的局部变量,每个线程可以通过set()和get()方法来对这个局部变量进行操作,但是不会和其他线程的局部变量产生冲突,实现了线程的数据隔离,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));
}

可以看得出,最后调用了是prepare(boolean quitAllowed)方法,而这个方法首先判断,如果sThreadLocal有值,就抛异常,没有值才会塞进去一个值。其实很好理解,就是说prepare方法必须调用但也只能调用一次,不调用没有值,抛异常,调用多次也还抛异常

接下来再看看这行sThreadLocal.set(new Looper(quitAllowed));做了什么吧,它是如何塞进去的呢?

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

set方法首先获取到了当前的线程,然后获取一个map。这个map是以键值对形式存储内容的。如果获取的map为空,就创建一个map。如果不为空就塞进去值。要注意的是,这里面的key是当前的线程,这里面的value就是Looper。也就是说,线程和Looper是一一对应的。也就是很多人说的Looper和线程绑定了,其实就是以键值对形式存进了一个map中。

3. handler如何延迟发送消息?
微信图片_20201119173143.png

因为messageQueue是一个按时间排序的一个单链表,所有消息是按照先后顺序进行处理的。

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

handler所有发送消息的方法最终都会走到sendMessageDelayed,只是delayMillis不同而已,这个delayMillis就是延时的时间。
然后这里会将DelayMillis加上当前开机的时间(这里可以理解就是这个time就是,现在的时间+需要延迟的时间=实际执行的时间),接下来进到sendMessageAtTime方法里面

 public boolean sendMessageAtTime(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);
    }
 
boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) { // msg 必须有 target
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) { // msg 不能正在被使用
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        if (mQuitting) { // 正在退出,回收消息并直接返回
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            // 插入消息队列头部,需要唤醒队列
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            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;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

MessageQueue是按照Message触发时间的先后顺序排列的,队头的消息是将要最早触发的消息。排在越前面的越早触发,那我们现在应该了解到了,这个所谓的延时呢,不是延时发送消息,而是延时去处理消息,我们在发消息都是马上插入到消息队列当中。

我们这里插入完消息之后,怎么又保证在我们预期的时间里处理消息呢,接下来我们看如何获取消息

MessageQueue::next()

    @UnsupportedAppUsage
    Message next() {
         ...
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
           //阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒,都会返回
            nativePollOnce(ptr, nextPollTimeoutMillis);
             //如果阻塞操作结束,则去获取消息 
            synchronized (this) {
                // 去获取下一条消息
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                 //当消息的Handler为空时,则查询异步消息
                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 {
                          // 获取一条消息,并返回
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        //设置消息的使用状态,即flags |= FLAG_IN_USE
                        msg.markInUse();
                        return msg;  //成功地获取MessageQueue中的下一条即将要执行的消息
                    }
                } else {
                    //没有消息
                    nextPollTimeoutMillis = -1;
                }
             ...
        }
    }

msg != null 我们看下这部分,如果当前时间小于头部时间(消息队列是按时间顺序排列的),那就更新等待时间nextPollTimeoutMillis,等下次再做比较
如果时间到了,就取这个消息并返回。
如果没有消息,nextPollTimeoutMillis被赋为-1,这个循环又执行到nativePollOnce继续阻塞

其实到这里handler的任务就完成了,把message发送到messageQueue里面,每个消息都会带有一个uptimeMillis参数,这就是延时的时间。

总结:
1、postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
2、紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
3、MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
4、Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;直到阻塞时间到或者下一次有Message进队;

4. Looper.loop()方法在主线程死循环,为啥不会造成ANR?

具体的流程是:
1、mainThread中ActivityThread首先创建了一个运行在主线程的Looper,并且把它和主线程进行了绑定。
2、Looper又创建了一个MessageQueue,然后调用Looper.loop方法不断地在主线程中尝试取出Message
3、Looper如果取到了Message,那么就在主线程中调用发送这个Message的Handler的handleMessage方法。
4、我们在主线程或者子线程中通过Looper.getMainLooper为参数创建了一个Handler。
5、在子线程中发送了Message,主线程中的Looper不断循环,终于收到了Message,在主线程中调用了这个Handler的handleMessage方法。

  • 本质上Android就是事件驱动的程序,界面刷新也好,交互也好,本质上都是事件,这些事件最后通通被作为了Message发送到了MessageQueue中。由Looper来进行分发,然后在进行处理。用人话来说就是,我们的Android程序就是运行在这个死循环中的。一旦这个死循环结束,app也就结束了。但是只有消息循环没有被阻塞,能一直处理事件就不会发生ANR异常。
  • ANR是Application Not Responding也就是Android程序无响应,只要有消息,looper就会及时的进行处理,所以不会造成消息没有被及时处理造成ANR。
    只是如果没有消息到来的时候,主线程会进行休眠,消息到来的时候会进行唤醒,这里面使用了 Linux 的 epoll 机制。
5. Handler为什么会造成内存泄漏?如何避免造成内存泄漏?

内部类持有外部类的对象,handler持有activity的对象,当页面activity关闭时,handler还在发送消息,handler持有activity的对象,导致handler不能及时被回收,所以造成内存泄漏。
为啥其他内部类不会呢?
因为当handler发送消息时,会有耗时操作,并且会利用线程中的looper和messageQueue进行消息发送,looper和messageQueue的生命周期是很长的,和application一样,所以handler不容易被销毁,所以造成内存泄漏。
如何解决?
1.把handler生命成静态内部类,静态内部类不会持有activity,所以不会造成内存泄漏,
2.弱引用(WeakReference):把使用handle的activity设置成弱引用,

        protected MyHandler handler = new MyHandler(this);
        public abstract void handlerMessage1(Message msg);
        public static class MyHandler extends Handler {
          private WeakReference<BaseActivity> weakReference;
          public MyHandler(BaseActivity activity) {
              this.weakReference = new WeakReference<BaseActivity>(activity);
          }
          @Override
          public void handleMessage(Message msg) {
            weakReference.get().handlerMessage1(msg);
        }
    }

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
3.页面销毁时,清空发送的消息:

handler.removeCallbacksAndMessages()
6. 子线程如何创建hander?主线程给子线程的Handler发送消息怎么写?

子线程如果要创建Handler,必须通过Looper.prepare()方法创建Looper,在主线程中ActivityThread已经帮我们创建好了,我们不需要自己去创建,但如果在子线程中创建Handler,要么使用Looper的mainLooper,要么自己调用Looper.prepare()方法创建属于这个线程的looper对象。如下是创建了一个子线程的Looper对象:

class LooperThread extends Thread {
    public Handler mHandler;
    public void run() {
        Looper.prepare();  
        mHandler = new Handler() {  
            public void handleMessage(Message msg) {
                //TODO 子线程处理消息
            }
        };
        Looper.loop(); 
    }
}
mHandler.sendMessage()//主线程发送消息
7. 为什么建议使用Message.obtain()来创建Message实例?

提高消息的复用

public static Message obtain() {
   synchronized (sPoolSync) {
       if (sPool != null) {
           Message m = sPool;
           sPool = m.next;
           m.next = null;
           m.flags = 0; // clear in-use flag
           sPoolSize--;
           return m;
       }
   }
   return new Message();
}

可以看到,obtain方法是将一个Message对象的所有数据清空,然后添加到链表头中。sPool就是个消息池,默认的缓存是50个。
Looper在分发结束以后,会将用完的消息回收掉,并添加到回收池里。
这样可以「避免重复创建多个实例对象」节约内存,还有,Message池其实是一个「单链表结构」,定位到下述代码可以看到:池的容量为50

8. 为什么子线程中不可以直接new Handler()而主线程中可以?

如果要创建Handler,必须通过Looper.prepare()方法创建Looper,在主线程中ActivityThread已经帮我们创建好了,我们不需要自己去创建,但如果在子线程中创建Handler,要么使用Looper的mainLooper,要么自己调用Looper.prepare()方法创建属于这个线程的looper对象。

9. 请描述MessageQueue的数据结构和工作流程。

按时间先后顺序排列的单链表,在Handler的构造方法中MessageQueue被赋值。最后发送消息都调用的是MessageQueue的queue.enqueueMessage(msg, uptimeMillis)方法。现在我们已经拿到了queue,然后在这个单链表中进行发消息。

// MessageQueue.java
//省略部分代码
boolean enqueueMessage(Message msg, long when) {

    synchronized (this) {
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;

        //【1】拿到队列头部
        Message p = mMessages;
        boolean needWake;

        //【2】如果消息不需要延时,或者消息的执行时间比头部消息早,插到队列头部
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            //【3】消息插到队列中间
            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;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

可以看到,消息队列是一个根据消息【执行时间先后】连接起来的单向链表。想要获取可执行的消息,只需要遍历这个列表,对比当前时间与消息的执行时间,就知道消息是否需要执行了。好了

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

推荐阅读更多精彩内容

  • 前言 Android中主线程是不能进行耗时操作的,子线程是不能进行更新UI的。所以就有了handler,它的作用就...
    cc_And阅读 4,968评论 0 5
  • 面试题总结 Handler是一个比较重要的东西,所以把网上发的Handler中的面试题总结了一下,这些面试题没问题...
    被虐的小鸡阅读 4,795评论 2 15
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,515评论 16 22
  • 创业是很多人的梦想,多少人为了理想和不甘选择了创业来实现自我价值,我就是其中一个。 创业后,我由女人变成了超人,什...
    亦宝宝阅读 1,804评论 4 1
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,559评论 0 11