Android应用篇 - 最全图片相关的优化

今天来总结一下图片相关的优化手段。

 

目录:

  1. 基础知识
  2. 分辨率的优化
  3. 图片的格式
  4. 图片的压缩
  5. JNI 调用 JPEG 库
  6. 图片的缓存
  7. 设置图片色彩模式
  8. 回收图片
  9. OutOfMemory 能否被 catch
  10. inBitmap

 

 

1. 基础知识

(1) decodeFile() 与 decodeResource() 内部调用了 decodeStream()。

(2) options 必须是大于 1 的数,小于 1 系统默认也是等于 1。options 最好是 2 的指数,如 2,4,8,否则会向下取整,如 3,则会取 2,但经测试不是在所有版本上都生效。options 如为 n,那么采样后,宽高均为原图的 1 / n,最终的像素为原图的 1 / n 平方,如 2, 则为原图的 1 / 4。

(3) 图片最终大小为像素值 * 每像素占用的内存,和色彩模式相关。

A: alpha 透明通道。R: red 红色通道。G: green 绿色通道。 B: blue 蓝色通道。

  • Bitmap.Config ARGB_4444:每个像素占四位,即 A=4,R=4,G=4,B=4,那么一个像素点占 4+4+4+4=16 位。
  • Bitmap.Config ARGB_8888:每个像素占八位,即 A=8,R=8,G=8,B=8,那么一个像素点占 8+8+8+8=32 位。
  • Bitmap.Config RGB_565:即 R=5,G=6,B=5,没有透明度,那么一个像素点占 5+6+5=16 位。
  • Bitmap.Config ALPHA_8:每个像素占四位,只有透明度,没有颜色。

一个字节为 8 位,所以 ARGB_8888 一个像素占 32位,也就是 4 个字节 (byte),那么一个 100 * 100 的 ARGB 图片,占用的大小就是 100 * 100 * 4 = 40000 byte。

 

 

2. 分辨率的优化

分辨率的适配主是针对放在 drawable 目录下的图片资源,目前 Android 分出了mdpi,hdpi,xdpi,xxdpi,xxxdpi。需要不同分辨率的图片来放在对应的目录下来做适配,否则 Android 系统可能会将我们的图片拉伸导致变形。

 

 

3. 图片的格式

Android 目前常用的图片格式有 png,jpeg 和 webp:

  • png:无损压缩图片格式,支持 alpha 通道,Android 切图素材多采用此格式。
  • jpeg:有损压缩图片格式,不支持背景透明,适用于照片等色彩丰富的大图压缩,不适合 logo。
  • webp:是一种同时提供了有损压缩和无损压缩的图片格式,派生自视频编码格式 VP8,从谷歌官网来看,无损 webp 平均比 png 小 26%,有损的 webp 平均比 jpeg 小 25%~34%,无损 webp 支持 alpha 通道,有损 webp 在一定的条件下同样支持,有损 webp 在 Android4.0 (API 14) 之后支持,无损和透明在 Android4.3 (API 18) 之后支持。采用 webp 能够在保持图片清晰度的情况下,可以有效减小图片所占有的磁盘空间大小。

 

 

4. 图片的压缩

 

  • 4.1 工具压缩

使用 Google 提供的 ImageOptim,可以实现图片的无损压缩,最大可压缩减少 40% - 50% 的大小。当然还有一些其他的压缩工具。

 

  • 4.2 质量压缩

质量压缩并不会改变图片在内存中的大小,仅仅会减小图片所占用的磁盘空间的大小,因为质量压缩不会改变图片的分辨率,而图片在内存中的大小是根据 width*height*一个像素的所占用的字节数计算的,宽高没变,在内存中占用的大小自然不会变,质量压缩的原理是通过改变图片的位深和透明度来减小图片占用的磁盘空间大小,所以不适合作为缩略图,可以用于想保持图片质量的同时减小图片所占用的磁盘空间大小。另外,由于 png 是无损压缩,所以设置 quality 无效,以下是实现方式:

