Android 对象共享,避免创建多对象 —— 享元模式

1. 享元模式介绍

享元模式是对象池的一种实现,它的英文名称叫做 Flyweight,代表轻量级的意思。享元模式用来尽可能减少内存使用量,它适合用于可能存在大量重复对象的场景,来缓存可共享的对象,达到对象共享、避免创建过多对象的效果,这样一来就可以提升性能、避免内存移除等。

享元对象中的部分状态是可以共享,可以共享的状态称为内部状态,内部状态不会随着环境变化;不可共享的状态则称为外部状态,它们会随着环境的改变而改变。在享元模式中会建立一个对象容器,在经典的享元模式中该容器为一个 Map,它的键是享元对象的内部状态,它的值就是享元对象本身。客户端程序通过这个内部状态从享元工厂中获取享元对象,如果有缓存则使用缓存对象,否则创建一个享元对象并且存入容器中,这样一来就避免了创建过多对象的问题。

2. 享元模式的定义

使用共享对象可有效地支持大量的细粒度的对象。

3. 享元模式的使用场景

(1)系统中存在大量的相似对象。

(2)细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。

(3)需要缓冲池的场景。

4. 享元模式的 UML 类图

角色介绍。

Flyweight: 享元对象抽象基类或者接口。

ConcreateFlyweight: 具体的享元对象。

FlyweightFactory: 享元工厂,负责管理享元对象池和创建享元对象。

5. 享元模式的简单示例

过年回家买火车票是一件很困难的事,无数人用刷票软件向服务端发出请求,对于每一个请求服务器都必须做出应答。在用户设置好出发地和目的地之后,每次请求都返回一个查询的车票结果。为了便于理解,我们假设每次返回的只有一趟列车的车票。那么当数以万计的人不问断在请求数据时,如果每次都重新创建一个查询的车票结果,那么必然会造成大量重复对象的创建、销毁,使得 GC 任务繁重、内存占用率高居不下。而这类问题通过享元模式就能够得到很好地改善,从城市 A 到城市 B 的车辆是有限的,车上的铺位也就是硬卧、硬卧、坐票 3 种。我们将这些可以公用的对象缓存起来,在用户查询时优先使用缓存,如果没有缓存则重新创建。这样就将成千上万的对象变为了可选择的有限数量。

首先我们创建一个 Ticket 接口,该接口定义展示车票信息的函数,具体代码如下。

java 复制代码
public interface Ticket {
    public void showTicketInfo(String bunk);
}

它的一个具体的实现类是 TrainTicket 类,具体代码如下。

java 复制代码
class TrainTicket implements Ticket {
    public String from; // 始发地
    public String to; // 目的地
    public String bunk; // 铺位
    public int price;

    TrainTicket(String from, String to) {
        this.from = from;
        this.to = to;
    }

    @Override
    public void showTicketInfo(String bunk) {
        price = new Random().nextInt(300);
        System.out.println("购买 从 " + from + " 到 " + to + "的 "
                + bunk + " 火车票" + ", 价格 : " + price);
    }

}

数据库中表示火车票的信息有出发地、目的地、铺位、价格等字段,在购票用户每次查询时如果没有用某种缓存模式,那么返回车票数据的接口实现如下。

java 复制代码
public class TicketFactory {
    public static Ticket getTicket(String from, String to) {
            return new TrainTicket(from, to);
    }
}

在 TicketFactory 的 getTicket 函数中每次会 new 一个 TrainTicket 对象,也就是说如果在短时间内有 10000 万用户求购北京到青岛的车票,那么北京到青岛的车票对象就会被创建 10000 次,当数据返回之后这些对象变得无用了又会被虛拟机回收。此时就会造成大量的重复对象存在内存中,GC 对这些对象的回收也会非常消耗资源。如果用户的请求量很大可能导致系统变得极其缓慢,甚至可能导致 OOM。正如上文所说,享元模式通过消息池的形式有效地减少了重复对象的存在。它通过内部状态标识某个种类的对象,外部程序根据这个不会变化的内部状态从消息池中取出对象。使得同一类对象可以被复用,避免大量重复对象。

使用享元模式很简单,只需要简单地改造一下 TicketFactory,具体代码如下。

java 复制代码
/**
 * 车票工厂,以出发地和目的地为key缓存车票
 * 
 */
public class TicketFactory {
    static Map<String, Ticket> sTicketMap = new ConcurrentHashMap<String, Ticket>();

    public static Ticket getTicket(String from, String to) {
        String key = from + "-" + to;
        if (sTicketMap.containsKey(key)) {
            System.out.println("使用缓存 ==> " + key);
            return sTicketMap.get(key);
        } else {
            System.out.println("创建对象 ==> " + key);
            Ticket ticket = new TrainTicket(from, to);
            sTicketMap.put(key, ticket);
            return ticket;
        }
    }
}

我们在 TicketFactory 中添加了一个 map 容器,并且以出发地 + "-" + 日的地为键、以车票对象作为值存储车票对象。这个 map 的键就是我们说的内部状态,在这里就是出发地、横杠、目的地拼接起来的字符串,如果没有缓存则创建一个对象,并且将这个对象缓存到 map 中,下次再有这类请求时则直接从缓存中获取。这样即使有 10000 个请求北京到青岛的车票信息,那么出发地是北京、目的地是青岛的车票对象只有一个。这样就从这个对象从 10000 减到了 1 个,避免了大量的内存占用及频繁的 GC 操作。简单实现代码如下。

