Apk瘦身压缩体验

文章目录

资源统一

尽量一个项目使用同一套资源,对于绝大对数APP来说,只需要取一套设计图就足够了。鉴于现在分辨率的趋势,建议取720p的资源,放到xhdpi目录。
相对于多套资源,只使用720P的一套资源,在视觉上差别不大,很多大公司的产品也是如此,但却能显著的减少资源占用大小,顺便也能减轻设计师的出图工作量了。不是xhdpi的目录都删除,而是强调保留一套设计资源就够了。
资源图片引入前先进行压缩,不使用原图(不要直接使用UI切的原图)

使用jpg格式图片

如果对于非透明的大图,jpg将会比png的大小有显著的优势,虽然不是绝对的,但是通常会减小到一半都不止。在启动页,活动页等之类的大图展示区采用jpg将是非常明智的选择。

使用webp格式图片

  1. WebP是一种支持有损压缩和无损压缩的图片文件格式,根据Google的测试,无损压缩后的WebP比PNG文件少了26%的体积,有损压缩后的WebP图片相比于等效质量指标的JPEG图片减少了25%~34%的体积。
  2. webp支持透明度,压缩比比jpg更高但显示效果却不输于jpg,官方评测quality参数等于75均衡最佳。
    相对于jpg、png,webp作为一种新的图片格式,限于android的支持情况暂时还没用在手机端广泛应用起来。从Android 4.0+开始原生支持,但是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。
  3. 根据Google的测试,目前WebP与JPG相比较,编码速度慢10倍,解码速度慢1.5倍,问题在于1.5倍的解码速度是否会影响用户体验。同时由于减少了文件体积,缩短了加载的时间,页面的渲染速度加快了。同时,随着图片数量的增多,WebP页面加载的速度相对JPG页面增快了。所以,使用WebP基本没有技术阻碍。
    Apk瘦身压缩体验

使用shape背景和selector着色方案

对于一些简单的图形或者背景图片,通过自定义绘制图形方式来代替引入图片本身。
对于一些外形相同仅有颜色不同的图片,可以使用selector文件来实现。

在线化素材库

如果有很多或者一组图形需要引入,比如说表情包,可以考虑使用在线引入的方式,省去本地的空间。但同时会增加代码复杂度和APP流量消耗。

lint检查

代码扫描工具,可帮助您发现并更正代码结构质量的问题,例如,如果 XML 资源文件包含未使用的命名空间,这样不仅占用空间,而且还会引起不必要的处理。
官网地址
终端执行命令

gradlew lint

或者您可以通过依次选择 Analyze > Inspect Code,手动运行配置的 lint 及其他 IDE 检查。检查结果将显示在 Inspection Results 窗口中,如果有未使用的资源可以删除。
Apk瘦身压缩体验

删除不必要的so库

基本上armable的so也是兼容armable-v7的,armable-v7a的库会对图形渲染方面有很大的改进,如果没有这方面的要求,可以精简。有极少数设备会Crash,测试通过再使用。x86包下的so在x86型号的手机是需要的,如果产品没用这方面的要求也可以精简。实际工作的配置是可以选择只保留armable、armable-x86下的so文件。

去除无用语言资源

只保留了中文和英文的语言资源,针对一些支持多语言的SDK。

android {
    defaultConfig {
        resConfigs "zh-rCN", "en-rUS"
    }
}

开启混淆

android {
    defaultConfig {
      minifyEnabled true
    }
}

开启shrinkResources去除无用资源

会把未使用到的资源文件在打包的时候移除内容,但源代码不会变,但此方案实测对apk的瘦身优化不一定会起到正向效果,可以酌情使用。但使用的时候要和上一步开启混淆同时使用,单独使用无效。
Apk瘦身压缩体验

android {
    buildTypes {
        release {
            shrinkResources true
        }
    }
}

使用zipAlign

资源对齐处理的工具(对齐到四字节边界并以此为单位进行访问),使得资源在被访问时候更有效率 能够对打包的应用程序进行优化,是的系统和APP之间的交互更加有效率,但对apk瘦身起到的效果不显著。

android {
    buildTypes {
        release {
           zipAlignEnabled true
        }
    }
}

使用AndResGuard对资源文件压缩

以上几种方式,主要对资源本身进行处理和代码压缩,但是apk中的资源并没有进行过多的操作。 AndResGuard微信退出的一个帮助你缩小APK大小的工具,但是只针对资源(主要是res)并不涉及编译过程。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a

官网及引入方式

添加混淆白名单:指定不需要进行混淆的资源路径规则,主要是一些第三方SDK,因为有些SDK的代码中引用到对应的资源文件,如果对其进行混淆,会导致找不到对应资源文件,出现crash,所以不能对其资源文件进行混淆。白名单链接如下;

白名单

引入时也可以选择自定义gradle的方式
Apk瘦身压缩体验

配置完成后使用如下方式打出需要的apk
Apk瘦身压缩体验
效果就类似于微信的apk压缩后的效果。把res文件改成了如下r文件夹的形式,并且压缩效果不错。
Apk瘦身压缩体验

编译webp解码器

手机厂商自带的webp解码器基本上只针对Android 4.3 以上的机型,4.3以下是有问题的,所以我们可以对webp的源码进行编译,打包成so库,使用自己的解码器。

下载webp源码放在创建的jni文件夹下

libwebp源码下载地址
Apk瘦身压缩体验
Apk瘦身压缩体验

引入jar包

将libwebp.jar引入到工程中
Apk瘦身压缩体验
Apk瘦身压缩体验

Android.mk配置

  1. 配置允许对当前源码目录进行编译
ENABLE_SHARED:=1

