补:《Android面试题思考与解答》2021年3月刊(三)

简介: 回来啦,《Android面试题思考与解答21年3月刊》送给大家。

MessageQueue没有消息时候会怎样?阻塞之后怎么唤醒呢?说说pipe/epoll机制?


接着上文的逻辑,当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制


epoll机制是一种IO多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒。


  • 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过epoll机制进入阻塞状态。
  • 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程。


那什么时候会怎么唤醒消息队列线程呢?


还记得刚才插入消息的enqueueMessage方法中有个needWake字段吗,很明显,这个就是表示是否唤醒的字段。


其中还有个字段是mBlocked,看字面意思是阻塞的意思,去代码里面找找:


Message next() {
        for (;;) {
            synchronized (this) {
                if (msg != null) {
                    if (now < msg.when) {
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        return msg;
                    }
                } 
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
            }
        }
    }


在获取消息的方法next中,有两个地方对mBlocked赋值:


  • 当获取到消息的时候,mBlocked赋值为false,表示不阻塞。
  • 当没有消息要处理,也没有idleHandler要处理的时候,mBlocked赋值为true,表示阻塞。


好了,确实这个字段就表示是否阻塞的意思,再去看看enqueueMessage方法中,唤醒机制:


boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                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; 
                prev.next = msg;
            }
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }


  • 当链表为空或者时间小于表头消息时间,那么就插入表头,并且设置是否唤醒为mBlocked


再结合上述的例子,也就是当有新消息要插入表头了,这时候如果之前是阻塞状态(mBlocked=true),那么就要唤醒线程了。


  • 否则,就需要取链表中找到某个节点并插入消息,在这之前需要赋值needWake = mBlocked && p.target == null && msg.isAsynchronous()


也就是在插入消息之前,需要判断是否阻塞,并且表头是不是屏障消息,并且当前消息是不是异步消息。也就是如果现在是同步屏障模式下,那么要插入的消息又刚好是异步消息,那就不用管插入消息问题了,直接唤醒线程,因为异步消息需要先执行。


  • 最后一点,是在循环里,如果发现之前就存在异步消息,那就还是设置是否唤醒为false


意思就是,如果之前有异步消息了,那肯定之前就唤醒过了,这时候就不需要再次唤醒了。


最后根据needWake的值,决定是否调用nativeWake方法唤醒next()方法。


同步屏障和异步消息是怎么实现的?


其实在Handler机制中,有三种消息类型:


  • 同步消息。也就是普通的消息。
  • 异步消息。通过setAsynchronous(true)设置的消息。
  • 同步屏障消息。通过postSyncBarrier方法添加的消息,特点是target为空,也就是没有对应的handler。


这三者之间的关系如何呢?


  • 正常情况下,同步消息和异步消息都是正常被处理,也就是根据时间when来取消息,处理消息。
  • 当遇到同步屏障消息的时候,就开始从消息队列里面去找异步消息,找到了再根据时间决定阻塞还是返回消息。


也就是说同步屏障消息不会被返回,他只是一个标志,一个工具,遇到它就代表要去先行处理异步消息了。


所以同步屏障和异步消息的存在的意义就在于有些消息需要“加急处理”


同步屏障和异步消息有具体的使用场景吗?


使用场景就很多了,比如绘制方法scheduleTraversals


void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 同步屏障,阻塞所有的同步消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 通过 Choreographer 发送绘制任务
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    msg.arg1 = callbackType;
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, dueTime);


在该方法中加入了同步屏障,后续加入一个异步消息MSG_DO_SCHEDULE_CALLBACK,最后会执行到FrameDisplayEventReceiver,用于申请VSYNC信号。


更多Choreographer相关内容可以看看这篇文章——https://www.jianshu.com/p/86d00bbdaf60


Message消息被分发之后会怎么处理?消息怎么复用的?


再看看loop方法,在消息被分发之后,也就是执行了dispatchMessage方法之后,还偷偷做了一个操作——recycleUnchecked


public static void loop() {
        for (;;) {
            Message msg = queue.next(); // might block
            try {
                msg.target.dispatchMessage(msg);
            } 
            msg.recycleUnchecked();
        }
    }
//Message.java
    private static Message sPool;
    private static final int MAX_POOL_SIZE = 50;
    void recycleUnchecked() {
        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++;
            }
        }
    }


recycleUnchecked方法中,释放了所有资源,然后将当前的空消息插入到sPool表头。


这里的sPool就是一个消息对象池,它也是一个链表结构的消息,最大长度为50。


那么Message又是怎么复用的呢?在Message的实例化方法obtain中:


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();
    }


