专栏首页进击的多媒体开发如何理解图片采样,这应该算是基础知识吧?

如何理解图片采样,这应该算是基础知识吧?

前言

最近线上有用户反馈在App使用过程中遇到大图的时,App异常的卡顿,甚至会出现崩溃的情况。后来排查了一番,发现一个同事在处理图片时,直接原图加载没有做任何“压缩”。这个case的出现,也就引出了这篇文章的必要性。

咱们日常开发过程中,都会使用各种各样的图片库比如Glide。由于所有图片操作都是一股脑的交给图片库去处理,所以即使在遇到大图加载的时候,也无法“复现”这类问题。

因为主流的图片库都帮咱们对大图进行了处理(正印证了那句话:当你能轻松进去的时候,你就该明白,不是你厉害,只是有人在前面替你开路——“鲁讯”)。

既然话都说开了,咱们作为新时代下的福报程序员,那就必须要在这条路上探探深浅。其实图片压缩的方式有很多种,今天咱们只要一种,那就是Google原生的高效加载大图的方案。

正文

进行压缩之前,咱们先来感受一下不压缩会怎样...

一、不压缩,直接加载大图

我随便new了一下项目,搞了一个这样的图:

其实也不是特别大,就是一张1080P的图。

然后随便的用一个ImageView去加载一下:

iv.setImageResource(R.drawable.test)

当我尝试run的时候,我高估了我的测试机....没有加载出来,就直接崩了。Logcat也是够直接,无情吐槽:

这么一张图,一共需要132710400Bytes的内存,也就是132m....等等,不对?!分辨率1080 * 1920的图片怎么可能会使用100+m的内存?

我们都知道,正常一个图片被加载到内存里的文件大小 = 图片分辨率的宽 * 图片分辨率的高 * 色彩格式。带入这个公式内存大小 = 1080 * 1920 * 4 = 7.9m,绝不可能是100+m这么多!

这里可能有朋友会有疑问,为啥JPEG的格式会乘4,JPEG格式没有alpha通道,不应该占这么大的空间。其实具体乘几,还是需要看这张图最终Bitmap.Config解出来的值,我这张图解出来是ARGB_8888,所以还是要乘4。

如果你也有这个疑问,那么接下来的内容你要好好看咯。这个知识点恐怕是盲区...

二、番外:drawble、drawble-xxhdpi有什么区别

作为一个番外的内容部分。这一章节其实和图片压缩没有什么关系,只是额外聊一聊drawble这个文件夹

上述问题的根本原因就是在于文件放置的位置,我只在drawble文件夹下放置了图片资源。

所以...这种case下,如果加载这个资源的手机是一个高密度屏幕,那么这张图片被展示时,并非1080 * 1920...

接下来咱们来看一看,为什么资源文件随便放会带来这么大的问题!(以下内容,部分来自于官方文档)

文档中提到,如果资源提供不当,会导致缩放失真...。这里为什么系统要进行缩放其实也很好理解:

  • 对于系统来说,如果它向下(低密度)才找到需要引用的资源文件,那么最佳的策略便是将找到的图片资源整体放大。因为那里的图,预期是给低分辨率手机准备的。
  • 那么同理,如果系统向上(高密度)找到了需要引用的资源文件,那么缩小无疑是最佳的选择。因为那里的图,预期是给高分辨率手机准备的。

所以基于此,上述中OOM的内存值132710400bytes是这么算出来的:1080 * 4(这个4是手机dpi640 / 资源dpi160 所得) * 1920 * 4 * 4

小贴士:dpi = 手机分辨率长宽各自平方之和开方,除以对角线长度(单位英寸)。当然我们也可以通过api:resources.displayMetrics.xdpi。这里得到的值就基本等于当前手机的dpi


所以,强制加载这么大的一张图,是不是不负责任!这么大,硬往里塞,搁谁谁受得了?

三、Google提供的解决方案

