Android面试整理 - Handler

Handler的实现原理

答案参考自:

Handler 机制

子线程中能不能直接new一个Handler,为什么主线程可以?主线程的Looper第一次调用loop方法,什么时候,哪个类?

答案参考自:

可以在子线程直接new一个Handler,不过需要在子线程里先调用Looper#preparenew一个Handler后,还需要调用Looper#loop方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
Looper.loop();
}
});

有人会问,在主线程中为什么没看到Looper.prepare()?其实系统已经给我们调用了,不过调用的是Looper.prepareMainLooper(),如下源码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SystemServer 类
/**
* The main entry point from zygote.
*/
public static void main(String[] args) {
new SystemServer().run();
}


private void run() {
// ...
Looper.prepareMainLooper();
// ...
}

main方法是整个android应用的入口,在子线程中调用Looper.prepare()是为了创建一个Looper对象,并将该对象存储在当前线程的ThreadLocal中。

每个线程都会有一个ThreadLocal,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,并且这种变量只在本线程的生命周期内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度。Looper.loop()方法是为了取出消息队列中的消息并将消息发送给指定的handler,通过msg.target.dispatchMassage()方法。

Handler导致的内存泄露原因及其解决方案

当不再需要某个实例后,这个对象却仍然被引用,阻止被垃圾回收(Prevent from being bargage collected),这个情况就叫做内存泄露(Memory Leak)。

考虑以下的代码;

1
2
3
4
5
6
7
8
public class MainActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
}

虽然不明显,但是这段代码可能导致内存泄露。Android Lint会提示以下信息:

1
In Android, Handler classes should be static or leaks might occur.

它到底是如何泄露的呢?

  1. 当一个Android应用程序启动的时候,Android框架为这个程序的主线程即UI线程创建了一个Looper对象,用于处理Handler中的Message
    Looper实现了一个简单的消息队列MessageQueue,不断循环的处理其中的message
    所有的应用程序框架的事件(比如Activity生命周期的调用,按钮的点击等)都被封装在这个Message对象里,然后被加入到LooperMessageQueue,最后一个一个的处理这些Message
    注意,Looper在整个应用程序的生命周期中一直存在。

  2. 在主线程中实例化一个Handler对象的时候,就和它关联了主线程Looper的消息队列MessageQueue
    被发送到这个消息队列的Message将保持对这个Handler对象的引用,这样框架就可以在处理这个Message的时候调用Handler.handleMessage(Message)来处理消息了。
    (也就是说,只要没有处理到这个MessageHandler就一直在队列中被引用)。

  3. Java中,非静态内部类和匿名内部类都隐式的保持了一个对外部类outerclass的引用。但是静态内部类不会有这个引用。

正确的解决方法:

  1. Handler静态内部类 + WeakReference<Activity>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends Activity {

// 静态内部类不会持有外部类的信用
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivity;

public MyHandler(MainActivity activity) {
mActivity = new WeakReference<MainActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}

private final MyHandler mHandler = new MyHandler(this);
}
  1. 静态Runnable,避免对外部类的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends Activity {

// 匿名类用static修饰后,不会再持有外部类的引用
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() {
// TODO
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}

静态内部类和非静态内部类的区别很微小,但是开发人员必须了解。

那么底线是什么?

当内部类可以独立于Activity的生命周期而存在的时候,应该避免使用非静态内部类,应该用静态内部类并且使用WeakReference保持对Activity的引用。

深入理解

像下面这样使用handler的时候,其实是将handler定义为了匿名内部类:

1
2
3
4
5
6
7
8
public class MainActivity extends Activity {
private Handler mLeakyHandler=new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
}

匿名内部类会默认持有外部类(MainActivity)的引用

学过handler的都知道,handler发送消息后,消息会进行入队操作,在enqueueMessage方法中:

1
msg.target = this;

this指的就是handler,所以handlermessage持有了,而message放入消息队列后,message又被MessageQueue持有了,而MessageQueue是在创建Looper的时候生成的:

1
2
3
4
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
}

所以MessageQueue又被looper所持有。如果这个handler是主线程的handler,那么此时的looper就是指的主线程的Looper,它的声明如下:

1
private static Looper sMainLooper;

可以看到主线程的looperstatic静态变量,而static静态变量在垃圾回收的时候是会被当做GC Root的,静态变量的生命周期与APP的生命周期、与虚拟机的生命周期是一样的,所以正是因为这个持有链的存在,导致了内存泄露。

引用链大致如下:

(static) Looper -->|持有| MessageQueue -->|持有| Message -->|持有| Handler -->|持有| MainActivity

所以解决 handler 内存泄露的办法就是要破坏这个持有链,比如只要 handler 不被 activity 持有就可以,所以可以把 handler 定义为static,因为 static 不会持有外部类,这样 handler 就不会持有 activity 了。

怎样判断一个内部类有没有被持有外部类?

比如上面的handler定义,没有加static的时候,在handleMessage方法里面可以正常使用MainActivity.this,这说明它持有了外部类。而一旦Handler加上static关键字,在handleMessage方法内部就不能再使用MainActivity.this,说明它没有持有外部类。

(为什么static变量,不会造成内存泄露?static 不会去持有外部类)

MessageQueue是什么数据结构

MessageQueue 是一个基于时间排序的优先队列

Message对象创建的方式有哪些 & 区别?

