1. 享元形式介绍

享元形式是目标池的一种完结,它的英文名称叫做 Flyweight,代表轻量级的意思。享元形式用来尽或许减少内存运用量,它适合用于或许存在很多重复目标的场景,来缓存可同享的目标,到达目标同享、防止创立过多目标的效果,这样一来就能够提升功用、防止内存移除等。

享元目标中的部分状况是能够同享,能够同享的状况称为内部状况,内部状况不会随着环境改动;不可同享的状况则称为外部状况,它们会随着环境的改动而改动。在享元形式中会树立一个目标容器,在经典的享元形式中该容器为一个 Map,它的键是享元目标的内部状况,它的值便是享元目标自身。客户端程序经过这个内部状况从享元工厂中获取享元目标,假如有缓存则运用缓存目标,不然创立一个享元目标而且存入容器中,这样一来就防止了创立过多目标的问题。

2. 享元形式的界说

运用同享目标可有效地支持很多的细粒度的目标。

3. 享元形式的运用场景

(1)体系中存在很多的相似目标。

(2)细粒度的目标都具备较挨近的外部状况,而且内部状况与环境无关,也便是说目标没有特定身份。

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

4. 享元形式的 UML 类图

人物介绍。

Flyweight: 享元目标笼统基类或许接口。

ConcreateFlyweight: 详细的享元目标。

FlyweightFactory: 享元工厂,担任办理享元目标池和创立享元目标。

Android 目标同享,防止创立多目标 —— 享元形式

5. 享元形式的简略示例

春节回家买火车票是一件很困难的事,无数人用刷票软件向服务端宣布恳求,对于每一个恳求服务器都有必要做出应对。在用户设置好出发地和意图地之后,每次恳求都回来一个查询的车票成果。为了便于理解,咱们假设每次回来的只要一趟列车的车票。那么当数以万计的人不问断在恳求数据时,假如每次都从头创立一个查询的车票成果,那么必然会形成很多重复目标的创立、销毁,使得 GC 使命深重、内存占用率高居不下。而这类问题经过享元形式就能够得到很好地改进,从城市 A 到城市 B 的车辆是有限的,车上的舱位也便是硬卧、硬卧、坐票 3 种。咱们将这些能够共用的目标缓存起来,在用户查询时优先运用缓存,假如没有缓存则从头创立。这样就将不计其数的目标变为了可选择的有限数量。

首要咱们创立一个 Ticket 接口,该接口界说展现车票信息的函数,详细代码如下。

public interface Ticket {
    public void showTicketInfo(String bunk);
}

它的一个详细的完结类是 TrainTicket 类,详细代码如下。

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

数据库中标明火车票的信息有出发地、意图地、舱位、价格等字段,在购票用户每次查询时假如没有用某种缓存形式,那么回来车票数据的接口完结如下。

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,详细代码如下。

/**
 * 车票工厂,以出发地和意图地为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 操作。简略完结代码如下。

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

工作成果:

创立目标二=>北京-青岛
购买从北京到青岛的上铺火车票,价格:28
运用缓存==>北京-青岛
购买从北京到青岛的下铺火车票,价格:104
运用缓存==>北京-青岛
购买从北京到青岛的坐票火车票,价格:148

从输出成果能够看到,只要第一次查询车票时创立了一次目标,后续的查询都运用的是音讯池中的目标。这其实便是相当于一个目标缓存,防止了目标的重复创立与收回。在这个比如中,内部状况便是出发地和意图地,内部状况不会发生改动;外部状况便是舱位和价格,价格会随着舱位的改动而改动。

在 JDK 中 String 也是相似音讯池,咱们知道在 Java 中 String 是存在于常量池中。也便是说一个 String 被界说之后它就被缓存到了常量池中,当其他地方要运用同样的字符串时,则直接运用的是缓存,而不会重复创立。例如下面这段代码。

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

输出如下:

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 线程中更新相关的视图。代码大致如下。

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) 函数。

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 函数中会週用 sendMessageDelayed 函数,但在此之前調用了 getPostMessage 将 Runnable 包装到一个 Message 目标中。然后再将这个 Message 目标传递给 sendMessageDelayed 函数,详细代码如下。

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

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() 的完结。

/**
 * 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 里是些什么程序。

/**
 *
 * 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 不或许是名不副实吧,咱们再仔细看,发现了这个字段。

// sometimes we store linked lists of these things
@UnsupportedAppUsage
/*package*/ Message next;

