Android性能优化之三级缓存-内存缓存详解面试题

Android性能优化之三级缓存-内存缓存详解面试题_第1张图片
image.png

缓存基础

概述

说起缓存,大家可能很容易想到Http的缓存机制,LruCache,其实缓存最初是针对于网络而言的,也是狭义上的缓存,广义的缓存是指对数据的复用,我这里提到的也是广义的缓存,比较常见的是内存缓存以及磁盘缓存,不过要想进一步理解缓存体系,其实还需要复习一点计算机知识。

Android性能优化之三级缓存-内存缓存详解面试题_第2张图片
image

CPU

CPU分为运算器跟控制器,是计算机的主要设备之一,功能主要是解释计算机指令以及处理计算机软件中的数据。计算机的可编程性主要是指对中央处理器的编程。中央处理器、内部存储器和输入/输出设备是现代电脑的三大核心部件。

存储器

存储器的种类很多,按用途可以分为主存储器和辅助存储器,下面依次介绍一下。

主存储器

又称内存是CPU能直接寻址的存储空间,它的特点是存取速率快。内存一般采用半导体存储单元,包括随机存储器(Random Access Memory)、只读存储器(Read Only Memory)和高级缓存(Cache)。

  • RAM:随机存储器可以随机读写数据,但是电源关闭时存储的数据就会丢失;
  • ROM:只能读取,不能更改,即使机器断电,数据也不会丢失
  • Cache:它是介于CPU与内存之间,常用有一级缓存(L1)、二级缓存(L2)、三级缓存(L3)(一般存在于Intel系列)。它的读写速度比内存还快,当CPU在内存中读取或写入数据时,数据会被保存在高级缓冲存储器中,当下次访问该数据时,CPU直接读取高级缓冲存储器,而不是更慢的内存。

辅助存储器

辅助存储器又称外存储器,简称外存,对于电脑而言,通常说的是硬盘或者光盘等,对于手机一般指的是SD卡,不过现在很多厂商都已经整合在一起了

缓存类型

  • 内存缓存:这里的内存主要指的存储器缓存
  • 磁盘缓存:这里主要指的是外部存储器,电脑指的是硬盘,手机的话指的就是SD卡

缓存容量

就是缓存的大小,到达这个限度之后,那么就需要进行缓存清理了

缓存策略

不管是内存缓存还是磁盘缓存,缓存的容量都是有限制的,所以跟线程池满了之后的线程处理策略类似,缓存满了的时候,我们也需要有相应的处理策略,常见的策略有:

  • FIFO(first in first out):先进先出策略,类似队列。

  • LFU(less frequently used):最少使用策略,RecyclerView的缓存采用了此策略。

  • LRU(least recently used):最近最少使用策略,Picasso在进行内存缓存的时候采用了此策略。

当缓存容量达到设定的容量的时候,会根据制定的策略进行删除相应的元素。

内存泄露

这个主要发生在内存缓存中,当生命周期长的对象持有了生命周期短的对象的引用就会发生内存泄露,解决这种问题通常有两种方式

  • 引用置空:将缓存中引用的对象置空,然后GC就能够回收这些对象
  • 采用弱引用:采用弱引用关联对象,这样就能够不干涉对象的生命周期,以便GC能够正常回收

实际上在防止内存泄露的过程中这两种方式都使用地比较平凡,不过我们大多数时候使用的还是弱引用。

其实Java有四种引用,强引用,软引用,弱引用,虚引用,这些并没什么好说的,我们平时使用最多的还是弱引用,也就是WeakReference。

弱引用VS软引用

只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

下面简单描述一下这两种防止内存泄露的方法的区别

引用置空

