android基础知识:Google提供的高效加载大图方案

4,222 阅读6分钟

前言

最近线上有用户反馈在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倍...所以真是不知道官网的葫芦里卖的什么药。

尾声

到这,该唠的基本也就唠完了...内容并不深奥,但也算是必备的知识点~

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:咸鱼正翻身