美文网首页Android开发经验谈Android开发
Handler的十七发面试连环炮,你能抗住吗?

Handler的十七发面试连环炮,你能抗住吗?

作者: 10块钱new一个对象 | 来源:发表于2020-12-04 17:49 被阅读0次

    前言

    在Android开发的多线程应用场景中,Handler机制十分常用。而在面试中,Handler机制又是面试官百问不厌的问题,由浅入深、非常难懂,这就导致了面试中很多朋友因为不懂Handler原理被淘汰。着实可惜!

    下面,我将大厂面试题的方式来详解 Handler机制 的工作原理,干货较多,大家需静心阅读,方可吸收知识。

    handler

    PS:关于我


    本人是一个拥有6年开发经验的帅气Android攻城狮,记得看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。

    另外耗时两年整理收集的Android一线大厂面试核心知识点出炉,【完整版】已更新在我的【Github】,有面试需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!

    Github地址:【https://github.com/733gh/xiongfan】

    1、Handler、Looper、MessageQueue、Thread 的对应关系

    首先,Looper 中的 MessageQueue 和 Thread 两个字段都属于常量,且 Looper 实例是存在 ThreadLocal 中,这说明了 Looper 和 MessageQueue 之间是一对一应的关系,且一个 Thread 在其整个生命周期内都只会关联到同一个 Looper 对象和同一个 MessageQueue 对象

    public final class Looper {
    
       final MessageQueue mQueue;
       final Thread mThread;
       static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
       private Looper(boolean quitAllowed) {
            mQueue = new MessageQueue(quitAllowed);
            mThread = Thread.currentThread();
        }
    
    }
    

    Handler 中的 Looper 和 MessageQueue 两个字段也都属于常量,说明 Handler 对于 Looper 和 MessageQueue 都是一对一的关系。但是 Looper 和 MessageQueue 对于 Handler 却可以是一对多的关系,例如,多个子线程内声明的 Handler 都可以关联到 mainLooper

    public class Handler {
    
        @UnsupportedAppUsage
        final Looper mLooper;
        final MessageQueue mQueue;
    
    }
    

    2、Handler 的同步机制

    MessageQueue 在保存 Message 的时候,enqueueMessage方法内部已经加上了同步锁,从而避免了多个线程同时发送消息导致竞态问题。此外,next()方法内部也加上了同步锁,所以也保障了 Looper 分发 Message 的有序性。最重要的一点是,Looper 总是由一个特定的线程来执行遍历,所以在消费 Message 的时候也不存在竞态

        boolean enqueueMessage(Message msg, long when) {
            if (msg.target == null) {
                throw new IllegalArgumentException("Message must have a target.");
            }
    
            synchronized (this) {
                ···
            }
            return true;
        }
    
        @UnsupportedAppUsage
        Message next() {
            ···
            for (;;) {
                if (nextPollTimeoutMillis != 0) {
                    Binder.flushPendingCommands();
                }
    
                nativePollOnce(ptr, nextPollTimeoutMillis);
    
                synchronized (this) {
                    ···
                }
    
                ···
            }
        }
    

    3、Handler 如何发送同步消息

    如果我们在子线程通过 Handler 向主线程发送了一个消息,希望等到消息执行完毕后子线程才继续运行,这该如何实现?其实像这种涉及到多线程同步等待的问题,往往都是需要依赖于线程休眠+线程唤醒机制来实现的

    Handler 本身就提供了一个runWithScissors方法可以用于实现这种功能,只是被隐藏了,我们无法直接调用到。runWithScissors首先会判断目标线程是否就是当前线程,是的话则直接执行 Runnable,否则就需要使用到 BlockingRunnable

        /**
         * @hide
         */
        public final boolean runWithScissors(@NonNull Runnable r, long timeout) {
            if (r == null) {
                throw new IllegalArgumentException("runnable must not be null");
            }
            if (timeout < 0) {
                throw new IllegalArgumentException("timeout must be non-negative");
            }
    
            if (Looper.myLooper() == mLooper) {
                r.run();
                return true;
            }
    
            BlockingRunnable br = new BlockingRunnable(r);
            return br.postAndWait(this, timeout);
        }
    

    BlockingRunnable 的逻辑也很简单,在 Runnable 执行完前会通过调用 wait()方法来使发送者线程转为阻塞等待状态,当任务执行完毕后再通过notifyAll()来唤醒发送者线程,从而实现了在 Runnable 被执行完之前发送者线程都会一直处于等待状态

    private static final class BlockingRunnable implements Runnable {
    
            private final Runnable mTask;
            //用于标记 mTask 是否已经执行完毕 
            private boolean mDone;
    
            public BlockingRunnable(Runnable task) {
                mTask = task;
            }
    
            @Override
            public void run() {
                try {
                    mTask.run();
                } finally {
                    synchronized (this) {
                        mDone = true;
                        notifyAll();
                    }
                }
            }
    
            public boolean postAndWait(Handler handler, long timeout) {
                if (!handler.post(this)) {
                    return false;
                }
    
                synchronized (this) {
                    if (timeout > 0) {
                        final long expirationTime = SystemClock.uptimeMillis() + timeout;
                        while (!mDone) {
                            long delay = expirationTime - SystemClock.uptimeMillis();
                            if (delay <= 0) {
                                return false; // timeout
                            }
                            try {
                                //限时等待
                                wait(delay);
                            } catch (InterruptedException ex) {
                            }
                        }
                    } else {
                        while (!mDone) {
                            try {
                                //无限期等待
                                wait();
                            } catch (InterruptedException ex) {
                            }
                        }
                    }
                }
                return true;
            }
        }
    

    虽然 runWithScissors 方法我们无法直接调用,但是我们也可以依靠这思路自己来实现 BlockingRunnable,折中实现这个功能。但这种方式并不安全,如果 Loop 意外退出循环导致该 Runnable 无法被执行的话,就会导致被暂停的线程一直无法被唤醒,需要谨慎使用

    4、Handler 如何避免内存泄漏

    当退出 Activity 时,如果 Handler 中还保存着待处理的延时消息的话,那么就会导致内存泄漏,此时可以通过调用Handler.removeCallbacksAndMessages(null)来移除所有待处理的 Message

    该方法会将消息队列中所有 Message.obj 等于 token 的 Message 均给移除掉,如果 token 为 null 的话则会移除所有 Message

        public final void removeCallbacksAndMessages(@Nullable Object token) {
            mQueue.removeCallbacksAndMessages(this, token);
        }
    

    5、Message 如何复用

    因为 Android 系统本身就存在很多事件需要交由 Message 来交付给 mainLooper,所以 Message 的创建是很频繁的。为了减少 Message 频繁重复创建的情况,Message 提供了 MessagePool 用于实现 Message 的缓存复用,以此来优化内存使用

    当 Looper 消费了 Message 后会调用recycleUnchecked()方法将 Message 进行回收,在清除了各项资源后会缓存到 sPool 变量上,同时将之前缓存的 Message 置为下一个节点 next,通过这种链表结构来缓存最多 50 个Message。这里使用到的是享元设计模式

    obtain()方法则会判断当前是否有可用的缓存,有的话则将 sPool 从链表中移除后返回,否则就返回一个新的 Message 实例。所以我们在发送消息的时候应该尽量通过调用Message.obtain()或者Handler.obtainMessage()方法来获取 Message 实例

    public final class Message implements Parcelable {
    
        /** @hide */
        public static final Object sPoolSync = new Object();
        private static Message sPool;
        private static int sPoolSize = 0;
        private static final int MAX_POOL_SIZE = 50;
    
        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();
        }
    
        @UnsupportedAppUsage
        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) {
                    next = sPool;
                    sPool = this;
                    sPoolSize++;
                }
            }
        }
    
    }
    

    6、Message 复用机制存在的问题

    由于 Message 采用了缓存复用机制,从而导致了一个 Message 失效问题。当 handleMessage 方法被回调后,Message 携带的所有参数都会被清空,而如果外部的 handleMessage方法是使用了异步线程来处理 Message 的话,那么异步线程只会得到一个空白的 Message

    val handler = object : Handler() {
        override fun handleMessage(msg: Message) {
            handleMessageAsync(msg)
        }
    }
    
    fun handleMessageAsync(msg: Message) {
        thread {
            //只会得到一个空白的 Message 对象
            println(msg.obj)
        }
    }
    

    7、Message 如何提高优先级

    Handler 包含一个 sendMessageAtFrontOfQueue方法可以用于提高 Message 的处理优先级。该方法为 Message 设定的时间戳是 0,使得 Message 可以直接插入到 MessageQueue 的头部,从而做到优先处理。但官方并不推荐使用这个方法,因为最极端的情况下可能会使得其它 Message 一直得不到处理或者其它意想不到的情况

        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;
            }
            return enqueueMessage(queue, msg, 0);
        }
    

    8、检测 Looper 分发 Message 的效率

    Looper 在进行 Loop 循环时,会通过 Observer 向外回调每个 Message 的回调事件。且如果设定了 slowDispatchThresholdMsslowDeliveryThresholdMs 这两个阈值的话,则会对 Message 的分发时机分发耗时进行监测,存在异常情况的话就会打印 Log。该机制可以用于实现应用性能监测,发现潜在的 Message 处理异常情况,但可惜监测方法被系统隐藏了

        public static void loop() {
            final Looper me = myLooper();
            ···
            for (;;) {
                Message msg = queue.next(); // might block
                ···
                //用于向外回调通知 Message 的分发事件
                final Observer observer = sObserver;
    
                final long traceTag = me.mTraceTag;
                //如果Looper分发Message的时间晚于预定时间且超出这个阈值,则认为Looper分发过慢
                long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
                //如果向外分发出去的Message的处理时间超出这个阈值,则认为外部处理过慢
                long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
                if (thresholdOverride > 0) {
                    slowDispatchThresholdMs = thresholdOverride;
                    slowDeliveryThresholdMs = thresholdOverride;
                }
                final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
                final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
    
                final boolean needStartTime = logSlowDelivery || logSlowDispatch;
                final boolean needEndTime = logSlowDispatch;
    
                if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                    Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
                }
    
                //开始分发 Message 的时间
                final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
                //Message 分发结束的时间
                final long dispatchEnd;
                Object token = null;
                if (observer != null) {
                    //开始分发 Message 
                    token = observer.messageDispatchStarting();
                }
                long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
                try {
                    msg.target.dispatchMessage(msg);
                    if (observer != null) {
                        //完成 Message 的分发,且没有抛出异常
                        observer.messageDispatched(token, msg);
                    }
                    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
                } catch (Exception exception) {
                    if (observer != null) {
                        //分发 Message 时抛出了异常
                        observer.dispatchingThrewException(token, msg, exception);
                    }
                    throw exception;
                } finally {
                    ThreadLocalWorkSource.restore(origWorkSource);
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
                if (logSlowDelivery) {
                    if (slowDeliveryDetected) {
                        if ((dispatchStart - msg.when) <= 10) {
                            //如果 Message 的分发时间晚于预定时间,且间隔超出10毫秒,则认为属于延迟交付
                            Slog.w(TAG, "Drained");
                            slowDeliveryDetected = false;
                        }
                    } else {
                        if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                                msg)) {
                            // Once we write a slow delivery log, suppress until the queue drains.
                            slowDeliveryDetected = true;
                        }
                    }
                }
                ···
            }
        }
    

    9、主线程 Looper 在哪里创建

    由 ActivityThread 类的 main() 方法来创建。该 main() 方法即 Java 程序的运行起始点,当应用启动时系统就自动为我们在主线程做好了 mainLooper 的初始化,而且已经调用了Looper.loop()方法开启了消息的循环处理,应用在使用过程中的各种交互逻辑(例如:屏幕的触摸事件、列表的滑动等)就都是在这个循环里完成分发的。正是因为 Android 系统已经自动完成了主线程 Looper 的初始化,所以我们在主线程中才可以直接使用 Handler 的无参构造函数来完成 UI 相关事件的处理

    public final class ActivityThread extends ClientTransactionHandler {
    
        public static void main(String[] args) {
            ···
            Looper.prepareMainLooper();
            ···
            Looper.loop();
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    
    }
    

    10、主线程 Looper 什么时候退出循环

    当 ActivityThread 内部的 Handler 收到了 EXIT_APPLICATION 消息后,就会退出 Looper 循环

            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case EXIT_APPLICATION:
                        if (mInitialApplication != null) {
                            mInitialApplication.onTerminate();
                        }
                        Looper.myLooper().quit();
                        break;
                }
            }
    

    11、主线程 Looper.loop() 为什么不会导致 ANR

    这个问题在网上很常见,我第一次看到时就觉得这种问题很奇怪,主线程凭啥会 ANR?这个问题感觉本身就是特意为了来误导人

    看以下例子。doSomeThing()方法是放在 for 循环这个死循环的后边,对于该方法来说,主线程的确是被阻塞住了,导致该方法一直无法得到执行。可是对于应用来说,应用在主线程内的所有操作其实都是被放在了 for 循环之内,一直有得到执行,是个死循环也无所谓,所以对于应用来说主线程并没有被阻塞,自然不会导致 ANR。此外,当 MessageQueue 中当前没有消息需要处理时,也会依靠 epoll 机制挂起主线程,避免了其一直占用 CPU 资源

        public static void main(String[] args) {
            for (; ; ) {
                //主线程执行....
            }
            doSomeThing();
        }
    

    所以在 ActivityThread 的 main 方法中,在开启了消息循环之后,并没有声明什么有意义的代码。正常来说应用是不会退出 loop 循环的,如果能够跳出循环,也只会导致直接就抛出异常

        public static void main(String[] args) {
            ···
            Looper.prepareMainLooper();
            ···
            Looper.loop();
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    

    所以说,loop 循环本身不会导致 ANR,会出现 ANR 是因为在 loop 循环之内 Message 处理时间过长

    12、子线程一定无法弹 Toast 吗

    不一定,只能说是在子线程中无法直接弹出 Toast,但可以实现。因为 Toast 的构造函数中会要求拿到一个 Looper 对象,如果构造参数没有传入不为 null 的 Looper 实例的话,则尝试使用调用者线程关联的 Looper 对象,如果都获取不到的话则会抛出异常

        public Toast(Context context) {
            this(context, null);
        }
    
        public Toast(@NonNull Context context, @Nullable Looper looper) {
            mContext = context;
            mToken = new Binder();
            looper = getLooper(looper);
            mHandler = new Handler(looper);
            ···
        }
    
        private Looper getLooper(@Nullable Looper looper) {
            if (looper != null) {
                return looper;
            }
            //Looper.myLooper() 为 null 的话就会直接抛出异常
            return checkNotNull(Looper.myLooper(),
                    "Can't toast on a thread that has not called Looper.prepare()");
        }
    

    为了在子线程弹 Toast,就需要主动为子线程创建 Looper 对象及开启 loop 循环。但这种方法会导致子线程一直无法退出循环,需要通过Looper.myLooper().quit()来主动退出循环

        inner class TestThread : Thread() {
    
            override fun run() {
                Looper.prepare()
                Toast.makeText(
                    this@MainActivity,
                    "Hello: " + Thread.currentThread().name,
                    Toast.LENGTH_SHORT
                ).show()
                Looper.loop()
            }
    
        }
    

    13、子线程一定无法更新 UI?主线程就一定可以?

    在子线程能够弹出 Toast 就已经说明了子线程也是可以更新 UI 的,Android 系统只是限制了必须在同个线程内进行 ViewRootImpl 的创建和更新这两个操作,而不是要求必须在主线程进行

    如果使用不当的话,即使在主线程更新 UI 也可能会导致应用崩溃。例如,在子线程先通过 show+hide 来触发 ViewRootImpl 的创建,然后在主线程再来尝试显示该 Dialog,此时就会发现程序直接崩溃了

    class MainActivity : AppCompatActivity() {
    
        private lateinit var alertDialog: AlertDialog
    
        private val thread = object : Thread("hello") {
            override fun run() {
                Looper.prepare()
                Handler().post {
                    alertDialog =
                        AlertDialog.Builder(this@MainActivity).setMessage(Thread.currentThread().name)
                            .create()
                    alertDialog.show()
                    alertDialog.hide()
                }
                Looper.loop()
            }
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            btn_test.setOnClickListener {
                alertDialog.show()
            }
            thread.start()
        }
    
    }
    
        E/AndroidRuntime: FATAL EXCEPTION: main
        Process: github.leavesc.test, PID: 5243
        android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
            at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6892)
            at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1048)
            at android.view.View.requestLayout(View.java:19781)
            at android.view.View.setFlags(View.java:11478)
            at android.view.View.setVisibility(View.java:8069)
            at android.app.Dialog.show(Dialog.java:293)
    

    ViewRootImpl 在初始化的时候会将当前线程保存到 mThread,在后续进行 UI 更新的时候就会调用checkThread()方法进行线程检查,如果发现存在多线程调用则直接抛出以上的异常信息

    public final class ViewRootImpl implements ViewParent,
            View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
    
         final Thread mThread;       
    
         public ViewRootImpl(Context context, Display display, IWindowSession session,
                boolean useSfChoreographer) {
            mThread = Thread.currentThread();
            ···
        }       
    
        void checkThread() {
            if (mThread != Thread.currentThread()) {
                throw new CalledFromWrongThreadException(
                        "Only the original thread that created a view hierarchy can touch its views.");
            }
        }
    
    }
    

    14、为什么 UI 体系要采用单线程模型

    其实这很好理解,就是为了提高运行效率和降低实现难度。如果允许多线程并发访问 UI 的话,为了避免竞态,很多即使只是小范围的局部刷新操作(例如,TextView.setText)都势必需要加上同步锁,这无疑会加大 UI 刷新操作的“成本”,降低了整个应用的运行效率。而且会导致 Android 的 UI 体系在实现时就被迫需要对多线程环境进行“防御”,即使开发者一直是使用同个线程来更新 UI,这就加大了系统的实现难度

    所以,最为简单高效的方式就是采用单线程模型来访问 UI

    15、如何跨线程下发任务

    通常情况下,两个线程之间的通信是比较麻烦的,需要做很多线程同步操作。而依靠 Looper 的特性,我们就可以用比较简单的方式来实现跨线程下发任务

    看以下代码,从 TestThread 运行后弹出的线程名可以知道, Toast 是在 Thread_1 被弹出来的。如果将 Thread_2 想像成主线程的话,那么以下代码就相当于从主线程向子线程下发耗时任务了,这个实现思路就相当于 Android 提供的 HandlerThread 类

        inner class TestThread : Thread("Thread_1") {
    
            override fun run() {
                Looper.prepare()
                val looper = Looper.myLooper()
                object : Thread("Thread_2") {
                    override fun run() {
                        val handler = Handler(looper!!)
                        handler.post {
                            //输出结果是:Thread_1
                            Toast.makeText(
                                this@MainActivity,
                                Thread.currentThread().name,
                                Toast.LENGTH_SHORT
                            ).show()
                        }
                    }
                }.start()
                Looper.loop()
            }
    
        }
    

    16、如何判断当前是不是主线程

    通过 Looper 来判断

            if (Looper.myLooper() == Looper.getMainLooper()) {
                //是主线程
            }
    
            if (Looper.getMainLooper().isCurrentThread){
                //是主线程
            }
    
    

    17、如何全局捕获主线程异常

    比较卧槽的一个做法就是通过嵌套 Loop 循环来实现。向主线程 Loop 发送 一个 Runnable,在 Runnable 里死循环执行 Loop 循环,这就会使得主线程消息队列中的所有任务都会被交由该 Runnable 来调用,只要加上 try catch 后就可以捕获主线程的任意异常了,做到主线程永不崩溃

            Handler(Looper.getMainLooper()).post {
                while (true) {
                    try {
                        Looper.loop()
                    } catch (throwable: Throwable) {
                        throwable.printStackTrace()
                        Log.e("TAG", throwable.message ?: "")
                    }
                }
            }
    
    

    Github地址:【https://github.com/733gh/xiongfan】

    相关文章

      网友评论

        本文标题:Handler的十七发面试连环炮,你能抗住吗?

        本文链接:https://www.haomeiwen.com/subject/cgnmwktx.html