java 复制代码
public class Test {
    public static void main(String[] args) {
         Ticket ticket01 = TicketFactory.getTicket("北京", "青岛");
         ticket01.showTicketInfo("上铺");
         Ticket ticket02 = TicketFactory.getTicket("北京", "青岛");
         ticket02.showTicketInfo("下铺");
         Ticket ticket03 = TicketFactory.getTicket("北京", "青岛");
         ticket03.showTicketInfo("坐票");
    }
}

运行结果:

java 复制代码
创建对象二=>北京-青岛
购买从北京到青岛的上铺火车票,价格:28

使用缓存==>北京-青岛
购买从北京到青岛的下铺火车票,价格:104

使用缓存==>北京-青岛
购买从北京到青岛的坐票火车票,价格:148

从输出结果可以看到,只有第一次查询车票时创建了一次对象,后续的查询都使用的是消息池中的对象。这其实就是相当于一个对象缓存,避免了对象的重复创建与回收。在这个例子中,内部状态就是出发地和目的地,内部状态不会发生变化;外部状态就是铺位和价格,价格会随着铺位的变化而变化。

在 JDK 中 String 也是类似消息池,我们知道在 Java 中 String 是存在于常量池中。也就是说一个 String 被定义之后它就被缓存到了常量池中,当其他地方要使用同样的字符串时,则直接使用的是缓存,而不会重复创建。例如下面这段代码。

java 复制代码
public class Test {
    public static void main(String[] args) {
        testString();
    }

    private static void testString() {
        String str1 = new String("abc");
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = "ab" + "c";
        // 使用equals只判定字符值
        System.out.println(str1.equals(str2));
        System.out.println(str1.equals(str3));
        System.out.println(str3.equals(str2));

        // 等号判等,判定两个对象是不是同一个地址
        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str3 == str2);
        System.out.println(str4 == str2);
    }
}

输出如下:

java 复制代码
true
true
true
false
false
false
true

在前 3 个通过 equals 函数判定中,由于它们的字符值都相等,因此 3 个判断都为 true,因此,String 的 equals 只根据字符值进行判断。而在后 4 个判断中则使用的是两个等号判断,两个等号判断代表的意思是判定这两个对象是否相等,也就是两个对象指向的内存地址是否相等。由于 str1 和 str3 都是通过 new 构建的,而 str2 则是通过字面值赋值的,因此这 3 个判定都为false,因为它们并不是同一个对象。而 str2 和 str4 都是通过字面值赋值的,也就是直接通过双引号设置的字符串值,因此,最后一个通过 "==" 判定的值为true,也就是说 str2 和 str4 是同一个字符串对象。因为 str4 使用了缓存在常量池中的 str2 对象。这就是享元模式在我们开发中的一个重要案例。

6. Android 源码中的享元模式

在用 Android 开发了一段时间之后,很多读者就应该知道了一个知识点:UI 不能够在子线程中更新。这原本就是一个伪命题,因为并不是 UI 不可以在子线程更新,而是 UI 不可以在不是它的创建线程里进行更新。只是绝大多数情况下 UI 都是从 UI 线程中创建的,因此,在其他线程更新时会抛出异常。在这种情况下,当我们在子线程完成了耗时操作之后,通常会通过一个 Handler 将结果传递给 UI 线程,然后在 UI 线程中更新相关的视图。代码大致如下。

java 复制代码
public class MainActivity extends Activity {
    Handler mHandler = new Handler(Looper.getMainLooper());

    private void doSomething() {
        new Thread() {
            @Override
            public void run() {
                // 耗时操作,得到结果,但不能在这个线程更新 UI
                // 可以通过 Handler 将结果传递到主线程中,并且更新 UI
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        // 在这里可以更新 UI
                    }
                });
            }
        };
    }
}

在 MainActivity 中首先创建了一个 Handler 对象,它的 Looper 就是 UI 线程的 Looper。在子线程执行完耗时操作之后,则通过 Handler 向 UI 线程传递一个 Runnable,即这个 Runnable 执行在 UI 线程中,然后在这个 Runnable 中更新 UI 即可。

那么 Handler、Looper 的工作原理又是什么呢?它们之间是如何协作的?在讲此之前我们还需要了解两个概念,即 Message 和 MessageQueue。其实 Android 应用是事件驱动的,每个事件都会转化为一个系统消息,即 Message。消息中包含了事件相关的信息以及这个消息的处理人 一 Handler。每个进程中都有一个默认的消息队列,也就是我们的 MessageQueue,这个消息队列维护了一个待处理的消息列表,有一个消息循环不断地从这个队列中取出消息、处理消息,这样就使得应用动态地运作起来。它们的运作原理就像工厂的生产线一样,待加工的产品就是 Message,"传送带"就是 MessageQueue,工人们就对应处理事件的 Handler。这么一来 Message 就必然会产生很多对象,因为整个应用都是由事件,也就是 Message 来驱动的,系统需要不断地产生 Message、处理 Message、销毀 Message,难道 Android 没有 iOS 流畅就是这个原因吗?答案显然没有那么简单,重复构建大量的 Message 也不是 Android 的实现方式。那么我们先从 Handler 发送消息开始一步一步学习它的原理。

就用上面的例子来说,我们通过 Handler 传递了一个 Runnable 给 UI 线程。实际上 Runable 会被包装到一个 Message 对象中,然后再投递到 UI 线程的消息队列。我们看看 Handler 的 post(Runnable run) 函数。

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

在 post 函数中会週用 sendMessageDelayed 函数,但在此之前調用了 getPostMessage 将 Runnable 包装到一个 Message 对象中。然后再将这个 Message 对象传递给 sendMessageDelayed 函数,具体代码如下。

