简单的实现三级缓存

简介

众所周知,缓存在Android中应用普遍,特别是图片不少的状况下,大量的图片加载,不只加重了性能消耗,并且容易形成OOM,致使程序崩溃。本文讲述三级缓存的使用以及遇到的问题总结。其中三级缓存分为:内存缓存,文件缓存,网络下载java

内存缓存

优势:android

加载显示效率很快,用户等待时间较短git

缺点:github

应用内存有限,不能存放大量的bitmapweb

代码实现:算法

MemoryCacheUtil.java数组

package com.example.edwardadmin.ormdatabase.cache;

import android.graphics.Bitmap;
import android.util.LruCache;

public class MemoryCacheUtil {

    private LruCache<String, Bitmap> mLruCache;
    private static MemoryCacheUtil instance;

    private MemoryCacheUtil() {
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;

        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount();
            }
        };
    }

    public static MemoryCacheUtil getInstance() {
        if (instance == null) {
            synchronized (LocalCacheUtil.class) {
                if (instance == null) {
                    instance = new MemoryCacheUtil();
                }
            }
        }
        return instance;
    }

    public Bitmap getBitmapFromMemory(String imagePath) {
        return mLruCache.get(imagePath);
    }

    public void setBitmapToMemory(String imagePath, Bitmap bitmap) {
        if (getBitmapFromMemory(imagePath) == null) {
            mLruCache.put(imagePath, bitmap);
        }
    }

    //clear memory cache
    public void clearMemoryCache(String imagePath) {
        if (getBitmapFromMemory(imagePath) != null) {
            mLruCache.remove(imagePath);
        }
    }
}

以上代码中能够看出:缓存

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

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

LruCache基础知识
- LruCache是计算机科学常常使用的一种近期最少使用算法
- LruCache内部采用的是LinkedHashMap
- LruCache的出现时为了取代SoftReference Android 3.0以前作图片缓存主要用的就是SoftReference,3.0之后虚拟机更倾向于用SoftReference来索引对象,因此LruCache的出现就是为了取代它。

文件缓存

  • 在初次经过网络获取图片后,咱们能够在本地SD卡中将图片保存起来
  • 能够使用MD5加密图片的下载地址,来做为图片的名称保存

代码实现:

LocalCacheUtil.java

package com.example.edwardadmin.ormdatabase.cache;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;

