享元模式-对象共享、避免创建多对象

一、享元模式介绍
享元模式(Flyweight-轻量级)是对象池的一种实现。享元模式用来尽可能减少内存使用量,它适用于可能存在大量重复对象的场景,来缓存可共享的对象,达到对象共享、避免创建过多对象的效果,这样一来就可以提升性能,避免内存移除等。
享元对象中的部分状态是可以共享的,可以共享的状态成为内部状态,内部状态不会随着环境变化;不可共享的状态则称之为外部状态,他们会随着环境的改变而改变。在享元模式中会建立一个对象容器,在经典的享元模式中该容器为一个Map,它的键是享元对象的内部状态,它的值就是享元对象本身。客户端程序通过这个内部状态从享元工厂中获取享元对象,如果有缓存则使用缓存对象,否则创建一个享元对象并且存入容器中,这样一来就避免了创建过多对象的问题。

二、享元模式定义
使用共享对象可有效地支持大量的细粒度的对象。

三、享元模式的使用场景
1、系统中存在大量的相似对象。
2、细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
3、需要缓冲池的场景。

四、享元模式的UML类图
(1)UML类图如下:
这里写图片描述
(2)角色介绍:
Flyweight: 享元对象抽象基类或者接口。
ConcreteFlyweight: 具体的享元对象。
FlyweightFactory: 享元工厂,负责管理享元对象池和创建享元对象。

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

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

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

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

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

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

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

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

public class TicketFactory {
    public static Ticket geTicket(String from,String to){
        return new TrainTicket(from, to);
    }
}

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

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

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

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

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

public class Test {
    public static void main(String[] args) {
        Ticket ticket01 = TicketFactory.geTicket("北京", "上海");
        ticket01.showTicktInfo("上铺");
        Ticket ticket02 = TicketFactory.geTicket("北京", "上海");
        ticket02.showTicktInfo("下铺");
        Ticket ticket03 = TicketFactory.geTicket("北京", "上海");
        ticket03.showTicktInfo("坐票");       
    }
}

运行结果:
这里写图片描述
从输出结果可以看到,只有第一次查询车票时创建了一次对象,后续的查询都使用的是消息池中的对象。这其实就是相当于一个对象缓存,避免了对象的重复创建与回收。在这个例子中,内部状态就是出发地和目的地,内部状态不会发生变化;外部状态就是铺位和价格,价格会随着铺位的变化而变化。
在JDK中String也是类似消息池,我们知道在java中String是存在于常量池中。也就是说一个String被定义之后它就是被缓存到了常量池中,当期它的地方要使用同样的字符串时,则直接使用的是缓存,而不是重复创建。例如下面这段代码。

public class testString {
    public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = "ab"+"c";
        //使用equals只判定字符值
        System.out.println(str1.endsWith(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);
    }
}

输出如下:
这里写图片描述

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

六、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

                    }
                });
                //end of run
            }
        };
    }
}

在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 线程。实际上 Runnable 会被包装到一个 Message 对象中,然后再投递到 UI 线程的消息队列。我们看看 Handler 的 post(Runnable run)函数。

    /**
     * Causes the Runnable r to be added to the message queue.
     * The runnable will be run on the thread to which this handler is 
     * attached. 
     *  
     * @param r The Runnable that will be executed.
     * 
     * @return Returns true if the Runnable was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.
     */
    public final boolean post(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 函数, 具体代码如下。

   /**
     * Enqueue a message into the message queue after all pending messages
     * before (current time + delayMillis). You will receive it in
     * {@link #handleMessage}, in the thread attached to this handler.
     *  
     * @return Returns true if the message was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.  Note that a
     *         result of true does not mean the message will be processed -- if
     *         the looper is quit before the delivery time of the message
     *         occurs then the message will be dropped.
     */
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    /**
     * Enqueue a message into the message queue after all pending messages
     * before the absolute time (in milliseconds) <var>uptimeMillis</var>.
     * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
     * Time spent in deep sleep will add an additional delay to execution.
     * You will receive it in {@link #handleMessage}, in the thread attached
     * to this handler.
     * 
     * @param uptimeMillis The absolute time at which the message should be
     *         delivered, using the
     *         {@link android.os.SystemClock#uptimeMillis} time-base.
     *         
     * @return Returns true if the message was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.  Note that a
     *         result of true does not mean the message will be processed -- if
     *         the looper is quit before the delivery time of the message
     *         occurs then the message will be dropped.
     */
    public boolean sendMessageAtTime(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 {
    //字段省略
    private static final Object sPoolSync = new Object();
    private static Message sPool;
    private static int sPoolSize = 0;
    //代码省略
}

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

   // sometimes we store linked lists of these things
    /*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 函数。具体代码如下:

    /**
     * Return a Message instance to the global pool.
     * 将 Message 对象回收到消息池中
     * <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.
     */
    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 = -1;
        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 这个指正后移动下一个元素。具体代码如下。

    /**
     * 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 的消息池,避免了上述问题的产生,使得这些问题迎刃而解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值