java 复制代码
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
java 复制代码
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

sendMessageDelayed 函数最终又调用了 sendMessageAtTime 函数,我们知道,post 消息时是可以延时发布的,因此,有一个 delay 的时间参数。在 sendMessageAtTime 函数中会判断当前 Handler 的消息队列是否为空,如果不为空那么就会将该消息追加到消息队列中。又因为我们的 Handler 在创建时就关联了 UI 线程的 Looper(如果不手动传递 Looper 那么 Handler 持有的 Looper 就是当前线程的 Looper,也就是说在哪个线程创建的 Handler,就是哪个线程的 Looper),Handler 从这个 Looper 中获取消息队列,这样一来 Runnable 就会被放到 UI 线程的消息队列了,因此,我们的 Runnable 在后续的某个时刻就会被执行在UI线程中。

这里我们不需要再深究 Handler、Looper 等角色的运作细节,我们这里关注的是享元模式的运用。在上面的 getPostMessage 中会将 Runnable 包装为一个 Message,在前文没有说过,系统并不会构建大量的 Message 对象,那么它是如何处理的呢?

我们看到在 getPostMessage 中的 Message 对象是从一个 Message.obtain() 函数返回的,并不是使用 new 来实现,如果使用 new 那么就是我们起初猜测的会构建大量的 Message 对象,当然到目前还不能下结论,我们看看 Message.obtain() 的实现。

java 复制代码
/**
 * 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();
}

实现很简单,但是有一个很引人注意的关键词 --- Pool,它的中文意思称为池,难道是我们前文所说的共享对象池?目前我们依然不能确定,但是,此时已经看到了一些重要线索。现在就来看看 obtain 中的 sPoolSync、sPool 里是些什么程序。

java 复制代码
/**
 *
 * Defines a message containing a description and arbitrary data object that can be
 * sent to a {@link Handler}.  This object contains two extra int fields and an
 * extra object field that allow you to not do allocations in many cases.
 *
 * <p class="note">While the constructor of Message is public, the best way to get
 * one of these is to call {@link #obtain Message.obtain()} or one of the
 * {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
 * them from a pool of recycled objects.</p>
 */
public final class Message implements Parcelable {
    ...
    public static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;

    private static final int MAX_POOL_SIZE = 50;
    
    private static boolean gCheckRecycle = true;
    ...
}

首先 Message 文档第一段的意思就是介绍了一下这个 Message 类的字段,以及说明 Message 对象是被发送到 Handler 的,对于我们来说作用不大。第二段的意思是建议我们使用 Message 的 obtain 方法获取 Message 对象,而不是通过 Message 的构造函数,因为 obtain 方法会从被回收的对象池中获取 Message 对象。然后再看看关键的字段,sPoolSync 是一个普通的 Object 对象,它的作用就是用于在获取 Message 对象时进行同步锁。再看 sPool 居然是一个 Message 对象,居然不是我们上面说的消息池之类的东西,既然它命名为 sPool 不可能是有名无实吧,我们再仔细看,发现了这个字段。

java 复制代码
// sometimes we store linked lists of these things
@UnsupportedAppUsage
/*package*/ Message next;

这个字段就在 sPoolSync 上面。一看上面的注释我们就明白了,原来 Message 消息池没有使用 map 这样的容器,使用的是链表。这个 next 就是指向下一个 Message 的。Message 的链表如图所示。

每个 Message 对象都有一个同类型的 next 字段,这个 next 指向的就是下一个可用的 Message,最后一个可用的 Message 的 next 则为空。这样一来,所有可用的 Message 对象就通过 next 串连成一个可用的 Message 池。

那么这些 Message 对象什么时候会被放到链表中呢?我们在 obtain 函数中只看到了从链表中获取,并没有看到存储。如果消息池链表中没有可用对象的时候,obtain 中则是直接返回一个通过 new 创建的 Message 对象,而且并没有存储到链表中。此时,我们再次遇到了难点,暂时找不到相关线索了。此时我们只好回过头再看看 Message 类的说明,发现一个重要的句子。

"which will pull them from a pool of recycled objects.",噢,原来在创建的时候不会把 Message 对象放到池中,在回收(这里的回收并不是指虚拟机回收 Message 对象)该对象时才会将该对象添加到链表中。

我们搜索一番之后果然发现了 Message 类中有类似 Bitmap 那样的 recycle 函数。具体代码如下。

java 复制代码
/**
 * Return a Message instance to the global pool.
 * <p>
 * You MUST NOT touch the Message after calling this function because it has
 * effectively been freed.  It is an error to recycle a message that is currently
 * enqueued or that is in the process of being delivered to a Handler.
 * </p>
 */
public void recycle() {
    // 判断是否该消息还在使用
    if (isInUse()) {
        if (gCheckRecycle) {
            throw new IllegalStateException("This message cannot be recycled because it "
                    + "is still in use.");
        }
        return;
    }
    // 清空状态,并且将消息添加到消息池中
    recycleUnchecked();
}
java 复制代码
/**
 * Recycles a Message that may be in-use.
 * Used internally by the MessageQueue and Looper when disposing of queued Messages.
 */
@UnsupportedAppUsage
void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    // 清空消息状态,设置该消息 in-use flag
    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++;
        }
    }
}