import com.example.edwardadmin.ormdatabase.config.Constant;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class LocalCacheUtil {
    private static LocalCacheUtil instance;

    private LocalCacheUtil() {
    }

    public static LocalCacheUtil getInstance() {
        if (instance == null) {
            synchronized (LocalCacheUtil.class) {
                if (instance == null) {
                    instance = new LocalCacheUtil();
                }
            }
        }
        return instance;
    }

    //从文件终获取file,而后生成bitmap
    public Bitmap getBitmapFromLocal(String imagePath) {
        String fileName;
        try {
            //根据MD5Encoder将imagePath生成fileName
            fileName = MD5Encoder.encode(imagePath);
            Log.d("edward", "getBitmapFromLocal fileName = " + fileName);
            File file = new File(Constant.SYSTEM_DATABASE_CACHE_PATH, fileName);
            if (file.exists()) {
                Bitmap bitmap = BitmapFactory.decodeStream(new FileInputStream(file));
                return bitmap;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //将bitmap保存成文件
    public void setBitmapToLocal(String imagePath, Bitmap bitmap) {
        FileOutputStream fos = null;
        try {
            //根据MD5Encoder将imagePath生成fileName
            String fileName = MD5Encoder.encode(imagePath);
            Log.d("edward", "setBitmapToLocal fileName = " + fileName);
            File file = new File(Constant.SYSTEM_DATABASE_CACHE_PATH, fileName);
            File parentFile = file.getParentFile();
            if (!parentFile.exists()) {
                parentFile.mkdirs();
            }
            fos = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //clear local file cache
    public boolean clearLocalCache(String imagePath) {
        String fileName = null;
        try {
            fileName = MD5Encoder.encode(imagePath);
            Log.d("edward", "clearLocalCache fileName = " + fileName);
            File file = new File(Constant.SYSTEM_DATABASE_CACHE_PATH, fileName);
            if (!file.exists()) {
                return false;
            } else {
                return file.delete();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

以上代码中能够看出:

①根据传入过来的FileName,使用Md5加密,生成新的Strig,做为文件缓存的名字
②当文件被执行删除等操做时,根据传入的FileName,清除文件缓存,节约存储空间

网络下载

  • 当本地内存缓存、文件缓存均不存在时,此时图片须要网络下载
  • 本地代码,须要将网络图片下载到本地,而后decode bitmap,将获取的bitmap依此存放到内存缓存、文件缓存中

代码实现:

NetWorkCacheUtil.java

package com.example.edwardadmin.ormdatabase.cache;

import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.ImageView;

import com.example.edwardadmin.ormdatabase.config.Constant;
import com.example.edwardadmin.ormdatabase.http.OkHttpUtil;
import com.example.edwardadmin.ormdatabase.util.BitmapDecodeUtil;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import okhttp3.Response;

public class NetworkCacheUtil {

    private MemoryCacheUtil memoryCacheUtil;
    private LocalCacheUtil localCacheUtil;

    public NetworkCacheUtil() {
        this.memoryCacheUtil = MemoryCacheUtil.getInstance();
        this.localCacheUtil = LocalCacheUtil.getInstance();
    }

    public void downloadImageFromNetWork(final Uri imageUri, final ImageView imageView) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                doGetRequest(imageUri, imageView);
            }
        }).start();
    }

    private void doGetRequest(final Uri imageUri, final ImageView imageView) {
        OkHttpUtil.getInstance().asyncGetRequest(imageUri, new OkHttpUtil.OkHttpResultCallback() {
            @Override
            public void onCallbackSuccess(Response response) {
                BufferedOutputStream bufferedOutputStream = null;
                FileOutputStream fileOutputStream = null;
                File file = null;
                String filePath = null;
                try {
                    byte[] bytes = response.body().bytes();
                    File fileDir = new File(Constant.SYSTEM_DATABASE_IMAGE_PATH);
                    if (!fileDir.exists()) {
                        fileDir.mkdirs();
                    }
                    //Download image, local file path, /sdcard/database/image/...jpg
                    file = new File(Constant.SYSTEM_DATABASE_IMAGE_PATH + imageUri.getLastPathSegment());
                    filePath = file.getPath();
                    fileOutputStream = new FileOutputStream(file);
                    bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
                    bufferedOutputStream.write(bytes);

                    new NetWorkDecodeAsyncTask().execute(filePath, imageView, imageUri.getLastPathSegment());
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (bufferedOutputStream != null) {
                        try {
                            bufferedOutputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (fileOutputStream != null) {
                        try {
                            fileOutputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

            @Override
            public void onCallbackError() {
                Log.d("edward", "doGetRequest onCallbackError !!");
            }
        });
    }

    private class NetWorkDecodeAsyncTask extends AsyncTask<Object, Void, Bitmap> {

        private String localFilePath;
        private ImageView imageView;
        private String fileName;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected Bitmap doInBackground(Object... objects) {
            localFilePath = (String) objects[0];
            imageView = (ImageView) objects[1];
            fileName = (String) objects[2];
            return decodeImage(localFilePath);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                String cacheFilePath = Constant.SYSTEM_DATABASE_CACHE_PATH + fileName;
                Log.d("edward", "NetWorkDecodeAsyncTask set bitmap from network cacheFilePath = " + cacheFilePath);
                localCacheUtil.setBitmapToLocal(cacheFilePath, bitmap);
                memoryCacheUtil.setBitmapToMemory(cacheFilePath, bitmap);
            }
        }
    }

    private Bitmap decodeImage(String localFilePath) {
        return BitmapDecodeUtil.decodeSampledBitmapFromFile(localFilePath, 150, 150);
    }

}
  • 本地开启子线程经过OkHttp下载图片到本地。
  • 下载完成以后,开启AsyncTask执行decode bitmap,ImageView.setBitmap等操做。
  • BitmapDecodeUtil对decode到的bitmap进行缩放处理,保证bitmap的大小在合理范围内。

图片加载

在用户可接受范围内,尽量的下降图片的质量,达到节约内存的效果。

BitmapDecodeUtil.java

package com.example.edwardadmin.ormdatabase.util;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import com.example.edwardadmin.ormdatabase.config.Constant;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class BitmapDecodeUtil {

    public static Bitmap decodeSampledBitmapFromFile(String filePath, int requestWidth, int requestHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        FileInputStream is = null;
        Bitmap bitmap = null;
        try {
            File file = new File(filePath);
            if (!file.exists()) {
                return bitmap;
            }
            is = new FileInputStream(filePath);
            BitmapFactory.decodeFileDescriptor(is.getFD(), null, options);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight);
        options.inJustDecodeBounds = false;
        try {
            bitmap = BitmapFactory.decodeFileDescriptor(is.getFD(), null, options);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NullPointerException ne) {
            ne.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                }
            }
        }
        return bitmap;
    }

    public static Bitmap decodeSampledBitmapFromBytes(byte[] bytes, int requestWidth, int requestHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        Bitmap bitmap = null;
        BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

        options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight);

        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        return bitmap;
    }

    private static int calculateInSampleSize(BitmapFactory.Options options, int requestWidth, int requestHeight) {
        final int width = options.outWidth;
        final int height = options.outHeight;
        int inSampleSize = 1;

        if (width > requestWidth || height > requestHeight) {
            final int halfWidth = width / 2;
            final int halfHeight = height / 2;
            while ((halfHeight / inSampleSize) >= requestHeight && (halfWidth / inSampleSize) > requestWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}
  • 第一种采起BitmapFactory.decodeFileDescriptor的方式获取,第二种采起BitmapFactory.decodeByteArray的方式获取。
  • 经过requestWidth、requestHeight来获得合适的inSampleSize。

缓存工具类

  • 初始化全部的缓存类
  • 按照内存缓存>文件缓存>网络下载的方式进行获取图片

效率:内存缓存>文件缓存>网络下载(速度)

代码实现:

CacheUtil.java

package com.example.edwardadmin.ormdatabase.cache;

import android.graphics.Bitmap;
import android.net.Uri;
import android.widget.ImageView;

import com.example.edwardadmin.ormdatabase.config.Constant;

public class CacheUtil {

    private static CacheUtil instance;
    private MemoryCacheUtil memoryCacheUtil;
    private LocalCacheUtil localCacheUtil;
    private NetworkCacheUtil networkCacheUtil;

    private CacheUtil() {
        this.memoryCacheUtil = MemoryCacheUtil.getInstance();
        this.localCacheUtil = LocalCacheUtil.getInstance();
        this.networkCacheUtil = new NetworkCacheUtil();
    }

    public static CacheUtil getInstance() {
        if (instance == null) {
            synchronized (CacheUtil.class) {
                if (instance == null) {
                    instance = new CacheUtil();
                }
            }
        }
        return instance;
    }

    public void putBitmapIntoCache(String filePath, Bitmap bitmap) {
        //1.将图片的字节数组写入到内存中
        memoryCacheUtil.setBitmapToMemory(filePath, bitmap);
        //2.将图片保存到文件中
        localCacheUtil.setBitmapToLocal(filePath, bitmap);
    }

    public void displayImage(Uri imageUri, ImageView imageView, String personNumber) {
        String cacheFilePath =  Constant.SYSTEM_DATABASE_CACHE_PATH + imageUri.getLastPathSegment();
        //1.先从缓存中取bitmap
        Bitmap bitmap;
        bitmap = memoryCacheUtil.getBitmapFromMemory(cacheFilePath);
        if (bitmap == null) {
            //2.再从缓存文件中取bitmap
            bitmap = localCacheUtil.getBitmapFromLocal(cacheFilePath);

            //3.bitmap存在文件中,可是没有在内存中,因此此处添加到内存中
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                memoryCacheUtil.setBitmapToMemory(cacheFilePath, bitmap);
                return;
            }

            //4.download image from network.
            if (bitmap == null) {
                networkCacheUtil.downloadImageFromNetWork(imageUri, imageView);
            }
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

}
  1. 先从缓存中取bitmap
  2. 再从缓存文件中取bitmap
  3. bitmap存在文件中,可是没有在内存中,因此此处添加到内存中
  4. 从网络上下载图片

案例使用

PersonAdapter.java

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder;
    if (convertView == null) {
        viewHolder = new ViewHolder();
        convertView = LayoutInflater.from(mContext).inflate(R.layout.person_item_layout, null, true);
        viewHolder.personName = convertView.findViewById(R.id.person_name);
        viewHolder.personSex = convertView.findViewById(R.id.person_sex);
        viewHolder.personAge = convertView.findViewById(R.id.person_age);
        viewHolder.personHeight = convertView.findViewById(R.id.person_height);
        viewHolder.personNative = convertView.findViewById(R.id.person_native);
        viewHolder.personNumber = convertView.findViewById(R.id.person_number);
        viewHolder.personTime = convertView.findViewById(R.id.person_time);
        viewHolder.personView = convertView.findViewById(R.id.person_image);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (ViewHolder) convertView.getTag();
    }

    PersonInfo personInfo = personInfoArrayList.get(position);
    viewHolder.personName.setText(personInfo.getPersonName());
    viewHolder.personSex.setText(personInfo.getPersonSex());
    viewHolder.personAge.setText(personInfo.getPersonAge());
    viewHolder.personHeight.setText(personInfo.getPersonHeight());
    viewHolder.personNative.setText(personInfo.getPersonNative());
    viewHolder.personNumber.setText(personInfo.getPersonNumber());

    ForeignCollection<PersonToken> personTokens = personInfo.personTokens;
    if (personTokens != null) {
        StringBuilder builder = new StringBuilder();
        for (PersonToken personToken : personTokens) {
            long time = personToken.getDataToken();
            String token = TimeConvertUtils.formatDate4(time);
            builder.append(token + ",");
        }
        viewHolder.personTime.setText(builder.toString());
    }

    //add new person view
    String personImage = personInfo.getPersonImage();
    String personNumber = personInfo.getPersonNumber();
    if (personImage != null) {
        CacheUtil.getInstance().displayImage(Uri.parse(personImage), viewHolder.personView, personNumber);
    } else {
        viewHolder.personView.setImageResource(R.drawable.head);
    }
    return convertView;
}
  1. 从缓存CacheUtil中拿图片,成功获取图片以后显示到界面上。
  2. 若是获取图片失败,那么使用默认的图片。

问题

1.若是使用AsyncTask,依然存在异常:

Android: Only the original thread that created a view hierarchy can touch its views。

解决方案:

查看下AsyncTask中的doInBackground方法,使用存在刷新view的操做,若是将此操做放到onPostExecute方法中,由于doInBackground方法至关于在子线程中执行操做,那么刷新View确定不能在子线程中。


Github地址:
ORMDataBase:https://github.com/EricWinner/ORMDataBase 有任何问题,欢迎指出.