oynix

于无声处听惊雷,于无色处见繁花

Android基础之Handler

说到Android的基础,就一定绕不开Handler,以及相关的Looper、Message、MessageQueue等。这篇文就说说它们之间的联系,来加深印象。

太基础的就不说了,那样要说的东西就太多太多了,多少会显得冗杂。

前言

众所周知,一个application启动后,会启动一个线程作为主线程,也就是ActivityThread,所有更新UI的操作都要放到这个线程里进行。除去这个主线程,其他线程均为子线程,在子线程里更新UI会报错:CalledFromWrongThreadException。这是因为在ViewRootImpl中做了检查,只要当前线程不是主线程,就会抛出异常。

那么,Android为什么这么做呢?

可以假想一下,如果不这么设计,允许所有线程更新UI,那么在并发量多的时候:一个TextView,同一时刻,有10个线程想要更新其显示的内容,这就会发生混乱。

如果想要解决这个问题,首先想到的应该就是加个同步锁synchronized,你们都可以更新UI,但是要排队,一个一个来。混乱的问题解决,但是性能很差,同步锁这个东西,不到万不得已的时候我都不会用。

最后,就有了现在的这种模式,单独出一个线程,也就是主线程,用于更新UI,其他线程若想更新UI,那么就向主线程发送一个消息,发送之后继续自己的任务,等到合适的时机,主线程就会根据发来的消息,做出相应的处理。

这就是Android的消息驱动模型。

团队组成

以前我只是知道个大概,为了写清楚这篇文,就去看了对应的源文件。搭配着AndroidStudio,源文件的可读性还是很不错的,如果有时间都可以去看看。

  • Handler
    用来发消息,即Message;同时,Handler也要提供Message的处理方式,也就是需要做哪些操作。
  • Looper
    如同名字一样,是一个循环器。它负责循环读取消息队列,MessageQueue中的消息,如果其中有新的未处理的消息,则就将其取出,并处理。
  • Message
    消息中包含了一个操作需要的一些信息,后面细说。
  • MessageQueue
    消息队列,所有的消息都将会发送到这里

这么说有些抽象,理解起来可能有些困难,打个比方:你去银行存款,到了银行需要先排号,然后等着喊号,排号期间,你可以做些别的事情,比如聊天、打游戏或者看视频,等到轮到你的时候,你就会去柜台,拿出自己装在兜里的钱,然后存到自己的账户上。

这个过程中,你就是一个Handler,排号就相当于发送了一个Message,所有排队中的号就相当于一个MessageQueue,而银行的叫号系统就如同一个Looper。

可以同时有多个Handler,一个Handler也可以发送多个Message。

Looper

  • 线程里本没有Looper,需要手动创建,调用静态方法Looper.prepare()。
  • 一个线程只允许创建一个Looper,Looper里面使用了一个类型为ThreadLocal<Looper>静态变量sThreadLocal,保存了所有线程的Looper
  • 一个线程创建多个Looper时会报错:RuntimeException(“Only one Looper may be created per thread”)
  • 主线程的Looper不需要手动创建,因为ActivityThread在main方法,也就是程序的入口,自动创建
  • Looper里面持有了一个MessageQueue,在构造方法里面将其初始化
  • 调用Looper中的loop方法之后,Looper便会循环从MessageQueue中取消息,没有消息时便会等待
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 只列出了源码中的部分重要代码
    public class Looper {
    static ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>()
    static Looper sMainLooper;
    final MessageQueue mQueue;

    // 构造方法中出实例化MessageQueue
    private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
    }

    // 通过prepare创建Looper,并添加到sThreadLocal中
    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 Looper myLooper() {
    return sThreadLocal.get(); // 获取当前线程的Looper
    }

    public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;
    for (;;) {
    Message msg = queue.next(); // 取出消息
    msg.target.dispatchMessage(msg); // 掉用Handler的方法来处理消息
    }
    }
    }

Handler

  • 在创建Handler时,会从Looper的静态变量sThreadLocal中获取当前线程的Looper,获取不到则报错RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");
  • 除会自动创建Looper的主线程,其他线程需要手动调用Loopr.prepare()进行创建,调用Looper.loop()开始循环取消息
  • 现在推荐的创建Handler的方式,是通过Looper.mainLooper()获取主线程的Looper,然后将其作为构造参数传到Handler的构造方法里
  • 所以,可以同时存在多个Handler发消息更新UI,因为使用的都是同一个Looper,即mainLooper
  • 可以通过Handler发送Message,也可以发送Runnable,在加入到MessageQueue前,Runnable也会被封装成Message,最后Looper统一处理Message
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // 只列出了源码中的部分重要代码
    public class Handler {
    Looper mLooper;
    MessageQueue mQueue;
    Handler.Callback mCallback;

    public Handler(Callback callback, boolean async) {
    mLooper = Looper.myLooper() // 获取当前线程的Looper
    if (mLooper == null) {
    throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLoopr.mQueue;
    mCallback = callback;
    }

    public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    }

    // 在Looper中调用,用来分发消息
    public void dispatchMessage(Message message) {
    if (msg.callback != null) {
    handleCallback(msg); // 是Runnable的时候走到这里
    } else { // 是Message的时候走的这里
    if (mCallback != null) {
    // 这个mCallback就是构造方法里传入的,
    // 如果不重写handleMessage方法,也可以传入一个Callback来处理Message
    if (mCallback.handleMessage(msg)) {
    return;
    }
    }
    handleMessage(msg); // 如果没传入Callback,则调用handleMessage
    }
    }

    // 用来处理Runnable
    private static void handleCallback(Message message) {
    message.callback.run();
    }

    // 用来处理Message
    public void handleMessage(@NonNull Message msg) {
    }
    }

