跟着业务的发展及版别迭代,客户端工程中不断增加新的业务逻辑、引进新的资源,随之而来的问题便是装置包体积变大,前期各个业务模块经过无用资源删减、大图紧缩或转上云、AB试验业务逻辑下线或其他手段在下降包体积上取得了必定的效果。

在减肥的进程中咱们重视到了R文件减肥的概念,现在京东APP是支撑插件化的,有业务插件工程、宿主工程,对业务插件包文件进行剖析,发现除了常规的资源及代码外,R类文件大约占包体积的3%~5%左右,对宿主工程包文件进行剖析,R类文件占比也有3%左右。咱们先后在对R类文件减肥的可行性及业界开源项目进行调研后,探究出了一套适用于插件化工程的R文件减肥技能计划。

理论基础—R文件

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

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

外部化运用资源后,咱们可在项目中运用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文件减肥的可行性剖析

日常开发阶段,在主工程中经过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文件瘦身技术方案 | 京东云技术团队

结合对业界开源项意图调研剖析,尝试拟定符合京东商城的技能计划并优先在业务插件内完结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文件瘦身技术方案 | 京东云技术团队

在业务模块引进R类减肥插件后,业务模块功用可正常运转,且插件包巨细均有3%~5%不同程度的削减。

公共资源R类ID内联

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

公共库跟随宿主一同打包,在宿主打包进程中引进R类减肥插件,打包后的apk有显着的减小,手机装置apk后发动主页正常展现无问题,但在翻开某些业务插件时,会有反常闪退现象,溃散类型为R.x resource not found。对溃散原因剖析如下:业务插件代码中运用了公共库中的R类资源、插件打包流程独立于宿主打包,在插件打包的进程中仅完结了业务模块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文件瘦身技术方案 | 京东云技术团队

修改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文件瘦身技术方案 | 京东云技术团队

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文件减肥插件后对全体打包时长并无明显影响。

至此,根据京东商城探究的插件化工程R文件减肥gradle plugin就开发完结,现在已在部分业务插件模块进行了线上验证,在功用上线今后咱们也及时的进行了溃散观测以及用户反馈的跟进,暂无反常问题。当然环绕R文件减肥减缩包体积这个意图,开发人员有各种各样的技能计划,上述计划不必定适用于一切的客户端开发系统,别的后续也将环绕包减肥这一常态业务建设一系列的相关东西,介入工作傍边的各个阶段,高效、有效的控制包体积的增长,如咱们在减肥方面有相关建议和想法也欢迎咱们来一同讨论。

参考文章:

Gradle Plugin:

docs.gradle.org/current/use…

Gradle Transform:

developer.android.com/reference/t…

APK 构建流程:

developer.android.com/studio/buil…

作者:耿蕾 田创新

来源:京东云开发者社区