recycle 函数会将一个 Message 对象回收到一个全局的池中,这个池也就是我们上文说的链表。recycle 函数首先判断该消息是否还在使用,如果还在使用则抛出异常,否则调用 recycleUnchecked 函数处理该消息。recycleUnchecked 函数中先清空该消息的各字段,并且将 flags 设置为 FLAG_IN_USE,表明该消息已被使用,这个 flag 在 obtain 函数中会被置为 0,这样根据这个 flag 就能够追踪该消息的状态。然后判断是否要将该消息回收到消息池中,如果池的大小小于 MAX_POOL_SIZE 时,将自身添加到链表的表头。例如,当链表中还没有元素时,将第一个 Message 对象添加到链表中,此时 sPool 为 null,next 指向了 sPool,因此,next 也为 null,然后 sPool 又指向了 this,因此,sPool 就指向了当前这个被回收的对象,并且sPoolSize 加 1。我们把这个被回收的 Message 对象命名为 m1,此时结构图如图所示。 此时如果再插入一个名称为 m2 的 Message 对象,那么 m2 将会被插到表头中,此时 sPool 指向的就是 m2,结构如图所示。

这个对象池的大小默认为 50,因此,如果池大小在小于 50 的情况下,被回收的 Message 就会被插到链表头部。

此时如果池中有元素,当我们调用 obtain 函数时,如果池中有元素就会从池中获取,实际上获取的也是表头元素,也就是这里的 sPool。然后再将 sPool 这个指针后移到下一个元素。具体代码如下。

java 复制代码
/**
 * 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();
}

在 obtain 函数中,首先会声明一个 Message 对象 m,并且让 m 指向 sPool。sPool 实际上指向了 m2,因此,m 实际上指向的也是 m2,这里相当于保存了 m2 这个元素。下一步是 sPool 指向 m2 的下一个元素,也就是 m1。sPool 也完成后移之后此时把 m.next 置空,也就相当于 m2.next 变成了 null。最后就是 m 指向了 m2 元素,m2 的 next 为空,sPool 从原来的表头 m2 指向了下一个元素 m1,最后将对象池的元素减 1,这样 m2 就顺利地脱离了消息池队伍,返回给了调用 obtain 函数的客户端程序。此时结构如图所示。

Message 通过在内部构建一个链表来维护一个被回收的 Message 对象的对象池,当用户调用 obtain 函数时会优先从池中取,如果池中没有可以复用的对象则创建这个新的 Message 对象。这些新创建的 Message 对象在被使用完之后会被回收到这个对象池中,当下次再调用 obtain 函数时,它们就会被复用。这里的 Message 相当于承担了享元模式中 3 个元素的职责,即是 Flyweight 抽象,又是 ConcreteFlyweight 角色,同时又承担了 FlyweightFactory 管理对象池的职责。因为 Android 应用是事件驱动的,因此,如果通过 new 创建 Message 就会创建大量重复的 Message 对象,导致内存占用率高、频繁 GC 等问题,通过享元模式创建一个大小为 50 的消息池,避免了上述问题的产生,使得这些问题迎刃而解。当然,这里的享元模式并不是经典的实现方式,它没有内部、外部状态,集各个职责于一身,甚至它更像是一个对象池,但这些都是很细节问题,我们关注的是灵活运用模式本身来解决问题。至于 Message 对象是否职责过多,既是实体类又是工厂类,这些问题每个人见仁见智,也许你觉得增加一个 MessagePool 来管理 Message 对象的回收、获取工作不会更好,这样也满足了单一职责原则;或者你觉得就这样用就挺好,没有必要增加管理类。这些我们不过多评论,原则上只是提供了一个可借鉴的规则,这个规则很多时候并不是一成不变的,可以根据实际场景进行取舍。规则是使读者避免走向软件大泥潭,灵活运用才是最终的目的所在。

7. 深度拓展

7.1 深入了解 Android 的消息机制

上文我们说到,Message、MessageQueue、Looper、Handler 的工作原理像是工厂的生产线,Looper 就是发动机,MessageQueue 就是传送带,Handler 就是工人,Message 则是待处理的产品。它们的结构图如图所示。前面的章节中我们多次提到 Android 应用程序的入口实际上是 ActivityThread.main 方法,在该方法中首先会创建 Application 和默认启动的 Activity,并且将它们关联在一起。而该应用的 UI 线程的消息循环也是在这个方法中创建的,具体源码如下。

java 复制代码
public static void main(String[] args) {
    ...
    Process.setArgV0("<pre-initialized>");
    // 1. 创建消息循环 Looper,就是 UI 线程的消息队列
    Looper.prepareMainLooper();

    // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
    // It will be in the format "seq=114"
    long startSeq = 0;
    if (args != null) {
        for (int i = args.length - 1; i >= 0; --i) {
            if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                startSeq = Long.parseLong(
                        args[i].substring(PROC_START_SEQ_IDENT.length()));
            }
        }
    }
    // 启动 ActivityThread,这里最终会启动应用程序
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    ...
    Looper.loop();   // 2. 执行消息循环

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

执行 ActivityThread.main 方法后,应用程序就启动了,UI 线程的消息循环也在Looper.1oop() 函数中启动。此后 Looper 会一直从消息队列中取消息,然后处理消息。用户或者系统通过 Handler 不断地往消息队列中添加消息,这些消息不断地被取出、处理、回收,使得应用迅速地运转起来。例如,我们在子线程中执行完耗时操作后通常需要更新 UI,但我们都"知道"不能在子线程中更新 UI。此时最常用的手段就是通过 Handler 将一个消息 post 到 UI 线程中,然后再在 Handler 的 handleMessage 方法中进行处理。但是有一点要注意,如果用在不传递 UI 线程所属的 Looper 的情况下,那么该 Handler 必须在主线程中创建!正确地使用示例如下。

java 复制代码
// 在 UI 线程中创建
MyHandler mHandler = new MyHandler();
// 开启新的线程
new Thread() {
    @Override
    public void run() {
        // 耗时操作
        mHandler.sendEmptyMessage(123);
    }
}.start();

class MyHandler extends Handler {
    @Override
    public void handleMessage(@NonNull Message msg) {
        // 更新 UI
    }
}

为什么必须要这么做呢?

其实每个 Handler 都会关联一个消息队列,消息队列被封装在 Lopper 中,而每个 Looper 又是 ThreadLocal 的,也就是说每个消息队列只会属于一个线程。因此,如果一个 Looper 在线程 A 中创建,那么该 Looper 只能够被线程 A 访问。而 Handler 则是一个消息投递、处理器,它将消息投递给消息队列,然后这些消息在消息队列中被取出,并且执行在关联了该消息队列的线程中。默认情况下,消息队列只有一个,即主线程的消息队列,这个消息队列是在 ActivityThread.main 方法中创建的,也就调用了 Lopper.prepareMainLooper() 方法,创建 Looper 之后,最后会执行 Looper.loop() 来启动消息循环。那么 Handler 是如何关联消息队列以及线程呢?我们还是深入源码来分析,首先看看 Handler 的构造函数。

java 复制代码
public Handler(@Nullable Callback callback, boolean async) {
    // 代码省略
    mLooper = Looper.myLooper(); // 获取 Looper
    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;
}

从 Handler 默认的构造函数中我们可以看到,Handler 会在内部通过 Looper.myLooper() 来获取 Looper 对象,并且与之关联,最重要的就是获取到 Looper 持有的消息队列 mQueue。那么 Looper.myLooper() 又是如何工作的呢?我们继续往下看。

java 复制代码
/**
 * Return the Looper object associated with the current thread.  Returns
 * null if the calling thread is not associated with a Looper.
 */
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. See also: {@link #prepare()}
 *
 * @deprecated The main looper for your application is created by the Android environment,
 *   so you should never need to call this function yourself.
 */
@Deprecated
public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}


