美文网首页
简单的实现三级缓存

简单的实现三级缓存

作者: EdwardWinner | 来源:发表于2018-08-22 09:47 被阅读9次

    简介

    众所周知,缓存在Android中应用广泛,特别是图片很多的情况下,大量的图片加载,不仅加剧了性能消耗,而且容易造成OOM,导致程序崩溃。本文讲述三级缓存的使用以及遇到的问题总结。其中三级缓存分为:内存缓存,文件缓存,网络下载

    内存缓存

    优点:

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

    缺点:

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

    代码实现:

    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方法,计算出要缓存的每张图片的大小。

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

    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
    有任何问题,欢迎指出.

    相关文章

      网友评论

          本文标题:简单的实现三级缓存

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