Learn && Live
虚度年华浮萍于世,勤学善思至死不渝
前言
Hey,欢迎阅读Connor学Android系列,这个系列记录了我的Android原理知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:,话不多说我们马上开始!
1.Bitmap 的高效加载
1.1 Bitmap 的加载
(1)Bitmap 在 Android 中指的是一张图片,可以是 png 格式也可以是 jpg 等其他常见的图片格式
(2)Bitmap 的加载可以通过 BitmapFactory 完成,这个类提供了四类方法
- decodeFile:从文件系统中加载出一个 Bitmap 对象
- decodeResource:从资源中加载出一个 Bitmap 对象
- decodeStream:从输入流中加载出一个 Bitmap 对象
- decodeByteArray:从字节数组中加载出一个 Bitmap 对象
- 其中 decodeFile 和 decodeResource 会间接调用 decodeStream 方法
(3)BitmapFactory 中的这四类方法最终是在 Android 的底层实现的,对应 BitmapFactory 类的几个 native 方法
1.2 Bitmap 的高效加载
(1)由于 Bitmap 的特殊性以及 Android 对应用的内存限制(16MB),导致加载 Bitmap 时很容易出现 OOM,因此需要更高效地加载
(2)Bitmap 高效加载的核心思想就是采用 BitmapFactory.Options 按一定的采样率来加载缩小后的图片,将缩小后的图片显示在页面上,这样就可以通过降低图片的内存占用,从而一定程度上避免了 OOM
(3)BitmapFactory 提供的四类方法都支持 BitmapFactory.Options 参数,通过它们就可以很方便地对一个图片进行采样缩放
(4)BitmapFactory.Options 缩放图片主要用到了它的 inSampleSize 参数,即采样率
- inSampleSize <= 1,采样后的图片大小为图片的原始大小
- inSampleSize > 1,缩放图片,缩放比例为 1 / (inSampleSize ^ 2)
- inSampleSize 推荐为 2 的整数次方,否则会向下取整并选择一个最接近的 2 的整数次方
(5)通过采样率即可有效地加载图片,那应如何获取采样率呢?过程如下
- 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 true 并加载图片,这里说明一下这个参数
- 此参数为 true,BitmapFactory 只会解析图片的原始宽、高及 MIME 类型信息,并不会真正地加载图片,是轻量级的操作
- 从 BitmapFactory.Options 中取出图片的原始宽高信息,对应 outWidth 和 outHeight 参数
- 注意此时获取的图片宽高信息和图片的位置(不同的 drawable 目录下)以及硬件设备(屏幕密度不同)有关
- 根据采样率的规则并结合目标 View 的所需大小计算出采样率 inSampleSize
- 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 false 并重新加载图片,此时就是真正加载缩小后的图片了
2.Android 中的缓存策略
2.1 基本介绍
缓存
Android 的缓存可用于避免过多的流量消耗,一般设计为三级的缓存机制
(1)当程序第一次从网络加载图片后,就将其缓存到存储设备上,这样下次使用这张图片就不需要再从网上获取了
(2)很多时候还会在内存中再缓存一份
(3)这样当应用要请求一张图片时,会首先从内存中获取,如果没有再从存储设备中获取,如果还没有就从网络上下载
缓存策略
(1)缓存策略主要包含缓存的添加、获取和删除,当缓存容量满了,则需要删除一些旧缓存来添加新缓存
(2)如何定义缓存的新旧就是一种缓存的策略,不同的策略对应不同的缓存算法
(3)常用的缓存算法是 LRU,最近最少使用算法,即优先淘汰最近最少使用的缓存对象,采用该算法的缓存主要有两种
- LruCache:用于实现内存缓存
- DiskLruCache:用于实现存储设备缓存
2.2 LruCache
(1)LruCache 是一个泛型类,内部采用一个 LinkedHashMap 以强引用的方式存储外界的缓存对象
public class LruCache<K, V> {
@UnsupportedAppUsage
private final LinkedHashMap<K, V> map;
/** Size of this cache in units. Not necessarily the number of elements. */
private int size;
private int maxSize;
...
}
(2)创建LruCache时只需要提供缓存的总容量大小并重写 sizeOf 方法即可
- 容量一般根据当前线程的可用内存来设置,单位是 KB:Runtime.getRuntime().maxMemory()
- sizeOf 方法用于计算缓存对象,即 Bitmap 对象的大小
- 此外,一些特殊情况下,还需要重写 entryRemoved 方法,当移除旧缓存时会调用该方法,可以完成一些资源回收工作
(3)通过 get、set 方法完成缓存的获取和添加操作
(4)还支持删除操作,可通过 remove 方法删除一个指定的缓存对象
(5)当缓存满时,会移除较早使用的缓存对象,然后再添加新的缓存对象
2.3 DiskLruCache
DiskLruCache 用于实现存储设备缓存,即磁盘缓存,通过将缓存对象写入到文件系统中实现缓存效果
创建
DiskLruCache 不能通过构造方法来创建,需要通过 open 方法来完成,open 方法共有四个参数
(1)第一个参数表示磁盘缓存在文件系统中的存储路径,可以选择 SD 卡上的缓存目录,也可以选择 SD 卡上的其他指定目录
- 如果应用卸载后就删除缓存文件,那么就选择 SD 卡上的缓存目录
- 如果希望保留缓存数据就应该选择 SD 卡上的其他指定目录
(2)第二个参数表示应用的版本号,一般设为1即可。当版本号发生改变时会清空之前所有的缓存文件,但在实际开发中并非那么有效
(3)第三个参数表示单个节点所对应的数据的个数,一般设为1即可
(4)第四个参数表示缓存的总大小,单位是 B,超出这个值时会清楚一些缓存来保证总大小不大于这个设定值
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if(!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
添加
缓存添加的操作是通过 Editor 完成的,Editor 表示一个缓存对象的编辑对象,添加步骤如下
(1)首先需要获取图片 url 对应的 key
- 这里使用 key 而非直接使用 url 是因为 url 中很可能有特殊字符,会影响它 Android 中的直接使用,一般采用 url 的 md5 值作为 key
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < bytes.length; ++i) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if(hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
(2)根据 key 通过 edit 方法获取 Editor 对象,获取到后即可根据它来得到一个文件输出流
- 如果当前不存在其他 Editor 对象,则 edit 方法会返回一个新的 Editor 对象
- 如果这个缓存正在被编辑,则 edit 方法会返回 null,即 DiskLruCache 不允许同时编辑一个缓存对象
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputstream = editor.newOutputStream(DISK_CACHE_INDEX);
}
(3)当我们从网络下载图片时,就可以通过拿到的文件输出流写入到文件系统上了
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpUrlConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpUrlConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
...
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
(4)此时还没有真正地将图片写入文件系统,还必须调用 Editor 的 commit 方法来提交写入操作,如果图片下载过程中发生了异常,还可以通过 Editor 的 abort 方法来回退整个操作
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
查找
和缓存的添加过程类似
(1)将 url 转换为 key
(2)根绝 key 调用 DiskLruCache 的 get 方法获取一个 Snapshot 对象
(3)根据 Snapshot 对象即可得到缓存的文件输入流,进而得到 Bitmap 对象
(4)一般不建议直接加载原始图片,而是加载缩放图片
- 但之前介绍的缩放方法会对 FileInputStream 的缩放存在问题,因为 FileInputStream 是有序的文件流,而两次 decodeStream 调用影响了文件流的位置属性,会导致第二次 decodeStream 时得到的是 null
- 可以通过文件流来获得其对应的文件描述符,然后再通过 BitmapFactory.decodeFileDescriptor 方法来加载一张缩放后的图片
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
删除
可通过 remove、delete 方法完成对磁盘缓存的删除操作