先看下APK打包流程:APK打包流程_贺兰猪的博客-CSDN博客
知道了APK打包流程后想要瘦身,其实无非就是把整个APK的一些文件进行一个瘦身。
看下apk的这个文件。包括class、资源,资源生成arsc(资源映射表),manifest清单,再就是meta。
对这些文件如果能缩小的话,就能达到缩小apk的目的
SVG导入
SVG(Scalable Vector Graphics),可缩放矢量图。SVG不会像位图一样因为缩放而让图片质量下降。优点在于可以减小APK的尺寸。常用于简单小图标(200dp*200dp以下,超过会损耗性能)。
svg是由xml定义的,标准svg根节点为<svg>。 Android中只支持<vector>,我们可以通过 vector 将svg的根节点<svg> 转换为<vector>。
不能直接把美工小姐姐的svg复制进drawable,要手动在Android Studio中打开工程,在res目录中点击右键创建vector,选择svg做一个这样的转换。
批量转换SVG
如果有多个svg需要转换为android的vector,则可以通过第三方工具 svg2vector 进行批量转换。
执行转换命令:
java -jar svg2vector-cli-1.0.1.jar -d . -o a -h 20 -w 20
-d 指定svg文件所在目录
-o输出android vector图像目录 -h 设置转换后svg的高 -w设置转换后svg的宽
可以优化倍图和不同颜色同样的图,用tint 属性设置你需要的颜色,也可以用颜色选择器,定义不同点击状态下的颜色。坑:编译(不是run)的时候会出现在每个倍图下面都生成了png,与期望不合。要进行适配
方式一:在gradle中设置生成的密度图
defaultConfig {
//只生成xxhdpi
generatedDensities = ['xxhdpi']
}
方式二:支持库
在 build.gradle 中配置如下,适用于 Gradle插件2.0及以上版本:
android{
// Gradle Plugin 2.0+
defaultConfig{
//利用支持库中的 VectorDrawableCompat类,可实现2.1版本及更高版本中支持VectorDrawable
vectorDrawables.useSupportLibrary=true
}
dependencies{
// 支持库版本需要是 23.2 或更高版本
compile'com.android.support:appcompat-v7:23.2.0'
}
使用矢量图 必须使用app:srcCompat属性,而不是android:src。
国际化资源配置
比如只配置英语
defaultConfig {
// 只适配英语
resConfigs 'en'
}
动态库打包配置
so文件是由ndk编译出来的动态库,是c/c++写的,所以不是跨平台的。即每一个平台需要使用对应的so库。
ABI 是应用程序二进制接口简称(Application Binary Interface),定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。
在Android系统上,每一个CPU架构对应一个ABl:armeabi,armeabi-v7a,arm64-v8a,x86,x86_64,mips,mips64.
现在我们一般只需要配置armeabi-v7a即可。
正常手机上运行支持armeabi-v7a/armeabi就行了,像微信也就只支持了armeabi-v7a,定制机可能要支持多个
defaultConfig {
ndk {
abiFilters "armeabi-v7a"
}
}
移除无用的资源
AS 给我们提供了一键移除所有无用的资源,如图。但是这种方式不建议使用,因为如果某资源仅存在动态获取资源id 的方式,那么这个资源会被认为没有使用过,从而会直接被删除。
动态获取方式:
getResources().getldentifier("name","defType",getPackageName());
或者lint检查也是
代码压缩
minifyEnabled设置成true就好了。当应用中包含许多库依赖项,但只使用它们的一小部分功能时,代码压缩起到的效果是很好的。
minifyEnabled不仅压缩了代码也混淆了代码,不需要混淆的类需要单独配置。未使用的Java代码会删除,开启的话编译会比较慢,因为要去做资源检查,平时运行一般关掉,不然打包调试很慢。
buildTypes{
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro'
}
}
在某些情况下, R8 编译器很难做出正确判断,因而可能会移除应用实际上需要的代码。下面列举了几个示例,说明它在什么情况下可能会错误地移除代码:
- 当应用通过 Java 原生接口 (JNI) 调用方法时
- 当您的应用在运行时查询代码时(如使用反射)
如需修复错误并强制 R8 保留某些代码,要在 ProGuard 规则文件中添加 -keep 代码行。例如:
-keep public class MyClass
或者,也可以为要保留的代码添加 @Keep 注解。在类上添加 @Keep
可按原样保留整个类。在方法或字段上添加该注释,将使该方法/字段(及其名称)以及类名称保持不变。请注意,只有在使用 AndroidX 注解库且添加 Android Gradle 插件随附的 ProGuard 规则文件时,此注解才可用。
资源压缩
shrinkResources true
buildTypes{
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),'proguard-rules.pro'
}
}
要和代码压缩结合使用。 why?
因为有些未使用的方法里面使用了某些资源,如果不做代码压缩,那么这个方法被保存,里面链接的资源也不会被清掉,资源压缩就删的不够干净。只有未使用的代码干掉,相应的资源才能清理更干净。
从编译结果来看,某个xml没被使用,使用资源压缩,文件还在,但是文件里面的内容会被清掉
资源文件有一个严格模式和非严格模式。默认情况下是非严格模式。非严格模式是怎样的?如果是动态加载xml文件,他会匹配文件名前缀,相当于匹配一个通配符,也就说只要是以设置的链接资源为前缀的xml,他都认为是在使用的,所以在APP里面其他有相同前缀的资源也不会被删除。那如何设置严格模式?
res-》raw-〉建立一个keep.xml,设置内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict"></resources>
但是严格模式的缺点是跟上面移除无用资源的方式一样,动态引用的资源文件也会被删除 ,需要单独在keep.xml里面设置tools:discard=""
注意:资源压缩器目前不会移除values/文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。这是因为 Android 资源打包工具不允许 Gradle插件为资源指定预定义版本。
启动图片优化
可以用webp,它是安卓它特制的一种格式,主要是用于颜色复杂的图片,转成webp清晰度不会变低但整体会变小。
也可以用jpg替代,png是有透明度的,如果图片是不需要透明度的话,那你可以用jpg去替换。那么这是一种形式,因为一个像素点你用png的话占的的位数要比jpg高。像你是用argb8888,一个像素占4字节RGB_565,一个像素占2字节。相比ARGB_8888将节省一半的内存开销。一张大图片往往省下几K或者几十k。
还可以进行图片压缩。Android 图片优化_贺兰猪的博客-CSDN博客
apk拆分
根据屏幕不同或者abi进行拆分。国内上架不支持,了解一下
splits {
density {
enable true
reset()
include "mdpi", "hdpi"
}
abi {
enable true
reset()
include "x86", "mips"
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
// Filter is null for universal APKs.
def filter = output.getFilter(com.android.build.OutputFile.ABI)
if (filter == null) {
tasks.findAll {
it.name.startsWith(
"crashlyticsUploadDistribution${variant.name.capitalize()}")
}.each {
it.doFirst {
ext.betaDistributionApkFilePath = output.outputFile.absolutePath
}
}
tasks.findAll {
it.name.startsWith(
"crashlyticsUploadSymbols${variant.name.capitalize()}")
}.each {
it.doFirst {
ext.betaDistributionApkFilePath = output.outputFile.absolutePath
}
}
}
}
}
效果如下图:
splits里面配置属性的具体含义可以看
资源混淆
启用资源文件res混淆及压缩的好处:
- 1、减少apk文件大小
- 2,增加反编译和二次打包的难度,混淆之后的apk不能用apktool之类的工具直接反编译。当然只是增加了难度,毕竟没有绝对安全;
- 3减少运行时内存占用,在运行时各个res的key都是以String形式加载到内存中的,混淆之后key变短了很多内存占用肯定会变少。
当然上述的这些优势是有在一些大体量的应用上才会比较明显,比较小的apk可能不太明显。
资源加载过程分析
在开发过程中我们通过aapt生成的R.java中的常量来使用资源,而在编译之后使用常量的地方都会被替换为常量的值。见下图。
也就是说我们通过Resource使用一个int数值来查找使用资源。那么Resource是怎么通过int数值找到具体的资源呢?我们解压apk可以看到里面有个resources.arsc文件,这个文件也是由aapt生成,文件中保存着资源id和资源key的映射关系。Resource就是按照这个映射关系找到资源的。
接下来我们具体看看这个arsc文件的具体内容。我们以单包名的应用来讲。结构见下图
其中res string pool 是res的字符串池,里面包含了字符串(就是大家开发过程中在String.xml文件增加的)和资源文件路径(包括图片,xml文件,raw文件。其中的值类似这样res/drawable/icon.png)
type string pool 是程序中使用的资源分类名称,包括 attr,anim,drawable。。。注意这里只是分类。
spec string pool 这个比较关键,这个字符串池中是所有资源的key。没有分类和config。具体的值就是R.java中各个常量的名称。
在这个字符串池后面就是各个资源分类列表,attr,anim。。。。每个分类中又包括若干config,config就是指hdpi,xhdpi,zh_rCN , land这些,大家知道android资源config有18个纬度。每个config中就是具体的res资源列表。
举例来说一下一个资源的查找过程,比如当我们调用getDrawable(R.drawable.icon)时候,先找到drawable分类,再根据当前的系统config找到匹配的config表。然后根据id找到对应的res数据。drawable在arsc文件中是当作string类型保存的。res数据中有这个资源在res string pool池中的索引。根据这个索引可以在字符串池中找到这个字符串。这个字符串其实是一个文件的路径,比如:res/drawable-hdpi/icon.png,然后读取apk中这个文件。
资源混淆原理和实现
看上面的例子,我们注意到一般情况下查找资源的过程中不会用到资源名,也就是key。当然res数据中也有当前res的key在key string pool中的索引,但一般情况下用不到。根据这个原理我们可以在打包完成之后对key进行混淆。而且刚刚说到res数据中保存的都是字符串在string pool中的索引。所以我们混淆只需要修改res string pool和spec string pool两个字符串池。其他的数据不用修改。
我们混淆的流程大概是这样的:
- 1,解压apk文件;
- 2,读取arsc文件;
- 3,混淆spec string pool;
- 4根据上一步混淆结果混淆res string pool;
- 5,生成新的arsc文件;
- 6,输出mapping文件;
- 7,根据3,4步结果,修改res目录下所有文件名;
- 8,打包压缩(7zip)(压缩程度更高);
- 9,签名;
- 10.zipalign。
keep
再说key的问题,刚刚说一般情况下用不到res的key。但是开发中我们有时候会通过key来查找资源。也就是说需要注意,需要通过key来使用的资源的key不能混淆。
可选方案
目前的实施方案有微信资源混淆打包工具AndResGuard和美团方案
美团方案需要自己实现函数,下面看下微信方案AndResGuard
1.根目录build文件中,添加插件的依赖
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21'
}
2.在主module(默认为APP)目录下,创建res_guard.gradle配置文件,名称可自定义。官方使用文档上是直接写在build.gradle文件中,这样会使build文件内容过多且繁杂,故建议放在单独的gradle中。
apply plugin: 'AndResGuard'
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// It will keep the origin path of your resources when it's true
keepRoot = false
// If set, name column in arsc those need to proguard will be kept to this value
fixedResName = "arg"
// It will merge the duplicated resources, but don't rely on this feature too much.
// it's always better to remove duplicated resource from repo
mergeDuplicatedRes = true
//需要将所有通过getIdentifier访问的资源放入白名单。
//各种第三方的也要加进来,看github 的md
whiteList = [
// 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",
"R.string.project_id",
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.21'
//path = "/usr/local/bin/7za"
}
/**
* Optional: if finalApkBackupPath is null, AndResGuard will overwrite final apk
* to the path which assemble[Task] write to
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* Optional: Specifies the name of the message digest algorithm to user when digesting the entries of JAR file
* Only works in V1signing, default value is "SHA-1"
**/
// digestalg = "SHA-256"
}
3.在主module(APP)目录下的build.gradle中,添加使用
apply from: 'res_guard.gradle'
资源混淆核心是修改资源映射表,资源混淆后查错比较麻烦,会有一个mapping 去看映射关系。
可以看到在gradle插件中有替换压缩工具(7zip替换apkbuilder )进行压缩,这样压缩率更高