找回密码
 会员注册
查看: 16|回复: 0

百度APPAndroid包体积优化实践(四)Dex注解优化

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
71113
发表于 2024-10-8 18:35:50 | 显示全部楼层 |阅读模式
一.?前言百度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包体积优化实践(三)资源优化二.?Dex注解类型丨1?注解的生命周期分类我们知道注解按生命周期来划分可分为3类:RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。RetentionPolicy.CLASS:注解被保留到class文件,但JVM加载class文件时候被遗弃,这是默认的生命周期。RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,JVM加载class文件之后仍然存在。丨2?Dex注解的可见性分类如下图所示,按照注解的可见性,Dex中的注解又可以分为以下3类:(1)编译时注解其中 BUILD 对应 Java RetentionPolicy.SOURCE 和 RetentionPolicy.CLASS,表明在源文件中和class文件中存在的注解,在运行时是无效的。(2)运行时注解RUNTIME 对应 RetentionPolicy.RUNTIME。(3)系统注解SYSTEM表示仅供系统使用,与业务代码无直接关系。三.?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"。四.?优化目标我们分析了Dex中所有的注解,总结出几种可以优化的注解类型,如下图所示,包括所有的build注解,system注解中的泛型注解和四种类关系注解。具体说明如下:可以优化的注解(标黄部分)丨1?build注解正如官方文档里所写的,build类型注解仅作用于编译期,最终apk中无需保留。proguard规则 -keepattribute **Annotations**会将其保留到最终dex中,由于proguard规则可能是由三方库引入的,所以我们需要后置处理build注解。丨2?system注解-泛型注解?描述泛型内容的注解,注解名为Ldalvik/annotation/Signature。每一处使用泛型的源码最终都会由编译器自动生成一个泛型注解,可存在于class、method、field。例如我们在一个类中定义了如下变量,由于jsonObjectList使用了泛型,因此Dex中会对该变量生成对应的泛型注解,如下所示:public List jsonObjectList = new ArrayList();.field public jsonObjectListjava/util/List; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/List;" } .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丨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/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五.?优化方案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}同时,我们还定义了白名单机制,对于一些调用了上面的系统接口的情况会跳过注解优化,保留原有注解。六.?自动化检测和加白在上述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和补充少量的白名单内容即可,减少了人工配置白名单的成本。七.?总结本文主要介绍了百度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
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-9 10:42 , Processed in 1.005049 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表