性能优化系列001-APK瘦身

1,038 阅读6分钟

0525检查删除冗余资源

1. 删除项目中冗余资源文件

  • 通过Android studio提供的lint工具去排查没有使用到的资源文件,然后让无用的资源文件删除掉 通过Analyze - Run Inspection By Name... 选项 ,然后输入 unused resources来查找无用的资源文件
  • Analyze -> Run Inspection By Name -> unused resources

2. 删除项目中未使用到的代码

  • 通过Android studio提供的lint工具去排查没有使用到的代码,然后让无用的代码删除掉 通过Analyze - Run Inspection By Name... 选项 ,然后输入 unused declaration来查找无用的代码
  • 通过反射引用的方法或者类,lint是识别不了的,也会给检查出来,所以在删除的时候要特别注意,通过的反射引用的方法或者类千万不能删除
  • Analyze - Run Inspection By Name -> unused declaration

1.文章

APK瘦身

  1. 给安装包APK文件瘦身
  • 精简无用资源: shrinkResources true 有效,APK由 9823K -> 7884K
  • 移除非ARM架构的so文件: ndk
  1. WebP
  1. 资源混淆
  1. Apk不得不看的瘦身大全
  2. Jenkins+Git+Walle+AndResGuard打造Android多渠道打包系统
  3. Android apk瘦身最佳实践 有一系列瘦身相关措施
  4. 动态下发 so 库在 Android APK 安装包瘦身方面的应用
  5. 美团 Android App包瘦身优化实践
  6. 滴滴 Booster

    Booster 是一款专门为移动应用设计的易用、轻量级且可扩展的质量优化框架,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。 Booster 提供了性能检测、多线程优化、资源索引内联、资源去冗余、资源压缩、系统 Bug 修复等一系列功能模块,可以使得稳定性能够提升 15% ~ 25%,包体积可以减小 1MB ~ 10MB。

  7. 西瓜视频apk瘦身之 Java access 方法删除 没有开源
  8. 支付宝 App 构建优化解析:Android 包大小极致压缩 直接删 dex 中的无用信息,降低APK大小
  9. APK瘦身-是时候给App进行减负了 assests目录优化写的不错
  10. Apk 极限压缩(说点不一样的) 这真是太硬核了

2.总结

APK瘦身

0. 瘦身优化3点方法论

  1. 删:删除无用的代码和资源
  2. 压:压缩代码和资源
  3. 移:实在不行就抽离出,动态加载,动态下发

1. 移除无用资源文件 及 仅保留arm架构对应的so文件

buildTypes {
    release {
        //移除无用的资源文件(包括xml布局和图片)
        shrinkResources true
        //移除非ARM架构的so文件
        ndk {
            abiFilters "arm64-v8a", "armeabi-v7a" // 保留这两种指令架构的so文件
        }
    }
}

若之前未打开 shrinkResources,shrinkResources会对APK尺寸有较大降低

2. WebP在android上的适配

3. 资源混淆

资源混淆的意义: 保护资源和缩减包体积. 在将resources.arsc也加入压缩情况下,资源混淆可以降低APK 1M以上.

  1. APK资源混淆使用 AndResGuard
  2. AndResGuard使用步骤
//1:在工程根目录下的build.gradle中添加 classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
buildscript {
    repositories {
        ***
    }
    dependencies {
        ***
        //自己改的
        classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
    }
}

//2:在app模块下新建gradle文件 and_res_guard.gradle
apply plugin: 'AndResGuard'

andResGuard {
    mappingFile = null
    use7zip = true
    useSign = true
    // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
    keepRoot = false
    // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
    fixedResName = "arg"
    // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
    mergeDuplicatedRes = true
    whiteList = [
            // for your icon
            "R.drawable.ic_launcher",

            //图标相关资源
            "R.mipmap.ic_launcher",
            "R.mipmap.ic_launcher_foreground",
            "R.mipmap.ic_launcher_round",
            "R.color.ic_launcher_background",
            
            //所有涉及到 getResources().getIdentifier 对应的资源ID
            "R.array.a1",
            "R.string.s1",
            "R.mipmap.m1",
            "R.dimen.d1",
            "R.integer.int1",
            
            //Firebase Crashlytics
            "R.bool.com.crashlytics.useFirebaseAppId",
            "R.string.com.crashlytics.useFirebaseAppId",
            "R.string.google_app_id",
            "R.bool.com.crashlytics.CollectDeviceIdentifiers",
            "R.string.com.crashlytics.CollectDeviceIdentifiers",
            "R.bool.com.crashlytics.CollectUserIdentifiers",
            "R.string.com.crashlytics.CollectUserIdentifiers",
            "R.string.com.crashlytics.ApiEndpoint",
            "R.string.io.fabric.android.build_id",
            "R.string.com.crashlytics.android.build_id",
            "R.bool.com.crashlytics.RequireBuildId",
            "R.string.com.crashlytics.RequireBuildId",
            "R.bool.com.crashlytics.CollectCustomLogs",
            "R.string.com.crashlytics.CollectCustomLogs",
            "R.bool.com.crashlytics.Trace",
            "R.string.com.crashlytics.Trace",
            "R.string.com.crashlytics.CollectCustomKeys",

            // for fabric
            "R.string.com.crashlytics.*",
            // for google-services
            "R.string.google_app_id",
            "R.string.gcm_defaultSenderId",
            "R.string.default_web_client_id",
            "R.string.ga_trackingId",
            "R.string.firebase_database_url",
            "R.string.google_api_key",
            "R.string.google_crash_reporting_api_key"
    ]
    compressFilePattern = [
            "*.png",
            "*.jpg",
            "*.jpeg",
            "*.gif",
            "resources.arsc"
    ]
    sevenzip {
        artifact = 'com.tencent.mm:SevenZip:1.2.21'
    }

    /**
     * 可选: 如果不设置则会默认覆盖assemble输出的apk
     **/
    // finalApkBackupPath = "${project.rootDir}/final.apk"

    /**
     * 可选: 指定v1签名时生成jar文件的摘要算法
     * 默认值为“SHA-1”
     **/
    // digestalg = "SHA-256"
}