/**
 * 质量压缩
 *
 * @param format  图片格式 jpeg,png,webp
 * @param quality 图片的质量,0-100,数值越小质量越差
 */
public static void compress(Bitmap.CompressFormat format, int quality) {
    File sdFile = Environment.getExternalStorageDirectory();
    File originFile = new File(sdFile, "originImg.jpg");
    Bitmap originBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    originBitmap.compress(format, quality, bos);
    try {
        FileOutputStream fos = new FileOutputStream(new File(sdFile, "resultImg.jpg"));
        fos.write(bos.toByteArray());
        fos.flush();
        fos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

  • 4.3 采样率压缩

采样率压缩是通过设置 BitmapFactory.Options.inSampleSize,来减小图片的分辨率,进而减小图片所占用的磁盘空间和内存大小。设置的 inSampleSize 会导致压缩的图片的宽高都为 1/inSampleSize,整体大小变为原始图片的 inSampleSize 平方分之一,当然,这些有些注意点:

  • 1. inSampleSize 小于等于 1 会按照 1 处理。
  • 2. inSampleSize 只能设置为 2 的平方,不是 2 的平方则最终会减小到最近的 2 的平方数,如设置 7 会按 4 进行压缩,设置15 会按 8 进行压缩。 
/**
 * 
 * @param inSampleSize  可以根据需求计算出合理的inSampleSize
 */
public static void compress(int inSampleSize) {
    File sdFile = Environment.getExternalStorageDirectory();
    File originFile = new File(sdFile, "originImg.jpg");
    BitmapFactory.Options options = new BitmapFactory.Options();
    // 设置此参数是仅仅读取图片的宽高到options中,不会将整张图片读到内存中,防止 oom
    options.inJustDecodeBounds = true;
    Bitmap emptyBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);
 
    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;
    Bitmap resultBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
    try {
        FileOutputStream fos = new FileOutputStream(new File(sdFile, "resultImg.jpg"));
        fos.write(bos.toByteArray());
        fos.flush();
        fos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

options.inJustDecodeBounds = true 不会把将整张图片加载到内存中,常用于获取图片宽高,设置采样值。

 

  • 4.4 缩放压缩

缩放图片以达到减少图片大小的目的,主要用于加载缩略图。

public void compress(View v) {
    File sdFile = Environment.getExternalStorageDirectory();
    File originFile = new File(sdFile, "originImg.jpg");
    Bitmap bitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
    // 设置缩放比
    int radio = 8;
    Bitmap result = Bitmap.createBitmap(bitmap.getWidth() / radio, bitmap.getHeight() / radio, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(result);
    RectF rectF = new RectF(0, 0, bitmap.getWidth() / radio, bitmap.getHeight() / radio);
    // 将原图画在缩放之后的矩形上
    canvas.drawBitmap(bitmap, null, rectF, null);
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    result.compress(Bitmap.CompressFormat.JPEG, 100, bos);
    try {
        FileOutputStream fos = new FileOutputStream(new File(sdFile, "sizeCompress.jpg"));
        fos.write(bos.toByteArray());
        fos.flush();
        fos.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

 

5. JNI 调用 JPEG 库

Android 的图片引擎使用的是阉割版的 skia 引擎,去掉了图片压缩中的哈夫曼算法:一种耗 CPU 但是能在保持图片清晰度的情况下很大程度降低图片的磁盘空间大小的算法,这就是为什么 ios 拍出的 1M 的照片比 Android 5M 的还要清晰的原因。著名的开源软件 Telegram 用的就是 JNI 调用 JPEG 库的方式。

 

 

6. 图片的缓存

在内存中使用二级缓存方式,现在的主流图片加载框架基本上都是这样处理:

  • 弱引用缓存就是把图片的弱引用缓存在一个 HashMap 中,被下载使用的图片首先会缓存在这个弱引用 HashMap 中。通过引用计数的方式来记录图片被引用的次数,当引用次数为 0 时,也就意味着图片资源不再被使用嘛,这时就会将图片资源从这个弱引用缓存中移除,并把这个图片资源加入到 LRU 算法内存缓存中。
  • LRU 算法内存缓存的原理就是将图片资源用强引用的方式存储在 LinkedHashMap 中,当达到容量限制时移除最近最少使用的资源。当 LRU 缓存中的图片被重新使用时将图片资源从 LRU 缓存中删除,并添加到弱引用的缓存中去。这样做的原因就是防止图片被 LRU 算法缓存回收。

 

 

7. 设置图片色彩模式

Android 默认的颜色模式为 ARGB_8888,这个颜色模式色彩最细腻,显示质量最高。但同样的,占用的内存也最大。 所以在对图片效果不是特别高的情况下使用 RGB_565 (565 没有透明度属性)。

 

 

8. 回收图片

先看 Bitmap 内存的创建,通过跟踪 Bitmap.createBitmap() 方法,可以发现是 native 方法里调用的 JVM 来创建的:

    jbyteArray arrayObj = env->NewByteArray(size);

native 使用的是通过其得到的一个固定地址:

    jbyte* addr = jniGetNonMovableArrayElements(&env->functions, arrayObj);

native 里会用一个 SkPixelRef 来存放它们:

    SkPixelRef* pr = new AndroidPixelRef(env, bitmapInfo, (void*) addr, bitmap->rowBytes(), arrayObj, ctable);

然后将这个传给 Bitmap:

    bitmap->setPixelRef(pr);

再看 recycle() 的过程:

    public void recycle() {
        if (!mRecycled && mFinalizer.mNativeBitmap != 0) {
            if (nativeRecycle(mFinalizer.mNativeBitmap)) {
                mBuffer = null;
                mNinePatchChunk = null;
            }
            mRecycled = true;
        }
    }

进入 nativeRecycle():

    Caches::getInstance().textureCache.removeDeferred(bitmap);
    fPixelRef->unref();   // fPixelRef是上面分配的SkPixelRef
    fPixelRef = NULL;

这里就是 Java 端将 mBuffer 置为 null。native 端释放 SkPixelRef,并延迟删除其对应的 TextureCache (最终的删除应该是在下一帧开始前)。再看 Bitmap 的 finalize(),发现 Bitmap 类自己没有 finalize(),专门用了一个静态内部类 BitmapFinalizer,其 finalize() 方法来做 native 资源的释放,其会调用到 native 里:

    

    Caches::getInstance().textureCache.removeDeferred(resource);
    delete resource;

延迟删除其对应的 TextureCache,并删除 SkBitmap。

这么来看,recycle() 方法会释放部分 native 内存,但并不会释放 Bitmap 占用内存最大的图像数据内存。
截屏时得到的 Bitmap 的图像数据内存并不是在 JVM 里申请的,其使用的 SkPixelRef 也不是上面的 AndroidPixelRef,而是ScreenshotPixelRef,里面持有着图像数据。在这种情况下,调用 recycle() 方法是会释放其图像数据的。

尽快的调用 recycle() 是个好习惯,会释放与其相关的 native 分配的内存。但一般情况下图像数据是在 JVM 里分配的,调用recycle() 并不会释放这部分内存。

  • 用 createBitmap() 创建的 Bitmap 且没有被硬件加速 Canvas draw 过,则主动调用 recycle() 产生的意义比较小,仅释放了 native里的 SkPixelRef 的内存,这种情况可以不主动调用 recycle()。
  • 被硬件加速 Canvas draw 过的由于有 TextureCache 应该尽快调用 recycle() 来尽早释放其 TextureCache。
  • 像截屏这种不是在 JVM 里分配内存的情况也应该尽快调用 recycle() 来马上释放其图像数据。
  • 如果是通过 Resources.getDrawable() 得到的 Bitmap,不应该调用 recycle(),因为它可能会被重用。

所以一般怎么做呢?

bitmap.recycle();
bitmap = null;

释放了图片的资源,但是 Bitmap 本身并没有释放,它依然在占用资源,所以还要在调用一次 bitmap=null 将 bitmap 赋空,让有向图断掉,等待 GC 回收。

 

 

9. OutOfMemory 能否被 catch

答案是可以的,但是 catch 了一般也没什么用,即使在里面马上调用 system.gc() (这个操作不一定立即执行)。因为一旦发生了 OOM,任何一个其他操作都可能马上触发 OOM。所以避免 OOM 的方法还是在编码时管理好内存。

 

 

10. inBitmap

Android 2.3.3 (API 级别 10) 之前,Bitmap 像素数据和 Bitmap 对象是分开存储的,像素数据是存储在 native memory 中,对象存储在 Dalvik heap 中,native memory 中的像素数据不是以一种可预见的方式释放,可能导致应用程序暂时超过其内存限制和崩溃,所以在 Android2.3.3 (API 10) 之前你必须要调用 recycle() 方法来释放掉内存避免出现 OOM,当然前提是确定这个 bitmap 不再使用,否则会出现 "Canvas: trying to use a recycled bitmap"。在 Android3.0 (API 11) 之后,Bitmap 的像素数据和 Bitmap 对象一起存储在 Dalvik heap 中,所以我们不用手动调用 recycle() 来释放 Bitmap 对象,内存的释放都交给垃圾回收器来做。

inBitmap 主要就是指的复用内存块,不需要在重新给这个 bitmap 申请一块新的内存,避免了一次内存的分配和回收,从而改善了运行效率。

需要注意的是 inBitmap 只能在 3.0 以后使用。2.3 上,bitmap 的数据是存储在 native 的内存区域,并不是在 Dalvik 的内存堆上。在 Android 3.0 开始,系统在 BitmapFactory.Options 里引入了 inBitmap 机制来配合缓存机制。如果在载入图片时传入了inBitmap 那么载入的图片就是 inBitmap 里的值,这样可以统一有缓存和无缓存的载入方式。

public static class Options {

        public Bitmap inBitmap;
        // ...
}

使用 inBitmap,在 4.4 之前,只能重用相同大小的 bitmap 的内存区域,而 4.4 之后你可以重用任何 bitmap 的内存区域,只要这块内存比将要分配内存的 bitmap 大就可以。例如给 inBitmap 赋值的图片大小为 100-100,那么新申请的 bitmap 必须也为 100-100 才能够被重用。从 SDK 19 开始,新申请的 bitmap 大小必须小于或者等于已经赋值过的 bitmap 大小。

解码:新申请的 bitmap 与旧的 bitmap 必须有相同的解码格式,例如大家都是 8888 的,如果前面的 bitmap 是 8888,那么就不能支持 4444 与 565 格式的 bitmap 了,不过可以通过创建一个包含多种典型可重用 bitmap 的对象池,这样后续的 bitmap 创建都能够找到合适的"模板"去进行重用。

DisplayingBitmaps:Managing Bitmap Memory 上的 demo 的 DisplayingBitmaps.zip,代码也有用到 inBitmap,但是DisplayingBitmaps 功能还是很弱,因为遇到过不同的 ImageView 设置不同 ScaleType,然后使用同一张图片会造成相互影响,设置图片圆角也是,所以这也是使用 inBitmap 要注意的地方。

使用:使用此方法需要 inMutable=true,inSampleSize=1。

用法很简单:

  • 1. 在载入图片时先从缓存里拿出 Bitmap,将此 Bitmap 赋值给 inBitmap。
  • 2. 然后将 inBitmap 的 Options 传入 decode 方法就可以了。

也可以用来和 LruCache 配合实现内存的两级缓存。将需要缓存的图片存入 LruCache,当 LruCache 删除不用的图片时,将删除的图片放入软引用中。

protected synchronized void entryRemoved(boolean evicted, Object key, BitmapDrawable oldValue,

BitmapDrawable newValue) {

    //reusableBitmaps是HashMap<Object, SoftReference<Bitmap>>用了缓存从LruCache中移除的Bitmap

     reusableBitmaps.put(key, new SoftReference<Bitmap>(oldValue.getBitmap()));

}

在载入图片时:先调用 LruCache.get(key); 如果拿不到那么再调用 reusableBitmaps.get(key)。

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值