直接复用消息池sPool中的第一条消息,然后sPool指向下一个节点,消息池数量减一。


Looper是干嘛呢?怎么获取当前线程的Looper?为什么不直接用Map存储线程和对象呢?


在Handler发送消息之后,消息就被存储到MessageQueue中,而Looper就是一个管理消息队列的角色。Looper会从MessageQueue中不断的查找消息,也就是loop方法,并将消息交回给Handler进行处理。


而Looper的获取就是通过ThreadLocal机制:


static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    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));
    }
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }


通过prepare方法创建Looper并且加入到sThreadLocal中,通过myLooper方法从sThreadLocal中获取Looper。


ThreadLocal运行机制?这种机制设计的好处?


下面就具体说说ThreadLocal运行机制。


//ThreadLocal.java
    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();
    }
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }


ThreadLocal类中的get和set方法可以大致看出来,有一个ThreadLocalMap变量,这个变量存储着键值对形式的数据。


  • key为this,也就是当前ThreadLocal变量。
  • value为T,也就是要存储的值。


然后继续看看ThreadLocalMap哪来的,也就是getMap方法:


//ThreadLocal.java
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    //Thread.java
    ThreadLocal.ThreadLocalMap threadLocals = null;


原来这个ThreadLocalMap变量是存储在线程类Thread中的。


所以ThreadLocal的基本机制就搞清楚了:


在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象。


这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样。


挺神奇的是不是,其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样。


这样做有什么好处呢?为什么不直接用Map存储线程和对象呢?


打个比方:


  • ThreadLocal就是老师。
  • Thread就是同学。
  • Looper(需要的值)就是铅笔。


现在老师买了一批铅笔,然后想把这些铅笔发给同学们,怎么发呢?两种办法:


  • 1、老师把每个铅笔上写好每个同学的名字,放到一个大盒子里面去(map),用的时候就让同学们自己来找。


这种做法就是Map里面存储的是同学和铅笔,然后用的时候通过同学来从这个Map里找铅笔。


这种做法就有点像使用一个Map,存储所有的线程和对象,不好的地方就在于会很混乱,每个线程之间有了联系,也容易造成内存泄漏。


  • 2、老师把每个铅笔直接发给每个同学,放到同学的口袋里(map),用的时候每个同学从口袋里面拿出铅笔就可以了。


这种做法就是Map里面存储的是老师和铅笔,然后用的时候老师说一声,同学只需要从口袋里拿出来就行了。


很明显这种做法更科学,这也就是ThreadLocal的做法,因为铅笔本身就是同学自己在用,所以一开始就把铅笔交给同学自己保管是最好的,每个同学之间进行隔离。


还有哪些地方运用到了ThreadLocal机制?


比如:Choreographer。


public final class Choreographer {
    // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
            if (looper == Looper.getMainLooper()) {
                mMainInstance = choreographer;
            }
            return choreographer;
        }
    };
    private static volatile Choreographer mMainInstance;


Choreographer主要是主线程用的,用于配合 VSYNC中断信号。


所以这里使用ThreadLocal更多的意义在于完成线程单例的功能。


可以多次创建Looper吗?


Looper的创建是通过Looper.prepare方法实现的,而在prepare方法中就判断了,当前线程是否存在Looper对象,如果有,就会直接抛出异常:


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));
    }
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }


所以同一个线程,只能创建一个Looper,多次创建会报错。


Looper中的quitAllowed字段是啥?有什么用?


按照字面意思就是是否允许退出,我们看看他都在哪些地方用到了:


void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;
            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }
        }
    }


哦,就是这个quit方法用到了,如果这个字段为false,代表不允许退出,就会报错。


但是这个quit方法又是干嘛的呢?从来没用过呢。还有这个safe又是啥呢?


其实看名字就差不多能了解了,quit方法就是退出消息队列,终止消息循环。


  • 首先设置了mQuitting字段为true。
  • 然后判断是否安全退出,如果安全退出,就执行removeAllFutureMessagesLocked方法,它内部的逻辑是清空所有的延迟消息,之前没处理的非延迟消息还是需要取处理,然后设置非延迟消息的下一个节点为空(p.next=null)。
  • 如果不是安全退出,就执行removeAllMessagesLocked方法,直接清空所有的消息,然后设置消息队列指向空(mMessages = null)


然后看看当调用quit方法之后,消息的发送和处理:


//消息发送
    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;
            }
        }


当调用了quit方法之后,mQuitting为true,消息就发不出去了,会报错。


再看看消息的处理,loop和next方法:


