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

@Override
public 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

作者:耿蕾 田创新

来源:京东云开发者社区

插件化工程R文件瘦身技术方案 | 京东云技术团队的更多相关文章

  1. 关于建立Android工程R文件丢失的问题

    今天开始学习Android了,好久没打开eclipse,建立Android工程老是报错,于是手残的把appcompat-v7给删了,然后建立工程以后重新出来的appcompat-v7有个小叉号,百度了 ...

  2. 技术沙龙|京东云DevOps自动化运维技术实践

    自动化测试体系不完善.缺少自助式的持续交付平台.系统间耦合度高服务拆分难度大.成熟的DevOps工程师稀缺,缺少敏捷文化--这些都是DevOps 在落地过程中,或多或少会碰到的问题,DevOps发展任 ...

  3. 揭秘丨7分钟看懂华为云鲲鹏Redis背后的自研技术【华为云技术分享】

    2019年5月,华为云发布全球首个基于自研ARM架构的分布式缓存鲲鹏Redis,搭载华为LibOS+华为编译器+安全容器引擎三项黑科技,在保证Redis强劲高性能外,还降低客户30%的使用成本,真正实 ...

  4. springboot升级过程中踩坑定位分析记录 | 京东云技术团队

    作者:京东零售 李文龙 1.背景 " 俗话说:为了修复一个小bug而引入了一个更大bug " 因所负责的系统使用的spring框架版本5.1.5.RELEASE在线上出过一个偶发的 ...

  5. 如何给你的Android 安装文件(APK)瘦身

    如何给你的Android 安装文件(APK)瘦身 本文翻译自:Putting Your APKs on Diet           原作者:Cyril Mottier Android的apk文件越来 ...

  6. 怎样给你的Android 安装文件(APK)瘦身

    本文源地址:怎样给你的Android 安装文件(APK)瘦身 Android的apk文件越来越大了这已经是一个不争的事实. 在Android 还是最初版本号的时候,一个app的apk文件大小也还仅仅有 ...

  7. Android 插件化开发(一):Java 反射技术介绍

    写在前面:学习插件化开发推荐书籍<Android 插件化开发指南>,本系列博客所整理知识部分内容出自此书. 在之前的项目架构的博文中,我们提到了项目插件化架构,提到插件化架构不得不提的到J ...

  8. 性能优化7--App瘦身

    1. 前言 如果你对App优化比较敏感,那么Apk安装包的大小就一定不会忽视.关于瘦身的原因,大概有以下几个方面: 对于用户来说,在功能差别不大的前提下,更小的Apk大小意味更少的流量消耗,也意味着更 ...

  9. Apk大瘦身

    Android的apk文件越来越大了这已经是一个不争的事实.在Android 还是最初版本的时候,一个app的apk文件大小也还只有2 MB左右,到了现在,一个app的apk文件大小已经升级到10MB ...

  10. 热门前沿知识相关面试问题-android插件化面试问题讲解

    插件化由来: 65536/64K[技术层面上]随着代码越来越大,业务逻辑越来繁杂,所以很容易达到一个65536的天花板,其65536指的是整个项目中的方法总数如果达到这个数量时则不无法创建新的方法了, ...

随机推荐

  1. 实现一个CRDT工具库——PSet

    PSet 这段代码实现了一个PSet,即Positive Set,是GSet的扩展.PSet是一个集合,支持添加和删除元素,但是不支持重复元素.PSet的实现是通过两个GSet来实现的,一个GSet存 ...

  2. 免费Midjourney AI绘画Prompt提示词平台合集

    Midjourney AI绘图最关键的地方在于Prompt提示词写的好,一个好的提示词可以让AI模型创造出更优质的绘图,以下是8个免费的Midjourney Prompt提示词辅助平台. ​ 编辑切换 ...

  3. 移动端网页--better-scroll容易采坑合集

    移动端网页--better-scroll容易采坑合集 一.better-scroll源码bug,浏览器需要刷新一次才能正常滑动 在new BScroll时,在options中加入 mouseWheel ...

  4. 线性规划的单纯形法—R实现

    table { margin: auto } 线性规划的单纯形法 线性规划是运筹学中的一个基本分支,它广泛应用现有的科学技术和数学方法,解决实际中的问题,帮助决策人员选择最优方针和决策,自1947年丹 ...

  5. Django之form表单相关操作

    目录 摘要 form表单 form表单的action参数 form表单的method参数 request.method方法 简介 get请求传递数据 post请求传递数据 GET/POST实际应用,简 ...

  6. Python程序笔记20230304

    抛硬币实验 random 模块 import random random.randint(a, b) 返回一个随机整数 N,范围是:a <= N <= b random.choice(&q ...

  7. vs的常用配置【以及vs常用的快捷键】

    1.颜色设置 (1) 编译器的主题颜色设置 (2) 字体和颜色设置 (3) 字体大小 更快捷的修改字体大小方式:ctr+鼠标滚轮 2.行号设置 默认就有,不用设置了 3.把解决方案资源管理器移动到左边 ...

  8. SRAM 测试总结

    SoC随着工艺进步设计复杂度增加,embeded sram也越来越多.在40nm SoC产品Sram一般在20Mbits左右,当工艺发展到28nm时Sram就增加到100Mbits.如果考虑AI产品, ...

  9. abp(net core)+easyui+efcore实现仓储管理系统——组织管理升级之下(六十二)

    Abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统--ABP总体介绍(一) abp(net core)+ ...

  10. CopyOnWriteArrayList的使用和优缺点

    CopyOnWriteArrayList允许并发读,读操作无锁,性能较高: 而写操作(含删除),比如向容器中添加/删除一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的 ...