关于android:面试官还问Handler那我要给你讲个故事

28次阅读

共计 7386 个字符,预计需要花费 19 分钟才能阅读完成。

来吧小兄弟,说说 Handler 怎么回事

Handler 的相干博客太多了,轻易一搜都一大把,然而根本都是上来就贴源码,讲姿态,短时间不太好弄明确整体的关系,和流程.

面试官,你坐好,听听我这个故事吹的怎么样?

本文就以生存点餐的例子再联合源码原理进行解析。心愿对你有一点帮忙。来,咱们进入角色。

Handler,Looper,MessageQueue,Message 的全程合作的关系就好比一个餐厅的整体运作关系

  • Handler 好比点餐员
  • Looper 好比后厨厨师长。
  • MessageQueue 好比订单打单机。
  • Message 好比一桌一桌的订单。

接下来咱们回顾下咱们餐厅点餐的场景,餐厅点餐分为规范点餐和非凡点餐,咱们合成来看。

规范流程

    1. 首先进入一家店,通过点餐员点餐把数据提交到后厨打单机。
    1. 而后厨师长一张一张的拿起订单,依照点餐的先后顺序,交代后厨的厨师开始制作。
    1. 制作好后上菜,并标记已做好的订单。

非凡流程

    1. 订单为提早订单,比方客人要求 30 分钟前人齐了再制作,这时会把该订单按工夫排序放到订单队列的适合地位,并通过 SystemClock.uptimeMillis()定好闹铃。至于为什么用 uptimeMillis 是因为该工夫是系统启动开始计算的毫秒工夫,不受手动调控工夫的影响。
    1. 如果打单机中全是提早订单,则下令给后厨厨师劳动,并在门口贴上免打搅的牌子(needWake),期待闹铃揭示,如有新的即时订单进来并且发现有免打搅的牌子,则通过 nativeWake()唤醒厨师再开始制作上菜。
    1. 然而为了晋升店铺菜品笼罩,很多相邻的店铺都抉择合作经营,就是你能够混搭旁边店的餐到本店吃,此时只需点餐员提交旁边店的订单即可,这样旁边店的厨师长就能够通过打单机取出订单并进行制作和上菜。

总结

一家店能够有多个点餐员,然而厨师长只能有一个。打单机也只能有一个。

映射到以上场景中

  • 一家店就好比一个 Thread
  • 而一个 Thread 中能够有多个 Handler(点餐员)
  • 然而一家店只能有一个 Looper(厨师长),一个 MessageQueue(打单机),和多个 Message(订单)。

面试官,我差不多吹完了,你要还不信,那就不好意思了?

依据以上的例子咱们类比看下源码,充沛钻研下整个机制的流程,和实现原理。

Looper 的工作流程

ActivityThread.main();// 初始化入口
    1. Looper.prepareMainLooper(); // 初始化
          Looper.prepare(false); // 设置不可敞开
              Looper.sThreadLocal.set(new Looper(quitAllowed)); // 跟线程绑定
                    1.1.Looper.mQueue = new MessageQueue(quitAllowed); //Looper 和 MessageQueue 绑定
                    1.2.Looper.mThread = Thread.currentThread();
    2. Looper.loop();
        2.1.myLooper().mQueue.next(); // 循环获取 MessageQueue 中的音讯
              nativePollOnce(); // 阻塞队列
                  native -> pollInner() // 底层阻塞实现
                        native -> epoll_wait();
        2.2.Handler.dispatchMessage(msg);// 音讯散发

myLooper().mQueue.next()实现原理

    1. 通过 myLooper().mQueue.next() 循环获取 MessageQueue 中的音讯,如遇到同步屏障 则优先解决异步音讯.
    1. 同步屏障即为用 Message.postSyncBarrier()发送的音讯,该音讯的 target 没有绑定 Handler。在 Hnandler 中异步音讯优先级高于同步音讯。
    1. 可通过创立 new Handler(true)发送异步音讯。ViewRootImpl.scheduleTraversals 办法就应用了同步屏障,保障 UI 绘制优先执行。

Handler.dispatchMessage(msg)实现原理

    1. 优先回调 msg.callback。
    1. 其次回调 handler 构造函数中的 callback。
    1. 最初回调 handler handleMessage()。