//3:在app下的build.gradle中引用刚刚创建的 and_res_guard.gradle
apply plugin: 'com.android.application'
apply plugin: ***
//添加AndResGuard
apply from: 'and_res_guard.gradle'

//4:使用Android Studio的同学可以在 andresguard 下找到相关的构建任务; 
//命令行可直接运行./gradlew resguard[BuildType | Flavor], 这里的任务命令规则和assemble一致。
  1. 7z过程中使用的是极限压缩模式,所以遍历次数会增多(7次),时间相对会比较长。同时我们需要注意是由于文件系统不一致,在window上面使用7z生成的安装包会较大.最后出包请使用Linux(Mac亦可),具体原因应该与文件系统有关.
  2. 如果不是对APK size有极致的需求,请不要把resources.arsc添加进compressFilePattern.
  • 按照Github上issue描述, resources.arsc 压缩后,会增大APP的内存占用.
  • 自己使用 1234003.apk/未压缩 和 1234005.apk/已压缩 对比,无明显差异.未压缩一开始比已压缩占用内存高7M,然后已压缩多试几次,TOTAL PSS后面和未压缩的基本一致. resources.arsc压缩会影响性能吗
  1. fixedResName = "arg" 和 mergeDuplicatedRes = true 设置后,可进一步降低APK体积.

4. 减少第三方库的使用

  1. 第三方库,尽量参考后自己实现
  2. 必须引用的,尽量避免完全引用

5. 避免使用枚举

  1. 一个枚举会增加APK 1KB左右大小
  2. 使用系统提供的或者自定义注解代替枚举

6. 图片资源优化

  1. 使用TinyPng或者Guetzli进行压缩.根据实际情况,除非图片本身太大,否则不想降低图片质量去压缩.
  • 可以使用pngcrush、pngquant或zopflipng等压缩工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
  • pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。
  • 对于JPEG文件,你可以使用packJPG或guetzli等工具将JPEG文件压缩的更小,这些工具能够在保持图片质量不变的情况下,把图片文件压缩的更小。guetzli工具更是能够在图片质量不变的情况下,将文件大小降低35%。
  1. 使用webp
  2. 使用矢量图/SVG
  3. Tint着色器
  • 同一张图片可以使用不同的颜色进行着色
<ImageView
    ***
    app:srcCompat="@drawable/int"
    />
<ImageView
    ***
    app:srcCompat="@drawable/int"
    android:tint="@color/colorTinit"
    />
  • tint也可以和selector结合使用
    • ImageView设置的src, tint 都可以是 selector
tint_color.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@android:color/c1" android:state_pressed="true" />
    <item android:color="@android:color/c2" />
</selector>

sel.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/d1" android:state_pressed="true" />
    <item android:drawable="@drawable/d1" />
</selector>

lay.xml
<ImageView
    ***
    app:srcCompat="@drawable/sel"
    android:tint="@color/tint_color" />

7. 移除冗余的资源

google给apk提供了国际化支持,如适应不同的屏幕分辨率的drawable资源,适应不同语言的字符串资源,但在很多情况下我们只需要指定分辨率和语言,这时可以使用resConfigs方法来配置.

android {
    defaultConfig {
        ...
        //添加了zh配置用于只保留中文资源
        //添加xxhdpi配置用于只保留一套图片资源
        resConfigs "zh-rCN", "xxhdpi"
    }
}

8. 代码混淆

  1. 代码压缩:从应用及其库依赖项中检测并安全地移除未使用的类、字段、方法和属性
  2. 资源压缩:从应用中移除未使用的资源,包括应用的库依赖项中未使用的资源
  3. 混淆:缩短类和成员的名称,从而减小 DEX 文件大小
  4. 优化:检查并重写代码,以进一步减小应用 DEX 文件的大小

9. 使用 滴滴 Booster中的资源索引内联

资源索引内联原理

  1. 资源索引内联引用步骤
//1:在项目根目录下的build.gradle中添加

