【Gradle-20】Android ABI 分包指南

3,097 阅读9分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

1、ABI是什么

ABI全称为 Application Binary Interface,即应用二进制接口,是应用程序和操作系统在二进制级别交互的一种接口标准。

不同的Android设备使用不同的CPU,而不同的CPU支持不同的指令集。CPU与指令集的每种组合都有专属的应用二进制接口(ABI)。

常见的 ABI 类型:

在Android生态系统中,x86架构(Intel)的非常少见,常用于模拟器或是早期的“古董”设备,主流架构是ARM

而在ARM架构中,又以64位架构(arm64-v8a)为主流,由Android 5.0开始支持,相比于32位架构(armeabi-v7a),寄存器和指令集的增强,显著提高了执行速度和运算性能,同时还能兼容32位架构。

2、分包是什么

虽然ARM 64位架构是Android生态系统中的主流设备,但还是有一些老的设备是32位架构,而为了更好的用户体验,这部分存量设备用户也要兼容到,这也是今天要介绍的内容——分包

分包是指由传统的构建一个APK文件变为根据架构来构建出多个APK文件,通常是arm64-v8a的64位包、armeabi-v7a的32位包、以及arm64-v8a加armeabi-v7a的合包(整包)

3、分包的优点

除了应用市场要求支持64位架构适配之外,对于应用来说,分包也有如下优点:

  • 减少APK大小:每个分包的APK只包含对应 ABI 的共享库(.so文件),减少了APK的大小;
  • 提高安装速度:用户设备只会下载和安装与其架构匹配的APK,可以提高下载和安装的速度,并节省下载资源;
  • 提升性能:64位架构相比于32位架构,在运算能力和执行速度上有显著提升;

4、分包前

以测试项目GradleX为例,分包前ndk中配置为arm64-v8a和armeabi-v7a:

        ndk {
            abiFilters.addAll(mutableSetOf("arm64-v8a", "armeabi-v7a"))
        }

测试依赖了阿里云音视频SDK:

implementation("com.aliyun.aio:AliVCSDK_Premium:6.4.0")

其中包含多个共享库文件(.so)。

然后执行打包:

./gradlew assembleDebug

打包完成后,在build/outputs/apk/debug/目录中双击APK文件,进入APK文件分析器窗口。

如下图:

lib目录下可以找到不同架构的so文件,这里包含了arm64-v8a和armeabi-v7a两种类型,即为前面介绍的合包,合包的APK大小要比单架构的APK大很多。

5、分包实现

实现分包的方式有两种:多渠道配置和splits配置。

5.1、多渠道

5.1.1、简介

在上一章中我们介绍了多渠道打包相关知识,多渠道配置如下:

android {
    defaultConfig {
        //...
        ndk { ... } 
    }
    // 多渠道打包配置
    flavorDimensions += listOf("version")
    productFlavors {
        create("huawei") {
            dimension = "version"
            applicationIdSuffix = ".huawei"
            versionNameSuffix = "-huawei"
            versionCode = 1
            buildConfigField("int", "CHANNEL_CODE", "1001")
        }
        create("oppo") {
            dimension = "version"
            applicationIdSuffix = ".oppo"
            versionNameSuffix = "-oppo"
            versionCode = 1
            buildConfigField("int", "CHANNEL_CODE", "1002")
        }
    }
}

productFlavors{ }中,我们可以对defaultConfig{ }中的配置进行覆写和追加,所以我们也可以在多渠道配置中对ndk{ }属性进行配置。

5.1.2、配置

配置如下:

    flavorDimensions += listOf("version")
    productFlavors {
        create("huawei-armeabi-v7a") {
            dimension = "version"
            ndk {
                abiFilters.addAll(mutableSetOf("armeabi-v7a"))
            }
        }
        create("huawei-arm64-v8a") {
            dimension = "version"
            ndk {
                abiFilters.addAll(mutableSetOf("arm64-v8a"))
            }
        }
        create("huawei-all") {
            dimension = "version"
            ndk {
                abiFilters.addAll(mutableSetOf("arm64-v8a", "armeabi-v7a"))
            }
        }
    }