RecyclerView的内部类LayoutManager持有了RecyclerView的使用,没有采用弱引用,但是提供了置空的方法

 public static abstract class LayoutManager {
        ChildHelper mChildHelper;
        RecyclerView mRecyclerView;
        @Nullable
        SmoothScroller mSmoothScroller;
        private boolean mRequestedSimpleAnimations = false;
        boolean mIsAttachedToWindow = false;
        private boolean mAutoMeasure = false;
        private boolean mMeasurementCacheEnabled = true;
        private int mWidthMode, mHeightMode;
        private int mWidth, mHeight;

    void setRecyclerView(RecyclerView recyclerView) {
            if (recyclerView == null) {
              //回收
                mRecyclerView = null;
                mChildHelper = null;
                mWidth = 0;
                mHeight = 0;
            } else {
              //初始化
                mRecyclerView = recyclerView;
                mChildHelper = recyclerView.mChildHelper;
                mWidth = recyclerView.getWidth();
                mHeight = recyclerView.getHeight();
            }
            mWidthMode = MeasureSpec.EXACTLY;
            mHeightMode = MeasureSpec.EXACTLY;
        }

采用弱引用

用Picasso中的Action为例,父类采用了WeakReference

Android性能优化之三级缓存-内存缓存详解面试题_第3张图片
image

Action父类

abstract class Action {
  final WeakReference target;
  Action(Picasso picasso, T target, Request request, int memoryPolicy, int networkPolicy,
      int errorResId, Drawable errorDrawable, String key, Object tag, boolean noFade) {
    this.picasso = picasso;
    this.request = request;
    this.target =target ;
    this.memoryPolicy = memoryPolicy;
    this.networkPolicy = networkPolicy;
    this.noFade = noFade;
    this.errorResId = errorResId;
    this.errorDrawable = errorDrawable;
    this.key = key;
    this.tag = (tag != null ? tag : this);
  }

ImageAction子类

class ImageViewAction extends Action {
  Callback callback;
  ImageViewAction(Picasso picasso, ImageView imageView, Request data, int memoryPolicy,
      int networkPolicy, int errorResId, Drawable errorDrawable, String key, Object tag,
      Callback callback, boolean noFade) {
    super(picasso, imageView, data, memoryPolicy, networkPolicy, errorResId, errorDrawable, key,tag, noFade);
    this.callback = callback;
  }

  @Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
    if (result == null) {
      throw new AssertionError(
          String.format("Attempted to complete action with no result!\n%s", this));
    }

    ImageView target = this.target.get();
    if (target == null) {
      return;
    }
    Context context = picasso.context;
    boolean indicatorsEnabled = picasso.indicatorsEnabled;
    PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
  }

由于ImageView持有Context的引用,所以导致Activity回收之后,如果ImageView是强引用,那么GC就不会去回收,而采用了弱引用之后,一旦Activity被回收,那么ImageViewAction的引用不会干扰到Activity的回收。

缓存时间

根据业务需要可以自行设定,但是注意,缓存的其实判断时间都应该以服务器时间为准,可以从服务器的返回数据的Response的header中的时间戳作为判断依据。

读取顺序

内存缓存读取速度远远高于磁盘缓存,我们都知道Picasso是采用了内存缓存跟磁盘缓存这两种缓存的,但是他获取的时候首先是从内存中进行读取,然后把磁盘缓存加到网络缓存中去,其实一开始,我不是这样子做的,我是把内存缓存,磁盘缓存以及网络缓存读取都实例化了一个Runnable,然后在加载下一页的时候,总是会出现图片闪烁,但是我用Picasso,UIL跟Glide就不会闪烁,但是当我设置Picasso他们的内存缓存策略为MemoryPolicy.NO_CACHE的时候,他们也会闪烁,下面展示一下闪烁的效果

image

其实上面两种情况都会出现闪烁,共同原因就是因为内存缓存的问题,Picasso的issue里面有人提过,作者JakeWharton是这么回答的

Android性能优化之三级缓存-内存缓存详解面试题_第4张图片
image

是的200ms,如果Bitmap没有读取成功,那么就会出现闪烁,这样正好解释了上面的两种情况,由于我们设置了占位图,第一种闪烁是因为我们把内存缓存的读取放到了一个线程里面,线程的创建,切换这些都是需要时间的,那么就导致了总时间会超过200ms;同理,第二种情况如果没有设置内存缓存,那么只能从网络或磁盘中读取这个时间肯定会超过200ms,同样会闪烁,所以这也是为什么图片加载框架优先从内存中读取,当不设置内存缓存的时候也会闪烁的原因。

同时磁盘缓存需要借助于Http缓存机制来保证缓存的时效性,后面会具体分析。

总结

其实缓存的改变比较好理解,就是在使用内存缓存的时候需要注意防止内存泄露,使用磁盘缓存的时候需要注意结合Http的缓存机制来来确保缓存的时效性

图片的缓存技术

  1. 当需要在界面上加载大量图片的时候,比如使用ListView,GridView,或者ViewPager这样的组件,屏幕上显示的图片可以通过滑动屏幕等事件不断增加,最终导致OOM。
  2. 为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行处理。这个时候,垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。如果为了让程序快速地运行,在界面上迅速地加载图片,我们又需要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。就需要避免又重新去加载刚刚加载过的图片,这个时候,可以使用内存缓存技术解决这个问题,它可以让组件快速地重新加载和处理图片。

内存缓存技术

内存缓存技术对那些大量占用应用程序内存的图片提供了快速访问的方法。其中最核心的类是LruCache(此类在android-support-v4的包中提供)。它的主要算法原理是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:

  1. 你的设备可以为每个应用程序分配多大的内存?
  2. 设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
  3. 你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
  4. 图片的尺寸和大小,还有每张图片会占据多少内存空间。
  5. 图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
  6. 你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。

并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。

LruCache

一般建议采用support-v4兼容包中提供的LruCache,可兼容到早期的Android版本,目前Android2.2以下的用户量已经很少了,因此在开发的应用兼容到Android2.2就已经足够了。

LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,它提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。

补充知识:强引用、软引用和弱引用的区别

强引用
直接的对象引用;内存不足时,JVM也不会被回收。(定义的成员变量都是强引用)
软引用
SoftReference 当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
弱引用
WeakReference 当一个对象只有弱引用存在时,此对象会随时被gc回收。
虚引用
PhantomReference 代码被调用的时候,就被清理了。

软引用示例代码(其他几种用法类似):

private SoftReference mSoftReference;

/**
 * 给imageView加载url对应的图片
 * @param iv
 * @param url
 */
public void display(ImageView iv,String url){
    mSoftReference=new SoftReference(iv);
    mSoftReference.clear();//这里是清除里面的iv图片资源
    //取引用--为null
    ImageView imageView = mSoftReference.get();
}

Android-->早期是davike虚拟机,Android Runtime

3.0之前,垃圾回收机制和JVM是相同的。(可以用这套软引用存储图片)
3.0之后,davike虚拟机做了升级,只要GC(回收机制)运行,SoftReference和WeakReference一律回收。(在Android中就没用了)

Android3.0之前软引用的写法(代码如下):

private static Map> mCaches = new LinkedHashMap>();

/**
 * 给imageView加载url对应的图片
 *
 * @param iv
 * @param url
 */
public void display(ImageView iv, String url) {
    SoftReference reference = mCaches.get(url);
    if (reference==null){
        //内存中没有--》本地去取
    }else {
        Bitmap bitmap = reference.get();
        if (bitmap==null){
            //gc回收了---》本地去取
        }else {
            //内存中有,就显示
        }
    }
}

解决方案:是用了LruCache。
LruCache是线程安全的,定义如下:

public class LruCache{
  private final LinkedHashMap map;
  ...
}

内存缓存解析

关于Android的三级缓存,其中主要的就是内存缓存和硬盘缓存。这两种缓存机制的实现都应用到了LruCache算法,今天我们就从使用到源码解析,来彻底理解Android中的缓存机制。

一、Android中的缓存策略

一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限的。当缓存满了之后,再想其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。

因此LRU(Least Recently Used)缓存算法便应运而生,LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。

二、LruCache的使用

LruCache是Android 3.1所提供的一个缓存类,所以在Android中可以直接使用LruCache实现内存缓存。而DisLruCache目前在Android 还不是Android SDK的一部分,但Android官方文档推荐使用该算法来实现硬盘缓存。

1.LruCache的介绍

LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作。

2.LruCache的使用

LruCache的使用非常简单,我们就已图片缓存为例。

 int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };

①设置LruCache缓存的大小,一般为当前进程可用容量的1/8。
②重写sizeOf方法,计算出要缓存的每张图片的大小。

注意:缓存的总容量和每个缓存对象的大小所用单位要一致。

三、LruCache的实现原理

LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队首,即将被淘汰。而最近访问的对象将放在队尾,最后被淘汰。

如下图所示:

Android性能优化之三级缓存-内存缓存详解面试题_第5张图片
image

那么这个队列到底是由谁来维护的,前面已经介绍了是由LinkedHashMap来维护。

而LinkedHashMap是由数组+双向链表的数据结构来实现的。其中双向链表的结构可以实现访问顺序和插入顺序,使得LinkedHashMap中的对按照一定顺序排列起来。

通过下面构造函数来指定LinkedHashMap中双向链表的结构是访问顺序还是插入顺序。

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

其中accessOrder设置为true则为访问顺序,为false,则为插入顺序。

以具体例子解释:
当设置为true时

public static final void main(String[] args) {
        LinkedHashMap map = new LinkedHashMap<>(0, 0.75f, true);
        map.put(0, 0);
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        map.put(4, 4);
        map.put(5, 5);
        map.put(6, 6);
        map.get(1);
        map.get(2);

        for (Map.Entry entry : map.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());

        }
    }

