zhengxiaoyong

也谈Manifest与资源Merge

前言

我们知道构建一个apk时必然存在的一个过程—res与manifest的合并,因为资源与manifest除了存在于主资源集中,对于第三方aar或构建变体中也可存在,当构建一个apk时,必然会对它们所含有的资源或manifest进行合并,而不是把所有存在的资源或manifest全打包进apk中,构建一个apk时,主要有以下三种资源会进行merge:

1、主资源集(src/main/)
2、构建变体(buildType、productFlavor、productFlavorBuildType)
3、三方依赖(aar)

当一个资源名在上述资源集中唯一存在时,那么将直接打包进apk中,当一个资源名在不同资源集中存在多个版本时(资源类型与资源限定符相同情况下),这时将只有一个会被打包进apk中,这种情况就是通过merge进行处理,同理,manifest也一样,对于相同的标签存在多个版本时,也将进行merge。资源和manifest的merge都遵循如下优先级:

build variant > build type > product flavor > main source set > library dependencies

下面讲res与manifest的合并策略之前先了解一下构建变体

Build Type

构建类型通过buildTypes代码块进行配置,AS在创建一个Module时会自动默认配置debugrelease两个构建类型,其中debug构建类型是每个项目默认的构建类型,任何一个项目创建时都会默认的创建它为默认的构建类型,且默认配置了一个公开通用的签名配置,这样的目的主要是为了更方便的调试应用,debug构建类型在新项目创建时并不会在buildTypes代码块中创建,不过我们可以手动的配置debug构建类型以便设置我们需要的配置,当然也可以在buildTypes中新建一个构建类型以满足个性需求,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-debug'
}

beta.initWith(buildTypes.debug)
beta {
applicationIdSuffix '.beta'
versionNameSuffix '-beta'
}

release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

其中新建一个构建类型可以通过initWith方式来创建(如beta),这样新建的构建类型的配置将于传入的构建类型的所有配置将一样,并且可以在新建的构建类型中来覆写某些不同的配置,另外一个方式即不通过initWith方式,所有的配置将由自己定义,否则将使用默认的值,创建一个构建类型gradle也会对应的创建一个资源集

Product Flavor

产品风味主要被用来创建App的不同的版本,如内测版与线上版,在productFlavors代码块中进行配置,创建一个产品风味对应的也会创建一个资源集,如:

1
2
3
4
5
productFlavors {
google {

}
}

Source Set

资源集位于Module中src目录下,共有以下几种:

src/main/
src/<buildType>/
src/<productFlavor>/
src/<productFlavorBuildType>/

如我们的主资源集src/main,AS会默认创建main/主资源集目录,用于存储要在所有构建类型、产品风味、构建变体之间共享的资源,我们项目中的主要资源也位于主资源集目录下,当我们新建一个buildTypeproductFlavor时,gradle也会相应的创建一个对应的资源集,该资源集的名称和构建类型、产品风味的名称一样,位于src目录下,只不过这需要我们手动创建对应的目录添加需要的资源,当构建类型和产品风味都存在时,gradle还会把它们进行组合成一个buildVariant构建变体,名称为产品风味与构建类型的名称的组合,gradle也会为构建变体创建一个资源集,名称和构建变体名称一样,所以,在创建了一个beta构建类型和google产品风味的情况下,共有如下的资源集:

对于每个资源集,我们可以在sourceSets代码块中进行各自配置,如manifestres自定义路径等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sourceSets {
main {
java.srcDirs = ['src/main/java', 'other/java']
res.srcDirs = ['src/main/res', 'src/main/res2']
}

beta {
res.srcDirs = ['src/beta/res', 'src/beta/res2']
manifest.srcFile 'src/beta/AndroidManifest.xml'
}

googleBeta {
res.srcDirs = ['src/googleBata/res']
}
}

Build Variant

构建变体=productFlavor+buildType,在创建productFlavor时gradle自动结合构建类型生成对应的构建变体,没有创建产品风味时将为构建类型,如:

Library Module

library依赖于其它module时将默认使用release构建类型,如果要更改为我们自己的构建类型时,有两种方法:

1、defaultPublishConfig

我们可以使用defaultPublishConfig来配置library的依赖构建变体,假如该library构建变体如上,新加了一个beta构建类型和google产品风味,那么依赖构建类型需要为googleBeta,即在library中配置:

1
2
3
4
android {
...
defaultPublishConfig "googleBeta"
}

告诉gradle该library默认发布构建类型为googleBeta

2、publishNonDefault

上面这种方式限定死了library的发布构建类型,下面这种将让library更加灵活,完全由宿主Module控制,在library中配置:

1
2
3
4
5
6
android {
...
// Note that this might increase build times because Gradle must
// build multiple AARs, instead of only one.
publishNonDefault true
}

