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,这类信息通常只能经过客户端(非体系)代码来间接获取。包括下面几种:
注解名 |
含义 |
.annotation system Ldalvik/annotation/MemberClasses |
内部类列表 |
.annotation system Ldalvik/annotation/InnerClass |
内部类本身的信息,与EnclosingClass或EnclosingMethod共同存在 |
.annotation system Ldalvik/annotation/EnclosingClass |
声明该内部类的地方为类,与EnclosingMethod互斥 |
.annotation system Ldalvik/annotation/EnclosingMethod |
声明该内部类的地方为办法,与EnclosingMethod互斥 |
例如,有一个如下结构的类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 可执行文件格局:source.android.com/docs/core/d…
[2] Android 注解:developer.android.com/studio/writ…
[3] Titan-Dex字节码操作框架:github.com/baidu/titan…
[4] gson源码:github.com/google/gson
推荐阅览:
百度工程师带你探秘C++内存办理(ptmalloc篇)
为什么 OpenCV 计算的视频 FPS 是错的
百度 Android 直播秒开体验优化
iOS SIGKILL 信号量溃散抓取以及优化实践
如安在几百万qps的网关服务中完成灵活调度策略
浅显易懂DDD编程