简单的实现三级缓存

简介

众所周知,缓存在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
有任何问题,欢迎指出.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容