Hander 发送音讯的流程

1.Handler handler = new Handler();// 初始化 Handler
        1.Handler.mLooper = Looper.myLooper();// 获取以后线程 Looper。2.Handler.mQueue = mLooper.mQueue;// 获取 Looper 绑定的 MessageQueue 对象。2.handler.post(Runnable);// 发送音讯
        sendMessageDelayed(Message msg, long delayMillis);
            sendMessageAtTime(Message msg, long uptimeMillis);
                Handler.enqueueMessage();//Message.target 赋值为 this。Handler.mQueue.enqueueMessage();// 增加音讯到 MessageQueue。

MessageQueue.enqueueMessage()办法实现原理

    1. 如果音讯队列被放弃,则抛出异样。
    1. 如果以后插入音讯是即时消息,则将这个音讯作为新的头部元素,并将此音讯的 next 指向旧的头部元素,并通过 needWake 唤醒 Looper 线程。
    1. 如果音讯为异步音讯则通过 Message.when 长短插入到队列对应地位,不唤醒 Looper 线程。

接下来该面试官问了

常常有人问为什么主线程的 Looper 阻塞不会导致 ANR?

    1. 首先咱们得晓得 ANR 是主线程 5 秒内没有响应。
    1. 什么叫 5 秒没有响应呢?Android 零碎中所有的操作均通过 Handler 增加事件到事件队列,Looper 循环去队列去取事件进行执行。如果主线程事件反馈超过了 5 秒则提醒 ANR。
    1. 如果没有事件进来,基于 Linux pipe/epoll 机制会阻塞 loop 办法中的 queue.next()中的 nativePollOnce()不会报 ANR。
    1. 对于以上的例子来说,ANR 能够了解为用户进行即时点餐后没按时上菜(当然未按时上菜的起因很多,可能做的慢(耗时操作 IO 等),也可能厨具被占用(死锁),还有可能厨师不够多(CPU 性能差)等等。。。),顾客发动了投诉,或差评。但如果约定工夫还没到,或者以后没人点餐,是不会有差评或投诉产生的,因而也不会产生 ANR。

以上的所有内容均围绕原理,源码,接下来咱们举几个非凡场景的例子

  • 1. 为什么子线程不能间接 new Handler()?
       new Thread(new Runnable() {
           @Override
           public void run() {Handler handler = new Handler();
           }
       }).start();


       以上代码会报以下下谬误

java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()
       at android.os.Handler.<init>(Handler.java:207)
       at android.os.Handler.<init>(Handler.java:119)
       at com.example.test.MainActivity$2.run(MainActivity.java:21)
       at java.lang.Thread.run(Thread.java:919)

  • 通过报错提醒 “not called Looper.prepare()” 能够看出提醒没有调用 Looper.prepare(),至于为什么咱们还得看下源码
 public Handler(Callback callback, boolean async) {
        ... 省略若干代码

       // 通过 Looper.myLooper()获取了 mLooper 对象,如果 mLooper ==null 则抛异样
        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;
    }

 public static @Nullable Looper myLooper() {// 而 myLooper()是通过 sThreadLocal.get()获取的,那 sThreadLocal 又是个什么鬼?return sThreadLocal.get();}

 // 能够看到 sThreadLocal 是一个 ThreadLocal<Looper> 对象,那 ThreadLocal 值从哪赋值的?static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

//sThreadLocal 的值就是在这个办法里赋值的
 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));
    }
  • 通过以上的源码正文,齐全明确了报错的日志的意思,报错日志提醒咱们没有调用 Looper.prepare()办法,而 Looper.prepare()办法就是 sThreadLocal 赋值的地位。

