android 性能优化-图片

本文详细介绍了Android平台上的图片压缩技巧,包括bitmap内存计算、质量压缩、尺寸压缩及编码格式改变等内容。通过对比不同压缩方式的特点,帮助读者理解如何有效减少图片占用的内存空间。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考文章: 最详细的Android图片压缩攻略,让你一次过足瘾

bitmap内存计算方式:

bitmap内存大小 = (图片长度 x 图片宽度)(分辨率压缩) x 单位像素占用的字节数 (改变编码格式);

  • 不过,这样的说法并不准确,我们常说的 图片长* 图片宽,这里最终需要换成px作为单位去计算图片大小。在 Android 原生的 Bitmap 操作中,某些场景下,图片被加载进内存时的分辨率会经过一层转换,所以,虽然最终图片大小的计算公式仍旧是分辨率像素点大小,但此时的分辨率已不是图片本身的分辨率了;
  • 而且当图片放在 res 内的不同目录中,最终图片加载进内存所占据的大小会不一样,因为系统在加载 res 目录下的资源图片时,会根据图片存放的不同目录做一次分辨率的转换,而转换的规则是

  • 新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )
    新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )

没有对应文件夹,那么会从xxx(最大分辨率文件夹往下找);

  • 因此同一个 app,但跑在不同 dpi 设备上,同样的界面,但所耗的内存有可能是不一样的

几种bitmap内存优化方式:

1.质量压缩(不会减少图片本身大小,但是会减少file文件大小)

在Android中,对图片进行质量压缩,通常我们的实现方式如下所示:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//quality 为0~100,0表示最小体积,100表示最高质量,对应体积也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream);

在上述代码中,我们选择的压缩格式是CompressFormat.JPEG,除此之外还有两个选择:

其一,CompressFormat.PNG,PNG格式是无损的,它无法再进行质量压缩quality这个参数就没有作用了,会被忽略,所以最后图片保存成的文件大小不会有变化;

其二,CompressFormat.WEBP,这个格式是google推出的图片格式,它会比JPEG更加省空间,经过实测大概可以优化30%左右。

Bitmap的compress这个API 最后调用了函数encoder->encodeStream(…)编码保存本地。该函数是调用skia引擎来对图片进行编码压缩;Skia 是一个 Google 自己维护的 c++ 实现的图像引擎,实现了各种图像处理功能,并且广泛地应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等)

 Skia 在 Android 中提供了基本的画图和简单的编解码功能,实际会调用 libjpeg.so 动态库进行编码压缩,Android编码保存图片的逻辑是Java层函数→Native函数→Skia函数→对应第三库函数(例如libjpeg)。 libjpeg是一个C语音编写的高效JPEG图像处理库,Android系统在7.0版本之前内部使用的是libjpeg非turbo版,并且为了性能关闭了Huffman编码(optimize_coding 设为 false),使用的是默认的哈夫曼表。在7.0之后的系统内部启用了Huffman进行编码,根据实际图片去计算相对应的哈夫曼表

        去获取每一个元素,对于图片就是每一个像素中 argb 的权重,去循环整个图片的像素信息,这无疑是非常消耗性能的,所以早期 android 就使用了默认的哈夫曼表进行图片压缩。

哈夫曼算法(还有定长算术编码)举例: Android 中图片压缩分析(上) - 腾讯云开发者社区-腾讯云

所以想要提升图片压缩率的可以从libjpeg库着手,网上资料也不少,后续有机会可以测试一下其他可替代的so库。

2.尺寸压缩(包含采样率压缩 + 长宽缩放)

inJustDecodeBounds为true会禁止为bitmap分配内存,获取到bitmap的长宽值,计算出insampelsize采样率;

 为什么要比较实际宽高和目标宽高比呢?

假设有张原图宽高3840x2400,要压缩成1920x1080,实际不可能100%压缩到这个值,因为要图片压缩保持宽高比,所以采用优先保证以最短边进行比例缩放

