享元模式在 Android 系统中的应用

享元模式

享元模式是对象池的一种实现,主打轻量级。它一般用来尽可能减少内存使用量,适用于可能存在大量重复对象的场景,缓存可共享的对象,达到对象共享、避免创建过多对象的效果,从而提升性能,减少内存占用,提高内存利用效率。

使用享元模式可有效地支持大量细粒度对象的复用。

看上去很厉害的样子,其实全是废话,说白了就是对象的复用。

经典实现

假设某个对象个别重要属性是不变的,但是有几个细微属性会不停地发生变化,如果每次都新建对象会浪费内存,这种情况下一般就可以考虑使用享元模式。

在享元模式中,会建立一个对象容器,经典的享元模式,该容器为一个 Map,它的键就是之前说到的不变属性,它的值则是对象本身。

举例来说,多个人同时抢购北京到上海的火车票,起始站和终点站都是固定的,但是即使是同一辆列车上也有不同的席别,有可能是硬座,有可能是软座,有可能是硬卧,还有可能是软卧,相对应的价钱也都是不同的。

public class TickeyFactory{
  static Map<String, Ticket> sTicketMap = new ConcurrentHashMap<String, Ticket>();
  
  public static Ticket getTickey(String fromStation, String toStation){
    String key = fromStation + "-" + toStation;
    if(sTicketMap.contains(key)){
      return sTicketMap.get(key);
    }else{
      return new Ticket(fromStation, toStation);
    }
  }
}

代码很简单,就是使用 ConcurrentHashMap 做了一个对象的缓存。如我们之前说的,重要属性是不变的(起始站和到达站),但是细微属性是变化的(席别和价格)。在这种情况下,我们使用 Map 集合,以不变的属性为 key,以对象为 value,从而实现对象的复用而不用每次都新创建对象,这就是经典享元模式。

Android 源码应用

