一、Bitmap的高效加载
1、加载图片
BitmapFactory类提供了四种方法:
-
decodeFile()
从文件系统加载出一个Bitmap对象,间接调用了decodeStream()方法 -
decodeResource()
从资源文件加载,间接调用了decodeStream()方法 -
decodeStream()
从输入流加载 -
decodeByteArray()
从字节数组加载
2、高效加载的Bitmap
核心思想:采用BitmapFactory.Options来加载所需尺寸的图片。
2.1、Options.inSampleSize(采样率)
如果是inSampleSize=1那么和原图大小一样
如果是inSampleSize=2那么宽高都为原图1/2, 而像素为原图的1/4, 占用的内存大小也为原图的1/4。
若取值小于1,则无效。建议取2的n次方。
现在有一张图片像素为:1024*1024, 如果采用ARGB8888(四个颜色通道每个占有一个字节,相当于1点像素占用4个字节的空间)的格式来存储,那么图片的占有大小就是1024*1024*4那现在这张图片在内存中占用4MB。
如果针对刚才的图片进行inSampleSize=2, 那么最后占用内存大小为512*512*4, 也就是1MB
public class MyBitmapLoadUtil {
/**
* 对一个Resources的资源文件进行指定长宽来加载进内存, 并把这个bitmap对象返回
*
* @param res 资源文件对象
* @param resId 要操作的图片id
* @param reqWidth 最终想要得到bitmap的宽度
* @param reqHeight 最终想要得到bitmap的高度
* @return 返回采样之后的bitmap对象
*/
public static Bitmap decodeFixedSizeForResource(Resources res, int resId, int reqWidth, int reqHeight){
// 首先先指定加载的模式 为只是获取资源文件的大小
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//Calculate Size 计算要设置的采样率 并把值设置到option上
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 关闭只加载属性模式, 并重新加载的时候传入自定义的options对象
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* 一个计算工具类的方法, 传入图片的属性对象和 想要实现的目标大小. 通过计算得到采样值
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//Raw height and width of image
//原始图片的宽高属性
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
// 如果想要实现的宽高比原始图片的宽高小那么就可以计算出采样率, 否则不需要改变采样率
if (reqWidth < height || reqHeight < width){
int halfWidth = width/2;
int halfHeight = height/2;
// 判断原始长宽的一半是否比目标大小小, 如果小那么增大采样率2倍, 直到出现修改后原始值会比目标值大的时候
while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
二、Android中的缓存策略
当程序第一次从网络上加载图片后,将其缓存在存储设备中,下次使用这张图片的时候就不用再从网络从获取了。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,因为从内存中加载图片比存储设备中快。一般情况会把图片存一份到内存中,一份到存储设备中,如果内存中没找到就去存储设备中找,还没有找到就从网络上下载。
缓存策略包含缓存的添加、获取和删除操作。不管是内存还是存储设备,缓存大小都是有限制的。如何删除旧的缓存并添加新的缓存,就对应缓存算法。
目前常用的一种缓存算法是LRU(Least Recently Used),最近最少使用算法。核心思想: 当缓存存满时, 会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种: LruCache和DiskLruCache。LruCache用于实现内存缓存, DiskLruCache则充当了存储设备缓存。
1、LruCache
LruCache是一个泛型类, 它内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象, 其提供了get和put方法来完成缓存的获取和添加的操作。当缓存满了时,LruCache会移除较早使用的缓存对象, 然后在添加新的缓存对象。LruCache是线程安全的。
强引用: 直接的对象引用
软引用: 当一个对象只有软引用存在时, 系统内存不足时此对象会被gc回收
弱引用: 当一个对象只有弱引用存在时, 对象会随下一次gc时被回收
LruCache 典型初始化过程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
只需要提供缓存的总容量大小(一般为进程可用内存的1/8)并重写 sizeOf 方法即可。sizeOf
方法作用是计算缓存对象的大小。这里大小的单位需要和总容量的单位(这里是kb)一致,因此除以1024。一些特殊情况下,需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved
方法,因此可以在entryRemoved中完成一些资源回收工作(如果需要的话)。
还有获取和添加方法,都比较简单:
get(K key) V
remove(K key) V
从Android 3.1开始,LruCache成为Android源码的一部分。
2、DiskLruCache
DiskLruCache用于实现磁盘缓存,DiskLruCache得到了Android官方文档推荐,但它不属于Android SDK的一部分。
2.1、DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建, 他提供了open()方法用于创建自身, 如下所示:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
- File directory:表示磁盘缓存在文件系统中的存储路径。可以选择SD卡上的缓存目录, 具体是指/sdcard/Andriod/data/package_name/cache目录,也可以选择data目录下. 或者其他地方。 这里给出的建议:如果应用卸载后就希望删除缓存文件的话,那么就选择SD卡上的缓存目录, 如果希望保留缓存数据那就应该选择SD卡上的其他目录。
- appVersion: 表示应用的版本号,一般设为1即可。当版本号发生改变的时候DiskLruCache会清空之前所有的缓存文件, 在实际开发中这个实用性不大。
- valueCount: 一般设为1。
- maxSize: 表示缓存的总大小。当缓存大小超出这个设定值后,会清除一些缓存而保证总大小不大于这个设定值。
//初始化DiskLruCache,包括一些参数的设置
public void initDiskLruCache() {
//配置固定参数
// 缓存空间大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
//下载图片时的缓存大小
private static final long IO_BUFFER_SIZE = 1024 * 8;
// 缓存空间索引,用于Editor和Snapshot,设置成0表示Entry下面的第一个文件
private static final int DISK_CACHE_INDEX = 0;
//设置缓存目录
File diskLruCache = getDiskCacheDir(mContext, "bitmap");
if (!diskLruCache.exists())
diskLruCache.mkdirs();
//创建DiskLruCache对象,当然是在空间足够的情况下
if (getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskLruCache,
getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskLruCache = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
//上面的初始化过程总共用了3个方法
//设置缓存目录
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
// 获取可用的存储大小
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD)
return path.getUsableSpace();
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
//获取应用版本号,注意不同的版本号会清空缓存
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
2.2、DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor完成的, Editor表示一个缓存对象的编辑对象.
如果还是缓存图片为例子, 每一张图片都通过图片的url为key, 这里由于url可能会有特殊字符所以采用url的md5值作为key. 根据这个key就可以通过edit()来获取Editor对象, 如果这个缓存对象正在被编辑, 那么edit()就会返回null. 即DiskLruCache不允许同时编辑一个缓存对象.
当用.edit(key)获得了Editor对象之后. 通过editor.newOutputStream(0)就可以得到一个文件输出流. 由于之前open()方法设置了一个节点只能有一个数据. 所以在获得输出流的时候传入常量0即可.
有了文件输出流, 可以当网络下载图片时, 图片就可以通过这个文件输出流写入到文件系统上.最后,要通过Editor中commit()来提交写操作, 如果下载中发生异常, 那么使用Editor中abort()来回退整个操作.
2.3、DiskLruCache的缓存查找
和缓存的添加过程类似, 缓存查找过程也需要将url转换成key, 然后通过DiskLruCache#get()方法可以得到一个Snapshot对象, 接着在通过Snapshot对象即可得到缓存的文件输入流, 有了文件输入流, 自然就可以得到Bitmap对象. 为了避免加载图片出现OOM所以采用压缩的方式. 在前面对BitmapFactory.Options的使用说明了. 但是这中方法对FileInputStream的缩放存在问题. 原因是FileInputStream是一种有序的文件流, 而两次decodeStream调用会影响文件的位置属性, 这样在第二次decodeStream的时候得到的会是null. 针对这一个问题, 可以通过文件流来得到它所对应的文件描述符, 然后通过BitmapFactory.decodeFileDescription()来加载一张缩放后的图片.
/**
* 磁盘缓存的读取
* @param url
* @param reqWidth
* @param reqHeight
* @return
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
if(Looper.myLooper() == Looper.getMainLooper())
Log.w(TAG, "it's not recommented load bitmap from UI Thread");
if(mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyForDisk(url);
Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null)
{
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fd = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if(bitmap != null)
addBitmapToMemoryCache(key, bitmap);
}
return bitmap;
}
3、ImageLoader的实现
一个好的ImageLoader应该具备以下几点:
- 图片的压缩
- 网络拉取
- 内存缓存
- 磁盘缓存
- 图片的同步加载
- 图片的异步加载
异步加载过程:
- bindBitmap先尝试从内存缓存读取图片,如果没有会在线程池中调用loadBitmap方法。获取成功将图片封装为LoadResult对象通过mMainHandler向UI线程发送消息。选择线程池和Handler来提供并发能力和异步能力。
- 为了解决View复用导致的列表错位问题,在给ImageView设置图片之前都会检查它的url有没有发生改变,如果改变就不再给它设置图片。
三、ImageLoader的使用
1、照片墙效果
实现照片墙效果,如果图片都需要是正方形;这样做很快,自定义一个ImageView,重写onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,widthMeasureSpec);//将原来的参数heightMeasureSpec换成widthMeasureSpec
}
2、优化列表的卡顿现象
- 不要在getView中执行耗时操作,不要在getView中直接加载图片。
- 控制异步任务的执行频率:如果用户刻意频繁上下滑动,getView方法会不停调用,从而产生大量的异步任务。可以考虑在列表滑动停止加载图片;给ListView或者GridView设置 setOnScrollListener 并在 OnScrollListener 的 onScrollStateChanged 方法中判断列表是否处于滑动状态,如果是的话就停止加载图片。
- 大部分情况下,可以使用硬件加速解决莫名卡顿问题,通过设置 android:hardwareAccelerated=”true” 即可为Activity开启硬件加速。