这个字段就在 sPoolSync 上面。一看上面的注释咱们就明白了,本来 Message 音讯池没有运用 map 这样的容器,运用的是链表。这个 next 便是指向下一个 Message 的。Message 的链表如图所示。

Android 目标同享,防止创立多目标 —— 享元形式

每个 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 函数。详细代码如下。

/**
 * 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();
}
/**
 * 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,结构如图所示。

Android 目标同享,防止创立多目标 —— 享元形式

这个目标池的巨细默许为 50,因而,假如池巨细在小于 50 的情况下,被收回的 Message 就会被插到链表头部。

此刻假如池中有元素,当咱们调用 obtain 函数时,假如池中有元素就会从池中获取,实际上获取的也是表头元素,也便是这儿的 sPool。然后再将 sPool 这个指针后移到下一个元素。详细代码如下。

/**
 * 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 函数的客户端程序。此刻结构如图所示。

Android 目标同享,防止创立多目标 —— 享元形式

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 线程的音讯循环也是在这个办法中创立的,详细源码如下。

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

Android 目标同享,防止创立多目标 —— 享元形式

履行 ActivityThread.main 办法后,运用程序就发动了,UI 线程的音讯循环也在Looper.1oop() 函数中发动。此后 Looper 会一直从音讯行列中取音讯,然后处理音讯。用户或许体系经过 Handler 不断地往音讯行列中增加音讯,这些音讯不断地被取出、处理、收回,使得运用迅速地作业起来。例如,咱们在子线程中履行完耗时操作后通常需求更新 UI,但咱们都“知道”不能在子线程中更新 UI。此刻最常用的手段便是经过 Handler 将一个音讯 post 到 UI 线程中,然后再在 Handler 的 handleMessage 办法中进行处理。可是有一点要留意,假如用在不传递 UI 线程所属的 Looper 的情况下,那么该 Handler 有必要在主线程中创立!正确地运用示例如下。

// 在 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 的结构函数。

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() 又是怎么作业的呢?咱们持续往下看。

/**
 * 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 函数,在这个函数中会不断地从音讯行列中取出、处理音讯,详细源码如下。

/**
 * 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 函数的中心代码。

@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 的结构函数中,详细代码如下。

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 类中,咱们持续跟踪代码。

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 类的结构函数。

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 文件中。

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;
}
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 中。

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 函数。详细代码如下。

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 函数如下。

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

该函数的中心在于调用了 pollInner,咱们看看 pollInner 的相关完结。

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 类型,咱们看看源码。

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 办法,相关代码如下。

/**
 * 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 目标的完结代码。

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 也是相似完结。

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 为何会抛出反常

先给一段程序。

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()” 反常,为什么会这样呢?咱们一同看源码。

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 目标,解决办法如下。

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. 小结

享元形式完结比较简略,可是它的作用在某些场景确实极其重要的。它能够大大减少运用程序创立的目标,降低程序内存的占用,增强程序的功用,但它同时也提高了体系的复杂性,需求分离出外部状况和内部状况,而且外部状况具有固化特性,不应该随内部状况改动而改动,不然导致体系的逻辑混乱。

享元形式的长处在于它大幅度地降低内存中目标的数量。可是,它做到这一点所付出的代价也是很高的。

• 享元形式使得体系愈加复杂。为了使目标能够同享,需求将一些状况外部化,这使得程序的逻辑复杂化。

• 享元形式将享元目标的状况外部化,而读取外部状况使得工作时刻略微变长。