配置publishNonDefault为true,将告诉gradle所有的构建变体将可用,具体使用哪个由所依赖的Module进行配置,所以,在宿主Module中进行依赖配置:

1
2
3
4
5
6
7
dependencies {
...
compile project(path:':library', configuration:'demoDebug')
//or
debugCompile project(path:':library', configuration:'demoDebug')
releaseCompile project(path:':library', configuration:'release')
}

宿主将引用library的demoDebug构建变体的版本

Resource Merge

一个资源是否需要merge主要看资源名是否唯一,该唯一性是在含有相同的资源类型和资源限定符的情况下,资源merge遵循如下优先级:

build variant > build type > product flavor > main source set > library dependencies

如:
主资源集中含有资源:

src/main/res/layout/foo.xml
src/main/res/layout-land/foo.xml
src/main/res/drawable/avatar.png

beta资源集中含有资源:

src/beta/res/layout/foo.xml

googleBeta资源集中含有资源:

src/googleBeta/res/drawable/avatar.png

若采用googleBeta构建变体构建,最终的apk中将包含的资源为:

src/main/res/layout-land/foo.xml
src/beta/res/layout/foo.xml
src/googleBeta/res/drawable/avatar.png

如果一个资源集中资源目录存在多个,且它们都包含一个相同的资源,那么在merge时将会报duplicate resources错误

上面说到资源merge是在相同的资源类型与资源限定符的情况下,资源类型比如drawable、layout、values等等,资源限定符是加在资源类型后面用于限定该资源所能应用的条件,比如drawable-xxhdpi、drawable-en-port等等,限定符目前共有19中,如下:

配置 限定符值示例 描述
MCC 和 MNC mcc310 移动国家代码、移动网络代码
语言和区域 en、fr 由两个字母组成的语言代码
布局方向 ldrtl、ldltr 应用的布局方向
最小宽度 sw720dp 设备可用屏幕区域的最小尺寸(宽或高)
可用宽度 w720dp 设备最小可用屏幕宽度
可用高度 h1024dp 设备最小可用屏幕高度
屏幕尺寸 normal、large 设备的屏幕密度
屏幕纵横比 long、notlong 设备屏幕是否为宽屏
圆形屏幕 round、notround 设备屏幕是否为圆屏
屏幕方向 port、land 设备当前的方向
UI 模式 car、watch 设备运行时变化的模式
夜间模式 night、notnight 是否夜间模式
屏幕像素密度 xhdpi、xxhdpi、nodpi 设备像素密度
触摸屏类型 finger、notouch 设备上的触摸屏类型
键盘可用性 keysexposed、keyssoft 设备可用的键盘
主要文本输入法 nokeys、qwerty 设备可用的主要文本输入法
导航键可用性 navexposed、navhidden 设备导航键是否处于隐藏状态
主要非触摸导航方法 nonav 设备可用的导航方法类型
平台版本(API 级别) v4、v7 设备支持的最小 API 级别

对于资源文件夹的命名,也必须遵循上表出现顺序的规则,如drawable-hdpi-port这是错误的命名,而应该是drawable-port-hdpi/,在资源进行merge时,也将是对相同限定符与类型的资源进行合并,而限定符或类型不相同的资源不进行合并

Resource Overlay

资源的overlay重叠包机制和资源merge差不多,主要是对资源一种额外补充,当在主资源集中未找到该资源时,这时会在我们添加的重叠包中进行查找,可通过aapt的-S--auto-add-overlay配置来添加额外的资源重叠包和确保重叠包可用

我们可以通过配置aaptOptions来为app添加资源重叠包提供资源,在gradle中配置:

1
2
3
4
5
aaptOptions {
additionalParameters '-S', 'src/main/res2',
'-S', 'src/main/res3',
'--auto-add-overlay'
}

当在主资源集中资源未定义时,而res2与res3中都定义了,那么在app中引用该资源将会使用res2目录中的,假如res2未定义,那么将使用res3中的,如果res2与res3互换位置,那么则优先使用最先扫描到的目录res3的资源

Manifest Merge

在构建一个apk时,manifest文件主要来自三种地方:主资源集、构建变体、第三方aar,对此,对于众多不同地方的manifest文件势必需要合成一个最终的manifest文件打包进apk中,对于manifest的merge,和资源merge一样遵循相同的优先级:

build variant > build type > product flavor > main source set > library dependencies

和资源merge不同的是,manifest文件的merge并不是从高优先级开始查找,找到了则将被添加至最终apk资源目录中,因为资源merge针对的是一个drawable、layout或string的类型的一个独立资源,而manifest是由众多节点标签和属性进行配置的,所以对于manifest的merge需要对任何一个manifest内容进行收集再合并
manifest合并遵循从低优先级到高优先级的合并顺序,如下:

