安卓 Handler面试(4)拓展面试题

一、Looper 既然是不停的死循环为啥,当前UI未发生anr卡死?

答:线程的死循环与安卓的ANR不是同一个概念。

线程是一段可执行的代码,当代码执行完后,线程生命周期便该终止了,线程退出。

对于安卓的应用主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?

简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。当然并非简单地死循环,无消息处理时线程会休眠。

但这里可能又引发了另一个问题,既然是死循环又如何去处理其他任务呢?

答案就是创建新的线程。ActivityThread #main中thread.attach(false)便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,处理安卓中的各种事件。

Looper不会一直消耗系统资源,当 Looper的 MessageQueue中没有消息时,或者定时消息没到执行时间时,当前持有 Looper的线程就会进入阻塞状态。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死,然后这个 ANR 完全是两个概。

1、ANR拓展

请添加图片描述
(2)哪些场景会造成ANR?

  • Service Timeout:比如前台服务在20s内未执行完成

对于前台服务,则超时为SERVICE_TIMEOUT = 20s
对于后台服务,则超时为SERVICE_BACKGROUND_TIMEOUT = 200s

  • BroadcastQueue Timeout:比如前台广播在10s内未执行完成

对于广播队列有两个: foreground队列和background队列:

对于前台广播,则超时为BROADCAST_FG_TIMEOUT = 10s
对于后台广播,则超时为BROADCAST_BG_TIMEOUT = 60s

  • ContentProvider Timeout:内容提供者,在publish过超时10s
  • InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件(Activity中的事件)。

具体的触发、时间监控都在AMS中。如四大组件的创建超时也会引起ANR,具体可自行查看相应的逻辑。

二、Handler使用过程中会引起内存泄漏你是如何处理的?

1、内存泄漏的原因

非静态内部类是持有外部类的引用的,所以非静态的handler持有Activity的引用。当MessageQueue中有延时、或者未处理的消息时。Activity是不能被及时销毁的。

因为Message中持有handle的引用的。handler持有activity的引用。这样当 handle发送消息后立刻关闭当前的activity,activity得不到销毁。就会造成内存泄漏,内存泄漏多了就会oom。

2、解决方案

(1)静态类+弱引用

静态类不持有外部类的强引用,这一样Handle就不会持有Activity的引用了。弱引用可以保证对象被及时回收,这样可以保证Activity对象被及时回收。

(2)及时移除消息队列中的消息

消息队列中的message还是持有Handler的引用的,所以还要及时清除handle发送的消息,调用removeCallbacksAndMessages清除Message和Runnable。

三、Looper对象与当前线程的绑定使用到了ThreadLocal,说说ThreadLocal的工作原理

1、首先简介下吧

ThreadLocal是一个线程内部的数据存储类,他的作用域范围就是线程内。通过他可以在指定的线程中存储数据,数据存储后也只能在指定的线程中获得。在其他线程中获取不到。

2、使用场景介绍下

我们日常开发中遇到的很少,但是某些特殊场景下还是会碰到的如安卓的Handler源码、ActivityThread、AMS中就有体现。以Handler为例构造handler对象时会通过ThreadLocal来获取当前线程的Looper对象。

(1) 一般来说某些数据的作用域为线程,且不同的线程有不同的数据副本时就可以考虑使用ThreadLocal

(2)复杂逻辑下对象的传递。线程内监听器的使用。如果不使用ThreadLocal的话有两种解决方案:

a、将监听器通过方法参数在函数调用中传递:有局限性,当函数调用栈很深时通过这种方式是不能接受的。
b、将监听器设置为静态供线程内访问:这样是可行的,但是监听器器的数量与线程有关,代码扩展性比较差。以为监听器属于某个线程。所以添加线程同样需要添加一份线程内的监听器。

3、ThreadLocal 为啥要使用,有替代方案吗?

以handler的例子为准,由于Looper是与线程绑定的。如果不使用ThreadLocal的方式那就要提供一个全局(静态)的HashMap。

把Looper与对应的线程绑定。供Handler查找。这一样的话就必须创建一个类似LooperManager的管理类来维护。然而使用ThreadLocal以当前线程为key值,绑定Looper比较方便。少了些维护工作。

3、原理简介

ThreadLocal set方法可以看出 ThreadLocal 是通过ThreadLocalMap 进行存储数据的。具体就是把当前的ThreadLocal作为key,要存的对象作为value存入ThreadLocalMap 中。ThreadLocalMap 中通过Entry数组进行数据存储维护。

而Thread类有个ThreadLocalMap 类型的成员,当ThreadLocal进行set时实际就是调用当前线程的ThreadLocalMap 进行存储数据的。每个线程通过ThreadLocalMap与ThreadLocal绑定。这样就可确保每个线程都有自己的一份ThreadLocal对象。

Threadlocal get方法 首先获取当前线程的ThreadLocalMap对象。从ThreadLocalMap中取值,最终是调用的ThreadLocalMap的getEntry方法,通过当前的threadLocal为key进行查询。

四、安卓系统为啥不允许在子线程中更新UI

安卓的UI访问不是线程安全的(所有操作未加锁),所以在多线程并发访问时就会出现不可预期的结果。

那为什么安卓不在UI操作上加锁呢?这样多线程的操作不就安全了?

