Android经典面试问题:请你设计一套图片异步加载缓存方案——图片的三级缓存

友情提示:文章最后附有项目源码

现在,Android有很多优秀的图片加载框架。例如:Picasso,Glide,Fresco。我们几乎只要简单调用几句代码就可以很好的实现图片的加载。很多时候也不需要我们亲自去写图片加载方案。但是,学习图片的三级缓存策略无论是在面试时,还是对于App的其他缓存框架设计都是很有必要的一件事。

今天就从头开始设计一套图片异步加载缓存方案。本方案用到以下技术,想了解更细致的内容可以去以下链接查看,在此不再赘述。

LruCache:LruCache详解 LruCache源码解析

DiskLruCache:Android DiskLruCache完全解析,硬盘缓存的最佳方案

使用Retrofit下载图片:使用Retrofit和Rxjava下载启动图图片

在代码结构的设计上参考了《Android源码设计模式解析与实战》

1、何为三级缓存

    所谓三级缓存,指的是:内存缓存,本地缓存(或者叫文件缓存),网络缓存(我个人认为把网络算在缓存里其实是不太合适的)。

    (1)内存缓存:只有当APP运行时才会涉及到。内存虽然有容量限制,但是从内存读取信息是速度最快的。

    (2)本地缓存:信息以文件的形式存储在本地。只要不清除这些文件,那么信息就一直持久化的保存着。需要时可以通过流的方式进行读取。本地容量大,速度次于内存。

    (3)网络:信息存储在远端Server。通过网络获取信息。完全依赖网络情况,速度相对上面两者来说要慢。

2、为什么要用三级缓存

    (1)为用户节省流量,对相同资源减少多次重复的网络请求。

    (2)部分业务需要。例如有些业务需要在用户断网时也可以进行一些浏览或操作。

    (3)各缓存读取速度不相同,结合使用提高效率。

3、图片异步加载缓存方案的工作流程

    Android经典面试问题:请你设计一套图片异步加载缓存方案——图片的三级缓存_第1张图片

4、技术选型

    如开头所提到的几个技术点。这里,内存缓存我们选用LruCache实现。本地缓存选用DiskLruCache实现。网络我们通过Retrofit进行图片文件的下载。当然,实现方式有很多种,可根据需要自己选择。

5、方案实现

    (1)定义缓存接口

    首先我们可以确认,无论是内存缓存,本地缓存,还是两者的结合。都需要获取图片的方法和插入图片的方法。因此我们直接定义一个缓存接口,面向接口编写缓存的代码。

    接口如下:

public interface ImageCache {

    Bitmap getBitmap(String url);

    void putBitmap(String url, Bitmap bitmap);

}

    (2)实现内存缓存

    内存缓存的实现很简单,把LruCache当成一个Map来用就好了的。代码如下:

public class MemoryCache implements ImageCache {

    private LruCache mLruCache;
    private static final int MAX_LRU_CACHE_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);

    public MemoryCache() {
        //初始化LruCache
        initLruCache();
    }

    private void initLruCache() {
        mLruCache = new LruCache(MAX_LRU_CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight();
            }
        };
    }

    @Override
    public Bitmap getBitmap(String url) {
        return mLruCache.get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mLruCache.put(url, bitmap);
    }

}

   (3)实现本地缓存

    DiskLruCache是Google自己写的一个类,用来做本地缓存方案十分方便。这个类的具体用法可以参看开头的相关文章链接。

    代码如下:

public class DiskCache implements ImageCache {

    private DiskLruCache mDiskLruCache;
    private static final String DISK_LRU_CACHE_UNIQUE = "Image";
    private static final int MAX_DISK_LRU_CACHE_SIZE = 10 * 1024 * 1024;