Apk瘦身压缩体验

  1. 由于libwebp.jar需要用到wig/libwebp_java_wrap.c 文件中的jni方法,所以需要将该文件也引入到Android.mk文件中进行配置
# libwebp

include $(CLEAR_VARS)

LOCAL_SRC_FILES := \
    $(dsp_enc_srcs) \
    $(enc_srcs) \
    $(utils_enc_srcs) \
	swig/libwebp_java_wrap.c \

Apk瘦身压缩体验

创建Application.mk

APP_ABI := armeabi-v7a
APP_PLATFORM := android-14

Apk瘦身压缩体验

编译解码器的so库

在libwebp源码目录下打开cmd,使用ndk去对目录下的文件进行编译,生成so库

C:\SDK\ndk\21.3.6528147\ndk-build.cmd  NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk APP_BUILD_SCRIPT=Android.mk

编译成功后导入so库文件
Apk瘦身压缩体验

对比png,jpeg,webp的编解码速度

Apk瘦身压缩体验

相关代码如下

    defaultConfig {
        applicationId "com.bliss.yang.webpapp"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        ndk {
            abiFilters "armeabi-v7a"//,添加ndk的支持
        }
    }
companion object{
        init {
            System.loadLibrary("webp")
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //        webp   解码速度   编码速度
        var l = System.currentTimeMillis()
        BitmapFactory.decodeResource(resources, R.drawable.splash_bg_webp)
        Log.e(TAG, "解码webp图片耗时:" + (System.currentTimeMillis() - l))

        l = System.currentTimeMillis()
        BitmapFactory.decodeResource(resources, R.drawable.splash_bg_jpeg)
        Log.e(TAG, "解码jpeg图片耗时:" + (System.currentTimeMillis() - l))

        l = System.currentTimeMillis()
        var bitmap = BitmapFactory.decodeResource(resources, R.drawable.splash_bg_png)
        Log.e(TAG, "解码png图片耗时:" + (System.currentTimeMillis() - l))


        //编码  png
        var bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.splash_bg_png)
        l  = System.currentTimeMillis()
        compressBitmap(bitmap1, CompressFormat.PNG, Environment
                .getExternalStorageDirectory().toString() + "/test.png")
        Log.e(TAG, "------->编码png图片耗时:" + (System.currentTimeMillis() - l))

        //编码  jpeg
        bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.splash_bg_jpeg)
        l = System.currentTimeMillis()
        compressBitmap(bitmap1, CompressFormat.JPEG, Environment
                .getExternalStorageDirectory().toString() + "/test.jpeg")
        Log.e(TAG, "------->编码jpeg图片耗时:" + (System.currentTimeMillis() - l))

        //编码  webp
        bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.splash_bg_webp)
        l = System.currentTimeMillis()
        compressBitmap(bitmap1, CompressFormat.WEBP, Environment
                .getExternalStorageDirectory().toString() + "/test.webp")
        Log.e(TAG, "------->编码webp图片耗时:" + (System.currentTimeMillis() - l))

//        编译的解码器
        l = System.currentTimeMillis()
        decodeWebp()
        Log.e(TAG, "libwebp解码图片耗时:" + (System.currentTimeMillis() - l))
        l = System.currentTimeMillis()
        encodeWebp(bitmap)
        Log.e(TAG, "libwebp编码图片耗时:" + (System.currentTimeMillis() - l))
    }

    private fun encodeWebp(bitmap: Bitmap) {
        //获取bitmap 宽高
        val width = bitmap.width
        val height = bitmap.height
        //获得bitmap中的 ARGB 数据 nio
        val buffer: ByteBuffer = ByteBuffer.allocate(bitmap.byteCount)
        bitmap.copyPixelsToBuffer(buffer)
        //编码 获得 webp格式文件数据  4 *width
        val bytes: ByteArray = libwebp.WebPEncodeRGBA(buffer.array(), width, height, width * 4, 75F)
        var fos: FileOutputStream? = null
        try {
            fos = FileOutputStream(Environment
                    .getExternalStorageDirectory().toString() + "/libwebp.webp")
            fos.write(bytes)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            if (null != fos) {
                try {
                    fos.close()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        }
    }

    private fun decodeWebp(): Bitmap? {
        @SuppressLint("ResourceType") val `is`: InputStream = resources.openRawResource(R.drawable.splash_bg_webp)
        val bytes = stream2Bytes(`is`)
        //将webp格式的数据转成 argb
        val width = IntArray(1)
        val height = IntArray(1)
        try {
            `is`.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        val argb: ByteArray = libwebp.WebPDecodeARGB(bytes, bytes.size.toLong(), width, height)
        //将argb byte数组转成 int数组
        val pixels = IntArray(argb.size / 4)
        ByteBuffer.wrap(argb).asIntBuffer().get(pixels)
        //获得bitmap
        return Bitmap.createBitmap(pixels, width[0], height[0], Bitmap.Config.ARGB_8888)
    }

    fun stream2Bytes(`is`: InputStream): ByteArray {
        val bos = ByteArrayOutputStream()
        val buffer = ByteArray(2048)
        var len: Int
        try {
            while (`is`.read(buffer).also { len = it } != -1) {
                bos.write(buffer, 0, len)
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return bos.toByteArray()
    }
    
    private fun compressBitmap(bitmap: Bitmap, format: CompressFormat, file: String) {
        var fos: FileOutputStream? = null
        try {
            fos = FileOutputStream(file)
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        }
        bitmap.compress(format, 75, fos)
        if (null != fos) {
            try {
                fos.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

源码地址

上一篇:.png .jpg批量转换成.webp(mac)


下一篇:优化加载图片