关于前端:百度APP-Android包体积优化实践四Dex注解优化

5次阅读

共计 8386 个字符,预计需要花费 21 分钟才能阅读完成。

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 类:

  1. RetentionPolicy.SOURCE:注解只保留在源文件,当 Java 文件编译成 class 文件的时候,注解被遗弃。
  2. RetentionPolicy.CLASS:注解被保留到 class 文件,但 JVM 加载 class 文件时候被遗弃,这是默认的生命周期。
  3. 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/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 编程

正文完
 0