Android插件化系列二: 资源与打包流程

好的朋友们,新的一周开始了,让咱们继续来学习插件化的知识吧。先回顾一下系列文章架构 java

Android插件化文章框架

根据个人行文思路,本篇文章讲解资源和App打包的一些知识。算是插件化系列的第二篇基础文章。阅读完本文后,你应该会了解:android

  • 资源id的组成,
  • R.java的秘密
  • App打包流程

资源这一部分将会先从你们的直观印象切入,逐步的加大深度。而后我会结合前半部分资源的铺垫讲解App的打包流程。你们若是阅读完之后发现,咦,这一点我还真不知道,那本文也算是有点意义了。由于本篇依然属于插件化的基础知识文章,因此仍是不会讲到插件化,可是后面讲到插件化的时候会引用到本篇文章的部分知识。从另外个角度来讲,本篇文章也是一篇知识比较自成一体的文章。OK,那我们开始吧。git

资源与R.java

先作一点准备工做,咱们建一个工程,这个工程下面有三个module,App和咱们自建的modulea,moduleb。这三个module的依赖关系是app->modulea->moduleb。而后咱们在每一个module里面放一点资源,好比string之类的,这里我在modulea中放了一个String叫testA, 在moduleb中放了一个String叫testB。而后咱们会发如今每一个module的build/generated/source/r(这个文件夹跟gradle版本有关系,3.5之后文件夹有变动)下面出现了R.java文件,这个就是android打包过程当中借助于aapt工具生成的资源id目录。github

而后咱们分别打开主模块和modulea的R.java。下面是主模块和普通模块的R.java文件中的id示例。安全

// 主模块app中的R.java
public static final int testA = 0x7f0b002a;
public static final int testB=0x7f0b002b;

// modulea中的R.java
public static int testA = 0x7f15002b;
public static int testB = 0x7f15002c;
复制代码

你们能够看到:微信

  • 为何资源组成都以0x7f开头?
  • 为何主模块(application module)资源有final修饰,非主模块(library module,后面也称库模块)都不是final的
  • 为何同一个资源,不一样模块产生的R.java中的资源id值是不统一的

为何会这样呢?咱们将在后面讲解这些内容并在最后给出结论架构

1.资源Id的组成

咱们先看看资源Id的组成。你们都知道,资源id是一个资源的惟一标识。那么问题来了,这么多的module,这么多的资源种类,甚至还有Android自带的资源,资源id为何不会重复呢?秘诀就在资源id的组成上面。app

packageId: 前两位是packageId,至关于一个命名空间,主要用来区分不一样的包空间(不是不一样的module)。目前来看,在编译app的时候,至少会遇到两个包空间:android系统资源包和我们本身的App资源包。你们能够观察R.java文件,能够看到部分是以0x01开头的,部分是以0x7f开头的。以0x01开头的就是系统已经内置的资源id,以0x7f开头的是我们本身添加的app资源id。框架

typeId:typeId是指资源的类型id,咱们知道android资源有animator、anim、color、drawable、layout,string等等,typeId就是拿来区分不一样的资源类型。ide

entryId:entryId是指每个资源在其所属的资源类型中所出现的次序。注意,不一样类型的资源的Entry ID有多是相同的,可是因为它们的类型不一样,咱们仍然能够经过其资源ID来区别开来。

经过资源id的三个区块的划分,在编译期间,同一个资源在普通的apk中只会属于一个package,一个type,只拥有一个次序,因此一个资源的id是不会和别的资源重复的。固然这只是正常状况下,要是咱们有部分资源没有参与打包呢?好比说咱们要说的插件化,插件化是要下发一个插件,插件中固然也有资源,这部分资源是没有通过统一的编译的,那么就可能存在和宿主(插件要下发到的App)资源冲突的状况。好比你已经给梁山排好了108将,每一个人都有一个称号,可是从山下又来了一个“及时雨”宋江,那岂不是同时存在两个及时雨了,听谁的呢?梁山就会大乱,app也是如此。

