包大小的重要性已经不需要多说,包大小直接影响用户的下载,留存,甚至部分厂商预装强制要求必须小于一定的值。
-dontshrink 关闭压缩
复制代码
-dontoptimize 关闭优化
-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5
复制代码
-dontobfuscate 关闭混淆
复制代码
1)、优化了 Gson 库的使用。
2)、把类都标记为 final。
3)、把枚举类型简化为常量。
4)、把一些类都垂直合并进当前类的结构中。
5)、把一些类都水平合并进当前类的结构中。
6)、移除 write-only 字段。
7)、把类标记为私有的。
8)、把字段的值跨方法地进行传递。
9)、把一些方法标记为私有、静态或 final。
10)、解除方法的 synchronized 标记。
11)、移除没有使用的方法参数。
复制代码
buildTypes {
release {
// 1、是否进行混淆
minifyEnabled true
// 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
zipAlignEnabled true
// 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
// 这些代码所引用的资源也会被标记为无用资源,然后
// 系统通过资源压缩功能将它们移除。
// 需要注意的是目前资源压缩器目前不会移除values/文件夹中
// 定义的资源(例如字符串、尺寸、样式和颜色)
// 开启后,Android构建工具会通过ResourceUsageAnalyzer来检查
// 哪些资源是无用的,当检查到无用的资源时会把该资源替换
// 成预定义的版本。主要是针对.png、.9.png、.xml提供了
// TINY_PNG、TINY_9PNG、TINY_XML这3个byte数组的预定义版本。
// 资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。
shrinkResources true
// 4、混淆文件的位置,其中 proguard-android.txt 为sdk默认的混淆配置,
// 它的位置位于android-sdk/tools/proguard/proguard-android.txt,
// 此外,proguard-android-optimize.txt 也为sdk默认的混淆配置,
// 但是它默认打开了优化开关。并且,我们可在配置混淆文件将android.util.Log置为无效代码,
// 以去除apk中打印日志的代码。而 proguard-rules.pro 是该模块下的混淆配置。
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
业务梳理:
开发模式升级:
代码
1. ProGuard
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.view.View
复制代码
// 情况一:变量
public String activityName = "com.sample.TestActivity";
// 情况二:方法体
startActivity(new Intent(this, "com.sample.TestActivity"));
// 情况三:通过运算得到,不支持
startActivity(new Intent(this, "com.sample" + ".TestActivity"));
复制代码
1)、Dex的编译时间更短。
2)、.dex文件更小。
3)、D8 编译的 .dex 文件拥有更好的运行时性能。
4)、包含 Java 8 语言支持的处理。
复制代码
android.enableR8=true
android.enableR8.libraries=true
复制代码
让本来存放在 App 中的行号对应关系提前抽离出来存放在服务端,crash 上报的时候通过提前
抽离的行号表进行行号反解,解决 crash 信息上报无行号,无法定位的问题,主要步骤如下:
1. 修改 proguard:利用 proguard 来删除 debugItem (去掉 -keep lineNumberTable),在删除行号表之前 dump 出一个临时的 dex。
2. 修改 dexdump:把临时的 dex 中的行号表关系 dump 成一个 dexpcmapping 文件(指令集行号和源文件行号映射关系),并存至服务端。
3. hook app runtime 的 crash handler,把 crash 时的指令集行号上报到反解平台。
4. 反解平台通过上报指令集行号和提前准备好 dexpcmapping 文件反解出正确的行号。
复制代码
保留一小块 debugItem,让系统查找行号的时候指令集行号和源文件行号保持一致,
这样就什么都不用做,任何监控上报的行号都直接变成了指令集行号,只需修改 dex 文件
复制代码
{
"redex" : {
"passes" : [
"StripDebugInfoPass",
"RegAllocPass"
]
},
"StripDebugInfoPass" : {
"drop_all_dbg_info" : false,
"drop_local_variables" : true,
"drop_line_numbers" : false,
"drop_src_files" : false,
"use_whitelist" : false,
"cls_whitelist" : [],
"method_whitelist" : [],
"drop_prologue_end" : true,
"drop_epilogue_begin" : true,
"drop_all_dbg_info_if_empty" : true
},
"RegAllocPass" : {
"live_range_splitting": false
}
}
复制代码
2. Dex 分包
{
"redex" : {
"passes" : [
"StripDebugInfoPass",
"InterDexPass",
"RegAllocPass"
]
},
"StripDebugInfoPass" : {
"drop_all_dbg_info" : false,
"drop_local_variables" : true,
"drop_line_numbers" : false,
"drop_src_files" : false,
"use_whitelist" : false,
"cls_whitelist" : [],
"method_whitelist" : [],
"drop_prologue_end" : true,
"drop_epilogue_begin" : true,
"drop_all_dbg_info_if_empty" : true
},
"InterDexPass" : {
"minimize_cross_dex_refs": true,
"minimize_cross_dex_refs_method_ref_weight": 100,
"minimize_cross_dex_refs_field_ref_weight": 90,
"minimize_cross_dex_refs_type_ref_weight": 100,
"minimize_cross_dex_refs_string_ref_weight": 90
},
"RegAllocPass" : {
"live_range_splitting": false
},
"string_sort_mode" : "class_order",
"bytecode_sort_mode" : "class_order"
}
复制代码
3. Dex 压缩
- Facebook App 的 classes.dex 只是一个壳,真正的代码都放到 assets 下面。它们把所有的 Dex 都合并成同一个 secondary.dex.jar.xzs 文件,并通过 XZ 压缩。
- XZ 压缩算法和 7-Zip 一样,内部使用的都是 LZMA 算法。对于 Dex 格式来说,XZ 的压缩率可以比 Zip 高 30% 左右。
- 存在的问题:
- 首次启动解压:应用首次启动的时候,需要将 secondary.dex.jar.xzs 解压缩,根据上图的配置信息,应该一共有 11 个 Dex。
- ODEX 文件生成:Facebook 为了解决这个问题,使用了 ReDex 另外一个超级硬核的方法,那就是oatmeal
复制代码
Native Library
//生产环境
release {
resValue "string", "app_name", "@string/app_name_release"
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
//有些观点是只留下armeabi即可,armeabi 目录下的 So 可以兼容别的平台上的 So,
//但是,这样 别的平台使用时性能上就会有所损耗,失去了对特定平台的优化,而且近期国内的应用市场也开始要求适配64位的应用了
}
}
//开发环境
debug {
resValue "string", "app_name", "@string/app_name_debug"
ndk {
rootProject.ext.ndkAbis.each { abi ->
abiFilter(abi)
}
}
}
复制代码
包体积监控
资源优化
使用 AndResGuard 工具
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
复制代码
android:extractNativeLibs=“true”
复制代码
apply plugin: 'AndResGuard'
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.18'
}
}
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字
keepRoot = false
// 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
// for your icon
"R.drawable.icon",
// 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",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.18'
//path = "/usr/local/bin/7za"
}
/**
* 可选: 如果不设置则会默认覆盖assemble输出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可选: 指定v1签名时生成jar文件的摘要算法
* 默认值为“SHA-1”
**/
// digestalg = "SHA-256"
}
复制代码
进阶的优化方法
一 资源合并: 所有的资源文件都合并成同一个大文件
// 系统默认的方式
Drawable drawable = getResouces().getDrawable(R.drawable.loading);
// 新的获取方式
Drawable drawable = CustomResManager.getDrawable(R.drawable.loading);
复制代码
同时我们还要实现自己的资源缓存池 ResourceCache,释放不再使用的资源文件,这部分内容你可以参考类似 Glide 图片库的实现。
二 无用资源
//如果 ProGuard 把部分无用代码移除,这些代码所引用的资源也会被标记为无用资源,然后通过资源压缩功能将它们移除
//没有真正删除,而是替换成空文件,防止resources.arsc 和 R.java 文件的资源 ID 会改变
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
复制代码
对于 Assets 资源,代码中会有各种各样的引用方式,如果想准确地识别出无用的 Assets 并不是那么容易。在 Matrix 中尝试提供了一套简单的实现,可以参考UnusedAssetsTask;
避免产生 Java access 方法
1、编译期间 内联常量字段:const_inline。
2、编译期间 移除多余赋值代码:field_assign_opt。
3、编译期间 移除 Log 代码:method_call_opt。
4、编译期间 内联 Get / Set 方法:getter-setter-inline-plugin。
复制代码
三 重复资源优化
实现代码如下:
variantData.outputs.each {
def apFile = it.packageAndroidArtifactTask.getResourceFile();
it.packageAndroidArtifactTask.doFirst {
def arscFile = new File(apFile.parentFile, "resources.arsc");
JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile);
def HashMap> duplicatedResources = findDuplicatedResources(apFile);
removeZipEntry(apFile, "resources.arsc");
if (arscFile.exists()) {
FileInputStream arscStream = null;
ResourceFile resourceFile = null;
try {
arscStream = new FileInputStream(arscFile);
resourceFile = ResourceFile.fromInputStream(arscStream);
List chunks = resourceFile.getChunks();
HashMap toBeReplacedResourceMap = new HashMap(1024);
// 处理arsc并删除重复资源
Iterator>> iterator = duplicatedResources.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry> duplicatedEntry = iterator.next();
// 保留第一个资源,其他资源删除掉
for (def index = 1; index < duplicatedEntry.value.size(); ++index) {
removeZipEntry(apFile, duplicatedEntry.value.get(index).name);
toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name);
}
}
for (def index = 0; index < chunks.size(); ++index) {
Chunk chunk = chunks.get(index);
if (chunk instanceof ResourceTableChunk) {
ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk;
StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool();
for (def i = 0; i < stringPoolChunk.stringCount; ++i) {
def key = stringPoolChunk.getString(i);
if (toBeReplacedResourceMap.containsKey(key)) {
stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key));
}
}
}
}
} catch (IOException ignore) {
} catch (FileNotFoundException ignore) {
} finally {
if (arscStream != null) {
IOUtils.closeQuietly(arscStream);
}
arscFile.delete();
arscFile << resourceFile.toByteArray();
addZipEntry(apFile, arscFile);
}
}
}
}
复制代码
四 图片压缩
但这可能会导致本来已经优化过的图片体积变大,因此,可以通过在 build.gradle 中 设置 cruncherEnabled 来禁止 AAPT 来优化 PNG 图片
aaptOptions {
cruncherEnabled = false
}
复制代码
五 R Field 的内联优化
要想实现内联 R Field,我们需要 通过 Javassist 或者 ASM 字节码工具在构建流程中内联 R Field;
六 资源文件最少化配置
最终 APK 包中会包含 AppCompat 中所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。 对此,我们可以 通过 resConfig 来配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源。 同理,也可以使用 resConfigs 去配置你应用需要的图片资源文件类,如 "xhdpi"、"xxhdpi" 等等
android {
...
defaultConfig {
...
resConfigs "zh", "zh-rCN"
resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}
...
}
//还可以利用 Density Splits 来选择应用应兼容的屏幕尺寸大小
android {
...
splits {
density {
enable true
exclude "ldpi", "tvdpi", "xxxhdpi"
compatibleScreens 'small', 'normal', 'large', 'xlarge'
}
}
...
}
复制代码
七资源在线化
八统一应用风格
九 插件化
十 使用 webp 格式图片
使用webp格式的图片可以在保持清晰度的情况下减小图片的磁盘大小,是一种比较优秀的,google推荐的图片格式。
十一 .修改三方库的源码,不需要的代码剔除
比如引入了一个功能很齐全的三方库utils,但实际只用到几个,对源码进行抽取也能减少. 包体积,同时还能减少网络下载的编译时间。
弊端就是升级成本较大。
十二 图片网络化
即把图片上传到服务器,通过动态下载的方式减少包体积,弊端就是首次加载的时候依赖网络环境,对加载速度、流量需要做一个平衡。 图片可以预加载,但是流量消耗是无法避免了,如果比较在意流量指标,需要权衡了。
十三 DebugItem
DebugItem 里面主要包含两种信息:
去除debug信息与行号信息,如果不是极致,不推荐。 可以参考支付宝的这篇 支付宝 App 构建优化解析:Android 包大小极致压缩。
十四 图片着色器
针对同图不同色的处理,可以使用tint
,比如原本是一个黑色的返回icon,现在另一个页面 要用白色了,就不需要两张图了,而是使用tint来修改为白色即可。
复制代码