其实这个都用不到在源码中应用,日常开发中偶尔也会用到,是比较基础的一个对象复用的形式。不过模式也是为了帮助我们解决问题的,源码中应用享元模式或者说享元思路的方式还挺多变的,可以挨个看看学习一下。

  1. LayoutInflater#createView

    如果我们在一个 LinearLayout 中包裹了五个 ImageView,那么在系统渲染布局的时候,并不是粗暴的直接 new ImageView() x 5,而是会应用享元模式,使用 Map 集合对 View 对象进行复用。

    private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
                new HashMap<String, Constructor<? extends View>>();
    
    public final View createView(String name, String prefix, AttributeSet attrs){
      // 根据 name 从构造器 Map 中取数据
      Constructor<? extends View> constructor = sConstructorMap.get(name);
      // 如果不为空,证明之前有缓存;校验一下 ClassLoader,如果不能通过就从 map 中移除
      if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
      }
      Class<? extends View> clazz = null;
      
      // 如果未能从缓存中拿到数据,或者没能通过 classLoader 校验
      // 就重新初始化,使用反射,拿到对应类的构造方法
      if (constructor == null) {
        clazz = mContext.getClassLoader().loadClass(
          prefix != null ? (prefix + name) : name).asSubclass(View.class);
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
      } 
      
      // 利用反射初始化 View 对象并返回
      final View view = constructor.newInstance(args);
      return view;
      
    }
    
  2. Message.obtain()

    这次就是享元思路了,而不是严格的享元模式。

    我们知道整个 Android 系统都是基于消息机制,如果不停地新建 Message 对象,那对虚拟机无疑是个沉重的负担。Google 在设计 Message 对象池的时候,利用 Message 链表的特性,维护了一个可缓存 50 条消息的缓存池。

    private static Message sPool;
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;
    

    所以在 Message 默认构造器的注释里更建议调用者使用 Message.obtain() 方法:

    // the preferred way to get a Message is to call {@link #obtain() Message.obtain()
    public Message() {}
    
    // obtain 静态工厂,第一次会返回一个初始化的对象,之后从缓存池中获取
    public static Message obtain() {
      synchronized (sPoolSync) {
        // 第一次进来 sPool 肯定是空
        if (sPool != null) {
          // 取出 sPool 并赋值给局部变量,最终返回给调用者
          Message m = sPool;
          // 最前面的消息对象已经取出,将 sPool 指向链表的下一条数据
          sPool = m.next;
          // 给要返回的消息进行重置操作,next 无指向,也没有 in-use 标记
          m.next = null;
          m.flags = 0; // clear in-use flag
          // 更新消息池数量
          sPoolSize--;
          return m;
        }
      }
      // 返回一个新创建的对象
      return new Message();
    }
    

    当我们的消息完成处理以后,会在 Looper#loop 方法中调用 Message#recycle 方法,对当前对象进行回收:

    public static void loop() {
      for (;;) {
        msg.target.dispatchMessage(msg);
        msg.recycleUnchecked();
      }
    }
    
    // 回收消息
    void recycleUnchecked() {
      
      // 将除 next 以外的所有属性重置
      // 同时标记消息为可用状态,不可操作,obtain 时才予以重置
      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) {
        // 如果当前消息数还没有达到 50 条
        if (sPoolSize < MAX_POOL_SIZE) {
          // 就将当前消息指向之前的 sPool 对象,当前消息变为消息池的首个对象
          next = sPool;
          // sPool 指向的应该是消息池的首个对象,即当前对象
          sPool = this;
          // 更新消息池数量
          sPoolSize++;
        }
      }
    }
    
    // 之前的 sPool,MessageA 是链表的表头
    MessageA(
      next = MessageB(
         next = MessageC(
         next = null
        )
      )
    );
    
    // 新回收一条消息
    // next = sPool
    MessageNew(
     next = MessageA(
        next = MessageB(
          next = MessageC(
            next = null
          )
        )
     )
    );
    //  sPool = this;
    // 现在 sPool 指向的就是新回收的消息,也就是链表表头了
    
  3. EventBus#FindState

    上面两个主要是系统源码级别的应用,很多第三方库也会有类似的应用。今天我们以 EventBus 为例,看一下它是怎么用另一种形式应用享元思想的。

    EventBus 的源码分析之前已经写过,具体细节就不展开了,直接上主菜。

    我们知道 EventBus 的原理是筛选订阅类中所有 @Subscribe 方法,然后将其构造成一个 @Subscribe 方法参数类型为键,订阅类以及订阅方法组成的新对象为值的 Map 集合,然后根据反射机制在 post 方法调用的时候进行调用对应的订阅方法。

    那么在遍历订阅类方法时,因为有太多类似的数据,EventBus 选择的实现思路正是享元模式。

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
      // 构建 FindState 类对象,存储系列相关的属性
      FindState findState = prepareFindState();
      findState.initForSubscriber(subscriberClass);
      while (findState.clazz != null) {
         ... ...
      }
      return getMethodsAndRelease(findState);
    }
    

    我们先来看一下 prepareFindState() :

    private static final int POOL_SIZE = 4;
    private static final FindState[] FIND_STATE_POOL = new FindState[POOL_SIZE];
    
    private FindState prepareFindState() {
      synchronized (FIND_STATE_POOL) {
        for (int i = 0; i < POOL_SIZE; i++) {
          FindState state = FIND_STATE_POOL[i];
          if (state != null) {
            FIND_STATE_POOL[i] = null;
            return state;
          }
        }
      }
      return new FindState();
    }
    

    经典的享元对象池的应用,这次的实现方式是数组。

    prepareFindState() 方法中,遍历获取 FIND_STATE_POOL 缓存池中的数组,返回第一个不为空的对象使用;如果全部为空,则初始化一个新对象使用。

    然后代码走到最后,返回数据时,需要将 FindState 类中存储的数据取出加工并返回给调用者了,也就是 FindState 对象该回收的时候了:

    private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
      List<SubscriberMethod> methods = new ArrayList<>(findState.subscriberMethods);
      findState.recycle();
      synchronized (FIND_STATE_POOL) {
        for (int i = 0; i < POOL_SIZE; i++) {
          if (FIND_STATE_POOL[i] == null) {
            FIND_STATE_POOL[i] = findState;
            break;
          }
        }
      }
      return methods;
    }
    

    很明显,代码取出 FindState 中存储的集合后,之后的工作都是在操作缓存池。

我们来看一下 recycle 方法:

void recycle() {
  subscriberMethods.clear();
  anyMethodByEventType.clear();
  subscriberClassByMethodKey.clear();
  methodKeyBuilder.setLength(0);
  subscriberClass = null;
  clazz = null;
  skipSuperClasses = false;
  subscriberInfo = null;
}

很简单,就是清空所有数据,方便下一次使用。

synchronized (FIND_STATE_POOL) {
  for (int i = 0; i < POOL_SIZE; i++) {
    if (FIND_STATE_POOL[i] == null) {
      FIND_STATE_POOL[i] = findState;
      break;
    }
  }
}

清空之前使用的 FindState 对象后,再次遍历缓存池,如果发现数组哪个位置没有缓存数据,就把最新的对象缓存到该位置上,等下次调用 prepareFindState 方法时,遍历到某个位置有缓存对象,就会直接使用,而不是再创建新对象了:

private FindState prepareFindState() {
  synchronized (FIND_STATE_POOL) {
    for (int i = 0; i < POOL_SIZE; i++) {
      FindState state = FIND_STATE_POOL[i];
      // 如果哪个位置不为空,就说明是之前缓存下来的数据
      if (state != null) {
        // 把数组对应位置清空,给下一个对象腾出空间
        FIND_STATE_POOL[i] = null;
        // 然后把刚取出的缓存对象返回给调用者
        return state;
      }
    }
  }
  return new FindState();
}

好吧,享元模式,对象的复用,差不多就是这些吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容