为了不这种状况,插件的资源id一般会采用0x02 - 0x7e之间的数值,避免和宿主资源冲突。至于怎么作到的,等后面的文章再聊~

2.资源id的使用

咱们一般会在编码的时候使用相似于R.layout.xxxx一类的引用,这些引用就是R.java文件中的字段。而且咱们在主模块和library模块中使用这些id的时候,好像并无什么区别,那么这二者中的id真的是毫无区别吗?

咱们先看看在主模块中和库模块中分别去使用id的区别。咱们分别在app模块和modulea模块中分别建一个Activity。每一个Activity中有一段这样的代码,你们应该都比较熟悉

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}
复制代码

而后咱们点击Android Studio的Tools->kotlin->show kotlin bytecode直接看这个类的字节码。固然直接看字节码仍是比较难,咱们再点面板上的decompile,把它解析成java代码。而后咱们就会发现,有点细微的区别。

// 主模块的代码
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.setContentView(-1300009);
}

// 库模块的代码
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.setContentView(layout.activity_module_a);
}
复制代码

你们能够看到,主模块中的R.layout.xxx彻底是做为常量,直接内联进了代码中。而库模块中的R.layout.xxx, 依然是做为变量引用到了代码中。这个规律在编译期间也是存在的。这个规律和前面对R.java中的字段的处理是一致的,也便是说,

  • 主模块中的R.java中的字段以final修饰,以常量形式存在。
  • 库模块中的R.java中的字段不以final修饰,以变量形式被项目中的代码所引用。

3.资源的合并

一般apk中的资源来源主要是3个,具体能够参考官网

  • 主资源(main source set):好比src/main/res
  • 编译变量(Build variant source set): 好比src/demoDebug/res
  • 库文件依赖(libraries): 也就是咱们引进的aar。

一个资源一般会使用它的文件名做为标识,也就是说,相同resource type(anim/drawable/string等)和相同resource qualifier(好比hdpi, value中的语言等)下相同文件名的资源,系统会认为他是惟一的。那么单一module下可能就会有相同的资源存在,好比有多个主资源集。那么当出现这种冲突的状况的时候,系统会怎么处理呢?系统会进行合并,低优先级的资源会被覆盖掉。

覆盖的优先级以下: build variant > build type > product flavor > main source set > library dependences

举个栗子,若是咱们主资源集下有两个资源: res/layout/a.xml, res/layout/b.xml, build type文件夹下面有res/layout/a.xml。那么最后打包生成的apk中的res/layout/a.xml来自于build type, res/layout/b.xml来自于main source set。

除了单一module不一样文件夹下的资源覆盖,不一样module间也会有资源覆盖。好比app模块依赖了modulea,两个module中都有一个资源文件res/layout/a.xml,那么最后编译的apk中的res/layout/a.xml必定是app模块下定义的那个。

资源合并有什么实际意义呢?我我的认为经过资源合并能够实现更高级别的自适应打包。好比说,咱们能够为不一样的product flavor去设置不一样的资源,好比页面xml,这样,只要改一下product flavor就能打出不同的包,实现更高级别的自适应。

4.R文件的生成

上面讲了资源id的一些机制,接下来咱们来探讨一下R文件的生成机制。这里的规律是基于gradle 3.1.2

首先咱们先看一下数量上的规律,仍是以咱们上面的例子为例。三个module的依赖关系是app->modulea->moduleb。modulea中有个string叫testA,moduleb中有个string叫testB。最后咱们发现app模块下面有三个R文件。

而且发现plugindemo(也便是App模块)下面的R文件里包含了咱们在modulea和moduleb中定义的string。

经过上面的例子能够给出结论,用一个图能够说明。

1.数量的规律:一个module被编译的时候,会生成当前module的R文件,而且该module依赖的module或者aar也会在当前module生成R文件。这种依赖关系不一样于gradle里面的implementation依赖传递,implementation是跨级不能传递,可是R文件的生成是跨级能够传递的。因此, module的R文件数 = 依赖的module/aar数量 + 1(自身的R文件)