也就是对于aar中的manifest文件会先进行合并至主资源集中的manifest文件中,然后主资源集的再合并至构建变体的manifest文件中,而这个构建变体的manifest为最终的manifest文件,将打包至apk中

值得注意的一点,在build.gradle中配置的构建属性将会直接替换manifest中的对应的属性,如在manifest中定义versionCodeversionNametargetSdkVersion等,只要在gradle文件中也相应的配置了对应的值,gradle的值总会覆盖manifest中的值,那么apk最终将使用gradle中配置的

Manifest合并策略-合并冲突启发式算法

Manifest合并工具在合并时会把某个manifest文件中标签元素与其它待合并的manifest文件中对应的标签元素进行匹配,而这个匹配的过程则是由一个合并关键字进行匹配的,比如manifest中配置了众多activity标签,单靠这个标签是不能对应的识别该Activity是否与另外一个manifest文件中所表示的一样。所以合并工具会根据每个标签所具备的唯一标识合并关键字进行与其它manifest中标签进行匹配,进而通过如下三种合并策略进行manifest文件的合并:

1、合并—将所有非冲突属性合并到同一标签中,如果配置了合并规则标记那么将会按各自配置的合并规则进行合并属性与子标签,如果有属性发生冲突,那么请使用合并规则标记来配置需要以哪种规则进行合并,否则在项目编译合并时将报错,大都数标签使用该策略
2、仅合并子项—该标签仅保留优先级最高的manifest文件提供的属性,如<manifest>标签中的属性将永远使用优先级最高的manifest文件
3、保留—将子标签直接将其添加至合并文件中的父元素,如<intent-filter>标签,将直接添加其它manifest文件中该标签至合并文件

上面所说大都标签元素都采用合并策略进行合并的,使用仅合并子项策略的有<manifest>标签元素,使用保留策略的有<intent-filter>标签元素。在进行manifest文件合并时,低优先级的manifest文件中的标签元素如果与高优先级的manifest文件中的不匹配时,那么这些标签元素将直接添加到合并后的manifest中,如果有匹配的标签元素,那么将会将匹配的标签的所有属性进行合并(入股配置了合并规则标记,那么将采取该标记的合并规则来进行合并),如果有相同的属性但值不同则合并会冲突,此时需要配置一个合并规则标记来处理冲突,如下,manifest默认的合并规则:

低优先级属性 高优先级属性 属性合并结果
没有值 没有值 没有值
值B 没有值 值B
没有值 值A 值A
值A 值A 值A
值B 值A 冲突-须使用合并规则标记处理

Manifest合并时一些特殊的处理方式:

1、<manifest>标签中的属性绝不合并,仅使用优先级最高的manifest文件中的属性
2、对于<manifest>的子标签<uses-feature><application>的子标签uses-library,如果在合并时出现冲突,那么系统将默认将应用该标签的android:required属性为true,并包含所有manifest对应该标签所需要的功能或库
3、<uses-sdk>标签始终使用优先级最高的manifest中的值,如果在gradle中定义了对应的值,那么将始终使用gradle中的,如果出现第三方aar中定义的minSdkVersion大于当前最高优先级manifest定义的,那么将需要使用overrideLibrary属性来处理此冲突
4、对于<intent-filter>标签,会将所有manifest中的对应该标签添加至合并后的manifest文件中

对于上面的当第三方aar的minSdkVersion大于最高优先级的manifest文件中定义的,那么在合并时将出错,使用overrideLibrary属性进行处理,该属性值是一个或者多个aar库的包名,假如lib1与lib2的minSdkVersion值都大于最高优先级清单中定义的14,那么则在该manifest中uses-sdk标签加入overrideLibrary属性进行处理,如:

1
2
<uses-sdk android:targetSdkVersion="24" android:minSdkVersion="14"
tools:overrideLibrary="com.example.lib1, com.example.lib2"/>

Manifest合并规则标记

合并规则标记是一个标签的属性,它属于android tools命名空间,它可改变合并时默认的合并行为,一般我们在某个manifest文件内定义了这些标记,那么在比它低优先级的manifest与它进行合并时,高优先级清单将应用这些合并规则进行合并,如我们最常用到的tools:replace="attr, ..."属性标记,通常是一些三方aar库中的清单文件中定义了一些属性与主清单或更高优先级的清单文件中的对应标签的属性冲突了,所以在高优先级的清单中冲突的标签配置该属性,表示始终使用高优先级清单中该标签的属性值

合并规则标记类型分两种:

节点标记—标记一个节点标签,对该标签的所有属性与所有子标签都应用该合并规则
属性标记—标记一个节点标签,仅对该标记中声明的属性应用该合并规则

节点标记

  • tools:node=”merge”

该规则是manifest所有标签默认的合并规则,可不显示声明,在manifest文件在合并时没有冲突时,则会合并该标记的标签中所有的属性与子标签,如:
低优先级清单:

