说到Dex文件,相信大家都比较熟悉了。 Dex是Android虚拟机支持的运行文件格式,相对于class文件,dex文件有下面几点优势:
- dex文件结构更密集,去掉了class文件的冗余信息,占用空间小
- dex文件会对字节码做部分代码优化,运行更快。
我们接下来看看Dex文件生成的整体流程,相关的任务Task有下面几个
generateBuildConfig
kaptGenerateStubsDebugKotlin
kaptDebugKotlin
compileDebugKotlin
compileDebugJavaWithJavac
transformDebugxxxx
mergeExtDexDebug
mergeLibDexDebug
dexBuilderDebug
整体的Dex文件生成的流程如下:
- 根据gradle配置生成BuildConfig参数
- 通过java的annotationProcess、kotlin的kapt生成java代码和kotlin代码
- 项目中java代码和kotlin代码并存,通过kotlinc和javac编译生成class文件
- 执行Transform流程,对class文件进行字节码处理
- 使用D8将class文件集转化为Dex文件
BuildConfig生成
我们在build.gradle会配置一些期望在代码中用到的参数,比如当前打包时间、打包类型等。 这些参数可以在build.gradle配置,如下所示:
buildTypes {
release {
minifyEnabled true
shrinkResources = false
buildConfigField("String", "BUILD_DATE", getBuildDate())
buildConfigField("String", "BRANCH_NAME", getBranchName())
}
}
上面的BUILD_DATE和BRANCH_NAME就可以在代码中使用,使用方式可以直接通过包名+BuildConfig的方式访问。
System.out.println("${BuildConfig.BUILD_DATE}")
有一些内置的默认参数,如下所示:
- DEBUG:是否Debug
- APPLICATION_ID / LIBRARY_PACKAGE_NAME: 包名
- VERSION_CODE: 版本号
- VERSION_NAME:版本名称
这个BuildConfig生成的并不是java文件,而是直接生成了class, 中间会使用ASM创建对应的类
private fun generateUsingAsm(): ClassWriter {
val cw = ClassWriter(COMPUTE_MAXS)
// Class Signature
cw.visit(
V1_8,
ACC_PUBLIC + ACC_FINAL + ACC_SUPER,
fullyQualifiedBuildConfigClassName,
null,
"java/lang/Object",
null
)
// Field Attributes
data.buildConfigFields.forEach {
it.value.emit(it.key, cw)
}
val constructorMethod = Method.getMethod("void <init> ()")
val cGen = GeneratorAdapter(ACC_PUBLIC, constructorMethod, null, null, cw)
cGen.loadThis()
cGen.invokeConstructor(Type.getType(Object::class.java), constructorMethod)
cGen.returnValue()
cGen.endMethod()
cw.visitEnd()
return cw
}
在创建完class之后,会直接输出到jar包中。
private fun writeToJar(
outputPath: Path, buildConfigPackage: String, bytecodeBuildConfig: ByteArray) {
outputPath.toFile().createNewFile()
JarFlinger(outputPath).use { jarCreator ->
jarCreator.setCompressionLevel(NO_COMPRESSION)
jarCreator.addEntry(buildConfigPackage, bytecodeBuildConfig.inputStream())
}
}
APT流程
相信大家对于java中APT技术都比较熟悉了,主要是用来处理在源码中添加的编译期注解,做一些代码的自动化生成。常见的ButterKnife、Room库都使用到这个技术。一个APT的使用流程如下所示:
- 定义编译期注解
- 自定义AbstractProcessor
- 在process方法中,获取定义的注解,生成期望的代码
这里不会详细讲解如何使用APT技术,会从打包的流程梳理下AnnotationProcessor是如何生效的以及如何执行的。
kapt生成java辅助代码
- 编译任务名称:kaptGenerateStubsDebugKotlin
- 源码传送门:KaptGenerateStubsTask源码
项目目前绝大部分项目都会使用kotlin,如果在kotlin代码使用了APT技术,就会使用kapt3。
可能大家都会感觉在使用了Kapt之后,整体编译的耗时变长了,这个并不是错觉。kapt内部仍然使用了java的AnnotationProcessor技术,而AnnotationProcessor的注解处理的前提是被处理的源文件需要是java文件。因此,kapt内部会把对应的kotlin代码转为java代码,再通过AnnotationProcessor进行注解处理。
所以先把对应的kotlin文件生成一份简单版的java文件,这个逻辑其实就是编译任务来实现的。
生成代码的的流程:
- 分析原始kotlin的文件,生成对应的语法树
- 根据对应的语法树,生成java文件
如果需要查看详细的从kotlin转化生成java文件的逻辑,可以直接查看这里: kotlin生成java文件
关键生成代码逻辑如下:
protected open fun saveStubs(kaptContext: KaptContext, stubs: List<KaptStub>) {
for (kaptStub in stubs) {
val stub = kaptStub.file
val className = (stub.defs.first { it is JCTree.JCClassDecl } as JCTree.JCClassDecl).simpleName.toString()
val packageName = stub.getPackageNameJava9Aware()?.toString() ?: ""
val packageDir = if (packageName.isEmpty()) options.stubsOutputDir else File(options.stubsOutputDir, packageName.replace('.', '/'))
packageDir.mkdirs()
val sourceFile = File(packageDir, "$className.java")
sourceFile.writeText(stub.prettyPrint(kaptContext.context))
kaptStub.writeMetadataIfNeeded(forSource = sourceFile)
}
}
kapt注解处理器执行
- 编译任务名称: kaptDebugKotlin
- 源码传送门:KaptTask源码
开发过注解处理器的小伙伴肯定会知道我们自己声明的Processor需要按照下面的方式定义:
- 创建Processor定义文件
src/main/resources/META-INF/services/javax.annotation.processing.Processor
- 在创建的Processor文件中声明自定义的注解处理器
com.xjl.test.apt.xxxxProcessor
在我们给一个依赖使用了kapt的configuration之后,kotlin就会尝试去读取对应Processor。如果需要查看详细的kapt的annotationProcessor的处理逻辑的,可以查看源码:ProcessorLoader传送门
处理META-INFO的关键逻辑如下:
open fun doLoadProcessors(classpath: LinkedHashSet<File>, classLoader: ClassLoader): List<Processor> {
val processorNames = mutableSetOf<String>()
val serviceFile = "META-INF/services/javax.annotation.processing.Processor"
for (file in classpath) {
when {
file.isDirectory -> {
file.resolve(serviceFile).takeIf { it.isFile }?.let {
processSingleInput(it.inputStream())
}
}
file.isFile && file.extension.equals("jar", ignoreCase = true) -> {
ZipFile(file).use { zipFile ->
zipFile.getEntry(serviceFile)?.let { zipEntry ->
zipFile.getInputStream(zipEntry).use {
processSingleInput(it)
}
}
}
}
else -> {
logger.info("$file cannot be used to locate $serviceFile file.")
}
}
}
return processorNames.mapNotNull { tryLoadProcessor(it, classLoader) }
}
- 通过源文件、或者jar包中去寻找javax.annotation.processing.Processor
- 按照读取配置在javax.annotation.processing.Processor的Processor名称
- 读取完成之后,通过反射创建Processor实例
private fun tryLoadProcessor(fqName: String, classLoader: ClassLoader): Processor? {
val providedClassloader = options.processingClassLoader?.takeIf { !options.separateClassloaderForProcessors.contains(fqName) }
val classLoaderToUse = if (providedClassloader != null) {
logger.info { "Use provided ClassLoader for processor '$fqName'" }
providedClassloader
} else {
logger.info { "Use own ClassLoader for processor '$fqName'" }
classLoader
}
val annotationProcessorClass = try {
Class.forName(fqName, true, classLoaderToUse)
} catch (e: Throwable) {
logger.warn("Can't find annotation processor class $fqName: ${e.message}")
return null
}
}
在创建完所有注解处理器实例之后,需要通过JavaCompile去执行AnnotationProcessor的能力。
if (isJava9OrLater()) {
val initProcessAnnotationsMethod = JavaCompiler::class.java.declaredMethods.single { it.name == "initProcessAnnotations" }
initProcessAnnotationsMethod.invoke(compiler, wrappedProcessors, emptyList<JavaFileObject>(), emptyList<String>())
} else {
compiler.initProcessAnnotations(wrappedProcessors)
}
如果jdk是9以上,那么通过反射调用initProcessAnnotations方法,,把当前构建的AnnotationProcessor注入到JavaCompile中。
其他jdk版本,直接调用initProcessAnnotations方法即可。javaCompile源码传送门
之所以这么实现,是因为apt过程本身是一个比较复杂的过程,刚开始直接复用java的annotationProcessor流程。所以会导致整体的编译耗时较长,kotlin自己也发现了这个问题,所以才重新开发了ksp, 相信在不久的时间之后,android编译流程就中就不会有kaptGenerateStubsDebugKotlin这个任务了,会大大降低整体APK打包的编译时间。
有兴趣的可以了解下KSP的实现。 KSP源码传送门
源码编译流程
- java代码会通过javac编译生成class
- kotlin代码会通过kotlinc编译class
tranform流程
Gradle Transform是gradle提供给开发者在项目构建阶段来修改.class文件的一套标准API,即把输入的.class文件转变成目标字节码文件。目前比较经典的应用是字节码插桩、代码注入。
一个Transform的使用方式如下所示:
class KClassKnifePlugin: Plugin<Project> {
override fun apply(project: Project) {
if (!project.plugins.hasPlugin("com.android.application")) {
throw GradleException("KClassKnifePlugin need Android Application plugin required.")
}
project.extensions.create("kClassKnife", KClassKnifeExtension::class.java)
val extension = project.extensions.getByType(AppExtension::class.java)
extension.registerTransform(KClassKnifeMainTransform(project))
}
}
如果注册了多个transform,那么在打包流程中就会有多个transformTask,所以每一个transform都对应着一个编译流程的Task
List<Transform> customTransforms = extension.getTransforms();
List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();
boolean registeredExternalTransform = false;
for (int i = 0, count = customTransforms.size(); i < count; i++) {
Transform transform = customTransforms.get(i);
List<Object> deps = customTransformsDependencies.get(i);
registeredExternalTransform |=
transformManager
.addTransform(
taskFactory,
componentProperties,
transform,
null,
...
.isPresent();
}
- 会从extension出注册到transform的列表,添加到taskManager中
- 在taskManager中添加transform时,会创建对应的transform任务
new TransformTask.CreationAction<>(componentProperties.getName(),
taskName,
transform,
...)
- 在TranformTask中,Task的执行就会直接调用我们注册进去的transform
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs) {
....
transform.transform(new TransformInvocationBuilder(context)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(
outputStream != null ? outputStream.asOutput() : null)
.setIncrementalMode(isIncremental.getValue())
.build());
}
DexBuild过程
- 编译任务名称: dexBuilderDebug
- 源码传送门:DexArchiveBuilderTask源码
在Android打包阶段,有两个Dex工具Dx和D8。目前新版的AndroidStudio默认使用的打包工具都是D8了。会将真正的Dex打包任务交给D8.
dexArchiveBuilder = DexArchiveBuilder.createD8DexBuilder(
com.android.builder.dexing.DexParameters(
minSdkVersion = dexSpec.dexParams.minSdkVersion,
debuggable = dexSpec.dexParams.debuggable,
dexPerClass = dexSpec.dexParams.dexPerClass,
withDesugaring = dexSpec.dexParams.withDesugaring,
desugarBootclasspath =
DexArchiveBuilderTaskDelegate.sharedState.getService(dexSpec.dexParams.desugarBootclasspath).service,
desugarClasspath =
DexArchiveBuilderTaskDelegate.sharedState.getService(dexSpec.dexParams.desugarClasspath).service,
coreLibDesugarConfig = dexSpec.dexParams.coreLibDesugarConfig,
coreLibDesugarOutputKeepRuleFile =
dexSpec.dexParams.coreLibDesugarOutputKeepRuleFile,
messageReceiver = messageReceiver
)
)
D8的优势:
- 编译更快、时间更短
- 编译时占用内存更小
- .dex文件更小
- .dex 文件拥有更好的运行时性能
- 支持在代码中使用Java 8 语言,进行脱糖
dex合并策略
- MERGE_EXTERNAL_LIBS: 仅合并外部library的dex文件
- MERGE_LIBRARY_PROJECTS:合并当前工程内Library的Dex文件
- MERGE_PROJECT: 合并当前工程的Dex文件
- MERGE_ALL:合并外部Library、library工程、主工程的Dex文件
dex文件结构:
如果需要了解详细的Dex文件,可以参照下面的文档:dex文件解析
)
本文属于学习过程中的记录,有不对的地方请谅解下可以提出来