/** Initialize the current thread as a looper.
  * This gives you a chance to create handlers that then reference
  * this looper, before actually starting the loop. Be sure to call
  * {@link #loop()} after calling this method, and end it by calling
  * {@link #quit()}.
  */
public static void prepare() {
    prepare(true);
}

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

我们看到 myLooper() 方法是通过 SThreadLocal.get() 来获取的,关于 ThreadLocal 的资料请参考 Java 相关的书籍。那么 Looper 对象叉是什么时候存储在 sThreadLocal 中的呢?有些读者可能看到了,上面给出的代码中给出了一个熟悉的方法 prepareMainLooper(),在这个方法中调用了 prepare() 方法,在 prepare() 方法中创建了一个 Looper 对象,并且将该对象设置给了 sThreadLocal。这样,队列就与线程关联上了。

我们再回到 Handler 中来,Looper 属于某个线程,消息队列存储在 Looper 中,因此,消息队列就通过 Looper 与特定的线程关联上。而 Handler 又与 Looper、消息队列关联,因此,Handler 最终就和线程、线程的消息队列关联上了,通过该 Handler 发送的消息最终就会被执行在这个线程上。这就能解释上面提到的问题了,"在不传递 Looper 参数给 Handler 构造函数的情况下,用更新 UI 的 Handler 为什么必须在 UI 线程中创建?"。就是因为 Handler 要与主线程的消息队列关联上,这样 handleMessage 才会执行在 UI 线程中,更新 UI 才是被允许的。

创建了 Looper 后,会调用 Looper 的 loop 函数,在这个函数中会不断地从消息队列中取出、处理消息,具体源码如下。

java 复制代码
/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
@SuppressWarnings("AndroidFrameworkBinderIdentity")
public static void loop() {
    final Looper me = myLooper(); // 1. 获取 Looper
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    ...

    for (;;) {  // 2. 死循环,即消息循环
        if (!loopOnce(me, ident, thresholdOverride)) {
            return;
        }
    }
}

/**
 * Poll and deliver single message, return true if the outer loop should continue.
 */
@SuppressWarnings("AndroidFrameworkBinderIdentity")
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    Message msg = me.mQueue.next(); // 3. 获取消息 might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;
    }
    ...
    try {
        msg.target.dispatchMessage(msg); // 4. 处理消息
        ...
    } catch (Exception exception) {
        ...
    } finally {
        ...
    }
    ...

    msg.recycleUnchecked(); // 5. 回收消息,也就是我们分析享元模式时提到的将 Message 添加到消息池的操作

    return true;
}

从上述程序可以看到,loop 方法中实质上就是建立一个死循环,然后通过从消息队列中逐个取出消息,最后就是处理消息、回收消息的过程。

在注释 3 处,调用了 MessageQueue 的 next 函数来获取下一条要处理的消息。这个MessageQueue 在 Looper 的构造函数中构建,我们看看 next 函数的核心代码。

java 复制代码
@UnsupportedAppUsage
Message next() {

    // Return here if the message loop has already quit and been disposed.
    // This can happen if the application tries to restart a looper after quit
    // which is not supported.
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // 1. 处理 Native 层的事件
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            // 2. Java 层的消息队列
            Message msg = mMessages;
            ...
            if (msg != null) {
                    // msg 这个消息有延迟,因此做一个延迟处理
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    //  3. 返回消息
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
            ...
        }
    }
}

