看看开源库Universal Image Loader中存在的Bug

379702597 8年前
   <p>话说几年前,Universal Image Loader是安卓应用中图片加载的主流开源库,很多千万级的应用中都可以看到它的身影。前段时间抱着学习的态度,把Universal Image Loader的源码下载下来,准备领略一下国际大牛的风采,结果意外的发现了一个Bug,由于项目已经停止维护了,也就没有pull request。最近突然喜欢上了写博客,所以今天就把它的代码贴上了,和大家一起学习一下。(几年前应用搞不好就会OOM,会不会就是因为这个bug啊。不管是不是,大家先来看代码分析吧)</p>    <p>这个bug是内存策略中存在的bug,会引起OOM。我们的分析会涉及以下四个类:</p>    <p><img src="https://simg.open-open.com/show/cc38925b1ed12bbafd974e290ddaff7a.png"></p>    <p>四个类的继承关系,请原谅我画的这么丑陋</p>    <p>其中MemoryCache中,就五个方法,很简单:</p>    <p><img src="https://simg.open-open.com/show/729eba94c542ee5f0dd3b10b99d3874c.png"></p>    <p>MemoryCache.png</p>    <p>在BaseMemoryCache中有成员变量:HashMap,它存储的值是Reference的Bitmap。其实问题就出在这里了,我们继续往下看。大家别着急,我文章最后会把四个类的全部代码贴上来,供大家仔细看。根据他的注释可以看出,要存储一个非强引用的对象,也就是弱引用或软引用。</p>    <p><img src="https://simg.open-open.com/show/c8c6b3c21646307665d680cb5caec799.png"></p>    <p>HashMap</p>    <p>我们再来看看他的put方法,到了往HashMap里放了什么。根据下面代码可以看出是调用了createReference()这个抽象方法,交给子类去实现了。</p>    <p><img src="https://simg.open-open.com/show/63d6e8230b5a87606a72532324e1bb0f.png"></p>    <p>BaceMemoryCache的put方法</p>    <p>BaseMemoryCache的直接子类LimitedMemoryCache也是一个抽象类,没有实现createReference()方法,在他的间接子类UsingFreqLimiteMemory中实现了.可以看出方法中创建了一个Bitmap的弱引用。弱引用就是只要发生GC,对象都有可能被回收。</p>    <p><img src="https://simg.open-open.com/show/3ccd9a5431c1b79ed450bf1f4164f8d4.png"></p>    <p>UsingFreqLimiteMemory的createReference()</p>    <p>重点来啦,我们看UsingFreqLimitedMemory中的remove方法。它掉用的是父类中的get方法,由于直接父类LimitedMemoryCache中没有实现get方法,所以它调用的是BaseMemoryCache中的get方法。</p>    <p><img src="https://simg.open-open.com/show/cf1c7122721f87528063d00965e2e0dc.png"></p>    <p>UsingFreqLimitedMemory</p>    <p>我们来看看BaseMemoryCache中的get方法:</p>    <p><img src="https://simg.open-open.com/show/e1c893a3cff4da941e7cd25af807431a.png"></p>    <p>BaseMemoryCache</p>    <p>get方法就是通过成员变量HashMap获得BitMap。由于存储的值是一个弱引用,所以可能随时被GC。那么它子类就可能拿不到这个Bitmap的引用了,并且两个子类中都有HashMap保存着对应的Bitmap,所以子类中持有的Bitmap就永远无法移除了,直到程序发生OOM。</p>    <p>下面就把全部代码贴上来,大家可以仔细研究一下,共四个类由父类到子类。欢迎大家在下面讨论交流。</p>    <pre>  <code class="language-java">/**   * Interface for memory cache   *   * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)   * @since 1.9.2   */  public interface MemoryCache {      /**       * Puts value into cache by key       *       * @return <b>true</b> - if value was put into cache successfully, <b>false</b> - if value was <b>not</b> put into       * cache       */      boolean put(String key, Bitmap value);        /** Returns value by key. If there is no value for key then null will be returned. */      Bitmap get(String key);        /** Removes item by key */      Bitmap remove(String key);        /** Returns all keys of cache */      Collection<String> keys();        /** Remove all items from cache */      void clear();  }</code></pre>    <pre>  <code class="language-java">/**   * Base memory cache. Implements common functionality for memory cache. Provides object references (   * {@linkplain Reference not strong}) storing.   *   * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)   * @since 1.0.0   */  public abstract class BaseMemoryCache implements MemoryCache {        /** Stores not strong references to objects */      private final Map<String, Reference<Bitmap>> softMap       = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());        @Override      public Bitmap get(String key) {          Bitmap result = null;          Reference<Bitmap> reference = softMap.get(key);          if (reference != null) {              result = reference.get();          }          return result;      }        @Override      public boolean put(String key, Bitmap value) {          softMap.put(key, createReference(value));          return true;      }        @Override      public Bitmap remove(String key) {          Reference<Bitmap> bmpRef = softMap.remove(key);          return bmpRef == null ? null : bmpRef.get();      }        @Override      public Collection<String> keys() {          synchronized (softMap) {              return new HashSet<String>(softMap.keySet());          }      }        @Override      public void clear() {          softMap.clear();      }        /** Creates {@linkplain Reference not strong} reference of value */      protected abstract Reference<Bitmap> createReference(Bitmap value);  }</code></pre>    <pre>  <code class="language-java">/**   * Limited cache. Provides object storing. Size of all stored bitmaps will not to exceed size limit (   * {@link #getSizeLimit()}).<br />   * <br />   * <b>NOTE:</b> This cache uses strong and weak references for stored Bitmaps. Strong references - for limited count of   * Bitmaps (depends on cache size), weak references - for all other cached Bitmaps.   *   * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)   * @see BaseMemoryCache   * @since 1.0.0   */  public abstract class LimitedMemoryCache extends BaseMemoryCache {        private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16;      private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;        private final int sizeLimit;        private final AtomicInteger cacheSize;        /**       * Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed       * limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any       * time)       */      private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());        /** @param sizeLimit Maximum size for cache (in bytes) */      public LimitedMemoryCache(int sizeLimit) {          this.sizeLimit = sizeLimit;          cacheSize = new AtomicInteger();          if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {              L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB);          }      }        @Override      public boolean put(String key, Bitmap value) {          boolean putSuccessfully = false;          // Try to add value to hard cache          int valueSize = getSize(value);          int sizeLimit = getSizeLimit();          int curCacheSize = cacheSize.get();          if (valueSize < sizeLimit) {              while (curCacheSize + valueSize > sizeLimit) {                  Bitmap removedValue = removeNext();                  if (hardCache.remove(removedValue)) {                      curCacheSize = cacheSize.addAndGet(-getSize(removedValue));                  }              }              hardCache.add(value);              cacheSize.addAndGet(valueSize);                putSuccessfully = true;          }          // Add value to soft cache          super.put(key, value);          return putSuccessfully;      }        @Override      public Bitmap remove(String key) {          Bitmap value = super.get(key);          if (value != null) {              if (hardCache.remove(value)) {                  cacheSize.addAndGet(-getSize(value));              }          }          return super.remove(key);      }        @Override      public void clear() {          hardCache.clear();          cacheSize.set(0);          super.clear();      }        protected int getSizeLimit() {          return sizeLimit;      }        protected abstract int getSize(Bitmap value);        protected abstract Bitmap removeNext();  }</code></pre>    <pre>  <code class="language-java">/**   * Limited {@link Bitmap bitmap} cache. Provides {@link Bitmap bitmaps} storing. Size of all stored bitmaps will not to   * exceed size limit. When cache reaches limit size then the bitmap which used the least frequently is deleted from   * cache.<br />   * <br />   * <b>NOTE:</b> This cache uses strong and weak references for stored Bitmaps. Strong references - for limited count of   * Bitmaps (depends on cache size), weak references - for all other cached Bitmaps.   *   * @author Sergey Tarasevich (nostra13[at]gmail[dot]com)   * @since 1.0.0   */  public class UsingFreqLimitedMemoryCache extends LimitedMemoryCache {      /**       * Contains strong references to stored objects (keys) and last object usage date (in milliseconds). If hard cache       * size will exceed limit then object with the least frequently usage is deleted (but it continue exist at       * {@link #softMap} and can be collected by GC at any time)       */      private final Map<Bitmap, Integer> usingCounts =                   Collections.synchronizedMap(new HashMap<Bitmap, Integer>());        public UsingFreqLimitedMemoryCache(int sizeLimit) {          super(sizeLimit);      }        @Override      public boolean put(String key, Bitmap value) {          if (super.put(key, value)) {              usingCounts.put(value, 0);              return true;          } else {              return false;          }      }        @Override      public Bitmap get(String key) {          Bitmap value = super.get(key);          // Increment usage count for value if value is contained in hardCahe          if (value != null) {              Integer usageCount = usingCounts.get(value);              if (usageCount != null) {                  usingCounts.put(value, usageCount + 1);              }          }          return value;      }        @Override      public Bitmap remove(String key) {          Bitmap value = super.get(key);          if (value != null) {              usingCounts.remove(value);          }          return super.remove(key);      }        @Override      public void clear() {          usingCounts.clear();          super.clear();      }        @Override      protected int getSize(Bitmap value) {          return value.getRowBytes() * value.getHeight();      }        @Override      protected Bitmap removeNext() {          Integer minUsageCount = null;          Bitmap leastUsedValue = null;          Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();          synchronized (usingCounts) {              for (Entry<Bitmap, Integer> entry : entries) {                  if (leastUsedValue == null) {                      leastUsedValue = entry.getKey();                      minUsageCount = entry.getValue();                  } else {                      Integer lastValueUsage = entry.getValue();                      if (lastValueUsage < minUsageCount) {                          minUsageCount = lastValueUsage;                          leastUsedValue = entry.getKey();                      }                  }              }          }          usingCounts.remove(leastUsedValue);          return leastUsedValue;      }        @Override      protected Reference<Bitmap> createReference(Bitmap value) {          return new WeakReference<Bitmap>(value);      }  }</code></pre>    <p> </p>    <p>来自:http://www.jianshu.com/p/e6f4e0ab57c3</p>    <p> </p>