随着业务的倒退及版本迭代,客户端工程中一直减少新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,后期各个业务模块通过无用资源删减、大图压缩或转上云、AB试验业务逻辑下线或其余伎俩在升高包体积上获得了肯定的成绩。

在瘦身的过程中咱们关注到了R文件瘦身的概念,目前京东APP是反对插件化的,有业务插件工程、宿主工程,对业务插件包文件进行剖析,发现除了惯例的资源及代码外,R类文件大略占包体积的3%~5%左右,对宿主工程包文件进行剖析,R类文件占比也有3%左右。咱们先后在对R类文件瘦身的可行性及业界开源我的项目进行调研后,摸索出了一套实用于插件化工程的R文件瘦身技术计划。

实践根底—R文件

R文件也就是咱们日常工作中常常打交道的R.java文件,在Android开发标准中咱们须要将利用中用到的资源别离放入专门命名的资源目录中,内部化利用资源以便对其进行独自保护。

内部化利用资源后,咱们可在我的项目中应用R类ID来拜访这些资源,且R类ID具备唯一性。

public class MainActivity  extends BaseActivity {    @Override    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }}

在android apk打包流程中R类文件是由aapt(Android Asset Packaing Tool)工具打包生成的,在生成R类文件的同时对资源文件进行编译,生成resource.arsc文件,resource.arsc文件相当于一个文件索引表,应用层代码通过R类ID 能够拜访到对应的资源。

R文件瘦身的可行性剖析

日常开发阶段,在主工程中通过R.xx.xx的形式援用资源,通过编译后R类援用对应的常量会被编译进class中。

 setContentView(2131427356);

这种变动叫做内联,内联是java的一种机制(如果一个常量被标记为static final,在java编译的过程中会将常量内联到代码中,缩小一次变量的内存寻址)。

非主工程中,R类资源ID以援用的形式编译进class中,不会产生内联。

 setContentView(R.layout.activity_main);

产生这种景象的起因是AGP打包工具导致的。具体细节,大家能够去查阅一下android gradle plugin在R文件上的处理过程。

论断:R类id内联后程序可运行,但并非所有的工程都会主动产生内联景象,咱们须要通过技术手段在适合的机会将R类id内联到程序中,内联实现后,因为不再依赖R类文件,则能够将R类文件删除,在利用失常运行的同时,达到包瘦身目标。

插件化工程R文件瘦身实战

制订技术计划

目前京东Android客户端是反对插件化的,整个插件化工程蕴含公共库(是一个aar工程,用来寄存组件和宿主共用的类和资源)、业务插件(插件工程是一个独立的工程,编译产物能够运行在宿主环境中)、宿主(主工程,提供运行环境)。在插件化的过程中为了避免宿主和插件资源抵触,通过批改插件packageId保障了资源的唯一性。因为公共资源库、宿主是被很多业务依赖,对这两个我的项目进行改变评估影响波及比拟多,插件个别都是业务模块自行保护,不存在被依赖问题,所以先在业务插件模块进行R类瘦身实际。

对业务插件工程打出的包进行反编译当前,发现R类ID无内联景象,且R类文件具备肯定的大小,对包内的R文件进行剖析,发现R文件中仅蕴含业务本身的资源,不蕴含业务依赖的公共资源R类。

public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle) {    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);}

联合对业界开源我的项目的调研剖析,尝试制订合乎京东商城的技术计划并优先在业务插件内实现R类ID内联并删除对应的R文件。

1.通过transformapi 收集要解决的class文件

Transform 是 Android Gradle 提供的操作字节码的一种形式,它在 class 编译成 dex 之前通过一系列 Transform 解决来实现批改.class文件。

@Overridepublic void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation);//  通过TransformInvocation.getInputs()获取输出文件,有两种//  DirectoryInpu以源码形式参加编译的目录构造及目录下的文件//  JarInput以jar包形式参加编译的所有jar包    allDirs = new ArrayList<>(invocation.getInputs().size());    allJars = new ArrayList<>(invocation.getInputs().size());    Collection<TransformInput> inputs = invocation.getInputs();    for (TransformInput input : inputs) {        Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();         for (DirectoryInput directoryInput : directoryInputs) {               allDirs.add(directoryInput.getFile());             }            Collection<JarInput> jarInputs = input.getJarInputs();         for (JarInput jarInput : jarInputs) {                allJars.add(jarInput.getFile());             }     }}

2.对收集到的.class文件联合ASM框架进行剖析解决

ASM是一个操作Java字节码的类库,通过ASM咱们能够不便对.class文件进行批改。

优先辨认R类文件,通过ClassVisitor拜访R.class文件,读取文件中的动态常量,进行长期变量存储:

