工作中,几乎总会遇到多样化打包的场景,本篇文章主要内容分为:
- 分析AndroidStudio打包一个AAR SDK(APK)的流程原理
- 如何自定义task来任意干预系统打包流程来实现多样化需求
AS如何打包
我们想要打包一个APK,一般有两种 方式,第一种是直接使用AS的图形化界面来操作。
第二种是我们运行gradle脚本来输出APK或者SDK。
脚本指令为 assembleRelease
,其中release为我们在gradle中定义的buildType,本篇文章就以release为例子来讲解说明。其实对于第一种方法,本质也是运行该指令,只不过AS存在友好交互界面。
- 如果我们在一个类型为
library
的module中,运行 该指令,那么会最终生成AAR包。 - 如果我们在一个类型为
application
的module中,运行该指令,那么最终会生成APK。
运行指令的方式也很简单,如果我们想要生成AAR包,那么就可以在控制台里,在指令assembleRelease
的前方加上module
名称,然后运行。
上图示例演示如何运行打包生成AAR,其中我的module
名称为SDK,所以我最后的指令为 :SDk:assembleRelease
回车运行。
然后就可以在build目录下看到AAR SDK。
同理,如果我们在APP的gradle中运行指令,那么会生成对应apk.
可以在APP module中找到对应的APK。
详解assembleRelease
我们可能会疑惑,我们怎么知道要执行指令 assembleRelease
呢?
我们来看下打包指令的组成:
- buildType
首先assemble
是固定的,但是后边的结构是动态的。 以打包AAR为例,我们到module
的gradle
文件中,可以看到存在buildType
,我们可以扩展定义我们自己的buildType
,也可以使用默认的Release
和Debug
,如果我们想打包Release
包,那么指令就是 assembleRelease
,同理打包Debug包就是 assembleDebug
。
我们可能会定义我们自己的buildType
,例如图中定义了test
,那么运行指令则为 assembleTest
。
buildType
的含义是指我们想要构建的SDK类型,一般常见的是release
,debug
和dev
,当然也可以根据我们的需要额外定义我们自己的。
- productFlavors
除了打包类型,可能我们还会存在多个产品,每个产品又可以有不同的类型包,所以这个时候只是一个buildType就不适用了,这就又出了一个定义productFlavors
,我们可以定义来更多样化操作打包,包括多样化定义包名等。
如果我们定义了 productFlavors
,例如,如图定义了normal
,那么我们打包指令就为 assembleNormal
- 同时定义buildType和productFlavors
往往我们的项目中都会同时定义这两个类型,那么这个时候指令怎么执行代表什么含义呢?以下说明中,默认buildType
为release和debug
,productFlavors
为normal
和low
。
- assembleNormalRelease
这个指令加上了 productFlavors
和buildType
,注意buildType
要在productFlavors
之后。这个指令会使用指定的buildType
和productFlavors
里的配置,然后生成对应的AAR或者APK。这里只会生成一个AAR或者APK包。
- assembleNormal
注意这里的Normal
是我们定义的 productFlavors
。如果我们执行上述命令,那么他会为每个buildType
生成一个normal
的APK或AAR。
- assembleRelease
这个指令含义和assembleNormal
类似,只不过这个是为当前选择的这个BuildType
,使用其里边声明定义的配置,为所有的 productFlavors
生成对应的APK或AAR。
注意的是 2和3 这两条指令,生成的最后APK/AAR的位置还是有些许区别。按照存在的意义,productFlavors
下存在多个buildType
,所以 productFlavors
为父目录,其下根据不同buildType
生成子目录。
- assemble
如果我们直接运行这个指令,没有任何后缀,那么代表的是运行所有buildType
下的所有productFlavors
,分别生成一个APK或AAR,假设我们有2个buildType
,3个productFlavors
,那么就一共会生成2x3=6个APK或AAR。分别对应不同的配置。
AS打包原理
如果我们在gradle中执行指令 assembleNormalRelease
,那么会发现控制台打印很多如下所示日志。没错,这些都是打包过程中执行的task。
所谓的打包APK/AAR,其实就是通过执行AS已经预制好的一系列task脚本,将APK/AAR所需要的资源和文件根据规则放到指定位置,然后压缩为APK/AAR格式。
我们不需要太多详细了解每一个task的作用,为了可以自定义打包,我们只需要知道一些关键task的执行顺序和作用,以便我们来控制流程。
本文接下来会详细分析打包AAR的原理。
打包AAR原理
一个AAR的组成可以分为几部分:
- class.jar文件
- so文件
- 资源文件
- R文件
- manifest文件
- 混淆文件
- 关于AAR中没发现混淆文件
如果我们发现打包后的AAR里没有混淆文件,那也不用慌,这是正常的,混淆文件需要特殊配置一下,才可以打包到我们的AAR中。
在AAR module中,图片对应位置中,添加如下配置,配置项加上你的混淆文件就可以了,可以加多个混淆文件,打包时候会自动merge为一个文件。
onsumerProguardFiles 'aar-proguard-rules.pro'
task执行顺序
插入我们自己的task自定义打包
上图图示为打包AAR过程中,所经历的一系列task任务,以及执行顺序。如果我们想要修改其中的某一步,只需要定义我们自己的task对build目录下的文件做修改,将task插入到目标动作task中间,即可完成功能。
以下列举多个例子来演示一下。
实际应用例子
1. 根据不同的buildType,自动打包出包含不同类型SO的AAR
需求:我们存在buildType
类型为elease1
和release2
,我们存在arm64
和armeabi-v7a
和armeabi
三种类型的SO。我们想要打包出的release1
只包含arm64
的so,release2
包含所有的so。
先看下最终想要的效果:
想要实现这种效果,我们就需要自定义task了。
对比上边发的那个task执行顺序图,我们知道想要修改AAR中的SO,可以定义task 插入到系统task mergeRelease1JniLibFolders
和mergeRelease1NativeLibs
之间执行即可。
首先找到我们当前SDK代码的module,笔者这里这个SDK代码的module名字就叫做SDK。
找到该module下的build.gradle,我们在gradle对应位置下,编写如下代码。
文本:
variantFilter { String buildTypeName = it.buildType.name if ("release1" == buildTypeName) { tasks.register("deleteUnusedSOForRelease1", { doLast { delete("build\\intermediates\\merged_jni_libs\\release1\\out\\armeabi") delete("build\\intermediates\\merged_jni_libs\\release1\\out\\armeabi-v7a") } }) } }
variantFilter
会遍历当前module
下的所有buildType
和productFlavors
,我们呢只需要对relese1
进行删除SO的处理,所以在这里呢过滤release1
的buildType
,然后为他注册一个task,task里做的操作就是删除build对应目录下的so。
现在删除SO的task制作好了,我们怎么才能让他在合适的时机去自动运行呢?毕竟我们打包指令和方法不想改,仍然想通过执行指令assemble
来执行打包。
我们找到工程根目录,工程根目录也会存在build.gradle
,来到这个文件,我们到文件最末尾,编写如下代码。注意编写完成后,要sync一下,每次修改完gradle都需要SYNC一下当前项目。
文本:
Task current = tasks.findByPath(":SDK:deleteUnusedSOForRelease1") Task firstTask = tasks.findByPath(":SDK:mergeRelease1JniLibFolders") Task nextTask = tasks.findByPath(":SDK:mergeRelease1NativeLibs") nextTask.dependsOn(current) current.mustRunAfter(firstTask) println("finish ===> ")
这段代码含义是,将我们刚才为release1注册的task deleteUnusedSOForRelease1
设置了编译顺序,让其在系统task mergeRelease1JniLibFolders
执行后,在mergeRelease1NativeLibs
执行前执行。
注意这里直接写到gradle中,是因为在gradle编译时候,就要将gradle执行顺序定义好。
接着我们运行一下指令assembleRelease1
进行打包,看输出:
会看到,合适的时机正常的执行了我们的task,删除了SO,看下生成的AAR,确实没有多余So了~
2. 根据不同的buildType 修改AAR中的manifest文件内容
我们现在有个需求,想要让打包好的AAR中的manifest
不指定targetSDKVersion
和minSDKVersion
。毕竟这两个会影响到集成方。
按照正常的打包配置是没法做到删除这两个东东的,那么我们只能自定义task来修改manifest
文件了。
老规矩,看下最开始理的那个task执行顺序,发现执行完task processRelease1Manifest
后,会将最终的manifest
放到build
下,那么我们定义的task就要放到这个task之后。
然后我们看到这个task之后对应的是 extractReleaseAnnotations
,那么我们将我们自定义修改manifest
内容的task放到这两个task之间即可。
- 我们一步一步来实现这个需求
- 先自定义好我们的task来删除
manifest
中的内容
参考流程图,可以找到manifest
在build
目录下的位置,我们自定义如下task,将对应内容移除。
String firstChar = buildTypeName.charAt(0).toUpperCase() String formatName = firstChar + buildTypeName.substring(1) Task manifestTask = tasks.findByPath("change${formatName}ManifestContentTask") if (manifestTask == null) { tasks.register("change${formatName}ManifestContentTask", { doLast { File manifestFile = file("build/intermediates/library_manifest/${buildTypeName}/AndroidManifest.xml") def manifestContent = manifestFile.text manifestContent = manifestContent.replaceAll("android:minSdkVersion=\"${project.MIN_SDK_VERSION}\"", ' ') manifestContent = manifestContent.replaceAll("android:targetSdkVersion=\"${project.TARGET_SDK_VERSION}\"", ' ') manifestFile.text = manifestContent println("i am here to change manifest content!!! \n $manifestContent") } }) }
这个task主要是根据当前buildtype
在build下找到manifest
,然后修改里边的内容,去掉targetSDKVersion
和minSDKVersion
。
注意,我们这里想针对所有的buildType
来进行这个修改manifest
内容的操作,所以可以针对每个buildType
都注册一个task来操作。
最终完整代码如图:
要注意这个task在gradle中的位置。
- 使用我们自定义的task
第一步我们自定义好task了,我们接下来需要将其插入到对应的位置中,让其在编译时候可以自动运行。
由于这里我们针对了所有的buildType
都注册了一个task,所以我们制定执行顺序需要依赖buildType
,故只能在varIanFilter
中去设置顺序了。我们使用方法afterEvaluate
。
afterEvaluate { Task currentManifest = tasks.findByName("change${formatName}ManifestContentTask") Task firstTaskManifest = tasks.findByName("process${formatName}Manifest") Task nextTaskManifest = tasks.findByName("extract${formatName}Annotations") nextTaskManifest.dependsOn(currentManifest) currentManifest.mustRunAfter(firstTaskManifest) println("i am here to sort task!!!") }
代码位置如图所示,一样在varianFileter
作用域中。
最后,我们SYNC一下代码,在gradle运行一下指令assembleRelease1
给release1打包,发现生成的AAR manifest中没有了targetSDKVersion
和minSDKVersion
。
PS:AAR中的manifest文件,空的uses-sdk也是支持的,没问题哦~
3. 去掉AAR中的注解包
我们如果在代码中使用了自定义的注解,那么注解信息是会打包到SDK中的,但是这些注解实际上并没有什么用处,他们的用处仅仅在编码时候规范我们,所以实际对SDK的运行并无影响,为了减小SDK大小,我们是可以直接将这个zip包删掉的。
看下怎么删掉这个注解包。
老规矩,从之前的流程图里可以知道,task extractReleaseAnnotations
会生成这个注解包,那么我们就定义一个task,在 extractReleaseAnnotations
之后运行,将build目录下的注解包删掉即可。
- 自定义task
注意我这个操作也是针对所有buildType
,如果是针对单个buildType
,请参考 删除SO 的task写法
// 删除 annotations.zip task tasks.register("delete${formatName}AnnotationTask", { doLast { println("do delete for ${formatName}") delete("./build/intermediates/annotations_zip/${buildTypeName}/annotations.zip") } })
注意要将task插入到对应位置。
Task deleteAnnotationTask = tasks.findByName("delete${formatName}AnnotationTask") Task deleteAnnotationFirstTask = tasks.findByName("extract${formatName}Annotations") Task deleteAnnotationNextTask = tasks.findByName("bundle${formatName}Aar") deleteAnnotationNextTask.dependsOn(deleteAnnotationTask) deleteAnnotationTask.mustRunAfter(deleteAnnotationFirstTask)
代码位置整体截图
运行assembleRelease1
,查看结果,不带注解包了。
打包APK和AAR原理流程一样,可以参考这个流程来。同时打包APK,AS本身提供了各种gradle配置,并且网上相关介绍很多,这里就不详细说了。