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.getTypeParametersjava/lang/Class.getGenericSuperclassjava/lang/Class.getGenericInterfacesjava/lang/reflect/Field.getGenericTypejava/lang/reflect/Method.getGenericReturnTypejava/lang/reflect/Method.getTypeParametersjava/lang/reflect/Method.getGenericParameterTypesjava/lang/reflect/Method.getGenericExceptionTypesjava/lang/reflect/Constructor.getTypeParametersjava/lang/reflect/Constructor.getGenericParameterTypejava/lang/reflect/Constructor.getGenericExceptionTypes
4.3 system注解—类关系注解
形容类关系的注解,仅存在于class,这类信息通常只能通过客户端(非零碎)代码来间接获取。包含上面几种:
例如,有一个如下构造的类OuterClass,蕴含着一个InnerClass的外部类。
publicclassOuterClass 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/Objectcom/google/gson/Gson.fromJson(Lcom/google/gson/JsonElement;Ljava/lang/Class;)Ljava/lang/Objectcom/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#superclasscom.google.gson.reflect.TypeToken#superclasscom.google.gson.annotations.SerializedName#annotationcom.google.gson.**#packagecom.alipay.**#packagecom.baidu.FinalDb#classname...
在Gradle Transform阶段获取到所有的class文件,匹配到加白规定的class( 类、类成员中的泛型信息)则退出白名单。这样能够主动生成大部分的白名单类,只须要人工check和补充大量的白名单内容即可,缩小了人工配置白名单的老本。
07 总结
本文次要介绍了百度APP Dex注解优化计划,其中重点讲述了Dex注解优化的指标,具体计划,自动化检测和加白机制。通过百度App上线验证,缩小了Dex体积约1.2M。感激各位浏览至此,如有问题请不吝指正。
——END——
参考资料:
[1] Dalvik 可执行文件格局:https://source.android.com/do...
[2] Android 注解:https://developer.android.com...
[3] Titan-Dex字节码操作框架:https://github.com/baidu/tita...
[4] gson源码:https://github.com/google/gson
举荐浏览:
百度工程师带你探秘C++内存治理(ptmalloc篇)
为什么 OpenCV 计算的视频 FPS 是错的
百度 Android 直播秒开体验优化
iOS SIGKILL 信号量解体抓取以及优化实际
如何在几百万qps的网关服务中实现灵便调度策略
深入浅出DDD编程