Message

  • Message是个链表结构,其中有个Message类型的成员变量,叫next,靠这个变量来建立起链表结构
  • Message中持有Handler的引用,叫做target,在Handler发送消息时,会把Handler赋值给Message的target变量
  • 在Looper从MessageQueue中取出Message时,便会调用其target的dispatchMessage方法来让Handler处理Message
  • Message一般不手动创建,而是通过Message.obtain()方法,如果Message池子里有可用的实例则会重用,没有则会返回一个new Message
  • 当使用Handler.post Runnable时,会将Runnable封装成Message,将Runnable赋值给其中的callback变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 只列出了源码中的部分重要代码
    public class Message {
    Message next; // 链表,指向下一个Message
    Handler target; // 发送和处理Message的Handler
    Runnable callback; // 发送的Runnable

    // obtain有很多重载方法
    public static Message obtain() { /*省略*/ }
    }

MessageQueue

  • 这就是个消息队列,持有队列头消息的引用
  • 对队列的操作都封装在这个类中,比如增加消息到链表
  • 因为可通过Handler消息发送延迟消息,所以队列中的消息是根据执行时间排队的

ThreadLocal

总觉得说一说这个ThreadLocal才算完整。ThreadLocal是用来存储数据的,带有一个范型的参数,也就是需要存储的数据类型,它能够将数据与线程相互关联,保证在同一个线程取到的数据是同一个。通过get方法获取值,通过set方法设置值。
实现原理就是,每个线程里维护了一个名字为threadLocals,类型是ThreadLocal.ThreadLocalMap的变量,这种类型就是一个定制的Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ThreadLocal<T> {

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 先获取到Thread里的map
if (map != null) // 然后将值set进去
map.set(this, value);
else
createMap(t, value);
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 先获取到Thread里的map
if (map != null) { // 然后从里面get值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
}

// 使用
ThreadLocal tl = new ThreadLocal<String>();
tl.set("something");
String value = tl.get()

Handler使用

当使用Handler发送Runnable时,直接使用Handler.post()方法即可,这个方法有很多重载方法,例如:可发送延迟的消息,也可以发送在某个具体时间执行的消息,等等。
当使用Handler发送Message时,一般通过sendMessage方法发消息,然后重写它的handleMessage方法,来处理对应的消息,同样这个发送方法也有几个重载方法,查看对应的API文档即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class HandlerTest : AppCompatActivity() {

// 创建一个Handler
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
1 -> { textView.text = "message 1" }
2 -> { textView.text = "message 2" }
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

object : Thread() {
override fun run() {
// 在子线程调用,如从网络获取到数据后
val message = Message.obtain()
message.what = 1
handler.sendMessage(message)
}
}.start()
}
}

一般用法就是这样,但是这样编译器会提示一个可能会导致内存溢出的警告,因为在Handler的构造方法里做了检查:

1
2
3
4
5
6
7
8
9
10
11
12
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());
}
}
// 省略了一些代码
}

什么是内存泄漏

简单说,就是当一个实例该被GC回收的时候,却无法被回收,这块无法被回收的内存,就是泄漏的内存。什么意思呢,举个例子,当从一个Activity返回的时候,Activity的实例从任务栈弹出,这个时候这个实例就该被GC回收掉,但是,如果这时还被其他实例引用的话,GC是不会对它回收的。

为何造成内存泄漏

我们来捋一捋,作为Activity的匿名内部类,Handler会持有外部类的引用,这是Java的机制;而Handler会被Message中的target变量引用,Message会被MessageQueue引用,MessageQueue会被Looper引用,而Looper的生命周期是和当前Thread的生命一样长,如果是主线程的话,那么这个Looper就会贯穿应用全程,因为Activity的实例间接被主线程引用,对于有引用的内存,虚拟机宁愿报错OOM,也不会让GC回收。

如何解决泄漏

原因既然清楚了,那么就好解决了,只要避免Handler引用外部类,这里就是Activity,那么就不会泄漏了。一般的方案,就是使用静态内部类,静态内部类对外部类是没有引用的,这也是Java的机制;或者将Handler独立出去,单独放到一个类文件中,使其不再是内部类,这样便不会在引用外部类。

总结

Android通过主线程的消息队列来更新UI,而Handler是这个消息驱动模型中的一员,负责发消息和处理消息,同时这个模型中还有其他几个成员:Message,消息;MessageQueue,消息队列;Looper,从队列去消息的循环器。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2021/08/79d5914b2375/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道