计算较短边举例:   3840/1920 = 2,   2400/1080 = 2.2,因此看出高缩放程度更大,优先保证高的缩放,scale = 2400/1080=2.22,因此宽width = 2840/2.2 = 1728,最终结果1728x1080;

鲁班库和微信是会设置长宽边不同的缩放阶层,根据不同的长宽比例,设置不同压缩率;

而Driver app项目中会设定一个最短边标准960, 如果长和宽中的最短值大于960,将会一直压缩,直到这个最短边小于等于960;

鲁班框架的缺点:

  • 不支持多文件合理并行压缩

  • 质量压缩写死 60不能自己选

  • 没有提供图片输出格式选择

  • 可能出现内存泄漏,需要自己合理处理生命周期

总流程如下:

3.改变编码格式:

RGB_565无透明度通道(Alpha),适合不透明图片(如照片、背景图)。但不支持透明度:无法显示透明或半透明像素。。

ARGB_4444 支持透明度,但已弃用:适合需要透明但质量要求不高的场景。(Android 4.4+ 已弃用,实际渲染时会强制升级为 ARGB_8888)。

4.图片放置优化

只需要 UI 提供一套高分辨率的图,图片建议放在 drawable-xxhdpi 文件夹下,这样在低分辨率设备中放大倍数是小于1的,图片的大小只是压缩,在保证画质的前提下,内存也是可控的。如若遇到不需缩放的文件,放在 drawable-nodpi 文件夹下。如果放到分辨率低的目录如 hdpi 目录,则可能会造成内存问题。

5.不合理大图检测

图片的大小不应该超过 View 的大小,否则会造成资源浪费。比如需要的尺寸是100X100,实际给的是200X200。我们可以重载 drawable 与 ImageView 设置图片的方法,检测图片大小与 View 的大小,若超过一定比例,可以上报或提示,避免不必要的大图载入。

open class CheckBigImageView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {

    override fun setImageDrawable(drawable: Drawable?) {
        super.setImageDrawable(drawable)
        drawable?.let {
            checkBitmap(it.intrinsicWidth, it.intrinsicHeight)
        }
    }

    // 检测实际数据大小
    private fun checkBitmap(bitmapWidth: Int, bitmapHeight: Int) {
        val width = this.width
        val height = this.height
        if (width > 0 && height > 0) {
            // 2、图标宽高都大于view的2倍以上,则警告
            if (bitmapWidth >= width shl 1 && bitmapHeight >= height shl 1) {
                warn(
                    bitmapWidth,
                    bitmapHeight,
                    width,
                    height,
                    RuntimeException("Bitmap size too large")
                )
            }
        } else {
            // 3、当宽高度等于0时,说明ImageView还没有进行绘制,使用ViewTreeObserver进行大图检测的处理。
            val stackTrace: Throwable = RuntimeException()
            this.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    val w = this@CheckBigImageView.width
                    val h = this@CheckBigImageView.height
                    if (w > 0 && h > 0) {
                        if (bitmapWidth >= w shl 1 && bitmapHeight >= h shl 1) {
                            warn(bitmapWidth, bitmapHeight, w, h, stackTrace)
                        }
                        this@CheckBigImageView.viewTreeObserver.removeOnPreDrawListener(this)
                    }
                    return true
                }
            })
        }
    }

    private fun warn(bitmapWidth: Int, bitmapHeight: Int, viewWidth: Int, viewHeight: Int, t: Throwable) {
        val warnInfo = """Bitmap size too large: bitmap: [$bitmapWidth,$bitmapHeight],
                          view: [$viewWidth,$viewHeight],
                          位置:${(context as? Activity)?.javaClass?.simpleName},
                          call stack trace: ${Log.getStackTraceString(t)}
                          """
        LogUtil.w(warnInfo, tag = "BigBitmap")
    }
}

Bitmap裁剪方法(createBitmap):

public static Bitmap createBitmap (Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)

matrix:来实现旋转等多种效果的截图;

Bitmap source:需要截图的原始图

int x:起始x坐标

int y:起始y坐标

int width:要截的图的宽度

int height:要截的图的宽度

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值