本文作者:履坎(xjy2061)
起源
近期,因为引入的新工具依赖 Android Gradle Plugin(前面都简写为 AGP)4.1 或以上版本,而我的项目以后应用的 AGP 版本为 3.5.0,需进行降级。思考到一些第三方库尚未对最新的 AGP 4.2 版本提供反对,决定将 AGP 降级到 4.1 中的最高版本 4.1.3,遂开启了本次 AGP 降级之旅。
根据官网文档适配
降级的第一步当然是浏览官网 Android Gradle 插件版本阐明 文档,依据文档所列版本变更进行适配。
AGP 3.6 适配
AGP 3.6 引入了如下行为变更:
默认状况下,原生库以未压缩的模式打包
该变更使原生库(native library)以未压缩形式打包,会减少 APK 大小,带来的收益无限,且局部收益依赖 Google Play,如评估后认为弊大于利,可在 AndroidManifest.xml
中增加如下配置改为压缩原生库:
<application
android:extractNativeLibs="true"
... >
</application>
AGP 4.0 适配
AGP 4.0 引入了如下新个性:
依赖项元数据
该变更会将利用依赖项的元数据进行压缩加密后存储于 APK 签名块中,Google Play 会应用这些依赖项来做问题揭示,收益无限,但会减少 APK 大小,如 App 不在 Google Play 上架,可在 build.gradle
中增加如下配置来敞开这个个性:
android {
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
}
AGP 4.1 适配
AGP 4.1 引入了如下行为变更:
从库我的项目中的 BuildConfig 类中移除了版本属性
该变更从库模块(library module)的 BuildConfig
类中删除了 VERSION_NAME
和 VERSION_CODE
字段。一般而言在库模块中获取版本号是心愿获取 App 的版本号,而库模块中的 BuildConfig.VERSION_NAME
和 BuildConfig.VERSION_CODE
为库模块本身的版本号,此时不应该应用库模块中的这 2 个字段,可用如下代码来在库模块中获取 App 的版本号:
private var appVersionName: String = ""
private var appVersionCode: Int = 0
fun getAppVersionName(context: Context): String {if (appVersionName.isNotEmpty()) return appVersionName
return runCatching {context.packageManager.getPackageInfo(context.packageName, 0).versionName.also {appVersionName = it}
}.getOrDefault("")
}
fun getAppVersionCode(context: Context): Int {if (appVersionCode > 0) return appVersionCode
return runCatching {
PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(context.packageName, 0)
).toInt().also { appVersionCode = it}
}.getOrDefault(0)
}
遇到的问题
按官网文档适配后,不出预料地还是遇到了不少问题,这些问题局部由未在官网文档中明确指出的行为变更导致,局部由不标准做法命中了新版 AGP 更严格的限度导致,上面介绍这些问题的体现、起因剖析和解决方案。
BuildConfig.APPLICATION_ID
找不到
咱们的局部组件库模块中应用了 BuildConfig.APPLICATION_ID
字段,编译时呈现 Unresolved reference 谬误。
起因是库模块中的 BuildConfig.APPLICATION_ID
字段名存在歧义,其值是库模块的包名,并不是利用的包名,因而该字段从 AGP 3.5 开始被废除,替换为 LIBRARY_PACKAGE_NAME
字段,且从 AGP 4.0 开始被彻底删除。
咱们原来在 App 模块中的局部代码应用 APPLICATION_ID
获取 App 包名,在前面的组件化拆分过程中将 App 模块中的代码抽取到组件库时,为防止谬误地用库模块的包名作为 App 包名,应该同步批改获取 App 包名形式,但脱漏了,没有批改,导致本次 AGP 降级后编译失败。
针对这个问题,将库模块中获取 App 包名形式改为应用 Context.getPackageName()
办法即可。
R 和 ProGuard mapping 文件找不到
咱们会备份构建公布包时产生的 R 和 ProGuard mapping 文件以备前面须要时应用,降级后备份失败。
这是因为从 AGP 3.6 开始,构建产物中这 2 个文件的门路会扭转:
R.txt
:build/intermediates/symbols/${variant.dirName}/R.txt
->build/intermediates/runtime_symbol_list/${variant.name}/R.txt
mapping.txt
:build/outputs/mapping/${variant.dirName}/mapping.txt
->build/outputs/mapping/${variant.name}/mapping.txt
其中 ${variant.dirName}
为 $flavor/$buildType
(例如 full/release),${variant.name}
为 $flavor${buildType.capitalize()}
(例如 fullRelease)。
可按如下形式将备份逻辑中的文件门路批改为上述新门路来解决这个问题:
afterEvaluate {
android.applicationVariants.all { variant ->
def variantName = variant.name
def variantCapName = variant.name.capitalize()
def assembleTask = tasks.findByName("assemble${variantCapName}")
assembleTask.doLast {
copy {from "${buildDir}/outputs/mapping/${variantName}/mapping.txt"
from "${buildDir}/intermediates/runtime_symbol_list/${variantName}/R.txt"
into backPath
}
}
}
}
固定资源 id 生效
为防止 App 降级笼罩装置后可能呈现 inflate 告诉等 RemoteView
时,因为通过资源 id 找到谬误的资源文件,导致解体的问题,咱们在构建时进行了固定资源 id 解决,使局部资源文件的 id 在屡次构建之间始终不变,降级后这部分资源 id 产生了变动。
原固定资源 id 的实现形式是在 afterEvaluate
后,应用 tasks.findByName
办法获取 process${variant.name.capitalize()}Resouces
(例如 processFullReleaseResources)工作对象,而后在 AGP 3.5 以前应用调 getAaptOptions
办法,在 AGP 3.5 中应用反射的形式获取工作对象中的 aaptOptions
属性对象,而后向其 additionalParameters
属性对象增加 --stable-ids
参数及对应的资源 id 配置文件门路值。但在 AGP 4.1 中,解决资源工作类不再有 aaptOptions
属性,导致固定生效。
对于 AGP 4.1,可换成如下间接设置 android.aaptOptions.additionalParameters
的形式来固定资源 id:
afterEvaluate {
def additionalParams = android.aaptOptions.additionalParameters
if (additionalParams == null) {additionalParams = new ArrayList<>()
android.aaptOptions.additionalParameters = additionalParams
}
def index = additionalParams.indexOf("--stable-ids")
if (index > -1) {additionalParams.removeAt(index)
additionalParams.removeAt(index)
}
additionalParams.add("--stable-ids")
additionalParams.add("${your stable ids file path}")
}
Manifest 文件批改失败
咱们会在构建过程中批改 AndroidManifest.xml
文件退出额定信息,降级后批改失败。
在剖析本次降级蕴含的各版本 AGP 构建日志后,发现 AGP 4.1 针对 Manifest 解决新增了 process${variant.name.capitalize()}ManifestForPackage
(例如 processFullReleaseManifestForPackage)工作,该工作在原 Manifest 解决工作 process${variant.name.capitalize()}Manifest
(例如 processFullReleaseManifest)后执行,其产物 1 跟原工作不同。而原来向 Manifest 增加额定信息的形式是在原 Manifest 解决工作执行后,执行自定义 Manifest 解决工作 cmProcess${variant.name.capitalize()}Manifest
(例如 cmProcessFullReleaseManifest),向原 Manifest 解决工作的产物 2 写入信息。降级后,如果 2 个解决 Manifest 的工作都命中了缓存(执行状态为 FROM-CACHE
),那么最终 APK 内的 Manifest 文件中的额定信息会是以前编译写入的旧信息。
因而,写入信息的形式应如下图所示,改为在新增的 Manifest 解决工作执行后,向其产物文件写入信息。
Transform 插件执行失败
咱们在构建过程中退出了一些 Transform 插件,降级后其中一个应用 ASM 进行代码插桩的插件在执行时呈现如下谬误:
Execution failed for task ':app:transformClassesWithxxx'.
> java.lang.ArrayIndexOutOfBoundsException (no error message)
下面谬误提醒中的异样也可能是 java.lang.IllegalArgumentException: Invalid opcode 169
。
为找到异样的具体起源,退出 --stacktrace
参数从新构建,定位异样由插件中引入的第三方库 Hunter 触发。这个插件运行时应用的 ASM 是 AGP 自带的,AGP 3.5 应用的是 ASM 6,而从 AGP 3.6 开始应用的是 ASM 7,应该是引入的 Hunter 在 ASM 7 上存在缺点,导致降级后出现异常。
思考到 Hunter 只是对应用 ASM 的 Transform 做了些简略封装,且这个插件实现的性能比较简单,所以采纳移除 Hunter 从新实现的形式解决这个问题。
Cannot change dependencies of dependency configuration
咱们应用了 resolutionStrategy.dependencySubstitution
来实现组件库源码切换,降级后,如果将组件库切成了源码,在 Android Studio 中点击 Run 按钮构建时会呈现如题谬误。
在排查问题的过程中发现在命令行中执行 ./gradlew assembleRelease
能构建胜利,而通过 Android Studio Run 构建与上述命令行构建的区别仅仅是所执行的工作前减少了模块前缀(:app:assembleRelease
)。从这个区别登程,最终找到问题的起因是在 gradle.properties
中开启了 org.gradle.configureondemand
这个孵化中的个性,使 gradle 只配置跟申请的工作相干的 project,导致以指定 module 形式执行工作时,切为源码的 project 没有配置。
敞开 org.gradle.configureondemand
个性即可解决这个问题。
Entry name ‘xxx’ collided
降级后构建,在执行打包工作 package${variant.name.capitalize()}
(例如 packageFullRelease)时会呈现如题谬误。
由官网文档可知 AGP 3.6 引入了如下新性能:
新的默认打包工具
该性能会在构建 debug 版时,应用新打包工具 zipflinger 来构建 APK,且从 AGP 4.1 开始,构建 release 版时也会应用这个新打包工具。
谬误产生在打包生成 APK 的工作中,很容易联想到跟上述新性能无关。应用官网文档提供的形式,在 gradle.properties
文件中增加 android.useNewApkCreator=false
配置复原应用旧打包工具后,能够胜利构建。但生成的 APK 中缺失 Java 资源文件,导致运行时呈现各种问题(如 OkHttp 短少 publicsuffixes.gz 文件,导致申请始终不返回)。
当初解决问题的方向有 2 个:解决 Java 资源文件缺失问题和解决如题构建谬误。为解决这些问题,须要先剖析问题产生的起因,通过调试 AGP 构建过程,剖析 AGP 源码,发现打包工作对应的实现类为 PackageApplication
,次要实现逻辑在其父类 PackageAndroidArtifact
中,向 APK 文件写入 Android 和 Java 资源文件的调用过程如下图所示:
updateSingleEntryJars
办法写入 asset 文件,addFiles
办法写入其余 Android 资源文件和 Java 资源文件。调 writeZip
之前会依据 android.useNewApkCreator
配置决定应用哪个打包工具,值为 true
用 ApkFlinger
,否则用 ApkZFileCreator
,android.useNewApkCreator
默认值为 true
。
如通过配置应用旧打包工具 ApkZFileCreator
,它会用 ZFile
读资源缩减后生成的文件3,以及混同后生成的文件4,将其中的 Android 和 Java 资源文件写入到 APK 文件中。
上面的源码片段展现了写入的次要逻辑,分为如下 3 步:
- 创立
ZFile
对象,读取 zip 文件将 central directory 中的每项退出到entries
中 - 遍历
ZFile
中的entries
,将压缩的资源文件合并到 APK 文件中 -
遍历
ZFile
中的entries
,将非压缩的资源文件写入到 APK 文件中// ApkZFileCreator.java public void writeZip(File zip, @Nullable Function<String, String> transform, @Nullable Predicate<String> isIgnored) throws IOException { // ... try {ZFile toMerge = closer.register(ZFile.openReadWrite(zip)); // ... Predicate<String> noMergePredicate = v -> ignorePredicate.apply(v) || noCompressPredicate.apply(v); this.zip.mergeFrom(toMerge, noMergePredicate); for (StoredEntry toMergeEntry : toMerge.entries()) {String path = toMergeEntry.getCentralDirectoryHeader().getName(); if (noCompressPredicate.apply(path) && !ignorePredicate.apply(path)) { // ... try (InputStream ignoredData = toMergeEntry.open()) {this.zip.add(path, ignoredData, false); } } } } catch (Throwable t) {throw closer.rethrow(t); } finally {closer.close(); } } // ZFile.java private void readData() throws IOException { // ... readEocd(); readCentralDirectory(); // ... if (directoryEntry != null) { // ... for (StoredEntry entry : directory.getEntries().values()) { // ... entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); //... } directoryStartOffset = directoryEntry.getStart();} else {// ...} // ... } public void mergeFrom(ZFile src, Predicate<String> ignoreFilter) throws IOException { // ... for (StoredEntry fromEntry : src.entries()) {if (ignoreFilter.apply(fromEntry.getCentralDirectoryHeader().getName())) {continue;} // ... } }
在调试过程中发现读取 minified.jar
文件创建的 ZFile
中的 entries
中没有 Java 资源文件,而在后面 IncrementalSplitterRunnable.execute
中调 PackageAndroidArtifact.getChangedJavaResources
获取扭转的 Java 资源文件时,应用 ZipCentralDirectory
能失常读取到 Java 资源文件,阐明 ZFile
存在缺点。
上述 Java 资源文件缺失的问题是在敞开 R8 时呈现的,前面开启 R8 测试失常,新建 demo 工程测试,无论是否开启 R8 都失常。因而,可失去如下论断:
- 如
ZFile
正文中所述,它不是通用的 zip 工具类,对 zip 格局和不反对的个性有严格的要求;它在某些非凡条件下存在限度,可能会呈现读取文件缺失等问题 - 因为旧打包工具应用了
ZFile
可能导致存在生成的 APk 缺失 Java 资源文件等问题,且已被官网废除,不应该再应用
当初解决问题的方向回到解决如题构建谬误上来,新打包工具 ApkFlinger
写入 Android 或 Java 资源文件的调用过程如下图所示:
从如下源码片段可看到,在 ZipArchive.writeSource
中会调 validateName
查看写入的 entry 名称的有效性,如果以后 zip 文件的 central directory 中已存在雷同名字的内容,则抛出 IllegalStateException
异样,提醒如题谬误。
// ZipArchive.java
private void writeSource(@NonNull Source source) throws IOException {
// ...
validateName(source);
// ...
}
private void validateName(@NonNull Source source) {byte[] nameBytes = source.getNameBytes();
String name = source.getName();
if (nameBytes.length > Ints.USHRT_MAX) {
throw new IllegalStateException(String.format("Name'%s'is more than %d bytes", name, Ints.USHRT_MAX));
}
if (cd.contains(name)) {throw new IllegalStateException(String.format("Entry name'%s'collided", name));
}
}
从源码和调试后果来看,呈现如题谬误的起因个别是某些不标准的做法使 jar 文件中存在同名的 Android 资源文件,咱们遇到的 2 例为:
- 某个第三方库的 aar 中存在 asset 文件,同时其 classes.jar 中也存在雷同的 asset 文件
- 某个第三方库将另外一个第三方库的 aar 文件当做一般 jar 文件依赖,导致其 classes.jar 中存在
AndroidManifest.xml
文件
晓得问题的起因后,可依据提醒的文件名在 shrunkJavaRes.jar
或 minified.jar
中找到对应的文件,而后依据文件中的信息(如 AndroidManifest.xml
中的包名)定位到工程中的具体位置,再做相应的批改即可。
so 文件没有 strip
降级后,构建生成的 APK 中的 so 文件没有 strip,应用 ndk 中的 nm 工具5(在 macOS 中也可用零碎自带的 nm)查看,发现符号表和调试信息仍然存在。
剖析构建日志后发现 strip${variant.name.capitalize()}Symbols
(例如 stripFullReleaseSymbols)工作有执行,接着剖析 AGP 源码,调试构建过程,发现该工作通过 StripDebugSymbolsRunnable
对 so 进行 strip,从如下源码片段可看到其次要逻辑为:
- 调
SymbolStripExecutableFinder.stripToolExecutableFile
获取 ndk 中的 strip 工具门路 - 如果没有找到工具,则间接拷贝 so 到指标地位并返回
-
调用这个工具对 so 进行 strip 并输入到指标地位
private class StripDebugSymbolsRunnable @Inject constructor(val params: Params): Runnable {override fun run() { // ... val exe = params.stripToolFinder.stripToolExecutableFile(params.input, params.abi) {UnstrippedLibs.add(params.input.name) logger.verbose("$it Packaging it as is.") return@stripToolExecutableFile null } if (exe == null || params.justCopyInput) { // ... FileUtils.copyFile(params.input, params.output) return } val builder = ProcessInfoBuilder() builder.setExecutable(exe) // ... val result = params.processExecutor.execute(builder.createProcess(), LoggedProcessOutputHandler(logger) ) // ... } // ... }
因而,so 没被 strip 的起因应该是没找到 ndk 中的 strip 工具。进一步剖析源码可知 SymbolStripExecutableFinder
通过 NdkHandler
提供的 ndk 信息找 strip 工具门路,而 NdkHandler
通过 NdkLocator.findNdkPathImpl
这个顶层函数找 ndk 门路,所以 so 是否被 strip 最终取决于是否找到 ndk 门路。查找 ndk 次要逻辑如下:
const val ANDROID_GRADLE_PLUGIN_FIXED_DEFAULT_NDK_VERSION = "21.1.6352462"
private fun findNdkPathImpl(
userSettings: NdkLocatorKey,
getNdkSourceProperties: (File) -> SdkSourceProperties?,
sdkHandler: SdkHandler?
): NdkLocatorRecord? {with(userSettings) {
// ...
val revisionFromNdkVersion =
parseRevision(getNdkVersionOrDefault(ndkVersionFromDsl)) ?: return null
// If android.ndkPath value is present then use it.
if (!ndkPathFromDsl.isNullOrBlank()) {// ...}
// If ndk.dir value is present then use it.
if (!ndkDirProperty.isNullOrBlank()) {// ...}
// ...
if (sdkFolder != null) {
// If a folder exists under $SDK/ndk/$ndkVersion then use it.
val versionedNdkPath = File(File(sdkFolder, FD_NDK_SIDE_BY_SIDE), "$revisionFromNdkVersion")
val sideBySideRevision = getNdkFolderRevision(versionedNdkPath)
if (sideBySideRevision != null) {return NdkLocatorRecord(versionedNdkPath, sideBySideRevision)
}
// If $SDK/ndk-bundle exists and matches the requested version then use it.
val ndkBundlePath = File(sdkFolder, FD_NDK)
val bundleRevision = getNdkFolderRevision(ndkBundlePath)
if (bundleRevision != null && bundleRevision == revisionFromNdkVersion) {return NdkLocatorRecord(ndkBundlePath, bundleRevision)
}
}
// ...
}
}
private fun getNdkVersionOrDefault(ndkVersionFromDsl : String?) =
if (ndkVersionFromDsl.isNullOrBlank()) {
// ...
ANDROID_GRADLE_PLUGIN_FIXED_DEFAULT_NDK_VERSION
} else {ndkVersionFromDsl}
下面源码片段对应的次要查找流程如下图所示:
根据上述 ndk 查找逻辑,能够晓得 so 没被 strip 的根本原因是咱们没有在 build.gradle
中配置 android.ndkPath
和 android.ndkVersion
,在打包机上打包时也不存在 local.properties
文件,也就不存在 ndk.dir
属性,打包机上安装的 ndk 版本也不是 AGP 指定的默认版本 21.1.6352462
,导致找不到 ndk 门路。
尽管找到了起因,但还是有个疑难:为什么降级之前能失常 strip?为了寻找答案,再来看看 AGP 3.5 查找 ndk 的形式,其次要逻辑如下:
private fun findNdkPathImpl(
ndkDirProperty: String?,
androidNdkHomeEnvironmentVariable: String?,
sdkFolder: File?,
ndkVersionFromDsl: String?,
getNdkVersionedFolderNames: (File) -> List<String>,
getNdkSourceProperties: (File) -> SdkSourceProperties?
): File? {
// ...
val foundLocations = mutableListOf<Location>()
if (ndkDirProperty != null) {foundLocations += Location(NDK_DIR_LOCATION, File(ndkDirProperty))
}
if (androidNdkHomeEnvironmentVariable != null) {
foundLocations += Location(
ANDROID_NDK_HOME_LOCATION,
File(androidNdkHomeEnvironmentVariable)
)
}
if (sdkFolder != null) {foundLocations += Location(NDK_BUNDLE_FOLDER_LOCATION, File(sdkFolder, FD_NDK))
}
// ...
if (sdkFolder != null) {val versionRoot = File(sdkFolder, FD_NDK_SIDE_BY_SIDE)
foundLocations += getNdkVersionedFolderNames(versionRoot)
.map { version ->
Location(
NDK_VERSIONED_FOLDER_LOCATION,
File(versionRoot, version)
)
}
}
// ...
val versionedLocations = foundLocations
.mapNotNull { location ->
// ...
}
.sortedWith(compareBy({ -it.first.type.ordinal}, {it.second.revision}))
.asReversed()
// ...
val highest = versionedLocations.firstOrNull()
if (highest == null) {
// ...
return null
}
// ...
if (ndkVersionFromDslRevision != null) {
// If the user specified ndk.dir then it must be used. It must also match the version
// supplied in build.gradle.
if (ndkDirProperty != null) {val ndkDirLocation = versionedLocations.find { (location, _) ->
location.type == NDK_DIR_LOCATION
}
if (ndkDirLocation == null) {// ...} else {val (location, version) = ndkDirLocation
// ...
return location.ndkRoot
}
}
// If not ndk.dir then take the version that matches the requested NDK version
val matchingLocations = versionedLocations
.filter {(_, sourceProperties) ->
isAcceptableNdkVersion(sourceProperties.revision, ndkVersionFromDslRevision)
}
.toList()
if (matchingLocations.isEmpty()) {
// ...
return highest.first.ndkRoot
}
// ...
val foundNdkRoot = matchingLocations.first().first.ndkRoot
// ...
return foundNdkRoot
} else {
// If the user specified ndk.dir then it must be used.
if (ndkDirProperty != null) {
val ndkDirLocation =
versionedLocations.find {(location, _) ->
location.type == NDK_DIR_LOCATION
}
// ...
val (location, version) = ndkDirLocation
// ...
return location.ndkRoot
}
// ...
return highest.first.ndkRoot
}
}
对应的大抵流程如下图所示:
能够看到在 AGP 3.5 中,如果没有配置 ndk 门路和版本,会取 ndk 目录中的最高版本,只有 ndk 目录中存在一个版本就能找到,所以降级前没问题。AGP 3.6 和 4.0 的查找逻辑跟 AGP 3.5 相似,只不过减少了在 android.ndkVersion
未配置时取 AGP 内置的默认版本逻辑,AGP 3.6 的默认版本为 20.0.5594570
,AGP 4.0 的默认版本为 21.0.6113669
。
通过下面的剖析找到问题起因后,解决形式就跃然纸上了,为具备更宽泛的适应性,可采纳配置 android.ndkVersion
将 ndk 版本设置为跟打包机统一的形式来解决问题。
小结
本文介绍了 AGP 降级(3.5 到 4.1)过程,对所遇问题提供了起因剖析和解决形式。尽管本次降级的初衷不是优化构建,但降级后,咱们的构建速度晋升了约 36%,包大小缩小了约 5M。心愿本文可能帮忙须要降级的读者顺利完成降级,享受到官网对构建工具继续优化的成绩。
至此,本次 AGP 降级之旅已到起点,而咱们的开发之旅还将持续。
参考资料
- Android Gradle 插件版本阐明
- Configuration on demand
- AGP 源码
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!
- processFullReleaseManifestForPackage 工作的产物为 build/intermediates/packaged_manifests/fullRelease/AndroidManifest.xml ↩
- processFullReleaseManifest 工作的产物为 build/intermediates/merged_manifests/fullRelease/AndroidManifest.xml ↩
-
build/intermediates/shrunk_processed_res/${varient.name}/resources-$flavor-$buildType-stripped.ap_
(例如 build/intermediates/shrunk_processed_res/fullRelease/resources-full-release-stripped.ap_)↩ -
build/intermediates/shrunk_java_res/${varient.name}/shrunkJavaRes.jar
(例如 build/intermediates/shrunk_java_res/fullRelease/shrunkJavaRes.jar),如敞开了 R8,则是build/intermediates/shrunk_jar/${varient.name}/minified.jar
(例如 build/intermediates/shrunk_jar/fullRelease/minified.jar)↩ -
toolchains/aarch64-linux-android-4.9/prebuilt/$HOST_TAG/aarch64-linux-android/bin/nm
,HOST_TAG
在不同操作系统中的值不同,在 macOS 中为 darwin-x86_64,在 Windows 中为 windows-x86_64 ↩