原标题:插件化工程R资料瘦身技术计划 | 京东云技术团队
随着业务的推动及平台迭代,客户端工程中不断提升新的业务逻辑、引入新的资源,随之而来的难题就是安装包体积变大 ,前期各个业务模块通过无用资源删减 、大图压缩或转上云、蓝莓市场外汇是不是正规的AB 实验业务逻辑下线或其他手段在降低包体积上取得了一定的成果 。
在瘦身的流程中我们留意到了 R 资料瘦身的概念,目前京东 APP 是拥护插件化的 ,有业务插件工程、宿主工程 ,对业务插件包资料进行解读 ,发现除了常规的资源及代码外,R 类资料大概占包体积的 3%~5% 左右 ,对宿主工程包资料进行解读,R 类资料占比也有 3% 左右 。我们先后在对 R 类资料瘦身的可行性及业界开源项目进行调研后,探索出了一套适用于插件化工程的bbmarkets蓝莓 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 资料 ,AVA爱华外汇平台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. 通过 transform api 整理要应对的 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
作者 :耿蕾 田创新 出处 :京东云开发者社区返回搜狐,查看更多
责任归纳 :