配置三个渠道,分别是:

  • huawei-armeabi-v7a 渠道,只包含32架构armeabi-v7a;
  • huawei-arm64-v8a 渠道,只包含64架构arm64-v8a;
  • huawei-all 渠道,则是合包,包含32位架构armeabi-v7a和64位架构arm64-v8a;

这里需要注意的是,如果defaultConfig{ }中已经有ndk配置了,要先defaultConfig{ }中的ndk配置进行注释或移除。

android {
    defaultConfig {
        ndk {
            //abiFilters.addAll(mutableSetOf("arm64-v8a", "armeabi-v7a"))
        }
    }
}

在上一章介绍到DefaultConfig和ProductFlavor都继承于同一个父类接口BaseFlavor,所以只要是BaseFlavor中包含的属性配置,渠道配置productFlavors{ }都可以对defaultConfig{ }中的配置进行覆写或追加。

然而ndk{ }配置并不属于BaseFlavor中的属性,而是在VariantDimension接口类中,所以defaultConfig{ }中的ndk配置依然会应用于所有构建变体,为了使productFlavors{ }中的ndk{ }配置生效,需要先注释或移除defaultConfig{ }中的ndk{ }配置。

5.1.3、效果

多渠道配置完成后,重新Sync让配置生效。

然后在Gradle面板中双击执行assembleDebug看看打包后的产物是否符合分包预期。

打包完成后如下图:

此时,在/app/build/outputs/apk/下有三个目录:

  • huawei-all
  • huawei-arm64-v8a
  • huawei-armeabi-v7a

这三个目录就是我们定义的渠道名称,现在再双击huawei-arm64-v8a/debug/目录下的app-huawei-arm64-v8a-debug.apk文件,进入APK文件分析器窗口。

如下图:

可以看到在app-huawei-arm64-v8a-debug.apk文件中,lib目录下只有arm64-v8a类型的.so文件,这就表示我们通过多渠道配置的方式实现了ABI 分包的功能,相比于合包,APK的大小也有明显减小

5.2、splits

5.2.1、简介

虽然说上面我们通过多渠道配置的方式实现了ABI 分包的功能,但其实官方也提供了正统的分包能力,就是splits

splits是分裂/分开的意思,支持屏幕密度(Density)和应用二进制接口(ABI)两种方式构建多个APK,所以我们可以通过splits来做 ABI 分包,即根据不同的架构生成不同的APK文件。

5.2.2、配置

app>build.gradle文件中配置如下:

android {
    // ...

    // ABI 分包
    splits {
        abi {
            isEnable = true
            reset()
            include("arm64-v8a", "armeabi-v7a")
            isUniversalApk = true
        }
    }
}

abi{ }闭包中的属性配置介绍:

  • isEnable,是否开启 ABI 分包,true表示开启,默认关;
  • reset(),清除默认的 ABI 列表,要与include属性结合使用,毕竟不能没有ABI 配置,清除了要再添加;
  • include(),指定生成哪些APK的 ABI 列表,多个由逗号分割;
  • isUniversalApk,除了按照include配置的 ABI 列表生成多个APK之外,是否生成一个包含所有 ABI 列表的合包,true表示开启,默认关;

在使用splits来配置 abi { }时,同样需要注释或移除defaultConfig{ }中的ndk { } 配置,否则会有冲突。

> com.android.builder.errors.EvalIssueException: Conflicting configuration : 'armeabi-v7a,arm64-v8a' in ndk abiFilters cannot be present when splits abi filters are set : arm64-v8a

5.2.3、效果

配置完成之后,重新Sync。

然后在Gradle面板中双击执行assembleDebug打包,或执行Gradle命令行打包:

./gradlew assembleDebug

打包完成后,会在build/outputs/apk/debug/目录中找到每个 ABI 单独的APK文件。

如下图:

有三个APK文件:

  • app-arm64-v8a-debug.apk:64位包;
  • app-armeabi-v7a-debug.apk:32位包;
  • app-universal-debug.apk:合包;