1
2
3
4
5
6
7
8
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

高优先级清单:

1
2
3
4
5
6
7
8
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait"
tools:node="merge">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
</activity>

合并后的清单:

1
2
3
4
5
6
7
8
9
10
11
12
<activity 
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
</activity>
  • tools:node=”merge-only-attributes”

该规则是仅仅合并所标记的标签中的属性,但不合并嵌套的子标签,如:
低优先级清单:

1
2
3
4
5
6
7
8
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

高优先级清单:

1
2
3
4
5
6
7
8
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait"
tools:node="merge-only-attributes">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
</activity>

合并后的清单:

1
2
3
4
5
6
7
<activity android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
</activity>
  • tools:node=”remove”

从合并清单中删除用此标记的标签,如:
低优先级清单:

1
2
3
4
5
6
7
8
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

高优先级清单:

1
2
3
4
5
6
7
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait">

<intent-filter tools:node="remove">
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
</activity>

合并后的清单:

1
2
3
4
5
6
7
8
<activity android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
  • tools:node=”removeAll”

与tools:node=”remove”类似,remove只是删除一个它所标记的标签,而removeAll则是删除与此标签相匹配的所有标签(在相同的父标签内),如:
低优先级清单:

1
2
3
4
5
6
7
8
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

高优先级清单:

1
2
3
4
5
6
7
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait">

<intent-filter tools:node="removeAll">
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
</activity>

合并后的清单:

1
2
3
4
<activity android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait">

</activity>

可以看到在该父标签内,所有有关<intetn-filter>的标签全部删除了

  • tools:node=”replace”

用此标记所标记的标签,将完全替换掉低优先级所对应的标签(包含子标签元素),也就是完全保留该标记所标记的标签的内容,如:
低优先级清单:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="low"
android:value="xixi"/>

</activity>

高优先级清单:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait"
tools:node="replace">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
<meta-data
android:name="high"
android:value="haha"/>

</activity>

合并后的清单:

1
2
3
4
5
6
7
8
9
10
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
<meta-data
android:name="high"
android:value="haha"/>

</activity>

因为<intent-filter>标签是以保留策略进行合并的,所以对它标记将无作用,可对meta-data进行标记,如:
低优先级清单:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="say"
android:value="hi"/>

</activity>

高优先级清单:

1
2
3
4
5
6
7
8
9
10
11
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
<meta-data
android:name="say"
android:value="hello"
tools:node="replace"/>

</activity>

合并后的清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait"
android:launchMode="singleTask">

<intent-filter>
<action android:name="com.zxy.merge.demo.action1"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="say"
android:value="hello"/>

</activity>
  • tools:node=”strict”

配置了严格模式的标签,当在合并时只要该标签任何属性或子标签与合并时另外清单中的不完全一样时,将会报错,如:
低优先级清单:

1
2
3
4
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

</activity>

高优先级清单:

1
2
3
4
5
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait"
tools:node="strict">

</activity>

合并时将报一个Merging Error,提示两个清单中所对应的元素不一样

属性标记

  • tools:remove=”attr, …”

从合并清单中删除指定的属性,如:
低优先级清单:

1
2
3
4
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

</activity>

高优先级清单:

1
2
3
4
5
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait"
tools:remove="android:launchMode">

</activity>

合并后的清单:

1
2
3
4
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:screenOrientation="portrait">

</activity>
  • tools:replace=”attr, …”

从合并清单中,利用高优先级清单文件中的属性值替换低优先级的属性值,如:
低优先级清单:

1
2
3
4
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

</activity>

高优先级清单:

1
2
3
4
5
6
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
tools:replace="android:launchMode">

</activity>

合并后的清单:

1
2
3
4
5
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="standard"
android:screenOrientation="portrait">

</activity>
  • tools:strict=”attr, …”

指定的属性值在合并时,若低优先级的与高优先级的不一直时将报错,这是所有属性默认的标记配置,如:
低优先级清单:

1
2
3
4
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="singleTask">

</activity>

高优先级清单:

1
2
3
4
5
6
<activity
android:name="com.zxy.merge.demo.MainActivity"
android:launchMode="standard"
android:screenOrientation="portrait"
tools:strict="android:launchMode">

</activity>

合并时将报Merging Error,因为launchMode在不同清单中属性值不一样

查看合并后的Manifest

通过AS的Merged Manifest工具进行查看,选择最高优先级的清单文件,如:

不同的颜色块对应右边Manifest Sources中所对应的manifest路径来源,上图表示manifest来源有三个不同的资源集,而下面的表示虽然这些资源集含有manifest文件,但是不含构成最终manifest文件的元素或属性值,右边的最下方将会显示Merging Log或合并出错后Merging Error

坚持原创技术分享,您的支持将鼓励我继续创作!