完整的 next 函数稍微有些复杂,但这里只分析核心的程序。next 函数的基本思路就是从消息队列中依次取出消息,如果这个消息到了执行时间,那么就将这条消息返回给 Looper,并且将消息队列链表的指针后移。这个消息队列链表结构与 Message 中的消息池结构一致,也是通过 Message 的 next 字段将多个 Message 对象串连在一起。但是在从消息队列获取消息之前,还有一个 nativePollOnce 函数的调用,第一个参数为 mPtr,第二个参数为超时时间。这与 Native 层有什么关系?mPtr 又是什么?其实这个 mPtr 可是大有来头,它存储了 Native 层的消息队列对象,也就是说 Native 层还有一个 MesageQueue 类型。mPtr 的初始化是在 MesageQueue 的构造函数中,具体代码如下。

java 复制代码
public final class MessageQueue {
    @UnsupportedAppUsage
    @SuppressWarnings("unused")
    private long mPtr; // used by native code

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }
}

可以看到,mPtr 的值是 nativeInit 函数返回的,该函数在 android_os_MessageQueue.cpp 类中,我们继续跟踪代码。

cpp 复制代码
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    // 1. 构造 NativeMessageQueue
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if (!nativeMessageQueue) {
        jniThrowRuntimeException(env, "Unable to allocate native queue");
        return 0;
    }

    nativeMessageQueue->incStrong(env);
    // 2. 将 NativeMessageQueue 转为一个整型变量
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

我们看到,在 nativeInit 函数中会构造一个 NativeMessageQueue 对象,然后将该对象转为一个整型值,并且返回给 Java 层中,而当 Java 层需要与 Native 层的 MessageQueue 通信时只要把这个 int 值传递给 Native 层,然后 Native 通过 reinterpret_cast 将传递进来的 int 转换为 NativeMessageQueue 指针即可得到这个 NativeMesageQueue 对象指针。首先看看 NativeMesageQueue 类的构造函数。

cpp 复制代码
NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        mLooper = new Looper(false);
        Looper::setForThread(mLooper);
    }
}

代码很简单,就是创建了一个 Native 层的 Looper,然后这个 Looper 设置给了当前线程。也就是说 Java 层的 MessageQueue 和 Looper 在 Native 层也都有,但是,它们功能并不是一一对应的。那么看看 Looper 究竟做了什么,首先看看它的构造函数,代码在 system/core/libutils/Looper.cpp 文件中。

cpp 复制代码
int eventfd(unsigned int initval, int flags) {
  // 1. 创建管道
  // 2. 创建 epoll 文件描述符
  return FDTRACK_CREATE(__eventfd(initval, flags));
}

int eventfd_read(int fd, eventfd_t* value) {
  return (read(fd, value, sizeof(*value)) == sizeof(*value)) ? 0 : -1;
}

int eventfd_write(int fd, eventfd_t value) {
  return (write(fd, &value, sizeof(value)) == sizeof(value)) ? 0 : -1;
}
cpp 复制代码
Looper::Looper(bool allowNonCallbacks)
    : mAllowNonCallbacks(allowNonCallbacks),
      mSendingMessage(false),
      mPolling(false),
      mEpollRebuildRequired(false),
      mNextRequestSeq(WAKE_EVENT_FD_SEQ + 1),
      mResponseIndex(0),
      mNextMessageUptime(LLONG_MAX) {
    mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
    LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));

    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

void Looper::rebuildEpollLocked() {
    // Close old epoll instance if we have one.
    if (mEpollFd >= 0) {
#if DEBUG_CALLBACKS
        ALOGD("%p ~ rebuildEpollLocked - rebuilding epoll set", this);
#endif
        mEpollFd.reset();
    }

    // Allocate the new epoll instance and register the WakeEventFd.
    mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));
    // 设置事件类型和文件描述符
    epoll_event wakeEvent = createEpollEvent(EPOLLIN, WAKE_EVENT_FD_SEQ);
    // 3. 监听事件
    int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &wakeEvent);
    LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s",
                        strerror(errno));

    for (const auto& [seq, request] : mRequests) {
        // 设置事件类型和文件描述符
        epoll_event eventItem = createEpollEvent(request.getEpollEvents(), seq);

        // 3. 监听事件
        int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);
        if (epollResult < 0) {
            ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
                  request.fd, strerror(errno));
        }
    }
}

首先创建了一个管道(pipe),管道本质上就是一个文件,一个管道中含有两个文件描述符,分别对应读和写。一般的使用方式是一个线程通过读文件描述符来读管道的内容,当管道没有内容时,这个线程就会进入等待状态;而另外一个线程通过写文件描述符来向管道中写入内容,写入内容的时候,如果另一端正有线程正在等待管道中的内容,那么这个线程就会被唤醒。这个等待和唤醒的操作是通过 Linux 系统的 epoll 机制。要使用 Linux 系统的 epoll 机制,首先要通过 epollcreate 来创建一个 epoll 专用的文件描述符,即注释 2 的代码。最后通过 epoll_ctl 函数设置监听的事件类型为 EPOLLIN。此时 Native 层的 MessageQueue 和 Looper 就构建完毕了,在底层也通过管道和 epoll 建立了一套消息机制。Native 层构建完毕之后则会返回到 Java 层 Looper 的构造函数,因此,Java 层的 Looper 和 MesageQueue 也构建完毕。

这个过程有点绕,我们总结一下。

(1)首先构造 Java 层的 Looper 对象,Looper 对象又会在构造函数中创建 Java 层的 MessageQueue 对象。

(2)Java 层的 MessageQueue 的构造函数中调用 nativeInit 函数初始化 Native 层的 NativeMessageQueue,NativeMessageQueue 的构造函数又会创建 Native 层的 Looper,并且通过管道和 epoll 建立一套消息机制。

