简介
众所周知,缓存在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);
}
}
}
- 先从缓存中取bitmap
- 再从缓存文件中取bitmap
- bitmap存在文件中,但是没有在内存中,所以此处添加到内存中
- 从网络上下载图片
案例使用
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;
}
- 从缓存CacheUtil中拿图片,成功获取图片之后显示到界面上。
- 如果获取图片失败,那么使用默认的图片。
问题
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
有任何问题,欢迎指出.