如何优雅地获取 Android 项目里所有的 so 信息

1,069 阅读2分钟

文章还在写,更新中。。。

1 概述

看完本文,你将收获:

  1. 区分 AndroidComponentsExtensiononVariantsAppExtensionapplicationVariants.forEachVariant,知道哪个方法适用于什么场景。
  2. MergeNativeLibsTask 的源码了解个大概。
  3. 能够打印项目里所有的 so 信息及其依赖信息。

2 最简单方案:通过 MergeNativeLibsTask 获取 so 信息

MergeNativeLibsTask 是一个 Gradle 任务,它用于合并 Android 应用中的本地库文件。MergeNativeLibsTask 的作用是将所有的本地库文件复制到一个统一的目录下,以便打包到 APK 或 AAB 文件中。这个任务是由 Android Gradle 插件自动创建和配置的,我们可以在这个任务开始前去获取 so 信息。可以直接通过任务类型去获取到 MergeNativeLibsTask

afterEvaluate {
    tasks.withType(MergeNativeLibsTask::class.java) {
        this.doFirst {
            projectNativeLibs.asFileTree.forEach {
                println("projectNativeLibs, ${it.path}")
            }
            subProjectNativeLibs.asFileTree.forEach {
                println("subProjectNativeLibs, ${it.path}")
            }
            externalLibNativeLibs.asFileTree.forEach {
                println("externalLibNativeLibs, ${it.path}")
            }
            if (profilerNativeLibs.isPresent) {
                profilerNativeLibs.asFileTree.forEach {
                    println("profilerNativeLibs, ${it.path}")
                }
            }
        }
    }
}

输出如下:

projectNativeLibs, D:\StudioProjects\xxx\app\build\intermediates\merged_jni_libs\basicDebug\out\x86\libucrop.so
...
subProjectNativeLibs, D:\StudioProjects\xxx\xxx\traceroute\build.transforms\db7deceeac3bc2a2f08352329ee919c1\transformed\jetified-traceroute-release\jni\x86\libtraceroute.so
...
externalLibNativeLibs, E:\gradle\caches\transforms-3\e244ec361d4eabe03f0a45d27fd3f4c7\transformed\jetified-sinaweibo-core-12.5.0\jni\arm64-v8a\libweibosdkcore.so
...

当然也可以用任务名去获取。需要注意的是,gradle 构建过程是有构建变体(Variant)的,这个变体是由 buildTypeflavor 组成的,可以用迪卡尔积去算出所有的组合变体,如下:

fun cartesianProductString(array1: Array<String>, vararg arrays: Array<String>): List<String> {
    return arrays.fold(array1.toList()) { acc, array ->
        acc.flatMap { list ->
            array.map { element -> list + element }
        }
    }
}

afterEvaluate {
    val flavors = arrayOf("flavor1", "flavor2")
    val buildTypes = arrayOf("Debug", "Release")
    cartesianProductString(flavors, buildTypes).forEach {
        tasks.findByName("merge${it}NativeLibs")?.run {
        }
    }
}

至于为什么是 merge${it}NativeLibs,变体为什么在中间,看下面的源码就懂了,Android 构建体系中很多任务都是这样的。

override val name: String
            get() = computeTaskName("merge", "NativeLibs")

/**
* 下面方法中的 name 就是变体名
*/
override fun computeTaskName(prefix: String, suffix: String): String =
        prefix.appendCapitalized(name, suffix)override val name: String
            get() = computeTaskName("merge", "NativeLibs")

3 在插件里打印 So 信息

基本思路就是获取到所有的跟 jni 有关的构建中间产物(artifact),然后打印 artifact 内的 so 文件相关信息。artifact 的获取有几种方式,可以通过 org.gradle.api.artifacts.Configuration,也可以通过 Variant,下面的代码演示了两种方式。

project.afterEvaluate {
    val android = project.extensions.getByType(AppExtension::class.java)
    android.applicationVariants.forEach { variant ->
        // 方式一
        val variantName = variant.name
        val configuration = project.configurations.getByName("${variantName}RuntimeClasspath")
        val artifacts = configuration.incoming.artifactView {
            attributes {
                attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, AndroidArtifacts.ArtifactType.JNI.name)
            }
        }.artifacts.artifacts

        // 方式二
        val variantData = (variant as com.android.build.gradle.internal.api.ApplicationVariantImpl).variantData
        val artifacts = variantData.variantDependencies.getArtifactCollection(
            AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
            AndroidArtifacts.ArtifactScope.ALL,
            AndroidArtifacts.ArtifactType.JNI
        ).artifacts
    }
}

这里能拿到所有的编译中间 jni 目录,到这一步基本可以“为所欲为”了,可以打印 so 所在的 aar 或者子工程信息,也可以将信息输出到单独的文件中,甚至复制 so 到别的文件然后删除当前目录的 so 做个动态下发,稳定起见要做删除操作的话最好设置下放在 MergeNativeLibsTask 之前。

但是注意这两种方式获取到的 so 都不包含 app project 下的,只有子 project 和第三方库的。在解决这个问题之前,我们先把 so 的信息打印下吧:

artifacts.forEach artifact@{ artifactResult ->
    val soFiles = artifactResult.file.walkTopDown()
        .filter { it.name.endsWith(".so") }
        .toMutableList()
        .takeIf { it.isNotEmpty() } ?: return@artifact
    println("artifact name: ${artifactResult.id.componentIdentifier.displayName}")
    val artifactPath = artifactResult.file.absolutePath
    val artifactPathLength = artifactPath.length
    println("jni path: $artifactPath")
    soFiles.forEach {
        println("\t\t${it.absolutePath.substring(artifactPathLength)}")
    }
}

输出如下:

artifact name: com.tencent.bugly:nativecrashreport:3.9.2
jni path: E:\gradle\caches\transforms-3\f3687876b97dd3bdd6fc701685d0e47b\transformed\jetified-nativecrashreport-3.9.2\jni
		\arm64-v8a\libBugly.so
...

4 MergeNativeLibsTask 是怎么做的