(3)Native 层构建完毕,将 NativeMesageQueue 对象转换为一个整型存储到 Java 层的 MessageQucue 的 mPtr 中。

(4)启动 Java 层的消息循环,不断地读取、处理消息。

这个初始化过程都是在 ActivityThread 的 main 函数中完成的,因此,main 函数运行之后,UI 线程消息循环就启动了,消息循环不断地从消息队列中读取、处理消息,使得系统运转起来。我们继续回到 nativePollOnce 函数本身,每次循环去读消息时都会调用这个函数,我们看看它到底做了什么?代码在 android_os_MessageQueue.cpp 中。

cpp 复制代码
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

首先将传递进来的整型转换为 NativeMessageQueue 指针,这个整型就是在初始化时保存到 mPtr 的数值。然后调用了 NativeMessageQueue 的 pollOnce 函数。具体代码如下。

cpp 复制代码
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    ...
    // 调用 Native 层 Looper 的 pollOnce 函数
    mLooper->pollOnce(timeoutMillis);
    ...
}

这里的代码很简单,调用了 Native 层 Looper 的 pollOnce 函数。Native 层 Looper 类的完整路径是 system/core/libutils/Looper.cpp,pollOnce 函数如下。

cpp 复制代码
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        ...
        result = pollInner(timeoutMillis);
    }
}

该函数的核心在于调用了 pollInner,我们看看 pollInner 的相关实现。

cpp 复制代码
int Looper::pollInner(int timeoutMillis) {
    ...
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    // 1. 从管道中读取事件
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

    // No longer idling.
    mPolling = false;

    // 获取锁 Acquire lock.
    mLock.lock();
    ...
Done: ;

    // Invoke pending message callbacks.
    mNextMessageUptime = LLONG_MAX;
    while (mMessageEnvelopes.size() != 0) {
        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
        const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
        // 判断执行时间
        if (messageEnvelope.uptime <= now) {
            // Remove the envelope from the list.
            // We keep a strong reference to the handler until the call to handleMessage
            // finishes.  Then we drop it so that the handler can be deleted *before*
            // we reacquire our lock.
            { // obtain handler
                sp<MessageHandler> handler = messageEnvelope.handler;
                Message message = messageEnvelope.message;
                mMessageEnvelopes.removeAt(0);
                mSendingMessage = true;
                mLock.unlock();

#if DEBUG_POLL_AND_WAKE || DEBUG_CALLBACKS
                ALOGD("%p ~ pollOnce - sending message: handler=%p, what=%d",
                        this, handler.get(), message.what);
#endif
                // 处理消息
                handler->handleMessage(message);
            } // release handler

            mLock.lock();
            mSendingMessage = false;
            result = POLL_CALLBACK;
        } else {
            // The last message left at the head of the queue determines the next wakeup time.
            mNextMessageUptime = messageEnvelope.uptime;
            break;
        }
    }

    // Release lock.
    mLock.unlock();
    ...
    return result;
}

从 pollInner 的核心代码中看,pollInner 实际上就是从管道中读取事件,并且处理这些事件。这样一来就相当于在 Native 层存在一个独立的消息机制,这些事件存储在管道中,而 Java 层的事件则存储在消息链表中。但这两个层次的事件都在 Java 层的 Looper 消息循环中进行不断地获取、处理等操作,从而实现程序的运转。但需要注意的是,Native 层的 NativeMessageQueue 实际上只是一个代理 NativeLooper 的角色,它没有做什么实际工作,只是把操作转发给 Looper。而 NativeLooper 则扮演了一个 Java 层的 Handler 角色,它能够发送消息、取消息、处理消息。

那么 Android 为什么要有两套消息机制呢?我们知道 Android 是支持纯 Native 开发的,因此,在 Native 层实现一套消息机制是必须的。另外,Android 系统的核心组件也都是运行在 Native 世界,各组件之间也需要通信,这样一来 Native 层的消息机制就变得很重要。在分析了消息循环与消息队列的基本原理之后,最后看看消息处理逻辑。我们看到在 Java 层的 MessageQueue 的 next 函数的第 4 步调用了 msg.target.dispatchMessage(msg) 来处理消息。其中 msg 是 Message 类型,我们看看源码。

java 复制代码
public final class Message implements Parcelable {
    ...
    @UnsupportedAppUsage
    /*package*/ Handler target; // target 处理

    @UnsupportedAppUsage
    /*package*/ Runnable callback; // Runnable 类型的 callback
    ...
}

从源码中可以看到,target 是 Handler 类型。实际上就是转了一圈,通过 Handler 将消息传递给消息队列,消息队列又将消息分发给 Handler 来处理,其实这也是一个典型的命令模式,Message 就是一条命令,Handler 就是处理人,通过命令模式将操作和执行者解轉。我们继续回到 Handler 代码中,消息处理是调用了 Handler 的 dispatchMessage 方法,相关代码如下。

java 复制代码
/**
 * Handler 子类必须实现这个方法来处理消息 Subclasses must implement this to receive messages.
 */
public void handleMessage(@NonNull Message msg) {
}

/**
 * Handle system messages here.
 */
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();
}

从上述程序中可以看到,dispatchMessage 只是一个分发的方法,如果 Runnable 类型的 callback 为空则执行 handlerMessage 来处理消息,该方法为空,我们会将更新 UI 的代码写在该函数中;如果 callback 不为空,则执行 handleCallback 来处理,该方法会调用 callback 的 run 方法。其实这是 Handler 分发的两种类型,比如我们 post(Runnablecallback) 则 callback 就不为空,此时就会执行 Runnable 的 run 函数;当我们使用 Handler 来 sendMessage 时通常不会设置 callback,因此,也就执行 handlerMessage 这个分支。下面我们看看通过 Handler 来 post 一个 Runnable 对象的实现代码。