创建Message对象的时候,有三种方式,分别为:

  1. Message msg = new Message();
  2. Message msg = Message.obtain();
  3. Message msg = handler.obtainMessage();

分析

  1. Message msg = new Message();

    这种就是直接初始化一个Message对象,没有什么特别的。

  2. Message msg = Message.obtain();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * Return a new Message instance from the global pool. Allows us to
    * avoid allocating new objects in many cases.
    */
    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();
    }

    从注释可以得知,从整个Message池中返回一个新的Message实例,通过obtainMessage能避免重复Message创建对象。

  3. Message msg = handler1.obtainMessage();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public final Message obtainMessage() {
    return Message.obtain(this);
    }

    public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;
    return m;
    }

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

    可以看到,第二种跟第三种其实是一样的,都可以避免重复创建Message对象,所以建议用第二种或者第三种任何一个创建Message对象。

Message.obtain()怎么维护消息池的?

使用了享元设计模式,当前message执行完后,把message置为空,然后重新给message进行赋值。 通过链表的形式,进行了复用和回收

Handler 有哪些发送消息的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//版本1
Message message = new Message();
message.what = 0;
message.obj = "hello";
mHandler.sendMessage(message);

//版本2
Message message = mHandler.obtainMessage();
message.what = 0;
message.obj = "hello";
mHandler.sendMessage(message);

//版本3
mHandler.obtainMessage(0,"hello").sendToTarget();

其中,版本3 使用到的 sendToTarget 方法只适用于有target值的Message

1
2
3
4
5
6
7
/**
* Sends this Message to the Handler specified by {@link #getTarget}.
* Throws a null pointer exception if this field has not been set.
*/
public void sendToTarget() {
target.sendMessage(this);
}

HandlerpostsendMessage的区别和应用场景

答案参考自:

子线程能不能更新UI

答案参考自:

极端情况下是可以的。

  1. 更新UI后会立即通过 ViewRootImpl 类执行里面的 performTraversal 方法。

  2. performTraversal 方法前,还会先执行一个 checkThread 方法。如果监测到当前的线程不是主线程,就会抛出异常。

    1
    2
    3
    4
    5
    6
    void checkThread() {
    if (mThread != Thread.currentThread()) {
    throw new CalledFromWrongThreadException(
    "Only the original thread that created a view hierarchy can touch its views.");
    }
    }
  3. 通过源码可以发现, ViewRootImpl 类的创建是在回调了 onResume 方法之后。所以我们在 onCreate 方法中通过子线程立即更新UI时,由于该类并没有创建,所以无法检测当前线程是否为主线程,所以程序没有崩溃一样能跑起来,如果修改了程序,让线程休眠了200毫秒后,程序就崩了。很明显200毫秒后ViewRootImpl已经创建了,可以执行checkThread方法检查当前线程。

为什么Android系统不建议子线程访问UI

Android的UI访问是没有加锁的,这样在多个线程访问UI是不安全的。所以Android中规定只能在UI线程中访问UI。

postDelay后消息队列有什么变化,假设先 postDelay 10s, 再postDelay 1s, 怎么处理这2条消息

答案参考自:

如果队列中只有这个消息,那么消息不会被发送,而是计算到时唤醒的时间,先将Looper阻塞,到时间就唤醒它。

但如果此时要加入新消息,该消息队列的对头跟delay时间相比更长,则插入到头部,按照触发时间进行排序,队头的时间最小、队尾的时间最大。(消息队列为优先队列)

MessageQueueenqueueMessage()方法如何进行线程同步的

通过源码可以发现,enqueueMessage 方法中,通过了 synchronized 关键字对 MessageQueue 进行了上锁的处理。保证了线程的同步。

ThreadLocalHandler机制中的作用

ThreadLocal 更多细节:

Threadlocal内部是一个Map实现,以当前线程Threadlocal为键,以Looper为值进行绑定,**保证一个线程对应一个Looper**。

Activity有多个Handler的时候,怎么样区分当前消息由哪个Handler处理

Looper#loop中,Loopermessage直接交给了target即发送这个消息的handler处理。

Handler 如何与 Looper 关联的

通过 ThreadLocal 进行关联。

Looper 如何与 Thread 关联的

通过 ThreadLocal 进行关联。

通过Handler如何实现线程的切换

实际线程间切换,就是通过线程间共享变量实现的。

在A线程new handler(),在b线程调用这个handler发送消息,这个message发送到了,A线程中的 messageQueue里面,又回到了a线程中执行。

Android中为什么主线程不会因为Looper#loop里的死循环卡死?

主线程确实是阻塞的,不阻塞那APP怎么能一直运行?

所以说主线程阻塞是一个伪命题,只不过是没有弄明白既然阻塞了,为什么还能调用各种声明周期而已。

调用生命周期是因为有Looper,有MessageQueue,还有沟通的桥梁Handler,通过IPC机制调用Handler发送各种消息,保存到MessageQueue中,然后在主线程中的Looper提取了消息,并在主线程中调用Handler的方法去处理消息.最终完成各种声明周期。

MessageQueue#next 在没有消息的时候会阻塞,如何恢复?

MessageQueue#enqueueMessage 时会唤醒 MessageQueue,这个方法会被 Handler#sendMessageHandler#post 等一系列发送消息的方法调用。


Android面试整理 - Handler
https://luoyuy.top/posts/c2b533c1e2de/
作者
LuoYu-Ying
发布于
2023年3月7日
许可协议