那子线程怎么创立 Handler 呢?只需在 new Handler()之前调用下 Looper.prepare()即可。


  • 2. 为什么主线程能够间接 new Handler?
  • 子线程间接 new Handler 会报错,主线程为什么就不会报错呢?主线程我也没有调用 Looper.prepare()啊?那么咱们还得看下源码了。
    // 咱们看下 ActivityMain 的入口 main 办法,调用了 Looper.prepareMainLooper();
    public static void main(String[] args) {
       ...
        Looper.prepareMainLooper();
        ...
    }

  // 看到这一下就明确了,原来主线程在启动的时候默认就调用了 prepareMainLooper(), 而在这个办法中调用了 prepare()。// 提前将 sThreadLocal 进行赋值了。public static void prepareMainLooper() {prepare(false);
        synchronized (Looper.class) {if (sMainLooper != null) {throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();}
    }

  • 3.Handler 为什么会内存泄露?
  • 首先遍及下什么叫内存泄露,当一个对象不再应用本该被回收时,但另外一个正在应用的对象持有它的援用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这种状况下就产生了内存透露。
  • 咱们举一个 Handler 内存泄露的场景。
public class HandlerActivity extends AppCompatActivity {private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        handler.sendEmptyMessageDelayed(1,5000);
    }
}
  • 当以上代码写完后编译器立马会报黄并提醒“this handler should be static or leaks might occur…Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.”

大抵意思就说 “因为这个处理程序被申明为一个外部类,它能够避免外部类被垃圾回收。如果处理程序正在对主线程以外的线程应用 Looper 或 MessageQueue,则不存在问题。如果处理程序正在应用主线程的 Looper 或 MessageQueue,则须要修复处理程序申明,如下所示:将处理程序申明为动态类;并且通过 WeakReference 援用外部类”。

  • 说了这么一大堆,简略意思就是说以上这种写法,默认会援用 HandlerActivity, 当 HandlerActivity 被 finish 的时候,可能 Handler 还在执行不能会回收,同时因为 Handler 隐式援用了 HandlerActivity,导致了 HandlerActivity 也不能被回收,所以内存泄露了。

咱们来写一种正确的写法

public class HandlerActivity extends AppCompatActivity {MyHandler handler = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        handler.sendEmptyMessageDelayed(1,5000);
    }
    private static class MyHandler extends Handler{
        private WeakReference<HandlerActivity> activityWeakReference;

        public MyHandler(HandlerActivity activity) {activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {super.handleMessage(msg);
        }
    }
}
  • 以上写法应用了动态外部类 + 弱援用的形式,其实如果在 handleMessage()办法中无需拜访 HandlerActivity 的成员则无需应用弱援用,只需动态外部类即可,弱援用只是不便调用 HandlerActivity 外部成员。
  • 非动态外部类和非动态匿名外部类中的确都持有外部类的援用,动态外部类中未持有外部类的援用,不影响后续的回收,因而没有内存泄露。

4. 补充个小知识点,啥是隐式援用?

  • 其实咱们写的非动态外部类和非动态匿名外部类,在编译器编译过程中,隐式帮咱们传入了 this 这个参数,这也是为什么,咱们平时在办法中能应用 this 这个关键字的起因,理解了隐式援用,那么为什么它会是导致内存透露?这里又得阐明一下,虚拟机的垃圾回收策略。
  • 垃圾回收机制:Java 采纳根搜索算法,当 GC Roots 不可达时,并且对象 finalize 没有自救的状况下,才会回收。也就是说 GC 会收集那些不是 GC roots 且没有被 GC roots 援用的对象,就像下边这个图一样。

  • 上图中的对象之间的连线就是这些对象之间的援用,垃圾回收的断定条件就在这些连线上,要预防非动态外部类的透露问题,就得治理好对象间的援用关系。
  • 去除隐式援用(通过动态外部类来去除隐式援用)手动治理对象援用(批改动态外部类的结构形式,手动引入其外部类援用)当内存不可用时,不执行不可控代码(Android 能够联合智能指针,WeakReference 包裹外部类实例)是解决内存泄露比拟好的形式。

留神 : 不是所有外部类都倡议应用动态外部类,只有在该外部类中的生命周期不可控的状况下,倡议采纳动态外部类。其余状况还是能够应用非动态外部类的。

好了 Handler 的介绍到此结束了,篇幅略长,如果给你带来了一点帮忙,麻烦给个点赞。

视频:Android 程序员备战 2022FrameWork 必问:handler 原理
原文: https://juejin.cn/post/6995854886386532388

正文完
 0