输出结果:

0:0
3:3
4:4
5:5
6:6
1:1
2:2

即最近访问的最后输出,那么这就正好满足的LRU缓存算法的思想。可见LruCache巧妙实现,就是利用了LinkedHashMap的这种数据结构。

下面我们在LruCache源码中具体看看,怎么应用LinkedHashMap来实现缓存的添加,获得和删除的。

 public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap(0, 0.75f, true);
    }

从LruCache的构造函数中可以看到正是用了LinkedHashMap的访问顺序。

put()方法

public final V put(K key, V value) {
         //不可为空,否则抛出异常
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }
        V previous;
        synchronized (this) {
            //插入的缓存对象值加1
            putCount++;
            //增加已有缓存的大小
            size += safeSizeOf(key, value);
           //向map中加入缓存对象
            previous = map.put(key, value);
            //如果已有缓存对象,则缓存大小恢复到之前
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        //entryRemoved()是个空方法,可以自行实现
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //调整缓存大小(关键方法)
        trimToSize(maxSize);
        return previous;
    }

可以看到put()方法并没有什么难点,重要的就是在添加过缓存对象后,调用 trimToSize()方法,来判断缓存是否已满,如果满了就要删除近期最少使用的算法。
trimToSize()方法

 public void trimToSize(int maxSize) {
        //死循环
        while (true) {
            K key;
            V value;
            synchronized (this) {
                //如果map为空并且缓存size不等于0或者缓存size小于0,抛出异常
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
                //如果缓存大小size小于最大缓存,或者map为空,不需要再删除缓存对象,跳出循环
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                //迭代器获取第一个对象,即队首的元素,近期最少访问的元素
                Map.Entry toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                //删除该对象,并更新缓存大小
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
            entryRemoved(true, key, value, null);
        }
    }

trimToSize()方法不断地删除LinkedHashMap中队首的元素,即近期最少访问的,直到缓存大小小于最大值。

当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,保持整个队列是按照访问顺序排序。这个更新过程就是在LinkedHashMap中的get()方法中完成的。

先看LruCache的get()方法

get()方法

public final V get(K key) {
        //key为空抛出异常
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            //获取对应的缓存对象
            //get()方法会实现将访问的元素更新到队列尾部的功能
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

其中LinkedHashMap的get()方法如下:

public V get(Object key) {
        LinkedHashMapEntry e = (LinkedHashMapEntry)getEntry(key);
        if (e == null)
            return null;
        //实现排序的关键方法
        e.recordAccess(this);
        return e.value;
    }

调用recordAccess()方法如下:

 void recordAccess(HashMap m) {
            LinkedHashMap lm = (LinkedHashMap)m;
            //判断是否是访问排序
            if (lm.accessOrder) {
                lm.modCount++;
                //删除此元素
                remove();
                //将此元素移动到队列的尾部
                addBefore(lm.header);
            }
        }

由此可见LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队头元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到尾部。

以上便是LruCache实现的原理,理解了LinkedHashMap的数据结构就能理解整个原理。如果不懂,可以先看看LinkedHashMap的具体实现。

DiskLruCache

DiskLruCache用于实现本地存储缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存的效果。

图片的三级缓存的实现

Android性能优化之三级缓存-内存缓存详解面试题_第6张图片
image.png

图片三级缓存的工具类,示例代码:

public class ImageUtils {

    private static LruCache mCaches;

    /**
     * 定义上下文对象
     */
    private Context mContext;

    private static Handler mHandler;

    //声明线程池,全局只有一个线程池,所有访问网络图片,只有这个池子去访问。
    private static ExecutorService mPool;

    //解决错位问题,定义一个存标记的集合
    private Map mTags = new LinkedHashMap();

    public ImageUtils(Context context) {
        this.mContext = context;
        if (mCaches == null) {
            //申请内存空间
            int maxSize = (int) (Runtime.getRuntime().freeMemory() / 4);
            //实例化LruCache
            mCaches = new LruCache(maxSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    //判断添加进入的value的占用内存的大小
                    //这里默认sizeOf是返回1,不占用,内存会不够用,所以要给它一个具体占用内存的大小
                    //                    return super.sizeOf(key, value);
                    //获取Bitmap的大小
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }
        if (mHandler == null) {
            //实例化Handler
            mHandler = new Handler();
        }

        if (mPool == null) {
            //创建固定大小的线程池
            mPool = Executors.newFixedThreadPool(3);
            //创建一个缓存的线程池,生产者和消费者,一个线程生产,必须得消费完成后再生产
            /*Executors.newCachedThreadPool();
            Executors.newSingleThreadExecutor();//创建一个单线程池
            Executors.newScheduledThreadPool();//创建一个计划的任务池*/
        }
    }

    /**
     * 给imageView加载url对应的图片
     *
     * @param iv
     * @param url
     */
    public void display(ImageView iv, String url) {
        //1.从内存中获取
        Bitmap bitmap = mCaches.get(url);
        if (bitmap != null) {
            //内存中有,显示图片
            iv.setImageBitmap(bitmap);
            return;
        }

        //2.内存中没有,从本地获取
        bitmap = loadFromLocal(url);
        if (bitmap != null) {
            //本地有,显示
            iv.setImageBitmap(bitmap);
            return;
        }

        //从网络中获取
        loadFromNet(iv, url);
    }

    private void loadFromNet(ImageView iv, String url) {

        mTags.put(iv, url);//url是ImageView最新的地址

        //耗时操作
        //        new Thread(new LoadImageTask(iv, url)).start();
        //用线程池去管理
        mPool.execute(new LoadImageTask(iv, url));
        //        Future submit = mPool.submit(new LoadImageTask(iv, url));
        //取消的操作(有机率取消),而使用execute没有办法取消
        //        submit.cancel(true);
    }

    private class LoadImageTask implements Runnable {
        private ImageView iv;
        private String    url;

        public LoadImageTask(ImageView iv, String url) {
            this.iv = iv;
            this.url = url;
        }

        @Override
        public void run() {
            try {
                HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();

                //连接服务器超时时间
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);

                //连接服务器(可写可不写)
                conn.connect();

                //获取流
                InputStream is = conn.getInputStream();

                //将流变成bitmap
                Bitmap bitmap = BitmapFactory.decodeStream(is);

                //存储到本地
                save2Local(bitmap, url);

                //存储到内存
                mCaches.put(url, bitmap);

                //在显示UI之前,拿到最新的url地址
                String recentlyUrl = mTags.get(iv);

                //把这个url和最新的url地址做一个比对,如果相同,就显示ui
                if (url.equals(recentlyUrl)) {
                    //显示到UI,当前是子线程,需要使用Handler。其中post方法是执行在主线程的
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            display(iv, url);
                        }
                    });
                }


            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 存储到本地
     *
     * @param bitmap
     * @param url
     */
    public void save2Local(Bitmap bitmap, String url) throws FileNotFoundException {
        File file = getCacheFile(url);
        FileOutputStream fos = new FileOutputStream(file);
        /**
         * 用来压缩图片大小
         * Bitmap.CompressFormat format 图像的压缩格式;
         * int quality 图像压缩率,0-100。 0 压缩100%,100意味着不压缩;
         * OutputStream stream 写入压缩数据的输出流;
         * 返回值:如果成功地把压缩数据写入输出流,则返回true。
         */
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
    }

    /**
     * 从本地获取图片
     *
     * @param url
     * @return bitmap
     */
    private Bitmap loadFromLocal(String url) {
        //本地需要存储路径
        File file = getCacheFile(url);

        if (file.exists()) {
            //本地有
            //把文件解析成Bitmap
            Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

            //存储到内存
            mCaches.put(url, bitmap);

            return bitmap;
        }

        return null;
    }


    /**
     * 获取缓存文件路径(缓存目录)
     *
     * @return 缓存的文件
     */
    private File getCacheFile(String url) {
        //把url进行md5加密
        String name = MD5Utils.encode(url);

        //获取当前的状态,Environment是环境变量
        String state = Environment.getExternalStorageState();
        if (Environment.MEDIA_MOUNTED.equals(state)) {
            //挂载状态,sd卡存在
            File dir = new File(Environment.getExternalStorageDirectory(),
                    "/Android/data/" + mContext.getPackageName() + "/icon");
            if (!dir.exists()) {
                //文件不存在,就创建
                dir.mkdirs();
            }
            //此处的url可能会很长,一般会使用md5加密
            return new File(dir, name);
        } else {
            File dir = new File(mContext.getCacheDir(), "/icon");
            if (!dir.exists()) {
                //文件不存在,就创建
                dir.mkdirs();
            }
            return new File(dir, name);
        }
    }
}

问题和总结

三级缓存线程池和错位问题及处理

页面在第一次加载的时候,从网络上下载,如果滑动图片比较快,里面滑动了1000个图片,就会有1000个new Thread()线程,会造成oom,解决方案是,在代码中,不用new Thread()方法去开启线程,用线程池管理,在全局定义一个线程池,使用new FixedThreadPool()方法创建一个固定大小的线程池,一般配置里面线程个数3-5个,所有的访问网络图片,只有这个线程池去访问,(使用execute()方法).线程池的好处,比方里面设置3个线程个数,表示固定只能同时运行3个线程,其中某一个执行完成后再去执行下一个.

错位问题:

两个线程去加载图片,第二个开启的线程先到服务器,把数据拉回来了展示出来,第一个线程后到服务器把数据拉回来,结果是第二条线程返回的是第一张图片,第一条线程返回的是第二张图片,出现图片错位.出现这种情况,一般是与网络加载速度有关系.解决方案是打标记.定义一个Map集合对象,用来存标记,每次调用加载网络数据的时候就存标记,通过put()方法把ImageView和Url存进去,即使在网络加载图片的过程中,第一次调用就会存一个url,第二次调用就会存第二个url,这个url永远是ImageView最新的地址,在图片显示到UI的时候,做一个判断,拿到最新的图片url地址,把它和网络加载的图片做一个equals比较,如果相同就显示图片,如果不同就不显示,但是图片还是存在内存,存在本地,只是不显示,这样就解决了图片错位的问题。

优化列表的卡顿现象

解决这个问题的方法是:
不要在主线程中做太耗时的操作,提高滑动的流畅度。
从以下三个方面来解决:

  1. 不要在getView中执行耗时操作。因为加载图片是一个耗时的操作,需要通过异步的方式来处理。
  2. 控制异步任务的执行频率。比如用户频繁上下滑动,就会产生上百个异步任务,这些异步任务会造成线程池的拥堵并且一瞬间存在大量的UI更新操作,这些UI更新操作是运行在主线程的,就会造成一定程度的卡顿。那么如何解决这个问题呢?可以在列表滑动的时候停止加载图片,当列表不滑动的时候加载图片,就能获得良好的用户体验。具体实现:可以给ListView或者GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是的话就停止加载图片,如下:
public void onScrollStateChanged(AbsListView view,int scrollState){
  if(scrollState==OnScrollListener.SCROLL_STATE_IDLE){
    mIsGridViewIdle=true;
    mImageAdapter.notifyDataSetChanged();
  }else{
    mIsGridViewIdle=false;
  }
}

然后在getView方法中,仅当列表静止时才能加载图片,如下:

if(mIsGridViewIdle && mCanGetBitmapFromNetWork){
  imageView.setTag(uri);
  mImageLoader.bindBitmap(uri,imageView,mImageWidth,mImageHeight);
}

经过上面两个步骤,列表一般就不会出现卡顿现象了。但在有些特殊情况下,需要开启硬件加速,通过设置 android:hardwareAccelerated="true"即可为Activity开启硬件加速。

你可能感兴趣的:(Android性能优化之三级缓存-内存缓存详解面试题)