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

享元模式

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

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

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

经典实现

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

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

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

public class TickeyFactory{
  static Map sTicketMap = new ConcurrentHashMap();
  
  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> sConstructorMap =
                new HashMap>();
    
    public final View createView(String name, String prefix, AttributeSet attrs){
      // 根据 name 从构造器 Map 中取数据
      Constructor constructor = sConstructorMap.get(name);
      // 如果不为空,证明之前有缓存;校验一下 ClassLoader,如果不能通过就从 map 中移除
      if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;
        sConstructorMap.remove(name);
      }
      Class 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 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 getMethodsAndRelease(FindState findState) {
      List 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();
}

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

你可能感兴趣的:(享元模式在 Android 系统中的应用)