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瘦身
- 精简无用资源: shrinkResources true 有效,APK由 9823K -> 7884K
- 移除非ARM架构的so文件: ndk
- WebP
- 资源混淆
- 抖音包大小优化-资源优化
- APK的资源混淆
- Android App Bundle的资源混淆
- Apk不得不看的瘦身大全
- Jenkins+Git+Walle+AndResGuard打造Android多渠道打包系统
- Android apk瘦身最佳实践 有一系列瘦身相关措施
- 动态下发 so 库在 Android APK 安装包瘦身方面的应用
- 美团 Android App包瘦身优化实践
- 滴滴 Booster
Booster 是一款专门为移动应用设计的易用、轻量级且可扩展的质量优化框架,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。 Booster 提供了性能检测、多线程优化、资源索引内联、资源去冗余、资源压缩、系统 Bug 修复等一系列功能模块,可以使得稳定性能够提升 15% ~ 25%,包体积可以减小 1MB ~ 10MB。
- 西瓜视频apk瘦身之 Java access 方法删除 没有开源
- 支付宝 App 构建优化解析:Android 包大小极致压缩 直接删 dex 中的无用信息,降低APK大小
- APK瘦身-是时候给App进行减负了 assests目录优化写的不错
- Apk 极限压缩(说点不一样的) 这真是太硬核了
2.总结
APK瘦身
0. 瘦身优化3点方法论
- 删:删除无用的代码和资源
- 压:压缩代码和资源
- 移:实在不行就抽离出,动态加载,动态下发
1. 移除无用资源文件 及 仅保留arm架构对应的so文件
buildTypes {
release {
//移除无用的资源文件(包括xml布局和图片)
shrinkResources true
//移除非ARM架构的so文件
ndk {
abiFilters "arm64-v8a", "armeabi-v7a" // 保留这两种指令架构的so文件
}
}
}
若之前未打开 shrinkResources,shrinkResources会对APK尺寸有较大降低
2. WebP在android上的适配
- Android 设备对 webp 的支持存在兼容性问题,在 4.3 以上才完全支持.
- 动态WebP,目前只能找到facebook的开源库Fresco对其支持.
- 使用AS将mipmap中的图片转换为WebP格式,即使是无损转换,图片的体积也至少降低了50%.打包APK后,APK体积至少降低了几百K.
- 有人将webp转换自动化了 App极限瘦身姿势: png 打包自动化转换 webp
3. 资源混淆
资源混淆的意义: 保护资源和缩减包体积. 在将resources.arsc也加入压缩情况下,资源混淆可以降低APK 1M以上.
- APK资源混淆使用 AndResGuard
- 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一致。
- 7z过程中使用的是极限压缩模式,所以遍历次数会增多(7次),时间相对会比较长。同时我们需要注意是由于文件系统不一致,在window上面使用7z生成的安装包会较大.最后出包请使用Linux(Mac亦可),具体原因应该与文件系统有关.
- 如果不是对APK size有极致的需求,请不要把resources.arsc添加进compressFilePattern.
- 按照Github上issue描述, resources.arsc 压缩后,会增大APP的内存占用.
- 自己使用 1234003.apk/未压缩 和 1234005.apk/已压缩 对比,无明显差异.未压缩一开始比已压缩占用内存高7M,然后已压缩多试几次,TOTAL PSS后面和未压缩的基本一致. resources.arsc压缩会影响性能吗
- fixedResName = "arg" 和 mergeDuplicatedRes = true 设置后,可进一步降低APK体积.
4. 减少第三方库的使用
- 第三方库,尽量参考后自己实现
- 必须引用的,尽量避免完全引用
5. 避免使用枚举
- 一个枚举会增加APK 1KB左右大小
- 使用系统提供的或者自定义注解代替枚举
6. 图片资源优化
- 使用TinyPng或者Guetzli进行压缩.根据实际情况,除非图片本身太大,否则不想降低图片质量去压缩.
- 可以使用pngcrush、pngquant或zopflipng等压缩工具来减少PNG文件大小,而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
- pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置。
- 对于JPEG文件,你可以使用packJPG或guetzli等工具将JPEG文件压缩的更小,这些工具能够在保持图片质量不变的情况下,把图片文件压缩的更小。guetzli工具更是能够在图片质量不变的情况下,将文件大小降低35%。
- 使用webp
- 使用矢量图/SVG
- 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. 代码混淆
- 代码压缩:从应用及其库依赖项中检测并安全地移除未使用的类、字段、方法和属性
- 资源压缩:从应用中移除未使用的资源,包括应用的库依赖项中未使用的资源
- 混淆:缩短类和成员的名称,从而减小 DEX 文件大小
- 优化:检查并重写代码,以进一步减小应用 DEX 文件的大小
9. 使用 滴滴 Booster中的资源索引内联
- 资源索引内联引用步骤
//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' 的下面第一行
- 如果在根目录下的build.gradle中,booster未配置任何模块,BoosterTransform 则不会被执行,这个特性可以用于仅构建 Release 包时启用 Booster 模块,加快 Debug 包的构建速度.
- 由于其它插件可能与 booster 有冲突,尽可能将 apply plugin: 'com.didiglobal.booster' 放在 apply plugin: 'com.android.application' 的下面第一行.
10. 删除Dex文件中data区中的debugItems区域
- 实现方法,直接修改混淆文件,将 -keepattributes SourceFile,LineNumberTable 注掉即可. 本地验证: APK由5615K->5479K.
proguard-rules.pro
#用于调试堆栈跟踪的行号信息, 抛出异常时会保留代码行号
#-keepattributes SourceFile,LineNumberTable
- -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数量不大,实际影响并不大.
- debugItems背景知识
11. assests目录优化
- assests目录存放通过AssetManager能够检索到的资源,包括MP3、视频、字体、webp等
- 删除冗余字体
- 字体文件一般都很大,但APP实际使用的可能只有几个字,因而需要对字体文件进行删减
- Github FontZip可以对字体文件进行裁剪.
- 比较大的音频视频文件可以zip压缩后7zip压缩后再放大assets中,使用时先解压.
12. 目前看过最极端的方式: 将必须依赖的库代码,拷贝下来,按需引用.
3.实践
略