举个例子,A模块依赖了B模块,同时也依赖了fresco,那么他生成的R文件有几个呢?答案是三个,B模块,fresco,和自身的R文件。

2.生成顺序的规律,三个模块的依赖关系是app->modulea->moduleb。生成R文件的顺序是从底层到上层,逐层生成。也就是说先生成moduleb的,再生成modulea的,再生成app模块的。

3.资源的规律:上层模块会把所依赖的模块的R文件merge进去。好比app模块并无testA和testB这两个string,可是app的R文件却包含了这两个资源的id。这就是由于上层的模块把下层模块的资源给merge进去了。

5.总结

讲完了这些规律,咱们就能够回答小节一开头提出的三个问题了。

1.为何资源id都以0x7f开头?
由于这些资源都是应用包的资源,统一是0x7f开头

2.为何主模块(application module)资源有final修饰,非主模块(library module)都不是final的?
比较早的aapt的版本生成的非主模块的资源id确实都是final修饰的,这样会带来一个问题,这些资源id所有内联到代码中,一旦新增或者删除,修改了资源,资源id就会有变化,全部的代码都须要从新编译,形成严重的编译耗时。后来改成主模块final常量方式内联,非主模块引用方式,这样等按照从下到上编译到App模块的时候,全部的资源id都已经肯定了,底层模块的资源只须要经过引用就能拿到本身对应的id,而修改(新增,删除,修改)了资源以后,也只须要从新生成R文件就行了。编译耗时大大减小。

3.为何同一个资源,不一样模块产生的R.java中的资源id值是不统一的?
由于资源id只是表示资源的次序,而不是别的跟资源自己绑定的属性。当到了不一样的模块之后,参与编译的资源变多了,那次序确定会改变。资源id也就改变了。而且子模块的资源id只是引用形式存在于代码中,id具体是什么值并非很care。

不知道你们看完这些,有没有什么收获呢?

6.补充知识

不知道你们有没有用过ButterKnife这个依赖注入框架,这个框架最核心的使用场景就是使用注解进行依赖注入。好比

@BindView(R.id.user) EditText username;
复制代码

你们应该常见这种用法,那么,这里有没有什么玄机呢?咱们上面讲到了,非主模块中资源id是变量,没有final修饰。可是注解你们都知道,传入的参数必须是final常量。这样的话岂不是相悖了吗?

其实上面的两个结论都没有错。Butterknife针对这种状况作了一个骚处理。他直接copy了一份module中的R.java,搞了个R2.java,把R.java中全部的资源id所有改成final的,这样就能在注解中使用了。等到真正使用的时候,再进行替换,使用真正的主模块的生成的资源id。

具体能够参考R.java、R2.java 是时候懂了

App打包

打包流程这一块我会先讲述基本流程,而后会补充一些关于打包流程的应用的扩展知识。

1.打包流程

先来一张打包流程图。

1.打包资源文件,生成R.java文件 这一过程主要是aapt对res和asset文件夹,AndroidManifest.xml,android库(aar,jar)等的资源文件进行处理。先检查AndroidManifest.xml的合法性,而后编译res与asserts目录下的资源并生成resource.arsc文件,再生成R文件。除了assets和res/raw资源被原封不动地打包进APK以外,其它的资源都会被编译或者处理,大部分文本格式的XML资源文件会被编译成二进制格式的XML资源文件。除了assets资源以外,其余的资源都会在R文件中被赋予一个资源ID。也就是说,R文件中只会存在id,真正的资源存在于resource.arsc中,resource.arsc至关于一个资源索引表,资源id是key,value是资源路径。咱们使用drawable-xdpi或者drawable-xxdpi这些不一样分辨率的图片的时候,就是依靠resource.arsc根据设备的分辨率选择不一样的图片。

2.处理aidl文件,生成相应的.java文件
这一步就是咱们代码中的aidl的文件被生成java文件。

3.编译工程源码,生成相应的class文件 R文件,aidl生成的java文件和咱们工程中的源代码被javac工具编译成了class文件。

