Android设计模式-享元模式

1.享元模式Flyweight

享元模式使用共享对象可有效地支持大量的细粒度的对象。享元模式属于结构型模式。

享元模式常用于系统底层开发,解决系统的性能问题。多用于存在大量重复对象的场景,或需要缓冲池的时候。享元模式是对象池技术的重要实现方式,池里都是创建好的对象,无需再创建可直接拿来使用,这样减少了重复对象的创建,从而降低内存、提升性能。

 

Android设计模式-享元模式_第1张图片

Flyweight:抽象的享元角色,是产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。

ConcreteFlyweight:具体的享元角色,是具体的产品类,实现抽象角色,定义相关业务。

UnsharedConcreteFlyweight:不可共享的角色,一般不会出现在享元工厂。

FlyweightFactory:享元工厂类,用于构建一个池容器(集合),同时提供从池中获取对象的方法。

内部状态:指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变。

外部状态:指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。

 

2.使用举例

比如人们浏览网站时,由于有各种各样的网站,大量的人使用同一个网站时,如果每次都创建新的对象,必然会造成大量重复对象的创建与销毁,此时可将这些可以共用的对象缓存起来,在用户查询时优先使用缓存,如果没有缓存则重新创建。对于某个网站来说,网站的类型type就是内部状态,而众多的使用者User就是外部状态。

Android设计模式-享元模式_第2张图片

 抽象的享元角色(网站):

public abstract class Website {

    public abstract void use(User user);

}

具体的享元角色(网站):

public class ConcreteWebsite extends Website {

    private String type;

    public ConcreteWebsite(String type) {

        this.type = type;

    }

    @Override

    public void use(User user) {

        System.out.println(user.getName() + "正在使用" + type + "网站");

    }

}

不可共享的角色(每个用户):

public class User {

    private String name;

    public User(String name) {

        this.name = name;

    }

    public String getName() {

        return name;

    }

    public void setName(String name) {

        this.name = name;

    }

}

享元工厂类:

public class WebsiteFactory {

    private Map cache = new ConcurrentHashMap<>();

    public Website getWebsite(String type) {

        if (!cache.containsKey(type)) {

           cache.put(type, new ConcreteWebsite( type));

        }

        return cache.get(type);

    }

    public int getCacheSize() {

        return cache.size();

    }

}

调用:

public class Client {

public static void main(String[] args) {

    WebsiteFactory factory = new WebsiteFactory();

    Website news = factory.getWebsite("新闻");

    news.use(new User("Tom"));

    Website blog = factory.getWebsite("博客");

    blog.use(new User("Jack"));

    Website blog2 = factory.getWebsite("博客");

    blog2.use(new User("Smith"));

    Website blog3 = factory.getWebsite("博客");

    blog3.use(new User("Alice"));

    System.out.println(factory.getCacheSize());

}

打印结果如下:

Tom正在使用新闻网站

Jack正在使用博客网站

Smith正在使用博客网站

Alice正在使用博客网站

2

可见,4个人在使用网站,而实际只创建了2个网站对象。这是因为在享元工厂类里,用map来保存各种不同的网站,以key查询,有重复就复用,没有就直接创建,避免了重复对象的大量创建。如果不使用享元模式,每个用户浏览一个网站都要创建一个网站的对象,当用户数据很大时势必会产生大量的内容重复的对象,当这些对象无用后GC回收将会非常耗费资源。

 

3.源码中的使用

Android中的Message、String、Parcel和TypedArray都利用了享元模式。

以Message为例,handler发送message如下:

public final boolean sendEmptyMessageAtTime( int what, long uptimeMillis) {

    Message msg = Message.obtain();

    msg.what = what;

    return sendMessageAtTime(msg, uptimeMillis);

}

发送消息的时候最终调用sendEmptyMessageAtTime,在该方法里通过Message.obtain();创建message并发送。享元模式就是从obtain这里切入。

public final class Message implements Parcelable {

    private static int sPoolSize = 0;

    Message next;

    private static final Object sPoolSync = new Object();

    private static Message sPool;

    private static final int MAX_POOL_SIZE = 10;

    public static Message obtain() {

        synchronized (sPoolSync) {

            if (sPool != null) {

                Message m = sPool; //m等于单链表

                sPool = m.next; //sPool单链表舍弃表头元素

                m.next = null; //m舍弃除表头之外的所有元素

                m.flags = 0; //flag置0标记

                sPoolSize--; //单链表大小减1

                return m;

            }

        }

        return new Message();

    }

}

Message是一个单链表对象。Message中包含一个next的Message对象。sPoolSize表示个数。其中Message通过next成员变量持有对下一个Message的引用,从而构成了一个Message链表。Message Pool就通过该链表的表头管理着所有闲置的Message。

可以看到,当sPool为空时,就new Message返回一个新创建的对象;当sPool不为空时,就取出了单链表的头元素返回,同时单链表sPool舍弃表头,这样就返回了已创建的重复对象,完成了元素的复用。

Message的类图如下:

Android设计模式-享元模式_第3张图片

一个Message在使用完后可以通过recycle()方法进入Message Pool,并在需要时通过obtain静态方法从Message Pool获取。

recycle实现代码如下:

public void recycle() {

    if (isInUse()) {

        if (gCheckRecycle) {

            throw new IllegalStateException("This message cannot be recycled because it is still in use.");

        }

        return;

    }

    recycleUnchecked();

}

void recycleUnchecked() {

    // Mark the message as in use while it remains in the recycled object pool. Clear out all other details.

    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) { //单链表大小还没超过MAX_POOL_SIZE,则开始插入

            next = sPool; //next直接等于自身

            sPool = this; //sPool等于现在插入的元素

            sPoolSize++; //大小+1

        }

    }

}

