当前位置: 首页 > 科技观察

插件项目R文件瘦身技术方案

时间:2023-03-21 12:33:48 科技观察

随着业务的发展和版本迭代,客户端项目不断增加新的业务逻辑和新的资源。随之而来的问题是安装包变大,各个业务模块通过无用资源删除、大图压缩或转云端、AB实验业务逻辑下线等方式在减小包体积方面取得了一定的效果。在瘦身的过程中,我们关注了瘦身R文件的概念。目前,京东APP支持插件,包括业务插件项目和宿主项目。分析业务插件包文件后发现,除了常规的资源和代码外,R类文件约占包体积的3%~5%。分析宿主工程包文件,R文件也占了3%左右。我们在研究了R文件瘦身和业界开源项目的可行性后,摸索出一套适用于插件项目的R文件瘦身技术方案。理论基础——R文件R文件就是我们在日常工作中经常接触到的R.java文件。在Android开发规范中,我们需要将应用中使用的资源放到专门命名的资源目录中,并将应用资源外化,使其单独维护。将应用资源外部化后,我们可以使用R类ID访问项目中的这些资源,R类ID是唯一的。publicclassMainActivityextendsBaseActivity{@OverrideprotectedvoidonCreate(@NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}}在androidapk打包过程中,通过aapt(AndroidAssetPackagingTool)打包生成R类文件,在生成R类文件的同时编译资源文件,生成resource.arsc文件。resource.arsc文件相当于一个文件索引表,应用层代码可以通过R类ID访问对应的文件。H.R文件瘦身可行性分析在日常开发阶段,通过R.xx.xx在主工程中引用资源。编译完成后,R类引用对应的常量会被编译到类中。设置内容视图(2131427356);这种变化叫做内联,是java的一种机制(如果一个常量被标记为staticfinal,那么在java编译的时候这个常量会被内联到代码中,减少变量一次寻址的内存)。在非主项目中,R类资源ID通过引用编译到类中,不会产生内联。setContentView(R.layout.activity_main);出现这种现象的原因是AGP打包工具造成的。具体可以查看R文件上的androidgradleplugin的处理过程。结论:R类id内联后程序可以运行,但不是所有项目都会自动产生内联现象。我们需要通过技术手段在合适的时候将R类id内联到程序中。如果再次依赖R文件,可以在应用程序正常运行的情况下删除R文件,达到减小包大小的目的。插件项目R文件细化实战制定技术方案目前京东Android客户端支持插件,整个插件项目包括一个公共库(一个aar项目,用于存放组件和宿主共享的类和资源),业务插件(插件项目为独立项目,编译后的产品可以在宿主环境中运行),宿主(主项目,提供运行环境)。为了防止插件运行过程中宿主和插件之间的资源冲突,通过修改插件packageId来保证资源的唯一性。由于公共资源库和宿主被很多业务所依赖,因此对这两个项目的变更影响评估涉及很多。插件一般由业务模块自己维护,不存在被依赖的问题。所以业务插件模块首先用于R型瘦身。实践。对业务插件项目产生的包进行反编译,发现R类ID没有内联现象,R类文件有一定大小。分析包中的R文件,发现R文件只包含业务本身的资源。不包含业务依赖项的公共资源R类。publicViewonCreateView(LayoutInflaterparamLayoutInflater,ViewGroupparamViewGroup,BundleparamBundle){this.b=paramLayoutInflater.inflate(R.layout.lib_pd_main_page,paramViewGroup,false);this.h=(PDBuyStatusView)this.b.findViewById(R.id.pd_buy_status_view);this.f=(PageRecyclerView)this.b.findViewById(R.id.lib_pd_recycle_view);}结合对业界开源项目的研究分析,尝试制定符合京东商城的技术方案,并优先考虑内嵌业务插件ID中的R类,并删除对应的R文件。1.通过transformapi收集待处理的类文件。Transform是AndroidGradle提供的一种处理字节码的方法。它在将class编译成dex之前,通过一系列Transform过程实现对.class文件的修改。@Overridepublicvoidtransform(TransformInvocationtransformInvocation)throwsTransformException,InterruptedException,IOException{super.transform(transformInvocation);//通过TransformInvocation.getInputs()获取输入文件,有两种//DirectoryInpu参与编译的目录结构和目录sourcecodeFilesunder//JarInput参与编译为jar包的所有jar包allDirs=newArrayList<>(invocation.getInputs().size());allJars=newArrayList<>(invocation.getInputs().size());集合inputs=invocation.getInputs();对于(TransformInput输入:输入){CollectiondirectoryInputs=input.getDirectoryInputs();for(DirectoryInputdirectoryInput:directoryInputs){allDirs.add(directoryInput.getFile());}集合jarInputs=input.getJarInputs();对于(JarInputjarInput:jarInputs){allJars.add(jarInput.getFile());}}}2.将收集到的.class文件结合ASM框架进行分析ProcessingASM是一个用于操作Java字节码的类库。通过ASM,我们可以很方便的修改.class文件。优先识别R类文件,通过ClassVisitor访问R.class文件,读取文件中的静态常量,存储临时变量:@OverridepublicFieldVisitorvisitField(intaccess,Stringname,Stringdesc,Stringsignature,Objectvalue){//收集R类中publicstaticfinalint对应的变量,价值);}returnsuper.visitField(access,name,desc,signature,value);}对于非R文件,代码中使用MethodVisitor识别R引用,获取引用对应的值,替换id值:@OverridepublicvoidvisitFieldInsn(intopcode,Stringowner,Stringname,Stringdesc){if(opcode==Opcodes.GETSTATIC){//owner:包名;name:具体变量名;value:R类变量Object对应的具体id值value=jdRstore.getRFieldValue(owner,name);if(value!=null){//调用此api实现值替换mv.visitLdcInsn(value);返回;}}super.visitFieldInsn(操作码,所有者,姓名,描述);}*注:以上代码只是部分原理图代码。业务模块引入非官方插件代码后,R型瘦身插件,业务模块功能可正常运行,插件包大小不同程度下降3%~5%。公共资源R类IDinline由于在京东android客户端代码中,更多的资源文件集中在公共资源库中,公共库生成的相对R类文件也更大,所以分析一下编译好的apk包的内容最后,R文件在公共存储库中的比例高达3%。公共库与宿主一起打包,在宿主打包的过程中引入了R型瘦身插件。打包后的apk明显减少。异常崩溃现象,崩溃类型为R.xresourcenotfound。崩溃原因分析如下:业务插件代码使用了公共库中的R资源,插件打包过程独立于宿主打包。在插件打包过程中,只完成了业务模块R类的内联,而公共资源R类的内联,基于以上原因,当宿主打包过程完成R类文件的删除和细化后,运行一个业务插件的过程中,自然会报找不到公共资源R类,导致崩溃的问题。为了解决这个问题,最初的计划是加入白名单机制,保留所有业务模块使用的公共资源,但这个想法很快就被推翻了。公共资源的存在本身就是希望各个业务模块可以直接引用这些资源。而不是自己定义,如果一直保留下去,肯定有很大一部分资源不能删除,减肥效果会大打折扣。由于保留方案不适用,所以代码中也内联了公共资源R类id。前面提到,京东是支持插件的,整个插件方案是基于aura平台实现的。我们咨询了光环团队,然后得到了解决方案的新切入点。aura平台在插件的过程中引入了通过aapt2固定公共资源id的能力。在这种能力下,定义的公共资源id会一直固定(每个业务插件中引用的公共资源id是相同的),公共资源库中已有的资源不能被其他模块重复定义,否则先前定义的资源将被覆盖。基于以上结果和规则,我们改进了之前R文件瘦身gralde插件的功能,将公共资源的R类id内联到项目中。利用appt2的-stable-ids和-emit-ids两个参数实现固化资源id的功能,将固化后的ids文件命名为shared_res_public.xml存放在公共资源库中。业务插件依赖公共资源库。在打包编译过程中,aura会将shared_res_public.xml复制到业务项目临时编译文件夹intermediates下的指定位置,参与业务模块的打包过程。读取并识别这部分公共资源,以的形式存储变量,并在后续流程中替换业务模块中公共资源的id。publicMapparse()抛出异常{if(in==null){returnnull;}DocumentBuilderFactoryfactory=DocumentBuilderFactory.newInstance();DocumentBuilderbuilder=factory.newDocumentBuilder();文档doc=builder.parse(in);元素rootElement=doc.getDocumentElement();NodeListlist=rootElement.getChildNodes();......返回资源节点;}}至此,我们的R文件瘦身gradleplugin将R资源分为两部分进行存储,一部分是业务本身的R资源,另一部分是我们解析固定目录下的公共R资源。之前的R文件瘦身过程修改如下:R资源idinline部分代码如下:publicvoidvisitFieldInsn(intopcode,Stringowner,Stringname,Stringdesc){if(opcode==Opcodes.GETSTATIC){//先从业务模块R类资源中查找Object值。value=jdRstore.getRFieldValue(所有者,名称);如果(值!=null){mv.visitLdcInsn(值);返回;}//从公共R资源中查找值=getPublicRFileValue(name);如果(值!=null){mv.visitLdcInsn(值);返回;}}super.visitFieldInsn(opcode,owner,name,desc);R文件内联细化完成后,业务模块的业务功能可以正常使用,没有异常现象。考虑到在打包编译阶段引入了R文件的inline瘦身gradle插件,我们也统计了引入该插件后的打包时间。数据影响如下:结合数据来看,R文件瘦身插件的引入对整体打包时间没有明显影响。至此,基于京东商城探索的插件工程R文件瘦身gradle插件开发完成。目前部分业务插件模块已经上线验证。功能上线后,我们也及时进行了崩溃观察和用户反馈的跟进。,没有例外。当然,为了减小R文件的体积,减小包的体积,开发者有多种技术方案。相关工具涉及工作的各个阶段,可以高效、有效地控制包量的增长。如果大家对减肥有相关的建议和想法,欢迎大家一起讨论。参考文章:GradlePlugin:https://docs.gradle.org/current/userguide/custom_plugins.htmlGradleTransform:https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/transform/TransformAPK构建过程:https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process

猜你喜欢