@Overridepublic FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {    //R类中收集 public static final int 对应的变量  if (JDASMUtil.isPublic(access) && JDASMUtil.isStatic(access) &&JDASMUtil.isFinal(access) &&JDASMUtil.isInt(desc)) {       jdRstore.addInlineRField(className, name, value);      }      return super.visitField(access, name, desc, signature, value);}

非R类文件,通过MethodVisitor辨认到代码中的R类援用,获取援用对应的值,进行id值替换:

@Override    public void visitFieldInsn(int opcode, String owner, String name, String desc) {        if (opcode == Opcodes.GETSTATIC) {            //owner:包名;name:具体变量名;value:R类变量对应的具体id值            Object value = jdRstore.getRFieldValue(owner, name);            if (value != null) {              //调用该api实现值替换                mv.visitLdcInsn(value);                return;            }        }        super.visitFieldInsn(opcode, owner, name, desc);    }

*注:以上代码仅为局部示意代码,非正式插件代码。

在业务模块引入R类瘦身插件后,业务模块性能可失常运行,且插件包大小均有3%~5%不同水平的缩小。

公共资源R类ID内联

因为在京东android客户端代码中,更多的资源文件集中在公共资源库中,绝对的公共库生成的R类文件也更大,对编译后的apk包内容进行剖析后,公共资源库的R类文件占比高达3%。

公共库追随宿主一起打包,在宿主打包过程中引入R类瘦身插件,打包后的apk有显著的减小,手机装置apk后启动首页失常展现无问题,但在关上某些业务插件时,会有异样闪退景象,解体类型为R.x resource not found。对解体起因剖析如下:业务插件代码中应用了公共库中的R类资源、插件打包流程独立于宿主打包,在插件打包的过程中仅实现了业务模块R类的内联,并没有思考到公共资源R类的内联,基于上述起因当宿主打包过程实现R类文件删除瘦身后,咱们在运行某业务插件的过程中,天然就会报公共资源R类找不到的问题从而产生解体。

为了解决这个问题一开始的计划构想是减少白名单机制,keep住所有被业务模块应用的公共资源,但很快这个想法就被颠覆,公共资源存在自身就是心愿各个业务模块间接援用这部分资源,而不是本人定义,如果keep住的话,必然有很大一部分的资源无奈删减,瘦身的成果会大打折扣。

既然保留的计划并不适合,那就将公共资源R类id也内联到代码中去。后面提到京东是反对插件化的,整个插件化计划是基于aura平台实现的,咱们向aura团队进行了征询,而后get到了新的计划切入点。

aura平台在插件化的过程中已通过aapt2引入了公共资源id固定的能力,在该能力下,已定义的公共资源id会始终固定(各个业务插件中援用的公共资源id统一),且公共资源库中已有的资源不可被其余模块反复定义,否则会笼罩之前已定义好的资源,基于上述的后果和规定,咱们对之前的R文件瘦身gralde plugin性能进行欠缺,将公共资源的R类id 内联到我的项目中。

利用appt2的-stable-ids和-emit-ids两个参数实现固化资源id的性能,并将将固化后的ids文件命名为shared\_res\_public.xml存储在公共资源库中,业务插件依赖公共资源库,在打包编译的过程中aura会将shared\_res\_public.xml复制到业务工程长期编译文件夹intermediates下的指定地位并参加业务模块的打包过程中,其文件内容格局如下:

批改R文件瘦身gradle plugin 代码,从指定地位读取并辨认这部分公共资源,依照<name,id>的模式进行变量存储,并在后续过程中对业务模块中的公共资源局部进行id替换。

public Map<String, String> parse() throws Exception {        if (in == null) {            return null;        }        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();        DocumentBuilder builder = factory.newDocumentBuilder();        Document doc = builder.parse(in);        Element rootElement = doc.getDocumentElement();        NodeList list = rootElement.getChildNodes();        ......        return resNode;    }}

R类资源id内联局部代码如下:

public void visitFieldInsn(int opcode, String owner, String name, String desc) {        if (opcode == Opcodes.GETSTATIC) {            //优先从业务模块R类资源中查找            Object value = jdRstore.getRFieldValue(owner, name);            if (value != null) {                mv.visitLdcInsn(value);                return;            }           //从公共R类资源中查找            value = getPublicRFileValue(name);            if (value != null) {                mv.visitLdcInsn(value);                return;            }        }        super.visitFieldInsn(opcode, owner, name, desc);    }

该计划欠缺后,联合商详业务插件进行了验证,在商详及宿主均实现R文件内联瘦身后,商详模块业务性能可失常应用,无异常现象。

思考到R文件内联瘦身gradle plugin是在打包编译阶段引入的,咱们也统计了一下引入该插件当前对打包时长的影响,数据如下:

联合数据来看,引入R文件瘦身插件后对整体打包时长并无显著影响。

至此,基于京东商城摸索的插件化工程R文件瘦身gradle plugin就开发实现,目前已在局部业务插件模块进行了线上验证,在性能上线当前咱们也及时的进行了解体观测以及用户反馈的跟进,暂无异样问题。当然围绕R文件瘦身缩减包体积这个目标,开发人员有各种各样的技术计划,上述计划不肯定实用于所有的客户端开发体系,另外后续也将围绕包瘦身这一常态事务建设一系列的相干工具,染指工作当中的各个阶段,高效、无效的管制包体积的增长,如大家在瘦身方面有相干倡议和想法也欢送大家来一起探讨。

参考文章:

Gradle Plugin:

https://docs.gradle.org/current/userguide/custom_plugins.html

Gradle Transform:

https://developer.android.com/reference/tools/gradle-api/7.0/...

APK 构建流程:

https://developer.android.com/studio/build/index.html?hl=zh-c...

作者:耿蕾 田翻新

起源:京东云开发者社区