写点什么

插件化工程 R 文件瘦身技术方案 | 京东云技术团队

  • 2023-06-13
    北京
  • 本文字数:4809 字

    阅读完需:约 16 分钟

插件化工程R文件瘦身技术方案 | 京东云技术团队

随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减、大图压缩或转上云、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/com/android/build/api/transform/Transform


APK 构建流程:


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


作者:耿蕾 田创新

来源:京东云开发者社区

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
插件化工程R文件瘦身技术方案 | 京东云技术团队_企业号 6 月 PK 榜_京东科技开发者_InfoQ写作社区