加锁有两个弊端,首先会让UI访问的逻辑变得复杂,其次加锁会降低UI访问的效率(其他的线程需要进行等待)所以最简单的方式就是采用单线程模型来处理UI对于开发者只需要在需要时坐下线程切换即可。

HandlerThread

还记得如何让主线程发送消息,子线程处理消息的栗子吗。明白了Handler的原理后你可能心里就知道在子线程中如何做了:

(1)子线程中创建Looper对象
(2)子线程中创建Handler对象重写handleMessage处理消息
(3)子线程中开启loop循环
(4)主线程发送一条消息

emmm,貌似这些东西都需要字节写,如果多个线程都有这样需求那么每个线程都要创建一套相应机制。。。好麻烦、也浪费线程资源。那么有没有更好的替代呢?答案是有的,系统提供了HandlerThread

简介

HandlerThread 继承于 Thread,所以它本质就是个 Thread。

与普通 Thread 的区别在于,它不仅建立了一个线程,并且创建了消息队列,有自己的 Looper,可以让我们在自己的线程中分发和处理消息,并对外提供自己的 Looper 的 get 方法。

HandlerThread 自带 Looper 使它可以通过消息队列来重复使用当前线程,节省系统资源开销。这是它的优点也是缺点,每个任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。

2、源码

/**
 * A {@link Thread} that has a {@link Looper}.
 * The {@link Looper} can then be used to create {@link Handler}s.
 * <p>
 * Note that just like with a regular {@link Thread}, {@link #start()} must still be called.
 */
public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    
    /**
     * Constructs a HandlerThread.
     * @param name
     * @param priority The priority to run the thread at. The value supplied must be from 
     * {@link android.os.Process} and not from java.lang.Thread.
     */
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
    
    /**
     * Call back method that can be explicitly overridden if needed to execute some
     * setup before Looper loops.
     */
    protected void onLooperPrepared() {
    }

    //线程 run方法被写死为特定逻辑
    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper(); //1、创建Looper对象
            notifyAll(); // 通知取 Looper 的线程,此时 Looper 已经创建好了
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();//2、开启loop轮询
        mTid = -1;
    }
    
    /**
     * This method returns the Looper associated with this thread. If this thread not been started
     * or for any reason isAlive() returns false, this method will return null. If this thread
     * has been started, this method will block until the looper has been initialized.  
     * @return The looper.
     */
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

    /**
     * @return a shared {@link Handler} associated with this thread
     * @hide
     */
    @NonNull
    public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }

    /**
     * Quits the handler thread's looper.
     * <p>
     * Causes the handler thread's looper to terminate without processing any
     * more messages in the message queue.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p class="note">
     * Using this method may be unsafe because some messages may not be delivered
     * before the looper terminates.  Consider using {@link #quitSafely} instead to ensure
     * that all pending work is completed in an orderly manner.
     * </p>
     *
     * @return True if the looper looper has been asked to quit or false if the
     * thread had not yet started running.
     *
     * @see #quitSafely
     */
    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }

    /**
     * Quits the handler thread's looper safely.
     * <p>
     * Causes the handler thread's looper to terminate as soon as all remaining messages
     * in the message queue that are already due to be delivered have been handled.
     * Pending delayed messages with due times in the future will not be delivered.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p>
     * If the thread has not been started or has finished (that is if
     * {@link #getLooper} returns null), then false is returned.
     * Otherwise the looper is asked to quit and true is returned.
     * </p>
     *
     * @return True if the looper looper has been asked to quit or false if the
     * thread had not yet started running.
     */
    public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }

    /**
     * Returns the identifier of this thread. See Process.myTid().
     */
    public int getThreadId() {
        return mTid;
    }
}

全部源码如上,十分easy!总结:

  • HandlerThread 是一个自带 Looper 的线程,因此只能作为子线程使用
  • HandlerThread 必须配合 Handler 使用,HandlerThread 线程中具体做什么事,需要在 Handler 的 callback 中进行,因为HandlerThread 的 run 方法被写死了。
  • 子线程的 Handler 与 HandlerThread 关系建立是通过构造子线程的Handler 传入 HandlerThread 的 Looper 。所以在此之前,必须先调用 mHandlerThread.start 让 run 方法跑起来 Looper 才能创建。
3、实战

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val handlerThread = HandlerThread("HandlerThread")
        handlerThread.start()

        // 2、子线程处理消息

        //注意:这里不能直接handleMessage,要用Handler.Callback+handlerThread.looper方式
        // 指明Handler处理消息回调时运行的looper所在线程。
        val childHandler = Handler(handlerThread.looper, object : Handler.Callback {
            @SuppressLint("SetTextI18n")
            override fun handleMessage(msg: Message): Boolean {
                if (msg.what == 0x11) {
                    btn.text = "子线程更新UI"
                }
                return false
            }
        })

        // 1、主线程发送消息
        btn.setOnClickListener {
            val message = Message.obtain()
            message.what = 0x11
            childHandler.sendMessage(message)
        }

    }
}

(1)按钮点击前
在这里插入图片描述
(2)按钮点击后
在这里插入图片描述

这里不仅验证了主线程发送消息子线程处理消息的case,还可以验证安卓中并非主线程才可以更新UI。

The end

参考1

参考2

参考3

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值