乐趣区

关于包管理:插件化工程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 文件。

@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/…

APK 构建流程:

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

作者:耿蕾 田翻新

起源:京东云开发者社区

退出移动版