美文网首页
享元模式在 Android 系统中的应用

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

作者: Joseph_L | 来源:发表于2019-12-09 17:25 被阅读0次

    享元模式

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

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

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

    经典实现

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

    在享元模式中,会建立一个对象容器,经典的享元模式,该容器为一个 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();
    }
    

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

    相关文章

      网友评论

          本文标题:享元模式在 Android 系统中的应用

          本文链接:https://www.haomeiwen.com/subject/onhigctx.html