Message next() {
        for (;;) {
            synchronized (this) {
                if (mQuitting) {
                    dispose();
                    return null;
                } 
            }  
        }
    }
    public static void loop() {
        for (;;) {
            Message msg = queue.next();
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
        }
    }


很明显,当mQuitting为true的时候,next方法返回null,那么loop方法中就会退出死循环。


那么这个quit方法一般是什么时候使用呢?


  • 主线程中,一般情况下肯定不能退出,因为退出后主线程就停止了。所以是当APP需要退出的时候,就会调用quit方法,涉及到的消息是EXIT_APPLICATION,大家可以搜索下。
  • 子线程中,如果消息都处理完了,就需要调用quit方法停止消息循环。


Looper.loop方法是死循环,为什么不会卡死(ANR)?


关于这个问题,强烈建议看看Gityuan的回答:https://www.zhihu.com/question/34652589


我大致总结下:


  • 1、主线程本身就是需要一只运行的,因为要处理各个View,界面变化。所以需要这个死循环来保证主线程一直执行下去,不会被退出。
  • 2、真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身。
  • 3、在主线程以外,会有其他的线程来处理接受其他进程的事件,比如Binder线程(ApplicationThread),会接受AMS发送来的事件
  • 4、在收到跨进程消息后,会交给主线程的Hanlder再进行消息分发。所以Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到onCreate方法。
  • 5、当没有消息的时候,会阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗CPU资源。


Message是怎么找到它所属的Handler然后进行分发的?


在loop方法中,找到要处理的Message,然后调用了这么一句代码处理消息:


msg.target.dispatchMessage(msg);


所以是将消息交给了msg.target来处理,那么这个target是啥呢?


找找它的来头:


//Handler
    private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {
        msg.target = this;
        return queue.enqueueMessage(msg, uptimeMillis);
    }


在使用Hanlder发送消息的时候,会设置msg.target = this,所以target就是当初把消息加到消息队列的那个Handler。


Handler 的 post(Runnable) 与 sendMessage 有什么区别


Hanlder中主要的发送消息可以分为两种:


  • post(Runnable)
  • sendMessage


public final boolean post(@NonNull Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }
    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }


通过post的源码可知,其实post和sendMessage的区别就在于:


post方法给Message设置了一个callback


那么这个callback有什么用呢?我们再转到消息处理的方法dispatchMessage中看看:


public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    private static void handleCallback(Message message) {
        message.callback.run();
    }


这段代码可以分为三部分看:


  • 1、如果msg.callback不为空,也就是通过post方法发送消息的时候,会把消息交给这个msg.callback进行处理,然后就没有后续了。
  • 2、如果msg.callback为空,也就是通过sendMessage发送消息的时候,会判断Handler当前的mCallback是否为空,如果不为空就交给Handler.Callback.handleMessage处理。
  • 3、如果mCallback.handleMessage返回true,则无后续了。
  • 4、如果mCallback.handleMessage返回false,则调用handler类重写的handleMessage方法。


所以post(Runnable) 与 sendMessage的区别就在于后续消息的处理方式,是交给msg.callback还是 Handler.Callback或者Handler.handleMessage


Handler.Callback.handleMessage 和 Handler.handleMessage 有什么不一样?为什么这么设计?


接着上面的代码说,这两个处理方法的区别在于Handler.Callback.handleMessage方法是否返回true:


  • 如果为true,则不再执行Handler.handleMessage
  • 如果为false,则两个方法都要执行。


那么什么时候有Callback,什么时候没有呢?这涉及到两种Hanlder的 创建方式:


val handler1= object : Handler(){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
        }
    }
    val handler2 = Handler(object : Handler.Callback {
        override fun handleMessage(msg: Message): Boolean {
            return true
        }
    })


常用的方法就是第1种,派生一个Handler的子类并重写handleMessage方法。而第2种就是系统给我们提供了一种不需要派生子类的使用方法,只需要传入一个Callback即可。


Handler、Looper、MessageQueue、线程是一一对应关系吗?


  • 一个线程只会有一个Looper对象,所以线程和Looper是一一对应的。
  • MessageQueue对象是在new Looper的时候创建的,所以Looper和MessageQueue是一一对应的。
  • Handler的作用只是将消息加到MessageQueue中,并后续取出消息后,根据消息的target字段分发给当初的那个handler,所以Handler对于Looper是可以多对一的,也就是多个Hanlder对象都可以用同一个线程、同一个Looper、同一个MessageQueue。


总结:Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的。


目录
相关文章
|
1月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
67 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
28天前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
26 8
|
26天前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
14 1
|
29天前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
32 3
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
1月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
35 2
|
1月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
27 1
|
27天前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
27 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
14 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
15 0