buildscript {
    //添加booster
    ext {
        kotlin_version = '1.3.31'
        booster_version = '3.1.0'
        debug = gradle.startParameter.taskNames.any { it.contains('debug') || it.contains('Debug') }
    }

    repositories {
        ***
        //添加booster
        maven { url 'https://oss.sonatype.org/content/repositories/public' }
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
    }
    dependencies {
        ***
        classpath 'com.android.tools.build:gradle:3.5.2'
        //添加booster
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.didiglobal.booster:booster-gradle-plugin:$booster_version"
        
        //如果未配置任何模块,BoosterTransform 则不会被执行
        //添加资源索引内联模块
        //Booster 提供了很多的功能模块,如果引入所有的模块,可能会增加 App 构建的时间,甚至影响开发调试的效率,
        //有些模块完全可以在 Debug 构建中忽略掉,可以在 build.gradle 中通过 Gradle 的启动参数来判断构建 task 是否是 Release
        if (!debug) {
            //仅对 Release 生效
            classpath "com.didiglobal.booster:booster-transform-r-inline:$booster_version"
        }
    }
}

allprojects {
    repositories {
        ***
        //添加booster
        maven { url 'https://oss.sonatype.org/content/repositories/public' }
        maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
    }
}
task clean(type: Delete) {
    delete rootProject.buildDir
}

//2:在 App 子工程中启用 booster-gradle-plugin
//app模块下的build.gradle文件
apply plugin: 'com.android.application'
apply plugin: 'com.didiglobal.booster'
***
//由于其它插件可能与 booster 有冲突,尽可能将 apply plugin: 'com.didiglobal.booster' 放在 apply plugin: 'com.android.application' 的下面第一行
  1. 如果在根目录下的build.gradle中,booster未配置任何模块,BoosterTransform 则不会被执行,这个特性可以用于仅构建 Release 包时启用 Booster 模块,加快 Debug 包的构建速度.
  2. 由于其它插件可能与 booster 有冲突,尽可能将 apply plugin: 'com.didiglobal.booster' 放在 apply plugin: 'com.android.application' 的下面第一行.

10. 删除Dex文件中data区中的debugItems区域

  1. 实现方法,直接修改混淆文件,将 -keepattributes SourceFile,LineNumberTable 注掉即可. 本地验证: APK由5615K->5479K.
proguard-rules.pro

#用于调试堆栈跟踪的行号信息, 抛出异常时会保留代码行号
#-keepattributes SourceFile,LineNumberTable
  1. -keepattributes SourceFile,LineNumberTable 是用于打印crash堆栈日志保留代码行号的.
  • 注释前打印的堆栈信息
java.lang.IllegalStateException: Could not execute method for android:onClick
	at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(SourceFile:4)
	at android.view.View.performClick(View.java:7465)
	at android.view.View.performClickInternal(View.java:7438)
	at android.view.View.access$3600(View.java:813)
	at android.view.View$PerformClick.run(View.java:28511)
	at android.os.Handler.handleCallback(Handler.java:938)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loop(Looper.java:268)
	at android.app.ActivityThread.main(ActivityThread.java:7917)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:627)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:997)
Caused by: java.lang.reflect.InvocationTargetException
	at java.lang.reflect.Method.invoke(Native Method)
	at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(SourceFile:3)
	... 11 more
Caused by: java.lang.ArithmeticException: divide by zero
	at ***Activity.func1(SourceFile:1)
  • 注释后打印的堆栈信息
java.lang.IllegalStateException: Could not execute method for android:onClick
	at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(Unknown Source:32)
	at android.view.View.performClick(View.java:7465)
	at android.view.View.performClickInternal(View.java:7438)
	at android.view.View.access$3600(View.java:813)
	at android.view.View$PerformClick.run(View.java:28511)
	at android.os.Handler.handleCallback(Handler.java:938)
	at android.os.Handler.dispatchMessage(Handler.java:99)
	at android.os.Looper.loop(Looper.java:268)
	at android.app.ActivityThread.main(ActivityThread.java:7917)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:627)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:997)
Caused by: java.lang.reflect.InvocationTargetException
	at java.lang.reflect.Method.invoke(Native Method)
	at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(Unknown Source:23)
	... 11 more
Caused by: java.lang.ArithmeticException: divide by zero
	at ***Activity.func1(Unknown Source:2)
	... 13 more
  • App中的代码行号信息,从 SourceFile:1 -> Unknown Source:2 . 对于分析崩溃日志增加了难度,但是如果有mapping文件,加上crash数量不大,实际影响并不大.
  1. debugItems背景知识 debugItem背景知识.png

11. assests目录优化

  1. assests目录存放通过AssetManager能够检索到的资源,包括MP3、视频、字体、webp等
  2. 删除冗余字体
  • 字体文件一般都很大,但APP实际使用的可能只有几个字,因而需要对字体文件进行删减
  • Github FontZip可以对字体文件进行裁剪.
  1. 比较大的音频视频文件可以zip压缩后7zip压缩后再放大assets中,使用时先解压.

12. 目前看过最极端的方式: 将必须依赖的库代码,拷贝下来,按需引用.

  1. 步骤 Apk 极限压缩(说点不一样的)
  2. Github Android Jetpack源码
  3. Github material-components-android源码

3.实践