APK打包流程-Dex编译流程

712 阅读4分钟

说到Dex文件,相信大家都比较熟悉了。 Dex是Android虚拟机支持的运行文件格式,相对于class文件,dex文件有下面几点优势:

  1. dex文件结构更密集,去掉了class文件的冗余信息,占用空间小
  2. dex文件会对字节码做部分代码优化,运行更快。

我们接下来看看Dex文件生成的整体流程,相关的任务Task有下面几个

generateBuildConfig
kaptGenerateStubsDebugKotlin
kaptDebugKotlin
compileDebugKotlin
compileDebugJavaWithJavac
transformDebugxxxx
mergeExtDexDebug
mergeLibDexDebug
dexBuilderDebug

整体的Dex文件生成的流程如下:

  1. 根据gradle配置生成BuildConfig参数
  2. 通过java的annotationProcess、kotlin的kapt生成java代码和kotlin代码
  3. 项目中java代码和kotlin代码并存,通过kotlinc和javac编译生成class文件
  4. 执行Transform流程,对class文件进行字节码处理
  5. 使用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辅助代码

项目目前绝大部分项目都会使用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过程

在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文件解析

)

本文属于学习过程中的记录,有不对的地方请谅解下可以提出来