既然咱们已经明确硬来是不行了,所以还是要采取一些技巧的。文章中开篇就道出了问题的所在:

Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI). For example, the system Gallery application displays photos taken using your Android devices's camera which are typically much higher resolution than the screen density of your device.

Given that you are working with limited memory, ideally you only want to load a lower resolution version in memory. The lower resolution version should match the size of the UI component that displays it. An image with a higher resolution does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional on the fly scaling.

简单翻译一下就是:太大就不要硬塞,缩到合适的尺寸再塞

文档里还有比较有意思的一句话:There are several libraries that follow best practices for loading images. You can use these libraries in your app to load images in the most optimized manner. We recommend the Glide

官方推荐,最为致命

其实文档中直接贴出了可以Ctrl +C/V就能使用的代码:

imageView.setImageBitmap(
    decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height > reqHeight || width > reqWidth) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    // First decode with inJustDecodeBounds=true to check dimensions
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)

        // Calculate inSampleSize
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

        // Decode bitmap with inSampleSize set
        inJustDecodeBounds = false

        BitmapFactory.decodeResource(res, resId, this)
    }
}

代码很好理解,就是将需要加载的图片,按目标所需的加载尺寸进行一次采样,通过采样的值进行等比缩放。

不过这里有一个有趣的细节:官方的代码里是将采样结果进行了 * 2 ( inSampleSize*=2)。当时通过实战我们会发现, inSampleSize并不一定要传2的幂,传3传5传其他也是有效果的。

文档中提到这么一句话:

Note: A power of two value is calculated because the decoder uses a final value by rounding down to the nearest power of two, as per the inSampleSize documentation.(以2的幂作为计算结果,是根据inSampleSize文档,解码器通过四舍五入到最接近的2的幂来使用最终值。)

按照文档的解释inSampleSize为2/3时,效果一样,毕竟3最接近2的幂的值还是2。当时事实跑起来会发现,2和3的结果并不一样:

当inSampleSize = 3时,图片长和宽就是比减少了3倍...所以真是不知道官网的葫芦里卖的什么药。

本文分享自微信公众号 - 音视频开发进阶(glumes_blog)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 简单易用的图像解码库介绍 —— stb_image

    说到图像解码库,最容易想起的就是 libpng 和 libjpeg 这两个老牌图像解码库了。

    glumes
  • 【数字视频技术介绍】| 编码中的时间冗余和空间冗余

    我们可以做个减法,我们简单地用 0 号帧减去 1 号帧,得到残差,这样我们就只需要对残差进行编码。

    glumes
  • Android JNI 中的引用管理

    在 Native 代码中有时候会接收 Java 传入的引用类型参数,有时候也会通过 NewObject 方法来创建一个 Java 的引用类型变量。

    glumes
  • 【jQuery进阶】子菜单插件Slight Submenu

    兼容所有浏览器(记住,jQuery的2 *及以上不支持<IE9,如果您使用的是,对于那些旧的浏览器不支持)

    用户5640963
  • 利用setTimeout和SetInterval构建Javascript计时器

    看到了一篇深入浅出的讲解setTimeout和setInterval的例子,直接讲英文贴出来吧,也不是很难。

    大江小浪
  • ONLYOFFICE 5.5 API变化

    The list of changes of ONLYOFFICE Document Server API.

    hotqin888
  • 零阶监督策略改进(CS AI)

    尽管策略梯度算法在强化学习(RL)中取得了显着进步,但次优策略通常是由策略梯度更新的局部探索属性导致的。在这项工作中,我们提出了一种称为零阶监督策略改进(ZOS...

    刘子蔚
  • 简单的实现土味实时声音可视化

    声音可视化顾名思义,就是把听到声音,通过视觉的方法呈现出来,人不仅可以听见声音,同时也可以看见。声音可视化最早是人们用来分析和了解声音的一种研究方式,不...

    UDM Lab
  • 径向模糊效果

    逍遥剑客
  • Custom Build Numbers in Team Build

    The Team Build service in Team Foundation Server includes the current date in th...

    张善友

扫码关注云+社区

领取腾讯云代金券