01 前言
百度APP Android包體積優化實踐系列文章的前三篇分別介紹了體積優化的整體方案、Dex行號優化和資源優化。和Dex行號優化一樣,Dex注解優化也是針對Dex文件進行的優化,但是優化的內容卻有所不同。Dex行號優化的對象是Dex文件中的DebugInfo字段,而注解優化則是通過去除Dex中的非必要注解來優化包體積。
注解是Java 5.0引入的注釋機制,Java語言的類、方法、變量、參數和包都可以被注解標注。不同于普通注釋,注解最終可以保留在字節碼里,虛擬機可通過反射獲取注解內容。我們分析了Dex中的不同注解類型和常見的幾種注解,發現Dex中所有的編譯時注解,大部分泛型與類關系信息注解是可以去掉的,同時不會對代碼運行有影響,因此我們使用自研的字節碼操作框架針對性的去掉了上述非必要的注解,并建立了注解優化自動化檢測和加白機制,實現優化Dex體積的目的。
本文將詳細描述Dex注解優化的內容,包括Dex注解類型、Dex注解格式、優化目標、優化方案以及Dex注解優化自動化檢測和加白。
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<>()
public List<JSONObject> jsonObjectList = new ArrayList<>()
同時系統也提供了如下接口來獲取泛型信息,如果代碼中不存在以下接口獲取泛型信息,那么泛型注解就可以被優化。
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。感謝各位閱讀至此,如有問題請不吝指正。
參考資料:
[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