安卓 Handler面试(3)IdleHandler&同步屏障

IdleHandler 知识点

1、read source code

MessageQueue 成员接口

    /**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     *
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

IdleHandler

1、IdleHandler 被定义在 MessageQueue 中,它是一个接口。IdleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。
2、空闲场景:IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么何时出现空闲?
(1)MessageQueue 为空,没有消息。也即链表头节点就是空。此时直接执行IdleHandle的逻辑
(2) MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),还未带执行时间。下调消息执行之前线程阻塞。线程阻塞到之前执行IdleHandle的逻辑。

queueIdle函数

  • 返回值为true代表IdleHandler 一直有效,线程每次idle时都可以收到回调
  • 返回值为false代表IdleHandler 执行完回调后要被清除,只能回调一次。
2、开发中如何使用

获取MessageQuque对象调用addIdleHandler方法添加个接口实现类即可。实现方法中实现自己的逻辑。

      Looper.myQueue().addIdleHandler(object : MessageQueue.IdleHandler {
             override fun queueIdle(): Boolean {
               //  TODO sth.
               return true
             }

         })
3、源码探究
  //MessageQueue.java
  
  private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
  
    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler); 
        }
    }

代码很简单,吧用户创建的IdleHandler 对象添加到IdleHandler集合列表中。那么IdleHandler列表中的每个IdleHandler 是在合适被处理的呢?请看MessageQueue的next取消息的操作。

MessageQueue#next

Message next() {
....
   int pendingIdleHandlerCount = -1; // -1 only during first iteration
   
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);
             ...
         // 1、消息列表有消息可以处理时就return消息,无消息时就走下面的IdleHandler逻辑


                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
//2、无IdleHandler处理直接跳过,进入下次循环
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

//3、有IdleHandler需要处理时,遍历处理。当IdleHandler #queueIdle return false时
// 处理完IdleHandler 后吧IdleHandler remove掉

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
}

处理逻辑很简单,nativePollOnce返回后,无消息可处理时,就在本轮循环中处理IdleHandler。

4、安卓系统源码的应用

内存不足时,强行 GC


ActivityThread.java

    void scheduleGcIdler() {
        if (!mGcIdlerScheduled) {
            mGcIdlerScheduled = true;
            //1、 往主线程消息队列IdleHandler列表中添加了个mGcIdler
            Looper.myQueue().addIdleHandler(mGcIdler);
        }
        mH.removeMessages(H.GC_WHEN_IDLE);
    }

    final class GcIdler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            doGcIfNeeded(); // 主线程第一次idel时调用一次GC(然后remove IdleHandler )
            purgePendingResources();
            return false;
        }
    }
    
    void doGcIfNeeded() {
        doGcIfNeeded("bg");
    }

    void doGcIfNeeded(String reason) {
        mGcIdlerScheduled = false;
        final long now = SystemClock.uptimeMillis();
        //Slog.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime()
        //        + "m now=" + now);
        if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
            //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!");
            BinderInternal.forceGc(reason);
        }
    }
5、适用场景

(1)延迟执行

不太重要的任务,在idel时再进行初始化工作,避免一开始就初始化,拖慢速度。启动优化。

(2)批量任务:在任务密集型,我们只关注结果时可以使用这个。

如每收到一次推送消息就去更新UI,我们可以开个工作线程,吧每条推送消息都封装成idelHandler,然后统一在idel时处理消息更新UI

IdleHandler 常见面试题

1、MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成对使用?

不是必须;IdleHandler.queueIdle() 的返回值,为false时可以移除加入MessageQueue 的 IdleHandler;

2、当 mIdleHanders 一直不为空时,为什么不会进入死循环?

只有在 pendingIdleHandlerCount 为 -1 时,才会尝试执行 mIdleHander; pendingIdlehanderCount 在 next() 中初始时为 -1,执行一遍后被置为 0,所以不会重复执行;

3、是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?

不建议,IdleHandler 的处理时机不可控,如果 MessageQueue 一直有待处理的消息,那么 IdleHander 的执行时机会很靠后。

4、IdleHandler 的 queueIdle() 运行在那个线程?

这是陷进问题,queueIdle() 运行的线程,只和当前 MessageQueue 的 Looper 所在的线程有关;子线程一样可以构造 Looper,并添加 IdleHandler。

5、IdleHandler何时可以使用

IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机。但它执行的时机依赖消息队列的情况, 那么如果 MessageQueue 一直有待执行的消息时,IdleHandler 就一直得不到执行,也就是它的执行时机是不可控的,不适合执行一些对时机要求比较高的任务

同步屏障

1、Handler发送的消息类型由几种
  • 普通消息(Normal):平时我们使用Handler发送的消息。
  • 屏障消息(Barrier):Message的target为空的消息。由于target为空所以这个消息不是用来分发的。主要是给普通消息“添堵”的。
  • 异步消息(Async):与普通消息几乎没啥区别,只是比普通消息多了个标记位。标记位异步消息。
2、为啥需要异步消息与同步屏障

在这里插入图片描述

1、屏障消息的出现就是给普通消息“添堵”的,如图当looper取消息时取到了barrier消息时,发现这里有个屏障消息,有时就会跳过后面所有的普通消息,找出async进行分发。
2、屏障消息前面的普通消息不受影响,只影响其之后的普通消息。
3、屏障消息保证了异步消息的优先级,这样即使异步消息执行时间在普通消息之后也能保证异步消息优先于普通消息执行。

3、屏障消息

(1)添加屏障

//MessageQueue.java

    @hide
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    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++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

1、通过MessageQueue的postSyncBarrier方法可往消息队列插入同步屏障。
2、同步屏障无target属性,也即target为null,可通过判断Message是否为空来判断消息是否为同步屏障
3、同步屏障也是有时间戳的(when),也是按照时间来排序的。
4、同步屏障只影响其后面的普通消息,其前面的普通消息不受影响。
5、消息队列可以插入说个同步屏障的
6、屏障插入消息队列后是不会唤醒线程的。(插入普通消息会唤醒线程)
7、插入同步屏障后会返回一个token,这个token就是一个序列号,标识这个屏障。以后通过这个标识可以remove屏障。
8、这个方法为hide想要使用只能通过反射

使用Handler发送不带target的消息是屏障消息吗?

不是,使用handler发送不带target消息时在MessageQueue#next方法中取消息前就会抛异常。系统做了保护普通消息必须带target属性,系统会自动添加。

Handler.java

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
            
        msg.target = this; // 系统自动添加target值 为当前handler
        
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

MessageQueue#next

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) { // 取消息之前检测
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
        ....
  }

1、所以通过Handler#sendMessage方式添加的消息最终都会给Message设置target属性值。
2、消息入队方式是最用调用MessageQueue#enqueueMessage方法。方法内部会首先检测消息的target属性。
3、通过MessageQueue#postSyncBarrier也可吧消息添加到队列中。并且添加的消息就是同步屏障。

(2)屏障删除

  public void removeSyncBarrier(int token) {
        // Remove a sync barrier token from the queue.
        // If the queue is no longer stalled by a barrier then wake it.
        synchronized (this) {
            Message prev = null;
            Message p = mMessages;
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
            if (p == null) {
                throw new IllegalStateException("The specified message queue synchronization "
                        + " barrier token has not been posted or has already been removed.");
            }
            final boolean needWake;
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            p.recycleUnchecked();

            // If the loop is quitting then it is already awake.
            // We can assume mPtr != 0 when mQuitting is false.
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }

1、添加屏障时不会唤醒线程,删除屏障时可能会唤醒线程。
2、简单举例来说唤醒线程的情况:假如屏障就在消息队列的头部,next 消息到了执行时间,删除屏障时就要唤醒线程执行下条消息。

(3)屏障处理


MessageQueue#next

       if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }

遍历屏障后面的所有消息,查找异步消息。异步消息到时间就执行,不到时间就阻塞等待。

(4)普通消息的添加&线程的唤醒 举例

    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");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            // 情景1、消息插入消息队列头部,如果当前线程休眠就要唤醒他。
            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.
               
                //情景2:如果插入链表中间,且头部为屏障:
                //(1)插入的为普通消息没必要唤醒线程
                //(2)插入的为异步消息,且异步消息为第一条消息。这时需要唤醒线程。
                //(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;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
4、消息队列为空时插入一条屏障消息会触发idelHandler吗?

插入一条屏障消息是不会唤醒线程的。无唤醒操作。

5、如果删除了屏障,消息队列为空了,会触发idelHandler吗?

不会,idelHandler已经被执行过了,不会在被重复触发了。之前消息队列在处理完最后一条消息之前肯定会调用idelHandler。后续的屏障消息删除即使唤醒了线程取消息为空走idelHandler的逻辑会发现无idelHandler处理。

6、如果消息队列只有一个屏障消息,插入一个普通消息会触发idelHandler吗?

答案有可能。idelHandler执行的时机为:

(1)消息队列为空,无消息可取,休眠前触发idelHandler
(2)消息队列不为空,下条消息未到执行时间,需要阻塞等待,阻塞等待前执行idelHandler

所以关键看普通消息执行时间。

7、如果消息队列只有一个屏障消息,插入一个异步消息会触发idelHandler吗?

答案有可能。关键看异步消息触发时间。

8、同步屏障在framework的应用

Android 系统中的 UI 更新相关的消息即为异步消息,需要优先处理。如View界面的更新。

//ViewRootImpl.java

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //开启同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //发送异步消息
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

异步消息


Message.java

   public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }
    
    public boolean isAsynchronous() {
        return (flags & FLAG_ASYNCHRONOUS) != 0;
    }

Handler.java

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) { //mAsynchronous 为true 时设置消息为异步
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

    @UnsupportedAppUsage // 不可外用
    public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

   @hide // 不可外用
   public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        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;
    }
1、那怎么来发送异步消息呢?

(1)Message有setAsynchronous方法可以直接设置为异步消息
(2)使用Handler构造(不能直接用,要反射才能使用)

The end

参考1

参考2

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值