recycle()方法首先判断Message对象是否正在被使用,如果是则抛出异常,否则开始进行recycleUnchecked单链表插入操作。插入之前先清空了各个参数。

虽然Message并不是最标准的享元模式用法,但它通过单链表方式一样实现了对象池的思想。

 

另外,JDK中的String也是类似的消息池。一个String被定义过后就存在于常量池中。当其他地方使用相同的字符串时,实际使用的是缓存。

String str1 = new String("abc");

String str2 = new String("abc");

String str3 = "abc";

String str4 = "ab" + "c";

string一般有两个判断,一个是equals,这个是比较内容,以上四个都相等。另一个就是“==”判断,这个是比较引用是否相同。

Java中有个String池,这个String池就是利用享元模式,同样的字符串会从String池内获得。比如str3和str4就是从String池中取得的相同的对象。

str1不等于str2,不等于str3。但是,str3是等于str4。因为str1和str2是new的,str3是等号字面赋值,所以它们是不同的对象。而str4也是字面赋值,且str4使用了缓存在缓存池中的str3常量对象。所以用==判断的时候,他们是相同对象。

 

4.总结

享元模式摒弃了在每个对象中保存所有数据的方式,而是通过共享多个对象所共有的相同状态,从而在有限的内存容量中载入更多对象。

为了更清楚地理解享元模式,可以看如下的例子:

比如一款游戏,玩家们在地图上移动并相互射击。这款游戏里大量的子弹、导弹和爆炸弹片会在整个地图上穿行。如果每个粒子(一颗子弹、一枚导弹或一块弹片)都由包含完整数据的独立对象来表示,那么当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了。

仔细观察粒子类,会发现颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。更糟糕的是,对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样(比如所有子弹的颜色和精灵图都一样)。每个粒子的另一些状态(坐标、移动矢量和速度)则是不同的,因为这些成员变量的数值会不断变化,这些数据代表粒子在存续期间不断变化的情景,但每个粒子的颜色和精灵图则会保持不变。

对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。 而对象的其他状态常常能被其他对象 “从外部” 改变,因此被称为外在状态。

享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此所需的对象数量会大大削减。

回到游戏中。假如能从粒子类中抽出外在状态,那么只需三个不同的对象(子弹、导弹和弹片)就能表示游戏中的所有粒子。这样一个仅存储内在状态的对象即称为享元。

那么外在状态会被移动到什么地方呢?总得有类来存储它们。在大部分情况中,它们会被移动到容器对象中,也就是应用享元模式前的聚合对象中。

在这个例子中,容器对象就是主要的游戏Game 对象,它会将所有粒子存储在名为粒子particles 的成员变量中。为了能将外在状态移动到这个类中,需要创建多个数组成员变量来存储每个粒子的坐标、方向矢量和速度。除此之外,还需要另一个数组来存储指向代表粒子的特定享元的引用。这些数组必须保持同步,这样才能够使用同一索引来获取关于某个粒子的所有数据。

更优雅的解决方案是创建独立的情景类来存储外在状态和对享元对象的引用,在该方法中,容器类只需包含一个数组。虽然这样的话情景对象数量会和不采用享元模式时的对象数量一样多,但是这些对象要比之前小很多,消耗内存最多的成员变量已经被移动到很少的几个享元对象中了。现在,一个享元大对象会被上千个情境小对象复用,因此无需再重复存储数千个大对象的数据。

享元与不可变性:

由于享元对象可在不同的情景中使用,就必须确保其状态不能被修改。享元类的状态只能由构造函数的参数进行一次性初始化,它不能对其他对象公开其设置器或公有成员变量。

享元工厂:

为了能更方便地访问各种享元,可以创建一个工厂方法来管理已有享元对象的缓存池。工厂方法从客户端处接收目标享元对象的内在状态作为参数,如果它能在缓存池中找到所需享元, 则将其返回给客户端;如果没有找到,它就会新建一个享元,并将其添加到缓存池中。

注意:享元模式只是一种优化。在应用该模式之前,要确定程序中存在有大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。

享元(Flyweight)类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。

情景(Context)类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。

通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。

客户端(Client)负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。

享元工厂(Flyweight Factory)会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。

享元模式优点:如果程序中有很多相似对象,那么将可以节省大量内存。

缺点:①可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。②代码会变得更加复杂。团队中的新成员总是会问:为什么要像这样拆分一个实体的状态?

你可能感兴趣的:(android)