当前位置: 首页 > 后端技术 > Java

百度APP安卓包体积优化实践(四)Dex注解优化

时间:2023-04-01 22:49:27 Java

01前言百度APP安卓包体积优化实践系列前三篇分别介绍了体积优化、Dex行数优化和资源优化的整体解决方案。和Dex行号优化一样,Dex注解优化也是针对Dex文件进行优化,只是优化的内容不同。Dex行号优化的对象是Dex文件中的DebugInfo字段,注解优化是通过去除Dex中不需要的注解来优化包大小。Annotation是Java5.0引入的注解机制。Java语言中的类、方法、变量、参数、包都可以进行注解。不同于普通的注解,注解最终可以保存在字节码中,虚拟机可以通过反射获取注解的内容。我们分析了Dex中不同的注解类型和几种常见的注解,发现Dex中所有的编译期注解,大部分泛型和类关系信息注解都可以去掉而不影响代码运行,所以我们使用自研的字节码运行框架来去掉上述不需要的注解,建立注解优化自动检测和白化机制,达到优化Dex体积的目的。本文将详细介绍Dex注解优化的内容,包括Dex注解类型、Dex注解格式、优化目标、优化方案以及Dex注解优化的自动检测和白化。百度APPAndroid包大小优化实践系列文章回顾:百度APPAndroid包大小优化实践(一)概述百度APPAndroid包大小优化实践(二)Dex行数优化百度APPAndroid包大小优化实践(三)资源优化02DexAnnotationType2.1Annotations的生命周期分类我们知道,Annotations按照生命周期可以分为三类:RetentionPolicy.SOURCE:Annotations只保存在源文件中。当Java文件被编译成类文件时,注释被丢弃。RetentionPolicy.CLASS:注解保留在类文件中,但在JVM加载类文件时被丢弃。这是默认的生命周期。RetentionPolicy.RUNTIME:注解不仅保存到class文件中,在JVM加载class文件后仍然存在。2.2Dex注解的可见性分类如下图所示。根据注解的可见性,Dex中的注解可以分为以下三类:(1)编译时注解,其中BUILD对应JavaRetentionPolicy.SOURCE和RetentionPolicy.CLASS,表示在源文件和类文件中的Annotations在运行时无效。(2)运行时注解RUNTIME对应RetentionPolicy.RUNTIME。(3)系统注解SYSTEM表示仅供系统使用,与业务代码无直接关系。03Dex注解格式在Dex中,smali识别的注解格式如下:.annotation[注解属性]<注解类名>[注解字段=值].endannotation如果注解的作用域是一个类,.annotationcommand会直接在smali文件中定义,如果作用域是方法或字段,则会包含在方法或字段定义中。我们具体反编译apk后,对于源码中某个方法上的注解@SuppressLint("BanParcelableUsage"),查看smali中的注解如下:.annotationbuildLandroid/annotation/SuppressLint;value={"BanParcelableUsage"}.endannotation以上图为例。可以看出build表示注解类型是编译时注解,Landroid/annotation/SuppressLint表示注解类型,value的内容表示注解的值为“BanParcelableUsage”。04优化目标我们分析了Dex中的所有注解,总结了几种可以优化的注解,如下图所示,包括所有构建注解、系统注解中的泛型注解和四种关系注解。具体说明如下:△可优化注解(黄色部分)4.1构建注解官方文档中写到,构建类型注解只在编译时使用,不需要在最终apk中保留。proguard规则-keepattribute**Annotations**将把它保留在最终的dex中。由于proguard规则可能是三方库引入的,所以我们需要对构建注解进行后处理。4.2系统注解——通用注解描述通用内容的注解,注解名称为Ldalvik/annotation/Signature。每一个使用泛型的源代码最终都会被编译器自动生成一个泛型注解,它可以存在于类、方法和字段中。例如,我们在一个类中定义如下变量。由于jsonObjectList使用了泛型,Dex会为变量生成对应的泛型注解,如下:publicListjsonObjectList=newArrayList<>()。字段公共jsonObjectList:Ljava/util/List;.注释系统Ldalvik/annotation/Signature;value={"Ljava/util/List<","Lorg/json/JSONObject;",">;"}.endannotation.end同时,系统还提供如下接口获取通用信息。如果获取泛型信息的代码中不存在如下接口,那么可以优化泛型注解。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.getGenericExceptionTypes4.3系统注解——类关系注解描述类关系的注解,其中仅存在于类中,此类信息通常只能通过客户端(非系统)代码间接获得。包括以下几种类型:比如有一个类OuterClass,结构如下,其中包含一个InnerClass的内部类。publicclassOuterClasspublicStringa;公共类InnerClass{公共字符串b;我们查看OuterClass类的smali文件,可以看到MemberClasses注解标识了内部类InnerClass。.classpublicLcom/baidu/searchbox/OuterClass;.superLjava/lang/Object;.source"OuterClass.java"#annotations.annotationsystemLdalvik/annotation/MemberClasses;value={Lcom/baidu/searchbox/OuterClass$InnerClass;}.endannotation...我们查看InnerClass类的smali文件,可以看到InnerClass注解标识了自己的内部类信息,EnclosingClass表示声明InnerClass的地方是OuterClass类。.classpublicLcom/baidu/searchbox/OuterClass$InnerClass;.superLjava/lang/Object;.source"OuterClass.java"#annotations.annotationsystemLdalvik/annotation/EnclosingClass;value=Lcom/baidu/searchbox/OuterClass;.结束注释。注释系统Ldalvik/annotation/InnerClass;accessFlags=0x1name="InnerClass".endannotation同时,系统还提供如下接口获取类关系信息。如果获取类关系信息的代码中不存在下面的接口,那么可以优化类关系注解。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/Object05优化方案Titan-Dex是百度开源的AndroidDalvik(ART)词的段代码运行框架可以修改二进制格式的现有类,或者动态生成新的类。由于Dex注解优化是直接修改生成的Dex,所以选择Titan-Dex来操作DexAnnotation。我们自定义了一个任务,在默认打包任务之前执行。首先遍历Dex中所有的类、方法、字段,扫描所有的DexAnnotations。当注解类型为build,或者注解名称为Sginature/MemberClasses/InnerClass/EnclosingClass/EnclosingMethod时,去掉DexAnnotation。overridefunvisitClass(dcn:DexClassNode){valoutDexClassNode=DexClassNode(dcn.type,dcn.accessFlags,dcn.superType,dcn.interfaces)outDexClassPoolNode.addClass(outDexClassNode)MarkedMultiDexSplitter.setDexIdForClassNode(outDexClassNode,dexId)//遍历下面的Dexdcn.accept(object:DexClassVisitor(outDexClassNode.asVisitor()){overridefunvisitAnnotation(annotationInfo:DexAnnotationVisitorInfo):DexAnnotationVisitor?{//检查类注解是否符合删除规则returnif(removeAnnotation(annotationInfo,dcn.type.toTypeDescriptor())){null}elsesuper.visitAnnotation(annotationInfo)}覆盖有趣的visitMethod(methodInfo:DexMethodVisitorInfo?):DexMethodVisitor{valsuperMethodVisitor=super.visitMethod(methodInfo)返回对象:DexMethodVisitor(superMethodVisitor){覆盖有趣的visitAnnotation(注释信息:DexAnnotation访客信息):DexAnnotationVisitor?{//检查方法注解是否匹配删除规则DexAnnotationVisitorInfo):DexAnnotationVisitor?{//检查方法参数的注解是否符合删除规则(fieldInfo:DexFieldVisitorInfo?):DexFieldVisitor{valsuperFiledVisitor=super.visitField(fieldInfo)返回对象:DexFieldVisitor(superFiledVisitor){覆盖乐趣visitAnnotation(annotationInfo:DexAnnotationVisitorInfo):DexAnnotationVisitor?{//检查类变量的注解是否符合删除规则*删除不需要的注解**@paramannotationInfo*@paramclassType*@returnBoolean*/privatefunremoveAnnotation(annotationInfo:DexAnnotationVisitorInfo,classType:String):Boolean{//构建类型注解优化,只根据配置开关判断if(annotationInfo.visibility.name==ANNOTATION_TYPE_BUILD&&optBuild){returntrue}//系统类型注解优化,根据开关和白名单决定if(!optSystem){returnfalse}when(annotationInfo.type.toTypeDescriptor()){ANNOTATION_SIGNATURE,ANNOTATION_INNERCLASS,ANNOTATION_ENCLOSINGMETHOD,ANNOTATION_ENCLOSINGCLASS,ANNOTATION_MEMBERCLASS->if(classType!inwhiteListSet){LogUtil.log("currentclassType",classType)LogUtil.log("currentannotationInfo.type",annotationInfo.type.toTypeDescriptor())LogUtil.log("系统注释","需要删除")returntrue}}returnfalse}同时,我们还定义了白名单机制。对于某些调用上述系统接口的情况,会跳过注解优化,保留原有注解。06自动检测和白化上述Dex注解优化开发完成后,当时的接入步骤是先扫描整个APK中的相关注解反射接口调用,然后根据对应的业务场景进行检查扫描结果,以确认是否可以删除相应的注释。业务确认需要加入白名单后,手动加入白名单并提交。整个过程比较复杂,过于滞后,依赖人工,导致整个标注优化方案的接入成本很高。因此,需要一套预注释自动检测方案。针对此类问题,我们选择了基于AndroidLint来检查注解反射接口调用。我们自定义了三个lint规则如下:1.自定义lint规则ClassShipUseDetector:扫描类关系接口调用。SignatureUseDetector:扫描通用注解接口调用。EncapsulationDetector:扫描Gson.fromJson封装。如果封装了fromJson方法,工具无法确认目标Bean类,需要封装者自行添加白名单。2、在当前的告警拦截流程中增加扫描触发流程,可在测试/登机时拦截,提前发现问题。3、在免检方法对应的方法中添加@SuppressLint("${detector\_name}")提取抽象规则,或者在目标类中添加@KeepAllDavilkAnnotation加白。4.自动白化为了避免手动对问题场景进行一个一个的白化,我们抽象出一套白化规则,开发了一套Gradle插件来实现自动白化。以下是五个抽象的白化规则。其中,子类白化规则优先于其他规则。每条规则都以#${type}结尾。子类白化规则格式:${父类名}#superclass如果声明了规则classA#superclass,classA和所有继承classA的子类都会保留注解。备注:如果子类签名不为null,需要解析加入白名单。常见场景:GsonTypeToken等注解白化规则格式:${注解名称}#annotation如果声明规则annotationA#annotation,使用@annotationA(类、方法、属性注解)的类会保留注解。常见场景:使用Gson进行序列化/反序列化的类经常使用@SerializedName整包白化规则格式:${包名}.**#package常见场景:三方sdk公用类白化规则格式:${类名}#classname常见场景:暂时不能抽象规则的类。比如百度开发的老jar包,不能通过包名区分匿名内部类。提前了解它的全名。该白化规则会将所有与匿名内部类处于同一级别的内部类列入白名单。范围不可控,匹配成本比较高,所以建议修改这个使用方式,改成可以命中的前4条规则。下面是百度App根据上述规则抽象出来的一组白名单。实现了具体类白名单的自动生成。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...在GradleTransform阶段获取所有class文件,将符合白化规则的class(类中的通用信息和类成员)加入白名单。这样可以自动生成大部分白名单类,只需要手动检查和添加少量白名单内容,减少了手动配置白名单的成本。07总结本文主要介绍百度APPDex注解优化方案,重点介绍了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不对百度安卓直播体验优化iOSSIGKILL信号量崩溃捕获及优化实践如何实现灵活调度具有数百万qps的网关服务中的策略简单的DDD编程