01 前言
百度 APP Android 包体积优化实践系列文章的前三篇分别介绍了体积优化的整体方案、Dex 行号优化和资源优化。和 Dex 行号优化一样,Dex 注解优化也是针对 Dex 文件进行的优化,但是优化的内容却有所不同。Dex 行号优化的对象是 Dex 文件中的 DebugInfo 字段,而注解优化则是通过去除 Dex 中的非必要注解来优化包体积。
注解是 Java 5.0 引入的注释机制,Java 语言的类、方法、变量、参数和包都可以被注解标注。不同于普通注释,注解最终可以保留在字节码里,虚拟机可通过反射获取注解内容。我们分析了 Dex 中的不同注解类型和常见的几种注解,发现 Dex 中所有的编译时注解,大部分泛型与类关系信息注解是可以去掉的,同时不会对代码运行有影响,因此我们使用自研的字节码操作框架针对性的去掉了上述非必要的注解,并建立了注解优化自动化检测和加白机制,实现优化 Dex 体积的目的。
本文将详细描述 Dex 注解优化的内容,包括 Dex 注解类型、Dex 注解格式、优化目标、优化方案以及 Dex 注解优化自动化检测和加白。
百度 APP Android 包体积优化实践系列文章回顾:
百度APP Android包体积优化实践(一)总览
百度APP Android包体积优化实践(二)Dex行号优化
百度APP Android包体积优化实践(三)资源优化
02 Dex 注解类型
2.1 注解的生命周期分类
我们知道注解按生命周期来划分可分为 3 类:
RetentionPolicy.SOURCE:注解只保留在源文件,当 Java 文件编译成 class 文件的时候,注解被遗弃。
RetentionPolicy.CLASS:注解被保留到 class 文件,但 JVM 加载 class 文件时候被遗弃,这是默认的生命周期。
RetentionPolicy.RUNTIME:注解不仅被保存到 class 文件中,JVM 加载 class 文件之后仍然存在。
2.2 Dex 注解的可见性分类
如下图所示,按照注解的可见性,Dex 中的注解又可以分为以下 3 类:
(1)编译时注解
其中 BUILD 对应 Java RetentionPolicy.SOURCE 和 RetentionPolicy.CLASS,表明在源文件中和 class 文件中存在的注解,在运行时是无效的。
(2)运行时注解
RUNTIME 对应 RetentionPolicy.RUNTIME。
(3)系统注解
SYSTEM 表示仅供系统使用,与业务代码无直接关系。
03 Dex 注解格式
在 Dex 中,用 smali 标识的注解格式如下所示:
.annotation [注解属性] <注解类名>
[注解字段 = 值]
.end annotation
复制代码
如果注解的作用范围是类, .annotation 指令会直接定义在 smali 文件中,如果作用范围是方法或者字段,则会包含在方法或字段定义中。
我们具体反编译 apk 后,对于在源码中一个方法上的注解 @SuppressLint("BanParcelableUsage"),查看 smali 中注解表现如下:
.annotation build Landroid/annotation/SuppressLint;
value = {
"BanParcelableUsage"
}
.end annotation
复制代码
以上图为例,可以看出 build 表明注解类型是编译时注解,Landroid/annotation/SuppressLint 表明注解的类型,而 value 的内容则表明注解的值是"BanParcelableUsage"。
04 优化目标
我们分析了 Dex 中所有的注解,总结出几种可以优化的注解类型,如下图所示,包括所有的 build 注解,system 注解中的泛型注解和四种类关系注解。具体说明如下:
△可以优化的注解(标黄部分)
4.1 build 注解
正如官方文档里所写的,build 类型注解仅作用于编译期,最终 apk 中无需保留。proguard 规则 -keepattribute **Annotations**会将其保留到最终 dex 中,由于 proguard 规则可能是由三方库引入的,所以我们需要后置处理 build 注解。
4.2 system 注解-泛型注解
描述泛型内容的注解,注解名为 Ldalvik/annotation/Signature。每一处使用泛型的源码最终都会由编译器自动生成一个泛型注解,可存在于 class、method、field。
例如我们在一个类中定义了如下变量,由于 jsonObjectList 使用了泛型,因此 Dex 中会对该变量生成对应的泛型注解,如下所示:
public List<JSONObject> jsonObjectList = new ArrayList<>()
复制代码
.field public jsonObjectList:Ljava/util/List;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/List<",
"Lorg/json/JSONObject;",
">;"
}
.end annotation
.end field
复制代码
同时系统也提供了如下接口来获取泛型信息,如果代码中不存在以下接口获取泛型信息,那么泛型注解就可以被优化。
java/lang/Class.getTypeParameters
java/lang/Class.getGenericSuperclass
java/lang/Class.getGenericInterfaces
java/lang/reflect/Field.getGenericType
java/lang/reflect/Method.getGenericReturnType
java/lang/reflect/Method.getTypeParameters
java/lang/reflect/Method.getGenericParameterTypes
java/lang/reflect/Method.getGenericExceptionTypes
java/lang/reflect/Constructor.getTypeParameters
java/lang/reflect/Constructor.getGenericParameterType
java/lang/reflect/Constructor.getGenericExceptionTypes
复制代码
4.3 system 注解—类关系注解
描述类关系的注解,仅存在于 class,这类信息通常只能通过客户端(非系统)代码来间接获取。包括下面几种:
例如,有一个如下结构的类 OuterClass,包含着一个 InnerClass 的内部类。
public
class
OuterClass
public String a;
public class InnerClass{
public String b;
}
}
复制代码
我们查看 OuterClass 类的 smali 文件,可以看到有 MemberClasses 注解标识了内部类 InnerClass。
.class public Lcom/baidu/searchbox/OuterClass;
.super Ljava/lang/Object;
.source "OuterClass.java"
# annotations
.annotation system Ldalvik/annotation/MemberClasses;
value = {
Lcom/baidu/searchbox/OuterClass$InnerClass;
}
.end annotation
...
复制代码
我们查看 InnerClass 类的 smali 文件,可以看到有 InnerClass 注解标识了自身的内部类信息,同时 EnclosingClass 表明了声明该 InnerClass 的地方是 OuterClass 类。
.class public Lcom/baidu/searchbox/OuterClass$InnerClass;
.super Ljava/lang/Object;
.source "OuterClass.java"
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lcom/baidu/searchbox/OuterClass;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x1
name = "InnerClass"
.end annotation
复制代码
同时系统也提供了如下接口来获取类关系信息,如果代码中不存在以下接口获取类关系信息,那么类关系注解就可以被优化。
com/google/gson/Gson.fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object
com/google/gson/Gson.fromJson(Lcom/google/gson/JsonElement;Ljava/lang/Class;)Ljava/lang/Object
com/google/gson/Gson.fromJson(Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object
复制代码
05 优化方案
Titan-Dex 是百度开源的面向 Android Dalvik(ART)字节码操作框架,可以在二进制格式下实现修改已有的类,或者动态生成新的类。
由于 Dex 注解优化是直接对生成的 Dex 进行修改,因此选用了 Titan-Dex 来操作 DexAnnotation。
我们自定义了一个 task 在默认的 packaging task 之前执行,首先遍历 Dex 中的所有类、方法、字段,扫描所有的 DexAnnotation,当扫描到注解类型为 build、或注解名为 Sginature/MemberClasses/InnerClass/EnclosingClass/EnclosingMethod 时,移除该 DexAnnotation。
override fun visitClass(dcn: DexClassNode) {
val outDexClassNode = DexClassNode(dcn.type, dcn.accessFlags, dcn.superType, dcn.interfaces)
outDexClassPoolNode.addClass(outDexClassNode)
MarkedMultiDexSplitter.setDexIdForClassNode(outDexClassNode, dexId)
//遍历该Dex下面的所有类
dcn.accept(object : DexClassVisitor(outDexClassNode.asVisitor()) {
override fun visitAnnotation(annotationInfo: DexAnnotationVisitorInfo):
DexAnnotationVisitor? {
//检查类注解是否匹配删除规则
return if (removeAnnotation(annotationInfo, dcn.type.toTypeDescriptor())) {
null
} else super.visitAnnotation(annotationInfo)
}
override fun visitMethod(methodInfo: DexMethodVisitorInfo?): DexMethodVisitor {
val superMethodVisitor = super.visitMethod(methodInfo)
return object : DexMethodVisitor(superMethodVisitor) {
override fun visitAnnotation(annotationInfo: DexAnnotationVisitorInfo):
DexAnnotationVisitor? {
//检查方法注解是否匹配删除规则
return if (removeAnnotation(annotationInfo, dcn.type.toTypeDescriptor())) {
null
} else super.visitAnnotation(annotationInfo)
}
override fun visitParameterAnnotation(parameter: Int, annotationInfo:
DexAnnotationVisitorInfo): DexAnnotationVisitor? {
//检查方法参数的注解是否匹配删除规则
return if (removeAnnotation(annotationInfo, dcn.type.toTypeDescriptor())) {
null
} else super.visitParameterAnnotation(parameter, annotationInfo)
}
}
}
override fun visitField(fieldInfo: DexFieldVisitorInfo?): DexFieldVisitor {
val superFiledVisitor = super.visitField(fieldInfo)
return object : DexFieldVisitor(superFiledVisitor) {
override fun visitAnnotation(annotationInfo: DexAnnotationVisitorInfo):
DexAnnotationVisitor? {
//检查类变量的注解是否匹配删除规则
return if (removeAnnotation(annotationInfo, dcn.type.toTypeDescriptor())) {
null
} else super.visitAnnotation(annotationInfo)
}
}
}
})
}
复制代码
/**
* 删除不必要的注解
*
* @param annotationInfo
* @param classType
* @return Boolean
*/
private fun removeAnnotation(annotationInfo: DexAnnotationVisitorInfo,
classType: String): Boolean {
// build类型注解优化,仅根据配置开关决定
if (annotationInfo.visibility.name == ANNOTATION_TYPE_BUILD && optBuild) {
return true
}
// system类型注解优化,根据开关与白名单决定
if (!optSystem) {
return false
}
when (annotationInfo.type.toTypeDescriptor()) {
ANNOTATION_SIGNATURE,
ANNOTATION_INNERCLASS,
ANNOTATION_ENCLOSINGMETHOD,
ANNOTATION_ENCLOSINGCLASS,
ANNOTATION_MEMBERCLASS ->
if (classType !in whiteListSet) {
LogUtil.log("current classType", classType)
LogUtil.log("current annotationInfo.type", annotationInfo.type.toTypeDescriptor())
LogUtil.log("系统注解", "需要删除")
return true
}
}
return false
}
复制代码
同时,我们还定义了白名单机制,对于一些调用了上面的系统接口的情况会跳过注解优化,保留原有注解。
06 自动化检测和加白
在上述 Dex 注解优化开发完成后,当时的接入步骤是首先扫描整个 APK 中相关的注解反射接口调用,然后根据扫描的结果去排查对应的业务场景,确认是否可以移除对应的注解。最后确认需要加白后,由业务手动加入白名单并提交。整个过程较为繁杂,过于滞后且依赖人工,导致整个注解优化方案接入成本过高,因此需要一套前置的注解自动化检测方案。
对于这种问题,我们选择了基于 Android Lint 来检查注解反射接口调用的情况。我们自定义了三个 Lint 规则如下:
1、自定义 lint 规则
ClassShipUseDetector:扫描类关系接口调用。
SignatureUseDetector:扫描泛型注解接口调用。
EncapsulationDetector:扫描 Gson.fromJson 封装,如果 fromJson 方法封装后,工具没办法确认目标 Bean 类,需要封装方自行添加白名单。
2、扫描触发流程
加入目前 warning 拦截流程,在提测/上车时拦截,能前置的发现问题。
3、豁免方法
对应方法添加 @SuppressLint("${detector_name}"),提取抽象规则,或者给目标类添加 @KeepAllDavilkAnnotation 加白。
4、自动化加白
为了避免对问题场景逐个手动加白,我们抽象了一套加白规则并开发了一套 Gradle 插件来实现自动化加白,下面是抽象出的五种加白规则。其中子类加白规则优先于其他规则。每条规则使用 #${type}做结尾。
规则格式:${父类名}#superclass
若声明规则 classA#superclass,则 classA 以及继承了 classA 的所有子类均保留注解。
备注:如果子类 signature 不为 null,需解析后一并加入白名单。
常见场景:Gson TypeToken 等
规则格式:${注解名}#annotation
若声明规则 annotationA#annotation,则使用了 @annotationA(类、方法、属性注解)的类均保留注解。
常见场景:使用 Gson 进行序列化/反序列化的类,常会使用 @SerializedName
规则格式:${包名}.**#package
常见场景:三方 sdk
规则格式:${类名}#classname
常见场景:暂时无法抽象规则的类。比如百度内开发的老 jar 包,无法通过包名进行区分
规则格式:${包含该匿名内部类的类名}#anonymous
匿名内部类的名字是由编译器分配的,我们无法提前得知它的全名。这个加白规则会将该匿名内部类平级的所有内部类都加入白名单。范围不可控,匹配成本也比较高,所以建议对这种使用方式进行改造,改为前 4 种规则可命中的方式
下面是百度 App 根据上述规则抽象出的一套白名单,同时我们通过 Gradle 插件实现了具体类白名单的自动生成。
com.baidu.searchbox.net.update.v2.AbstractCommandListener#superclass
com.google.gson.reflect.TypeToken#superclass
com.google.gson.annotations.SerializedName#annotation
com.google.gson.**#package
com.alipay.**#package
com.baidu.FinalDb#classname
...
复制代码
在 Gradle Transform 阶段获取到所有的 class 文件,匹配到加白规则的 class( 类、类成员中的泛型信息)则加入白名单。这样可以自动生成大部分的白名单类,只需要人工 check 和补充少量的白名单内容即可,减少了人工配置白名单的成本。
07 总结
本文主要介绍了百度 APP Dex 注解优化方案,其中重点讲述了 Dex 注解优化的目标,详细方案,自动化检测和加白机制。经过百度 App 上线验证,减少了 Dex 体积约 1.2M。感谢各位阅读至此,如有问题请不吝指正。
——END——
参考资料:
[1] Dalvik 可执行文件格式:https://source.android.com/docs/core/dalvik/dex-format?hl=zh-cn
[2] Android 注解:https://developer.android.com/studio/write/annotations?hl=zh-cn
[3] Titan-Dex 字节码操作框架:https://github.com/baidu/titan-dex
[4] gson 源码:https://github.com/google/gson
推荐阅读:
百度工程师带你探秘C++内存管理(ptmalloc篇)
为什么 OpenCV 计算的视频 FPS 是错的
百度 Android 直播秒开体验优化
iOS SIGKILL 信号量崩溃抓取以及优化实践
如何在几百万qps的网关服务中实现灵活调度策略
深入浅出DDD编程
评论