这些读书笔记是根据《Android开发艺术探索》和《Android群英传》这两本书,然后将书上的一些知识点做一下记录。方便学习和理解,如果有存在侵犯版权的地方,还麻烦告知。个人强烈建议购买这两本书。真心不错。
本节是和《Android开发艺术探索》中的第12章 “Bitmap的加载和Cache” 有关系,建议先买书查看这一章。
[] Bitmap的高效加载
加载Bitmap是很容易出现内存溢出,因为Android对单个app有内存限制,可以采用Bitmap的二次采样来压缩图片。
Bitmap占用的内存为:像素总数 * 每个像素占用的内存,在Android中,Bitmap有四种像素类型:ARGB_8888、ARGB_4444、ARGB_565、ALPHA_8,他们每个像素占用的字节数分别为4、2、2、1。因此,一个2000*1000的ARGB_8888类型的Bitmap占用的内存为2000*1000*4=8000000B=8MB。
获取单个app的内存限制(返回的单位是M)
ActivityManager activityManager = (ActivityManager)
this.getSystemService(Context.ACTIVITY_SERVICE);
int memory= activityManager.getMemoryClass();
Bitmap不能直接实例化,可以通过BitmapFactory的decode…()方法获得Bitmap对象,分别是decodeFile(),decodeResource(),decodeStream(),decodeByteArray()。分别从文件,资源,输入流,字节数组中加载一个Bitmap对象。
Bitmap的二次采样,其实就是通过BitmapFactory.Options按照一定的采样率来加载缩小后的图片,
1.将 BitmapFactory.Options 的 inJustDecodeBounds 参数设置为 true 并加载图片。
inJustDecodeBounds 参数为true时,BitmapFactory并不会真正去加载图片,只会去解析图片的原始宽和高信息。
2.从 BitmapFactory.Options 中获取图片的原始宽和高,分别是outWidth 和 outHeight 参数。
inJustDecodeBounds 参数为true时,BitmapFactory获取图片的宽和高与图片存放的位置和手机分辨率有关。
3.根据采样率的规则并结合目标View所需的大小计算出采样率inSampleSize ,并设置给 BitmapFactory.Options 的 inSampleSize 参数。
采样率为n时,图片的宽和高为别为原始图的1/n,内存大小为1/n*n,但是n必须是2的指数,如果n小于1,按照为1来处理。如果n不为2的指数,系统会向下取整选择一个最接近2的指数来代替。
4.将 BitmapFactory.Options 的 inJustDecodeBounds 参数设置为 false 并加载图片
Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, id, options);
options.inSampleSize = computeSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(res, id, options);
**ImageView.setImageBitmap(bitmap);
bitmap.recycle();
//计算采样率
public int computeSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (width > reqWidth || height > reqHeight) {
int halfWidth = width / 2;
int halfHeight = height / 2;
while ((halfWidth / inSampleSize) >= reqWidth && (halfHeight / inSampleSize) >= reqHeight) {
inSampleSize = inSampleSize * 2;
}
}
return inSampleSize;
}
注:
1. decodeStream()方法有点特殊,和其他的3个方法有点不一样。是因为FileInputStream 是一种有序的文件流,两次调用decodeStream()影响了文件流的位置属性,导致第二次调用decodeStream()得到的是null。
[] 内存缓存(LruCache)
Lru是一种算法,核心思想是当缓存快要满时,会移除最近最少使用的缓存目标。
LruCache 用于实现内存缓存,LruCache 是一个泛型类,LruCache 是线程安全的。
LruCache 是Android 3.1提供的一个缓存类,可以通过support-4兼容包来兼容早期的Anroid版本。
LruCache 内部采用一个 LinkedHashMap 以强引用的方式存储外界的缓存对象,其提供的get()和put()方法来完成缓存的获取和添加,当缓存满时,LruCache会移除最近最少使用的缓存对象。然后再添加新的缓存对象。
强引用:直接的对象引用
软引用:当一个对象只有软应用存在时,系统内存不足时此对象会被gc回收。
弱引用:当一个对象只有弱应用存在时,此对象随时会被gc回收。
{}LruCache 初始化
//获取当前进程的可用内存(单位为kb)
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//把可用内存的1/8作为缓存内存
int cacheSize = maxMemory / 8;
//创建LruCache 对象,初始化缓存的总容量大小
LruCache mLruCache = new LruCache<String, Bitmap>(cacheSize) {
//移除缓存对象回调
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
/**
*
* @param key
* @param bitmap
* @return
* 计算缓存对象的大小
*/
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//计算Bitmap 的大小
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
只需要初始化缓存的总容量大小,并重写sizeOf(),sizeOf()的作用是计算缓存对象的大小。sizeOf()中返回缓存对象的大小的单位要和初始化缓存的总容量大小单位保持一致。
LruCache 移除缓存对象回调 entryRemoved(),可以在entryRemoved()中完成一些资源的回收。
{}LruCache 添加缓存对象
mLruCache.put(K key, V value)
{}LruCache 获取缓存对象
mLruCache.get(K key)
{}LruCache 删除缓存对象
mLruCache.remove(K key)
[] 存储缓存(DiskLruCache)
DiskLruCache用于实现存储设备缓存,即磁盘缓存,它是通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache得到了Android的官方推荐,但是不是sdk的一部分。需要去下载DiskLruCache源码修改一下才能使用。
DiskLruCache 修改后的源码:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
{}DiskLruCache 的创建
DiskLruCache 不能通过构造方法来创建,而是通过open()来创建。
open(File directory, int appVersion, int valueCount, long maxSize)
directory参数:磁盘缓存在文件系统中的存储路径,缓存路径可以选择sd卡上的缓存目录,具体是指/sdcard/Android/data/包名/cache目录,当应用被卸载后,此目录会一并删除。当然也可以选择sd卡上的其他指定目录,如果应用卸载后就希望删除缓存文件,那么就选择sd卡上的缓存目录,如果希望保留缓存数据那就选择sd卡上的其他特定目录。
appVersion参数:应用的版本号,一般设为1,当版本号发生改变时DiskLruCache 会清空之前所有的缓存文件。
valueCount参数:单个节点所对应的数据的个数,一般设为1,
maxSize参数:缓存的总大小,
{}DiskLruCache 的缓存添加
DiskLruCache 的缓存添加是通过 Editor 来完成的,Editor 表示一个缓存对象的编辑对象,
以图片缓存为例,先获取图片 url 对应的 key (一般采用 url 的 md5 值作为 key,因为图片的 url 中可能有特殊字符),再根据key调用DiskLruCache 对象的 edit()来获取 Editor 对象。如果这个缓存对象正在被编辑,那么edit()会返回null,即DiskLruCache 不允许同时编辑一个缓存对象,如果当前不存在其他的Editor 对象,那么edit() 会返回一个新的Editor 对象。
通过Editor 对象调用newOutputStream()获得文件输出流,再通过文件输出流写入到文件系统中,
通过Editor 对象的commit()来提交写入操作,提交操作执行成功后才真正写入到文件系统中,如果出现异常,可以通过Editor 对象的abort()来回退整个操作。
DiskLruCache mDiskLruCache = DiskLruCache.open(new File(""), 1, 1, int maxSize);
//获取图片url对应的md5值
String key = hashKeyFromUrl("");
DiskLruCache.Editor mEditor = mDiskLruCache.edit(key);
if (mEditor != null) {
//因为创建DiskLruCache对象时单个节点只有一个数据,所以参数直接设置为0
OutputStream mOutputStream = mEditor.newOutputStream(0);
try {
//写入到文件系统中
mOutputStream.write(byte[] buffer);
//提交写操作
mEditor.commit();
} catch (Exception e) {
e.printStackTrace();
//回退整个操作
mEditor.abort();
} finally {
mDiskLruCache.flush();
}
}
{}DiskLruCache 的缓存查找
以图片缓存为例,先获取图片 url 对应的 key (一般采用 url 的 md5 值作为 key,因为图片的 url 中可能有特殊字符),再根据key调用DiskLruCache 对象的 get()来获取 Snapshot对象。
通过Snapshot对象调用getInputStream()得到缓存的文件输入流,再通过文件输入流就能得到Bitmap对象。
DiskLruCache mDiskLruCache = DiskLruCache.open(new File(""), 1, 1, int maxSize);
//获取图片url对应的md5值
String key = hashKeyFromUrl("");
DiskLruCache.Snapshot mSnapshot = mDiskLruCache.get(key);
if (mSnapshot != null) {
//因为创建DiskLruCache对象时单个节点只有一个数据,所以参数直接设置为0
FileInputStream mFileInputStream = (FileInputStream) mSnapshot.getInputStream(0);
//在通过图片二次采样来加载压缩图片
FileDescriptor mFileDescriptor = mFileInputStream.getFD();
.......
BitmapFactory.decodeFileDescriptor(mFileDescriptor , null, options);
.......
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(mFileDescriptor , null, options);
.......
}
注:
1,FileInputStream 是一种有序的文件流,两次调用decodeStream()影响了文件流的位置属性,导致第二次调用decodeStream()得到的是null,解决这个问题,可以通过文件流来得到对应的文件描述符,再通过BitmapFactory.decodeFileDescriptor()来加载一张压缩图片。
{}DiskLruCache 的删除操作
mDiskLruCache.remove(String key)
mDiskLruCache.delete()
[] 优化列表的卡顿现象
{}避免发生列表item错位的解决方法
给显示图片的ImageView添加tag属性,值为要加载的图片的目标url,显示的时候判断一下url是否匹配,如果匹配就加载图片,如果不匹配就不加载图片。
{}列表滑动时停止加载图片,列表停下来再加载图片。
给ListView设置setOnScrollListener监听,并在setOnScrollListener的onScrollStateChanged()判断列表是否处在滑动状态,如果是的话就停止加载图片,如果不是的话就加载图片。
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
//加载图片
} else {
//不加载图片
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
{}通过设置 android:hardwareAccelerated=”true” 即可以为 Activity开启硬件加速