4.转换全部的class文件,生成classes.dex文件
Android系统的dalvik虚拟机的可执行文件为dex格式,程序运行所需的classes.dex文件就是在这一步生成的,使用的工具为dx,dx工具主要的工做是将java字节码转换为dalvik字节码、压缩常量池、消除冗余信息等。 这里在生成dex的时候,就会遇到65536的问题。一个DEX文件中的method个数采用使用原生类型short来索引文件的方法,也就是4个字节共计最多表达65536个method。因此当method数过多的时候,就必须使用multidex。

5.打包生成apk
把全部的dex文件打包为一个apk文件。

6.对apk文件进行签名 apk须要签名才能在手机上安装。平时咱们测试主要是使用了一个debug.keystore对apk进行签名。正式发布时须要提供一个符合android开发文档中要求的签名文件。好比jarsigner和APK Signature Scheme v2。

7.对签名后的apk进行对齐处理 一步须要使用的工具为zipalign,它位于android-sdk/tools目录,源码位于android系统资源的build/tools/zipalign目录,它的主要工做是将apk包进行对齐处理,使apk包中的全部资源文件举例文件起始偏移为4字节的整数倍,这样经过内存映射访问apk时的速度会更快。为何快呢?若是每一个资源的开始位置上都是一个资源以后的4n字节,那么访问下一个资源就不用遍历,直接跳到4字节以后便可。

8.混淆proguard:proguard主要的目的是混淆代码,保护应用源代码。次要的功能还有移除无用类等,优化字节码,缩小包体积。

  • 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
  • 优化(Optimize):字节码进行优化,移除无用的指令。
  • 混淆(Obfuscate):使用a、b、c、d这样简短而无心义的名称,对垒、字段和方法进行重命名。
  • 预检测(Preveirfy):在Java平台对处理后的代码进行预检测,确保加载class文件是可执行的。

2.一些技术点

1.资源去重,极致缩包
前面咱们讲到了proguard的功能是混淆代码和缩减体积。可是proguard是不能处理资源文件的。为了解决资源文件的混淆问题,微信推出了AndResGuard。使用AndResGuard能够更加缩减包体积。

除了AndResGuard以外,咱们还会遇到资源被重复使用的问题,识别重复资源很简单,只要计算一下md5就好了。而且咱们在resources.arsc中能够拿到全部的资源,那么咱们就能够对resources.arsc中的全部资源进行处理,根据md5进行去重,把使用了相同资源的资源id都指向同一个资源,把多余的资源删除掉,再回写入resources.arsc就行了。固然,这里面仍是有挺多学问的。

2.Transform
Transform是Android gradle plugin提供给开发者的一套API,容许开发者在编译以后,dex以前对class进行修改。开发者能够经过AppExtension或者LibraryExtension进行注册Transform。多个transform会造成一条链。上一个Transform的输出是下一个Transform的输入,所以,Transform的顺序也很重要。

既然有了这个Transform,就意味着咱们有机会去操做java的字节码。网上常见的处理字节码的框架有AspectJ, Javasist, ASM。能够利用这些工具进行字节码插桩。这样能够把一些不能耦合在业务代码中的代码在字节码阶段给merge进去。

3.多渠道打包
Android和iOS不同的是市场和渠道众多,为了区分和统计不一样的渠道包的效果,须要有一种方法来标记他们。你们可能会想到使用productFlavor,可是这样的话要打多少包就须要build多少次,耗时很是长。

如今比较好的方案是在apk进行v2签名的时候在签名块中写入一些信息,这样更快更安全。详情能够参考Android美团多渠道打包Walle集成

3.总结

本小节讲解了打包过程,和利用打包机制能够作的一些技术点。打包过程若是能学的透彻的话,仍是能给android开发带来不少的可能性。

参考文章:
罗升阳 Android应用程序资源的编译和打包过程分析

Android美团多渠道打包Walle集成

R.java、R2.java 是时候懂了

我是Android笨鸟之旅,一个陪着你慢慢变强的公众号,欢迎关注我一块儿学习,一块儿进步哈~

相关文章
相关标签/搜索