java 复制代码
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;
}

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

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

从上述程序可以看到,在 post(Runnable r)时,会将 Runable 包装成 Message 对象,并且将 Runnable 对象设置给 Message 对象的 callback 字段,最后会将该 Message 对象插入消息队列。sendMessage 也是类似实现。

java 复制代码
public final boolean sendMessage(@NonNull Message msg) {
    return sendMessageDelayed(msg, 0);
}

因此不管是 post 一个 Runnable 还是 Message,都会调用 sendMessageDelayed(msg,time) 方法,然后该 Message 就会追加到消息队列中,当在消息队列中取出该消息时就会调用 callback 的 run 方法或者 Handler 的 handleMessage 来执行相应的操作。

最后总结一下就是消息通过 Handler 投递到消息队列,这个消息队列在 Handler 关联的 Looper 中,消息循环启动之后会不断地从队列中获取消息,其中消息的处理分为 Native 层和 Java 层,两个层次都有自己的消息机制,Native 层基于管道和 epoll,而 Java 层则是一个普通的链表。获取消息之后会调用消息的 callback 或者分发给对应 Handler 的 handleMessage 函数进行处理,这样就将消息、消息的分发、处理隔离开来,降低各个角色之间的耦合。消息被处理之后会被收回到消息池中便于下次利用,这样整个应用通过不断地执行这个流程就运转起来了。

7.2 子线程中创建 Handler 为何会抛出异常

先给一段程序。

java 复制代码
new Thread() {
    Handler handler = null;

    @Override
    public void run() {
        handler = new Handler();
    }
}.start();

上面的代码有问题吗?

如果你能够发现并且解释上述代码的问题,那么应该说你对 Handler、Looper、Thread 这几个概念已经很了解了。如果你还不太清楚,那么我们一起往下学习。

前面说过,Looper 对象是 ThreadLocal 的,即每个线程都有自己的 Looper,这个 Looper 可以为空。但是,当你要在子线程中创建 Handler 对象时,如果 Looper 为空,那么就会抛出 "Can't create handler inside thread that has not called Looper:prepare()" 异常,为什么会这样呢?我们一起看源码。

java 复制代码
public Handler(@Nullable Callback callback, boolean async) {
    ...
    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;
}

从上述程序中我们可以看到,当 mLooper 对象为空时,抛出了该异常。这是因为该线程中的 Looper 对象还没有创建,因此,sThreadLocal.get() 会返回 null。我们知道 Looper 是使用 ThreadLocal 存储的,也就是说它是和线程关联的,在子线程中没有手动调用 Looper.prepare 之前该线程的 Looper 就为空。因此,解决方法就是在构造 Handler 之前为当前线程设置 Looper 对象,解决方法如下。

java 复制代码
new Thread() {
    Handler handler = null;

    @Override
    public void run() {
        Looper.prepare(); // 1. 创建 Looper,并且会绑定到 ThreadLocal 中
        handler = new Handler();
        Looper.loop(); // 2. 启动消息循环
    }
}.start();

在代码中我们增加了 2 处,第一是通过 Looper.prepare() 来创建 Looper,第二是通过 Looper.1oop() 来启动消息循环。这样该线程就有了自己的 Looper,也就是有了自己的消息队列。如果只创建 Looper,而不启动消息循环,虽然不会抛出异常,但是你通过 handler 来 post 或者 sendMessage 也不会有效,因为虽然消息被追加到消息队列了,但是并没有启动消息循环,也就不会从消息队列中获取消息并且执行!

在应用启动时,会开启一个主线程(UI 线程),并且启动消息循环,应用不停地从该消息队列中取出、处理消息达到程序运行的效果。Looper 对象封装了消息队列,Looper 对象被封装在 ThreadLocal 中,这使得不同线程之间的 Looper 不能被共享。而 Handler 通过与 Looper 对象绑定来实现与执行线程的绑定,handler 会把 Runnable(包装成 Message)或者 Message 对象追加到与线程关联的消息队列中,然后在消息循环中逐个取出消息,并且处理消息。当 Handler 绑定的 Looper 是主线程的 Looper,则该 Handler 可以在 handleMessage 中更新 UI,否则更新 UI 则会抛出异常。

8. 小结

享元模式实现比较简单,但是它的作用在某些场景确实极其重要的。它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但它同时也提高了系统的复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。

享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的。

• 享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。

• 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。

相关推荐
思忖小下10 小时前
梳理你的思路(从OOP到架构设计)_简介设计模式
设计模式·架构·eit
liyinuo201712 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范
aaasssdddd9614 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
T1an-114 小时前
设计模式之【观察者模式】
观察者模式·设计模式
思忖小下16 小时前
梳理你的思路(从OOP到架构设计)_设计模式Factory Method模式
设计模式·工厂方法模式·eit
霁月风17 小时前
设计模式——工厂方法模式
c++·设计模式·工厂方法模式
发飙的蜗牛'19 小时前
23种设计模式
android·java·设计模式
NorthCastle1 天前
设计模式-创建型模式-简单工厂模式详解
设计模式·简单工厂模式
越甲八千1 天前
重拾设计模式-外观模式和适配器模式的异同
设计模式·适配器模式·外观模式
越甲八千1 天前
重拾设计模式--适配器模式
设计模式·适配器模式