要求:了解drawable加载规则以及图片缓存
Android 中 Bitmap 内存优化
在进行内存优化之前应考虑这些问题:
- 预估加载整张图片的所需要的内存
- 为加载这张图片愿意提供多少内存
- 用于展示这张图片的控件的实际大小
- 当前设备的屏幕尺寸和分辨率
主要是避免资源浪费
Bitmap 占用内存的大小 = Bitmap 宽度大小 * Bitmap 高度大小 = Bitmap 高度 * Bitmap 宽度 * (手机的 dpi / drawable 文件夹的最大 dpi)^ 2 * 每个像素的字节大小
每个像素的字节大小:
Config | 占用字节大小(byte) | 说明 |
---|---|---|
ALPHA_8 (1) | 1 | 单透明通道 |
RGB_565 (3) | 2 | 简易RGB色调 |
ARGB_4444 (4) | 4 | 已废弃 |
ARGB_8888 (5) | 4 | 24位真彩色 |
RGBA_F16 (6) | 8 | Android 8.0 新增(更丰富的色彩表现HDR) |
HARDWARE (7) | Special | Android 8.0 新增 (Bitmap直接存储在graphic memory)注1 |
由此我们可以知道 Bitmap 内存大小的优化方式为:
- 资源文件合理放置,高分辨率图片可以放到高分辨率目录下。
- 使用低色彩的解析模式,如 RGB565,减少单个像素的字节大小。
- 图片缩小,减少尺寸。
Android drawable 微技巧
图片资源放在 drawable 文件夹中,image 最后的大小会根据 dpi 进行缩放,缩放规则是 (image 显示的高宽 = image 图片实际高宽 * 手机的 dpi / drawable 文件夹的最大 dpi),并且最后的值会进行四舍五入。
drawable 文件夹对应的 dpi 范围表格如下:
dpi 范围 | 密度 |
---|---|
0dpi ~ 120dpi | ldpi |
120dpi ~ 160dpi | mdpi |
160dpi ~ 240dpi | hdpi |
240dpi ~ 320dpi | xhdpi |
320dpi ~ 480dpi | xxhdpi |
480dpi ~ 640dpi | xxxhdpi |
根据上面的公式,我们很容易得知,将图片放在低密度的文件夹中时,在高密度的手机显示,图片会被放大;而如果将图片放在高密度文件夹中,在低密度手机上显示,图片会被缩小。而由于图片的放大会增加像素点,并且图片会显得很模糊,但是缩小图片不会有什么副作用。
由于 UI 不可能给每种密度都设置一套图像,但是我们可以要求尽量根据高密度设备设置来设置图片资源,这样将图片放在高密度文件夹中能够节省内存开销。最后根据测试最后将图片放在 drawable-xxhdpi 文件夹是最优的选择,因为 drawable-xxxhdpi 的屏幕密度的设备并不常见,并且密度太高的图片占用的内存本身就已经比较大。综上设置图片时根据 320~480 dpi 进行设置图片,然后将图片放在 drawable-xxhdpi 文件夹中。
图片缓存
内存缓存(LruCache)
在使用内存缓存之前应该考虑如下问题:
- 你的设备可以为每个应用程序分配多大的内存?
- 设备的屏幕上一次最多能显示多少张图片?有多少图片需要预加载,因为有可能很快就要显示在屏幕上。
- 设备的屏幕大小和屏幕分辨率是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
- 图片的尺寸和大小,还有每张图片会占多大的内存空间。
- 图片的被访问频率有多高?应该让访问频率高的图片常驻在内存中,或者使用多个 LruCache 对象来区分不同组的图片。
- 你能维持好数量和质量之间的平衡吗?有时候,存储多个低像素的图片,而后在后台去开启线程加载高像素的图片会更加有效。
用到的就是 LruCache 类,这个类是 Google 官方已经帮我写在源码中的。
具体作用就是利用 LinkedHashMap(散列表)实现的一个 LRU(最近最少使用) 缓存淘汰算法。
使用方法:
使用方法就是用 LruCache.put(K key, V value) 方法,使用「键-值对」的方式存放,然后使用 LruCache.put(K key) 获取资源就行了。
硬盘缓存(DiskLruCache)
主要是用到 DiskLruCache 类,这个类是第三方的,可以去 Google Source 下载,或者去 GitHub 或者 CSDN 上找找。下载好之后将其复制到项目中即可。
打开缓存
用到 DiskLruCache.open() 方法
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
第一个参数就是缓存文件存放的位置,一般都是放在 /sdcard/Android/data//cache 这个路径,这个路径被认定为应用程序的缓存路径,当程序被卸载后会被一起清除。
第二个参数是当前应用程序的版本号,当版本号改变时,缓存的数据会被清除。
第三个参数每个缓存条目的数量,一般填 1 就行。
第四个参数填 10M 就行了。
private void open(Context context) {
try {
File cacheDir = getDiskCacheDir(context, "bitmap");
if (!cacheDir.exists()) {
// mkdirs() 方法不仅会创建 bitmap 文件夹,如果当前文件夹的父文件夹不存在时,也会一并创建
cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
private File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
// 检测是否有 SD 卡,以及 SD 卡是否能被移除
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| Environment.isExternalStorageRemovable()) {
// /sdcard/Android/data/<application package>/cache
cachePath = context.getExternalCacheDir().getPath();
} else {
// /data/data/<application package>/cache
cachePath = context.getCacheDir().getPath();
}
// File.separator 是 Java 封装好的解决不同平台文件分隔符不一样的方法
// uniqueName 是不同资源文件的名称
return new File(cachePath + File.separator + uniqueName);
}
private int getAppVersion(Context context) {
try {
// 当软件的版本号发生变化时,系统会删除缓存路径下所有的数据,这时候需要重新从网络上获取资源
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
写入缓存
用到方法 DiskLruCache.editor()
public Editor edit(String key) throws IOException
editor() 方法的参数很简单就是一个 key,这个 key 将会成为缓存文件的文件名,并且必须和图片的 URL 是一一对应的。
private void download(final String imgUrl) {
new Thread(new Runnable() {
@Override
public void run() {
try {
String key = hashKeyForDisk(imgUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(0);
if (downloadUrlToStream(imgUrl, outputStream)) {
editor.commit();
} else {
editor.abort();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
读取缓存
读取缓存就简单很多,DiskLruCache.get() 方法
public synchronized Snapshot get(String key) throws IOException
使用 key 从缓存文件夹中得到对应的文件,得到一个 Snapshot 对象,我们调用它的 getInputStream() 方法得到缓存文件的输入流。然后将输入流保存为 bitmap。
private void get(ImageView imageView, String imgUrl) {
try {
String key = hashKeyForDisk(imgUrl);
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
InputStream is = snapshot.getInputStream(0);
Bitmap bitmap = BitmapFactory.decodeStream(is);
imageView.setImageBitmap(bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
}
移除缓存
用的就是 DiskLruCache.remove() 方法
public synchronized boolean remove(String key) throws IOException
根据 key 删除对应的图片文件
最后
如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。谢谢!