    ExecutorService mExecutorsService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );

    public DiskCache(Context context) {
        //初始化DiskLruCache
        initDiskLruCache(context);
    }

    private void initDiskLruCache(Context context) {
        try {
            File cacheDir = getDiskCacheDir(
                    context,
                    DISK_LRU_CACHE_UNIQUE
            );
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            mDiskLruCache = DiskLruCache.open(
                    cacheDir,
                    getAppVersion(context),
                    1,
                    MAX_DISK_LRU_CACHE_SIZE
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    private int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(
                    context.getPackageName(),
                    0
            );
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    @Override
    public Bitmap getBitmap(String url) {
        String bitmapUrlMD5 = Md5Util.getMD5String(url);
        Bitmap bitmap = null;
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = mDiskLruCache.get(bitmapUrlMD5);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (snapshot != null) {
            InputStream inputStream = snapshot.getInputStream(0);
            bitmap = BitmapFactory.decodeStream(inputStream);
        }
        return bitmap;
    }

    @Override
    public void putBitmap(String url, final Bitmap bitmap) {
        final String bitmapUrlMD5 = Md5Util.getMD5String(url);
        mExecutorsService.submit(
                new Runnable() {
                    @Override
                    public void run() {
                        writeFileToDisk(mDiskLruCache, bitmap, bitmapUrlMD5);
                    }
                }
        );
    }

    private static void writeFileToDisk(
            DiskLruCache diskLruCache,
            Bitmap bitmap,
            String bitmapUrlMD5
    ) {
        DiskLruCache.Editor editor = null;
        OutputStream outputStream = null;
        try {
            editor = diskLruCache.edit(bitmapUrlMD5);
            if (editor != null) {
                outputStream = editor.newOutputStream(0);
                if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
                    editor.commit();
                }
            }
        } catch (Exception e) {
            try {
                if (editor != null) {
                    editor.abort();
                }
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                diskLruCache.flush();
            } catch (Exception e) {

            }
        }
    }

}

    可以看到本地缓存的时候对url做了一次MD5加密。这是为了从安全考虑。毕竟直接把url暴露在文件上实在不太雅观。

    (4)完成内存缓存加本地缓存的双缓存逻辑实现

    这一块很简单。参看之前的三级缓存工作流程图。

    对于图片的获取:先从内存缓存获取图片。如果不为空直接返回。如果为空,再从本地缓存获取图片。

    对于图片的保存:就是往内存缓存和本地缓存分别添加图片。

    代码如下:

public class MemoryAndDiskCache implements ImageCache {

    private MemoryCache mMemoryCache;
    private DiskCache mDiskCache;

    public MemoryAndDiskCache(Context context) {
        mMemoryCache = new MemoryCache();
        mDiskCache = new DiskCache(context);
    }

    @Override
    public Bitmap getBitmap(String url) {
        Bitmap bitmap = mMemoryCache.getBitmap(url);
        if (bitmap != null) {
            return bitmap;
        } else {
            bitmap = mDiskCache.getBitmap(url);
            return bitmap;
        }
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mMemoryCache.putBitmap(url, bitmap);
        mDiskCache.putBitmap(url, bitmap);
    }

}

    (5)实现ImageLoader类

    这个类中我们会在构造函数中传入ImageCache的实例。那么在获取和保存图片时,只需要调用接口中定义的两个方法即可,无需关注细节。实现细节完全交由构造函数中传入的ImageCache实例。当要获取图片时,先调用ImageCache接口实例的getBitmap方法,如果为空。那么需要我们从网络下载图片。下载完成后我们只要调用ImageCache接口示例的putBitmap方法,即可完成整个图片缓存方案。

    代码如下:

public class ImageLoader {

    private ImageCache mImageCache;

    public ImageLoader(ImageCache imageCache) {
        mImageCache = imageCache;
    }

    public void displayImage(String url, ImageView imageView, int defaultImageRes) {
        imageView.setImageResource(defaultImageRes);
        imageView.setTag(url);

        Bitmap bitmap = mImageCache.getBitmap(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        } else {
            downloadImage(imageView, url);
        }
    }

    private void downloadImage(final ImageView imageView, final String url) {
        Call resultCall = ServiceFactory.getServices().downloadImage(url);
        resultCall.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                if (response != null && response.body() != null) {
                    InputStream inputStream = response.body().byteStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    if (TextUtils.equals((String) imageView.getTag(), url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.putBitmap(url, bitmap);
                }
            }

            @Override
            public void onFailure(Call call, Throwable t) {
            }
        });
    }

}

    (6)实际使用

    这块只需要new一个ImageLoader对象。并在构造函数中传入你希望使用的缓存策略。之后调用它的displayImage方法即可。

    代码如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = (ImageView) findViewById(R.id.iv);
        String url = "图片的url地址";
        ImageLoader imageLoader = new ImageLoader(
                new MemoryAndDiskCache(getApplicationContext())
        );
        imageLoader.displayImage(url, iv, R.mipmap.ic_launcher);
    }

}

6、演示(动图较大,加载略慢,有兴趣的同学请直接跳到7,去下载源码吧)

    好了,折腾这么一通后我们来找个图片试一下吧。

    首先看一下,在有网的时候,加载一张网络图片:


    之后,我们杀掉程序,并且关闭网络。再将程序打开,可以看到之前的图片仍然能正常显示:


7、源码下载(觉得这篇文章对你有帮助的同学们,欢迎Star一下!):

    源码下载

你可能感兴趣的:(技术探究)