背景
一般来说, 图片是 APP 占用内存高的主要原因, 所以优化图片的内存占用是避免 OOM 的根本手段. 对于图片占用的内存, 我们可能总有这样的误区: 图片本身所占的存储空间越小, 占用的内存越小. 所以认为只要将图片进行压缩, 就相当于减小了内存占用. 其实这是不对的, 图片占用的存储空间的大小与所占的内存大小没有直接关系.
既然与内存没有关系, 那压缩图片有什么意义呢? 对于 APK 而言, 压缩图片是为了减小 APK 的体积, 而对于需要网络请求的图片, 压缩则是为了更快的网络响应.
所以优化之前需要清楚 2 个基本原则:
图片占用内存的大小与图片本身的大小没有直接关系;
webP 格式的图片虽然小, 但占用内存和其他格式无差别;
图片占用内存的大小
memorySize width * height * 每个像素需要的字节数
优化策略
既然需要的内存公式已得到, 那优化就显而易见了, 无非就是减小的这三个参数的值, 具体的策略如下:
这里我们将图片分为 2 种情况来探讨:
drawable 中的图片
单独探讨这种情况, 是因为 Android 系统会对 drawable 中的图片进行缩放, 缩放系数与设置的屏幕分辨率和 drawable 所表示的分辨率有关, 具体的公式如下:
scale = 设备分辨率 / 资源目录分辨率 如: 1080x1920 的图片显示 xhdpi 中的图片, scale = 480 / 320 = 1.5
所以此时图片占用的内存大小为:
memorySize (width * scale) * (height * scale) * 每个像素需要的字节数
width * height * scale ^ 2 * 每个像素需要的字节数
具体的缩放过程可参考 Android 中 Bitmap 内存优化 https://www.jianshu.com/p/3f6f6e4f1c88
这里我们只探讨一下 scale 系数的影响因素: 设备分辨率和资源目录分辨率. 至于其他的可变因子会在另一种情况中介绍. 设备分辨率我们没法改变, 所以影响因素只有资源目录分辨率, 也就是说, 同一张图片, 放在不同的 drawable 中, 占用的内存大小不同. 从公式可看出, 使用同一个设备时, drawable 表示的分辨率越高, 则图片占用的内存越小, 反之越大. 所以, 在做图片的兼容性时, 如果只想使用一张图片, 则应使用 3 倍甚至 4 倍的图片(3 倍是主流机型, 但在 4 倍手机上会被放大, 图片可能失真), 这样在低分辨率的手机上, 不仅显示清晰, 而且系统会自动进行缩放, 从而确保占用较小的内存.
同样是存放图片的位置, 为什么 mipmap 不在这种情况的考虑范围之内呢? 因为 mipmap 是 Android 系统为了避免 Launcher Icon 变形而添加的资源目录, 也就是说, mipmap 中的图片不会被缩放. 所以 Google 也不推荐将除 Launcher Icon 之外的图片放在 mipmap 目录中.
其他位置的图片
其他位置的图片包括 mipmap, asset, 本地图片, 网络图片等. 这些位置的图片都有一个共同点 -- 不会被缩放. 所以只需要考虑如何改变图片分辨率和每个像素需要的字节数即可.
本地图片
本地图片通常都是通过 Android 提供的 BitmapFactory 来加载的, 这里看几个常用的 API:
- // 根据路径加载
- public static Bitmap decodeFile(String pathName, Options opts);
- // 加载 drawable 或 mipmap 中的图片
- public static Bitmap decodeResource(Resources res, int id, Options opts)
- // 根据字节流加载
- public static Bitmap decodeByteArray(byte[] data, int offset, int length)
- // 根据 IO 流加载
- public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
图片的优化可通过 Options 参数来实现(Options 的介绍可参考从 fresco 看图片优化 https://coolegos.github.io/2018/01/25/从fresco-看图片优化/ ):
方式一: inSampleSize
inSampleSize 可理解为图片的缩小比例, 若 inSampleSize 小于 1, 则当做 1 处理. 设置 inSampleSize 后, 图片的宽度和高度将变成原来的 1/inSampleSize, 其占用的内存空间将是原来的 1/(inSampleSize ^ 2). 但是具体如何取值呢, 可通过以下代码来获取:
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
- options.inSampleSize = getSampleSize(options, 100, 100);
- options.inJustDecodeBounds = false;
- Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.abc, options);
- imageView.setImageBitmap(bitmap);
- public static int getSampleSize(BitmapFactory.Options options, int viewWidth, int viewHeight) {
- if (viewWidth == 0 || viewHeight == 0 || options == null) {
- return 1;
- }
- int widthScale = options.outWidth / viewWidth;
- int heightScale = options.outHeight / viewHeight;
- Log.i("out", "width==" + widthScale + "heightScale==" + heightScale);
- return widthScale>= heightScale ? heightScale : widthScale;
- }
方式二: inDensity
inDensity 相当于上面说的资源目录分辨率, 前面说了, 这里考虑的情况, 图片不会被缩放, 其原因就是 inDensity 和设备分辨率的取值是一致的, 因为 inDensity = 设备分辨率, 所以 scale=1, 如果将 inDensity 设置为大于设备分辨率的值, 那么图片就会被缩小. 例如, 当前的手机 1dp=2px, 即 2X 屏幕, 此时的 inDensity 为 320, 如果将 inDensity 修改为 480, scale=320f/480f=2/3, 那么图片所占用的内存将变成原来的 4/9.
方式三: inPreferredConfig
inPreferredConfig 的取值为 Bitmap.Config 类型(这里只考虑以下几种情况), 它是一个枚举类型, 用来设置每个像素需要的字节数:
ALPHA_8: 占 1 个字节
RGB_565: 占 2 个字节
ARGB_4444: 占 2 个字节, 已废弃, 不推荐使用
ARGB_8888:32 位真彩色, 带透明度, 占 4 个字节
显示图片时默认都是 ARGB_8888, 所以我们可通过 inPreferredConfig 的值进行内存优化. 但实际上 inPreferredConfig 的取值对内存的影响并不是简单的 Bitmap.Config.ALPHA_8 占 1 个字节, ARGB_4444 和 RGB_565 占 2 个字节, ARGB_8888 占 4 个字节, 而是与具体的图片格式有关:
inPreferredConfig 对 jpeg 和 gif 格式的图片无作用, 无论 inPreferredConfig 的值取什么, jpeg 格式的图片每个像素始终占用 4 个字节, 而 gif 格式的图片始终站 1 个字节;
对于 webp 格式的图片, inPreferredConfig 取值为 RGB_565 的时候, 每个像素占用 2 个字节, 其余的取值每个像素仍然占 4 个字节;
对于 png 格式的图片, 需要分 png8, png24, png32 三种情况来说. png8 格式的图片每个像素占用的字节数随 inPreferredConfig 的取值而变化, 取值为 ARGB_ALPHA 时占用一个字节, 取值为 RGB_565 时占用 2 个字节, 取值为 ARGB_4444 或 ARGB_8888 时占用 4 个字节. png24 格式的图片, 当 inPreferredConfig 的取值为 RGB_565 时, 每个像素占用 2 个字节, 取其他的值 (ARGB_ALPHA, ARGB_4444 和 ARGB_8888) 每个像素都占用 4 个字节. 而对于 png32 格式的图片, inPreferredConfig 的取值 (ARGB_ALPHA, RGB_565, ARGB_4444 或 ARGB_8888) 对每个像素占用的字节数无影响.
所以, 如果通过 inPreferredConfig 来优化图片的内存占用, 就需要 webp 或 png24 格式的图片, png24 与 png32 相比, 也就是不支持透明度而已, 对于大多数图片来说, 两者没有明显的差别. 当然, 作为一种新的图片格式, web 可认为是一种不错的选择.
注意: 9patch 图虽然在使用时会根据 View 的尺寸进行放大, 但其像素仍然不变, 可视为普通图片来处理;
网络图片
网络图片通常我们都是使用开源库进行加载(这里顺便推荐一个好用的图片加载库 https://github.com/JuHonggang/ImageSet ), 所以不需要拿到 Bitmap 再进行缩放或裁剪. 这时可让后台实现网络图片的裁剪, 即: 根据图片的请求参数返回合适的尺寸, 最大也只需要控件的大小即可. 再大也没意义, 不仅浪费流量, 还占用内存. 如果你的 APP 中有很多图片, 那么可对图片的宽高根据设备的内存情况进行适当的缩小:
- // 根据设置内存大小设置缩放系数
- public static float getDefaultScale() {
- float scale = 1.0f;
- int totalMemorySize = AndroidPlatformUtil.getTotalMemorySize();
- if (totalMemorySize>= 4) {
- scale = 1.0f;
- } else if (totalMemorySize>= 2 && totalMemorySize < 4) {
- scale = 0.8f;
- } else {
- scale = 0.6f;
- }
- return scale;
- }
- // 获取设备的内存大小, 返回值单位为 G
- public static int getTotalMemorySize(){
- String path = "/proc/meminfo";
- String firstLine = null;
- FileReader fileReader = null;
- BufferedReader bufferedReader = null;
- try{
- fileReader = new FileReader(path);
- bufferedReader = new BufferedReader(fileReader,8192);
- firstLine = bufferedReader.readLine().split("\\s+")[1];
- } catch (Exception e){
- e.printStackTrace();
- } finally {
- try {
- if (bufferedReader != null) {
- bufferedReader.close();
- }
- } catch (Exception e) {
- e.printStackTrace(System.out);
- }
- try {
- if (fileReader != null) {
- fileReader.close();
- }
- } catch (Exception e) {
- e.printStackTrace(System.out);
- }
- }
- if(TextUtils.isEmpty(firstLine)){
- return (int)Math.ceil((new Float(Float.valueOf(firstLine) / (1024 * 1024)).doubleValue()));
- }
- return 0;
- }
总结
对于一个多图片的 APP 来说, 图片所占内存的优化是一项必不可少的工作. 总的来说, 其优化也就是通过缩放和指定 Bitmap.Config 的值来实现的, 只是不同位置, 不同格式的图片有所差异而已.
来源: https://juejin.im/post/5af84f4b51882542714fdaa9