LruCache与图片的二次采样笔记:
一: LruCache :least recently used
1.为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作(garbage collection:垃圾回收)。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,开发者会经常使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
2.为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
1》你的设备可以为每个应用程序分配多大的内存?(这个在程序中可以获取到)
2》设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
3》你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
4》图片的尺寸和大小,还有每张图片会占据多少内存空间。
5》图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
3.LruCache的用法
1》.先从内存缓存中查找图片,如果找到,直接加载
private static LruCache<String, Bitmap> mLruCache;
private static void initLruCache() {
// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
int maxMemory = (int) Runtime.getRuntime().maxMemory();
// 一般情况,我们使用最大可用内存值的1/8作为缓存的大小即可。
int cacheSize = maxMemory / 8;
// LruCache通过构造函数传入缓存值,以KB为单位。
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
// 重写此方法来衡量每张图片的大小。
// final int sizeOf = value.getRowBytes() * value.getHeight();
final int sizeOf = value.getByteCount();
return sizeOf;
}
//强引用的缓存值达到预设定值之前,会从内存中移除最近较少用的对象。
//这时候我们可以使用软引用保存起来被强引用移除的对象。留作备份。
@Override -----不推荐使用
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
//被系统删除引起的才移到软引用。目前已不推荐使用
if (evicted) {
addBitmapToSoftCache(key, oldValue);
}
}
};
}
//添加Bitmap到LruCache中
public static void addBitmapToLruCache(String key, Bitmap bitmap) {
if (getBitmapFromLruCache(key) == null) {
mLruCache.put(key, bitmap);
Log.d(TAG, "---图片缓存到LruCache中 key ==" + key);
Log.d(TAG, "---图片缓存到LruCache中 bitmap ==" + bitmap);
}
}
//从Bitmap中取出Bitmap
private static Bitmap getBitmapFromLruCache(String key) {
Bitmap bitmap = mLruCache.get(key);
Log.d(TAG, "---LruCache中得到key == " + key);
Log.d(TAG, "---LruCache中得到Bitmap == " + bitmap);
return bitmap;
}
2》.如果没有找到,从网络下载
3》图片的三级(四级缓存)流程:
一:根据指定的url(Key)从强引用中查找BItmap对象,如果找到直接使用,否则:
二:根据指定的url(Key)从软引用中查找Bitmap对象,如果找到直接使用,否则:
三:根据指定的url(FileName)从本地磁盘SDCard中查找缓存的Bitmap对象,如果找到直接加载使用,否则:
四:根据url请求网络加载图片,显示使用。
注意:第四要缓存第一和第三;第三要缓存第一;第二要缓存第一并删除自己;不推荐使用第二,建议直接跳过第二。
4》强引用:(在Android中LruCache就是强引用缓存)
平时我们编程的时候例如:Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OOM异常,使程序异常终止,也不会回收具有强引用的对象来解决内存不足问题。
5》软引用(SoftReference):
软引用类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
使用软引用能防止内存泄露,增强程序的健壮性。
二.例子:加载图片
MainActivity.java
package hsl.com.lrucachedemo;
import android.app.Activity;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.content.Loader;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import hsl.com.lrucachedemo.HttpUtils;
import hsl.com.lrucachedemo.R;
//1.实现LoaderCallBacks
//2.获取LoaderManager
//3.启动Lodaer
public class MainActivity extends Activity implements LoaderCallbacks<Bitmap> {
ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
}
// 点击开始下载
public void download(View view) {
// 启动Loader
LoaderManager manager = getLoaderManager();
// restartLoader:如果loader是初次启动,initLader,如果已经启动,就重新再加载
manager.restartLoader(0, null, this);
}
@Override
public Loader<Bitmap> onCreateLoader(int id, Bundle args) {
MyLoader loader = new MyLoader(this);
return loader;
}
@Override
public void onLoadFinished(Loader<Bitmap> loader, Bitmap data) {
// TODO Auto-generated method stub
iv.setImageBitmap(data);
}
@Override
public void onLoaderReset(Loader<Bitmap> loader) {
// TODO Auto-generated method stub
}
// 拼接url
static int i = 0;
// 实现MyLoader,一定要写成static
private static class MyLoader extends AsyncTaskLoader<Bitmap> {
public MyLoader(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
@Override
public Bitmap loadInBackground() {
String http = "http://www.ytmfdw.com/image/img" + (++i%5+1) + ".jpg";
Bitmap bitmap = null;
try {
bitmap = HttpUtils.getBitmap(http);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return bitmap;
}
@Override
protected void onStartLoading() {
// TODO Auto-generated method stub
super.onStartLoading();
// 取消前面的任务,重新加载
forceLoad();
}
}
}
三.例子:文件缓存
ImageCache.java
package hsl.com.lrucachedemo;
import java.util.HashMap;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.util.LruCache;
import hsl.com.intentservicedemo.SdUtil;
public class ImageCache {
/**
* 文件缓存位置
*/
public static final String CACHE_PATH = "qianfeng/cache/";
//单例模式
private static ImageCache imageCache = null;
private LruCache<String, Bitmap> cache;
private ImageCache() {
initLruCache();
}
public static ImageCache getInstance() {
if (imageCache == null) {
imageCache = new ImageCache();
}
return imageCache;
}
//初始化
private void initLruCache() {
//计算最大内存
long max = Runtime.getRuntime().maxMemory();
//缓存大小一般设置为最大的1/8
Log.d("TAG", "cache size=" + max / 8);
cache = new LruCache<String, Bitmap>((int) (max / 8)) {
@Override
protected int sizeOf(String key, Bitmap value) {
//重写该方法,计算每张图片所占内存大小
return value.getByteCount();
}
};
}
public Bitmap get(String http) {
//先从内存中找,如果没有,再查下磁盘中是否有
Bitmap bitmap = cache.get(http);
if (bitmap == null) {
String fileName = http.substring(http.lastIndexOf("/") + 1);
bitmap = SdUtil.getBitmapFromSD(fileName, CACHE_PATH);
}
return bitmap;
}
public void put(String http, Bitmap bitmap) {
cache.put(http, bitmap);
//保存一份到磁盘中
try {
String fileName = http.substring(http.lastIndexOf("/") + 1);
SdUtil.saveFile(bitmap, fileName, CACHE_PATH);
} catch (Exception e) {
e.printStackTrace();
}
}
}
SDUtils.java
package hsl.com.intentservicedemo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Created by Administrator on 2016/8/5 0005.
*/
public class SdUtil {
/**
* 保存文件到SD卡中
*
* @param in 网络文件输入流
* @param fileName 要保存的文件名
* @param filePath 文件保存在哪个目录,不包含SD卡跟路径
*/
public static void saveFile(InputStream in, String fileName, String filePath) throws Exception {
//先判断SD卡是否可用
if (!isMount()) {
Log.e("TAG", "================SD卡不可用===========");
return;
}
String path;
//获取SDcart路径
if (filePath == null) {
//保存到SDcard根目录
path = getSDcardPath();
} else {
path = getSDcardPath() + filePath;
}
//创建文件
File file = new File(path);
//如果目录文件不存在,则创建一个
if (!file.exists()) {
file.mkdirs();
} else {
//目录文件已经存在,但如果不是目录,则直接返回
if (!file.isDirectory()) {
Log.e("TAG", "============保存的路径不是有效的目录============");
return;
}
}
//创建文件
File newFile = new File(file, fileName);
OutputStream out = new FileOutputStream(newFile);
byte[] buffer = new byte[2048];
int tmp = 0;
while ((tmp = in.read(buffer)) != -1) {
out.write(buffer, 0, tmp);
}
out.flush();
out.close();
in.close();
}
/**
* 保存文件到SD卡中
*
* @param arr 保存的字节数组
* @param fileName 要保存的文件名
* @param filePath 文件保存在哪个目录,不包含SD卡跟路径
* @return 返回保存后的文件名及路径
* @throws Exception
*/
public static String saveFile(byte[] arr, String fileName, String filePath) throws Exception {
//先判断SD卡是否可用
if (!isMount()) {
Log.e("TAG", "================SD卡不可用===========");
return null;
}
String path;
//获取SDcart路径
if (filePath == null) {
//保存到SDcard根目录
path = getSDcardPath();
} else {
path = getSDcardPath() + filePath;
}
//创建文件
File file = new File(path);
//如果目录文件不存在,则创建一个,创建多级目录
if (!file.exists()) {
file.mkdirs();
} else {
//目录文件已经存在,但如果不是目录,则直接返回
if (!file.isDirectory()) {
Log.e("TAG", "============保存的路径不是有效的目录============");
return null;
}
}
//创建文件
File newFile = new File(file, fileName);
String newPath = newFile.getAbsolutePath();
OutputStream out = new FileOutputStream(newFile);
out.write(arr, 0, arr.length);
out.flush();
out.close();
return newPath;
}
/**
* 保存图片到SD卡中
*
* @param bitmap 要保存的位图
* @param fileName 要保存的文件名
* @param filePath 文件保存在哪个目录,不包含SD卡跟路径
* @return 返回保存后的文件名及路径
* @throws Exception
*/
public static String saveFile(Bitmap bitmap, String fileName, String filePath) throws Exception {
//先判断SD卡是否可用
if (!isMount()) {
Log.e("TAG", "================SD卡不可用===========");
return null;
}
String path;
//获取SDcart路径
if (filePath == null) {
//保存到SDcard根目录
path = getSDcardPath();
} else {
path = getSDcardPath() + filePath;
}
//创建文件
File file = new File(path);
//如果目录文件不存在,则创建一个
if (!file.exists()) {
file.mkdirs();
} else {
//目录文件已经存在,但如果不是目录,则直接返回
if (!file.isDirectory()) {
Log.e("TAG", "============保存的路径不是有效的目录============");
return null;
}
}
//创建文件
File newFile = new File(file, fileName);
String newPath = newFile.getAbsolutePath();
OutputStream out = new FileOutputStream(newFile);
//保存bitmap到sd卡
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
out.flush();
out.close();
return newPath;
}
/**
* 从指定目录得到 Bitmap
*
* @param name 文件名
* @param path 缓存目录
* @return
*/
public static Bitmap getBitmapFromSD(String name, String path) {
String filePath = getSDcardPath() + path;
File file = new File(filePath, name);
if (file.exists()) {
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
return bitmap;
}
return null;
}
/**
* 判断SD卡是否挂载
*
* @return
*/
public static boolean isMount() {
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
/**
* 获取SD卡路径,后面加上分隔符File.separator:/
*
* @return
*/
public static String getSDcardPath() {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
}
}
四。二次采样
1.为了避免加载图片的时候,出现OOM
2.android在内存中,只会开辟出当前屏幕大小的位图的大小的四倍的空间。
举个例子:假如你的手机是854*480的那么像素数是409920,每个像素在内存中占4B,那么就是1639680个字节,乘以4后得到 6405KB,那么也就是说所有的Bitmap对象只能占据内存的6405KB空间,当你对Bitmap对象做缩放等等操作时,一定要保证小于当前值。
Android系统支持几种图片(.png , .jpg , .gif ), 其中Bitmap位图#ffffffff,包括图片透明度Alpha和RGB,图片质量很好,每一个像素位占4个字节,如果图片很大将会占据很大的内存空间。存储在SDCard的image很小,加载进内存可能就会很大。因此,对bitmap图像进行操作,应该特别小心,可能出现内存溢出问题。为此对于大图片,应该引入该图片的二次采样生成缩略图。
3.封装二次采样生成缩略图工具类
**
* 对图片进行二次采样,生成缩略图。放置加载过大图片出现内存溢出
* 参数一:文件的路径
* 参数二:缩略图的宽度(一般是需要展示的View的宽度)
* 参数三:缩略图的高度(同上)
*/
public static Bitmap createThumbnail(String filePath, int newWidth, int newHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//
/**
* BitmapFactory.Options 的inJustDecodeBounds 一旦设置为true,
* BitmapFactory解码后返回值为null,只会
* 通过Options的outHeight和outWidth可以获得图片的宽高。
*/
BitmapFactory.decodeFile(filePath, options);//开始解析图片的边缘
int imageHeight = options.outHeight;// 拿到图片的高度
int imageWidth = options.outWidth;// 拿到图片的宽度
Log.d(TAG, "imageHeight =" + imageHeight + " imageWidth = " + imageWidth);
Log.d(TAG, "newHeight =" + newHeight + " newWidth = " + newWidth);
// 计算缩放比
int scole = 1;// 默认缩放比例为1;
Log.d(TAG, "scole =" + scole);
// Math.ceil---->取最近的整数。如:2.3=>3。7.1=>8(为了让得到的比例强转)
int scoleY = (int) Math.ceil(imageHeight / newHeight);// 得到高度的缩放比例
int scoleX = (int) Math.ceil(imageWidth / newWidth);// 得到缩放的宽度比例
Log.d(TAG, "scoleY =" + scoleY + " scoleX = " + scoleX);
if (scoleY > 1 || scoleX > 1) {
if (scoleY > scoleX) {
scole = scoleY;
} else {
scole = scoleX;
}
}
Log.d(TAG, "修改完比例之后 scole =" + scole);
// 修改完比例之后。就把JustDecodeBounds设置为false。真正去解析图片里面的数据
options.inJustDecodeBounds = false;
options.inPreferredConfig = Config.ALPHA_8;// 设置图片的质量
options.inSampleSize = scole;// 设置图片采样的比例
return BitmapFactory.decodeFile(filePath, options);
}
在开发中对图片的处理是必要的,接下来我们来解决这个图片内存溢出问题:
3.资料:Android.graphics.Bitmap类里有一个内部类Bitmap.Config类,
1》枚举变量
public static final Bitmap.Config ALPHA_8
public static final Bitmap.Config ARGB_4444
public static final Bitmap.Config ARGB_8888
public static final Bitmap.Config RGB_565
2》其实这都是色彩的存储方法:我们知道ARGB指的是一种色彩模式,里面A代表Alpha,R表示red,G表示green,B表示blue,其实所有的可见色都是右红绿蓝组成的,所以红绿蓝又称为三原色,每个原色都存储着所表示颜色的信息值
3》说白了就ALPHA_8就是Alpha由8位组成
ARGB_4444就是由4个4位组成即16位,
ARGB_8888就是由4个8位组成即32位,
RGB_565就是R为5位,G为6位,B为5位共16位
4》由此可见:
ALPHA_8 代表8位Alpha位图
ARGB_4444 代表16位ARGB位图
ARGB_8888 代表32位ARGB位图
RGB_565 代表8位RGB位图
5》位图位数越高代表其可以存储的颜色信息越多,当然图像也就越逼真
五。例子:加载大图片
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
iv.setImageResource(R.drawable.oom);
}
1.使用二次采样
/**
* 对图片进行二次采样,生成缩略图。放置加载过大图片出现内存溢出
* 参数一:文件的路径
* 参数二:缩略图的宽度(一般是需要展示的View的宽度)
* 参数三:缩略图的高度(同上)
*/
public static Bitmap createThumbnail(String filePath, int newWidth, int newHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//
/**
* BitmapFactory.Options 的inJustDecodeBounds 一旦设置为true,
* BitmapFactory解码后返回值为null,只会
* 通过Options的outHeight和outWidth可以获得图片的宽高。
*/
BitmapFactory.decodeFile(filePath, options);//开始解析图片的边缘
int imageHeight = options.outHeight;// 拿到图片的高度
int imageWidth = options.outWidth;// 拿到图片的宽度
Log.d(TAG, "imageHeight =" + imageHeight + " imageWidth = " + imageWidth);
Log.d(TAG, "newHeight =" + newHeight + " newWidth = " + newWidth);
// 计算缩放比
int scole = 1;// 默认缩放比例为1;
Log.d(TAG, "scole =" + scole);
// Math.ceil---->取最近的整数。如:2.3=>3。7.1=>8(为了让得到的比例强转)
int scoleY = (int) Math.ceil(imageHeight / newHeight);// 得到高度的缩放比例
int scoleX = (int) Math.ceil(imageWidth / newWidth);// 得到缩放的宽度比例
Log.d(TAG, "scoleY =" + scoleY + " scoleX = " + scoleX);
if (scoleY > 1 || scoleX > 1) {
if (scoleY > scoleX) {
scole = scoleY;
} else {
scole = scoleX;
}
}
Log.d(TAG, "修改完比例之后 scole =" + scole);
// 修改完比例之后。就把JustDecodeBounds设置为false。真正去解析图片里面的数据
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.ALPHA_8;// 设置图片的质量
options.inSampleSize = scole;// 设置图片采样的比例
return BitmapFactory.decodeFile(filePath, options);
}
1》BitmapFactory.Options options = new BitmapFactory.Options();
2》options.inJustDecodeBounds = true;//关键
/**
* BitmapFactory.Options 的inJustDecodeBounds 一旦设置为true,
* BitmapFactory解码后返回值为null,只会
* 通过Options的outHeight和outWidth可以获得图片的宽高。
*/
3》BitmapFactory.decodeFile(filePath, options);//开始解析图片的边缘
int imageHeight = options.outHeight;// 拿到图片的高度
int imageWidth = options.outWidth;// 拿到图片的宽度
4》计算缩放比
// 计算缩放比
int scole = 1;// 默认缩放比例为1;
Log.d(TAG, "scole =" + scole);
// Math.ceil---->取最近的整数。如:2.3=>3。7.1=>8(为了让得到的比例强转)
int scoleY = (int) Math.ceil(imageHeight / newHeight);// 得到高度的缩放比例
int scoleX = (int) Math.ceil(imageWidth / newWidth);// 得到缩放的宽度比例
Log.d(TAG, "scoleY =" + scoleY + " scoleX = " + scoleX);
if (scoleY > 1 || scoleX > 1) {
if (scoleY > scoleX) {
scole = scoleY;
} else {
scole = scoleX;
}
}
5》// 修改完比例之后。就把JustDecodeBounds设置为false。真正去解析图片里面的数据
options.inJustDecodeBounds = false;
6》options.inPreferredConfig = Bitmap.Config.ALPHA_8;// 设置图片的质量
options.inSampleSize = scole;// 设置图片采样的比例
7》return BitmapFactory.decodeFile(filePath, options);//返回重新采样的图片