如果是直接在Android Studio中点击Run的情况下,属于debug编译环境,会根据手机设备的架构类型来选择一种 ABI 进行编译构建,比如手机设备的架构是arm64-v8a,则默认选择64位架构来编译构建。

到此,基本效果已经完成了。

可以看到,splits配置的方式要比多渠道配置的方式更简单,也更灵活一些。

5.2.4、进阶

场景一:本地开发时,编译构建debug包的时候不进行 ABI 分包,使用合包进行开发验证,可以简化调试和缩短编译时长。

前面介绍到isEnable属性表示是否开启 ABI 分包,所以我们可以增加一个项目属性作为开关,如果是debug编译则不进行分包,release编译再分包。

配置如下:

    val isRelease  = project.hasProperty("isRelease") && project.property("isRelease") == "true"

    // ABI 分包
    splits {
        abi {
            isEnable = isRelease
            reset()
            include("arm64-v8a", "armeabi-v7a")
            isUniversalApk = true
        }
    }

通过变量isRelease来控制是否分包,变量isRelease为项目属性。

如果是release构建且需要分包,则需要在打包命令中加上isRelease参数。

执行命令如下:

./gradlew assembleRelease -PisRelease=true

其实也可以根据构建任务(Task)的名字来判断是否需要开启 ABI 分包,但release构建的时候可能还是有依赖debug之类的组件,所以release构建环境可能并不能作为准确条件,而且项目属性也能灵活一些,也可以作为一个公共属性全局复用。

场景二:在不涉及 ABI 兼容性测试的情况下,日常测试只构建主流的arm64-v8a包,缩短编译时长提升开发效率。

结合场景一的经验,我们可以增加一个项目属性来控制splits.abi.include属性的值。

配置如下:

    val isRelease  = project.hasProperty("isRelease") && project.property("isRelease") == "true"
      
    val onlyArm64  = project.hasProperty("onlyArm64") && project.property("onlyArm64") == "true"
    
    // ABI 分包
    splits {
        abi {
            isEnable = isRelease
            reset()
            if (onlyArm64) {
                include("arm64-v8a")
                isUniversalApk = false
            } else {
                include("arm64-v8a", "armeabi-v7a")
                isUniversalApk = true
            }
        }
    }

项目属性onlyArm64为true表示只构建64位包(arm64-v8a),其他情况则分别构建32/64位包以及合包。

打包命令中加上onlyArm64参数:

./gradlew assembleRelease -PisRelease=true -PonlyArm64=true

这里需要注意的是,只构建64位包的情况下,isUniversalApk属性要设置为false,虽然 ABI 类型只有arm64-v8a,但是如果isUniversalApk属性为true的话,还是会构建一个合包,相当于重复构建了,所以需要设置为false。

除了用项目属性onlyArm64来实现之外,include()参数的值也可以用外部List来替换,是否构建合包的isUniversalApk属性根据List的size来判断。

示例:

    val abiFilters = listOf("arm64-v8a")

    // ABI 分包
    splits {
        abi {
            isEnable = isRelease
            reset()
            include(*abiFilters.toTypedArray())
            isUniversalApk = abiFilters.size > 1
        }
    }

这个list可以作为项目属性,也可以定义在local.properties文件中。

6、注意

6.1、适配

分包本身并不难,但是要确保项目中的依赖是支持32/64位共享库的,这可能需要升级依赖库以及重新导入so文件到src/main/jniLibs/目录下,并且检查打包后的APK文件是否都包含所需要的so文件。

6.2、兼容

特别是在第一次进行分包之后,在上传应用市场之前,要分别测试APK在不同架构的设备上能否正常运行。

7、总结

本章主要是Android ABI 分包的知识点,并分别介绍了两种实现方式,推荐使用splits能力来进行分包。

ABI 分包不仅满足应用市场对于64位架构适配的要求,还能显著减小APK大小,提升用户体验。

8、GitHub

github.com/yechaoa/Gra…

9、相关文档