Android源码解析之LruCache(五)

作者: 大虾啊啊啊 | 来源:发表于2018-08-23 15:48 被阅读0次

    前言

    在实际开发中,我们经常要加载很多大图,大多数情况下都是从服务器中下载下来,然后显示到图片的控件中。这里就会存在一个问题,如果每次加载图片都要从网络上下载,这样就会导致效率非常慢,影响用户的体验。所以我们会想到可以把图片缓存到内存中,第二次之后加载图片就从内存中加载,这样效率就会很快。但是我们要知道,如果我们把大量的图片都存到内存中,当内存不足的时候,就会导致OOM。所以这样一来LruCache就是为了解决这个问题而出现的。

    一、LruCache的简单使用

    LruCache用中文翻译就是最近最少使用算法,它会将图片缓存到内存中。具体原理我们先不说,我们先介绍LruCache的简单使用方法,然后再从源码的角度分析LruCache的实现原理。

    /**
     * 自定义图片加载类
     */
    public class MyImageLoader {
        private static final String TAG ="MyImageLoader" ;
        /**
         * LruCache 内存缓存类
         */
        private LruCache<String, Bitmap> lruCache;
        private Context context;
    
        public MyImageLoader(Context context) {
            this.context = context;
            //获取最大可用内存
            int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //使用最大内存的1/8作为缓存大小
            int cacheSize = maxMemory / 8;
            lruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                    //每次存入缓存的时候调用该方法
                    //存入的图片的大小
                    return super.sizeOf(key, value);
                }
            };
        }
        /**
         * 显示图片
         *
         * @param imageView 显示控件
         * @param url       url 作为key
         */
        public void showImage(ImageView imageView, String url) {
            //从缓存中获取bitmap
            Bitmap bitmap = lruCache.get(url);
            //判断缓存中的bitmap是否为空
            if (bitmap == null) {
                //如果为空,则去下载该图片
                downLoad(imageView, url);
                Log.e(TAG, "showImage: 从网上下载的图片" );
    
            } else {
                //如果不为空,则直接展示缓存中的bitmap
                imageView.setImageBitmap(bitmap);
                Log.e(TAG, "showImage: 从缓存中加载图片" );
            }
        }
        /**
         * 下载图片
         *
         * @param imageView
         * @param url
         */
        private void downLoad(final ImageView imageView, final String url) {
            AsyncTask<String, Integer, Bitmap> asyncTask = new AsyncTask<String, Integer, Bitmap>() {
                @Override
                protected Bitmap doInBackground(String... integers) {
                    //在这里执行异步任务,下载图片。在这里就模拟从网络下载图片
                    //从项目中加载一张图片,模拟从网络下载
                    Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher);
                    return bitmap;
                }
    
                @Override
                protected void onPreExecute() {
                    super.onPreExecute();
                }
    
                @Override
                protected void onPostExecute(Bitmap bitmap) {
                    super.onPostExecute(bitmap);
                    //显示图片
                    imageView.setImageBitmap(bitmap);
                    //并把图片放入缓存中
                    lruCache.put(url, bitmap);
                }
    
                @Override
                protected void onProgressUpdate(Integer... values) {
                    super.onProgressUpdate(values);
                }
            };
            asyncTask.execute(url);
        }
    
    }
    

    我们自定义一个加载图片的类MyImageLoader ,这个类很简单

    • 首先在构造方法中初始化缓存的大小,一般为最大可用内存的8分之1
    • 然后创建LruCache对象,两个参数分别为key 和 value,也就是说缓存中以键值对的显示存在。当我们取缓存中的值的时候,可用通过键来取。
    • showImage方法用来加载图片,传入图片控件ImageView 以及要下载图片的url参数,并用url做完缓存的键。首先从缓存中读取bitmap,然后判断缓存中的bitmap是否为空,如果不为空直接显示图片。如果为空调用downLoad方法去下载图片,在downLoad方法中,我们就不去网络上下载了,直接从项目中加载一张图片用来模拟下载。下载成功后我们加载图片,并把图片通过url为键存入到缓存中。
    //并把图片放入缓存中
      lruCache.put(url, bitmap);
    

    接下来我们看下具体的使用:

    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
        private ImageView ivEtxt;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ivEtxt = (ImageView) findViewById(R.id.iv_etxt);
            final MyImageLoader imageLoader = new MyImageLoader(this);
            ivEtxt.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    imageLoader.showImage(ivEtxt, "http://baidu.com");
                }
            });
        }
    
    
    }
    

    我们点击ImageView事件,加载图片,点击两次得到的打印结果如下:

    08-23 14:24:26.377 3071-3071/com.mujin.keji.myapplication E/MyImageLoader: showImage: 从网上下载的图片
    08-23 14:24:27.144 3071-3071/com.mujin.keji.myapplication E/MyImageLoader: showImage: 从缓存中加载图片
    
    

    我们看到第一次是从网下下载的图片,第二次是从缓存中加载的图片,并成功的显示到了ImageViw中。


    image.png

    以上我们就介绍了LruCache的简单使用,具体详细的用法就不具体介绍了。下面将从源码的角度分析LruCache的原理。

    二、从源码的角度分析LruCache的实现原理

    首先我们看下LruCache的构造方法。

     /**
         * @param maxSize for caches that do not override {@link #sizeOf}, this is
         *     the maximum number of entries in the cache. For all other caches,
         *     this is the maximum sum of the sizes of the entries in this cache.
         */
        public LruCache(int maxSize) {
            if (maxSize <= 0) {
                throw new IllegalArgumentException("maxSize <= 0");
            }
            this.maxSize = maxSize;
            this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        }
    

    在LruCache的构造方法中传入了maxSize参数,并把maxSize设置到成员变量,也就是我们设定的缓存的大小。接着创建了一个LinkedHashMap对象。在创建LinkedHashMap对象的时候就是最重要的核心内容。 new LinkedHashMap<K, V>(0, 0.75f, true);分别传入三个参数,第一个参数0表示初始化长度为0、第二个参数0.75表示已0.75作为因子,当容量达到总容量的百分之75的时候,会把内存增加一半。第三个参数最为重要,true表示以访问顺序排序,就是当我们对LinkedHashMap的某个元素进行put或者get操作的时候,会把当前元素放到最后,重新排序。所以每次我们get或者put的时候,其实就是把元素放到了最后,所以最少访问的数据自然跑到了最前面。
    接下来我们看put方法,把数据放入缓存中。

     /**
         * Caches {@code value} for {@code key}. The value is moved to the head of
         * the queue.
         *
         * @return the previous value mapped by {@code key}.
         */
        public final V put(K key, V value) {
            if (key == null || value == null) {
                throw new NullPointerException("key == null || value == null");
            }
    
            V previous;
            synchronized (this) {
                putCount++;
                size += safeSizeOf(key, value);
                previous = map.put(key, value);
                if (previous != null) {
                    size -= safeSizeOf(key, previous);
                }
            }
    
            if (previous != null) {
                entryRemoved(false, key, previous, value);
            }
    
            trimToSize(maxSize);
            return previous;
        }
    

    在put方法中首先对size自增,就是在当前缓存中存在的元素的大小中再增加当前元素的大小,然后再返回当前缓存中元素的大小。

     size += safeSizeOf(key, value);
    

    然后把数据以键值对的形式存到map中

    previous = map.put(key, value);
    

    在这里要注意的是,如果mao中已经存在了该元素,则返回previous ,不会重复插入。所以当previous 不为空的时候,size 需要自减。
    接着如果previous 不为空,调用了entryRemoved方法,该方法是一个空方法,我们如果需要了解具体的数据信息,可以重写该方法。

      if (previous != null) {
                entryRemoved(false, key, previous, value);
            }
     protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
    

    最后调用trimToSize方法

         trimToSize(maxSize);
     /**
         * Remove the eldest entries until the total of remaining entries is at or
         * below the requested size.
         *
         * @param maxSize the maximum size of the cache before returning. May be -1
         *            to evict even 0-sized elements.
         */
        public void trimToSize(int maxSize) {
            while (true) {
                K key;
                V value;
                synchronized (this) {
                    if (size < 0 || (map.isEmpty() && size != 0)) {
                        throw new IllegalStateException(getClass().getName()
                                + ".sizeOf() is reporting inconsistent results!");
                    }
    
                    if (size <= maxSize || map.isEmpty()) {
                        break;
                    }
    
                    Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    size -= safeSizeOf(key, value);
                    evictionCount++;
                }
    
                entryRemoved(true, key, value, null);
            }
        }
    

    在这个方法中其实是一个while循环的操作,我们看下跳出循环的条件。

     if (size <= maxSize || map.isEmpty()) {
                        break;
                    }
    
    

    也就是说,如果缓存中元素的大小还没有超过我们设定的缓存大小,或者map链表中还没有值,就跳出循环,不做任何操作。但是如果说,缓存中的元素的大小已经达到了我们设定的缓存的大小,那么就需要把链表中的某个元素给去掉,具体去掉哪一个呢,我们看下面的代码。

      Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    size -= safeSizeOf(key, value);
                    evictionCount++;
    

    我们看到移除的是map中的第一个元素,为什么移除第一个元素呢,我们在上面分析的时候知道,在创建map的时候调用了 new LinkedHashMap<K, V>(0, 0.75f, true);方法,传入了true这个参数,每次被访问的元素都会放在最后然后重新进行排序,那么最少访问次数的元素自然而然就在链表的头部了。所以移除掉第一个元素,也就是达到了最近最少使用算法这个效果。也就是经常不访问的元素就被移除掉了。
    下面我们在看下get方法,从缓存中读取元素。

      /**
         * Returns the value for {@code key} if it exists in the cache or can be
         * created by {@code #create}. If a value was returned, it is moved to the
         * head of the queue. This returns null if a value is not cached and cannot
         * be created.
         */
        public final V get(K key) {
            if (key == null) {
                throw new NullPointerException("key == null");
            }
    
            V mapValue;
            synchronized (this) {
                mapValue = map.get(key);
                if (mapValue != null) {
                    hitCount++;
                    return mapValue;
                }
                missCount++;
            }
    
            /*
             * Attempt to create a value. This may take a long time, and the map
             * may be different when create() returns. If a conflicting value was
             * added to the map while create() was working, we leave that value in
             * the map and release the created value.
             */
    
            V createdValue = create(key);
            if (createdValue == null) {
                return null;
            }
    
            synchronized (this) {
                createCount++;
                mapValue = map.put(key, createdValue);
    
                if (mapValue != null) {
                    // There was a conflict so undo that last put
                    map.put(key, mapValue);
                } else {
                    size += safeSizeOf(key, createdValue);
                }
            }
    
            if (mapValue != null) {
                entryRemoved(false, key, createdValue, mapValue);
                return mapValue;
            } else {
                trimToSize(maxSize);
                return createdValue;
            }
        }
    
    

    首先从map中根据key获取元素,如果元素不为空,说明缓存中有值,返回当前元素。

      V mapValue;
            synchronized (this) {
                mapValue = map.get(key);
                if (mapValue != null) {
                    hitCount++;
                    return mapValue;
                }
                missCount++;
            }
    
    

    如果元素为空,继续往下走。

        /*
             * Attempt to create a value. This may take a long time, and the map
             * may be different when create() returns. If a conflicting value was
             * added to the map while create() was working, we leave that value in
             * the map and release the created value.
             */
    
            V createdValue = create(key);
            if (createdValue == null) {
                return null;
            }
    
            synchronized (this) {
                createCount++;
                mapValue = map.put(key, createdValue);
    
                if (mapValue != null) {
                    // There was a conflict so undo that last put
                    map.put(key, mapValue);
                } else {
                    size += safeSizeOf(key, createdValue);
                }
            }
    
            if (mapValue != null) {
                entryRemoved(false, key, createdValue, mapValue);
                return mapValue;
            } else {
                trimToSize(maxSize);
                return createdValue;
            }
    

    如果为空的话,我们调用 V createdValue = create(key);方法,create方法是一个空方法,我们可以手动实现,也就是说,如果为空的话,我们可以进行手动实现,给缓存中添加指定的元素。然后调用trimToSize方法,一样的原理,根据缓存中的大小和设定的大小比较看是否要移除第一个元素。
    以上我们就对LruCache的源码做了简单的分析。接下来我们做一个简单的小结。
    1、在LruCache中维护了一个LinkedHashMap链表,并且该链表以访问顺序进行排序,每次访问的元素会放在最后。
    2、调用put方法给LinkedHashMap中添加元素,并且每插入一个元素,就要计算当前链表中的元素大小和设定的缓存的大小进行比较,如果达到了设定缓存的大小,把链表的第一个元素移除,也就是很少访问的元素移除掉。
    3、调用get方法,获取LinkedHashMap中的元素。如果元素为空,我们还可以重写create方法,手动添加特定的元素到LinkedHashMap中。

    相关文章

      网友评论

        本文标题:Android源码解析之LruCache(五)

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