Android引入Flutter后缺失armeabi-v7a包问题解决

0x00. 前言: Android中ABI管理简介

1. ABI是什么?

ABI(Application Binary Interface):应用程序二进制接口,描述了应用程序和操作系统之间,一个应用和它的库之间,或者应用的组成部分之间的低接口。ABI涵盖了各种细节,如:

·机器代码应使用的 CPU 指令集。
·运行时内存存储和加载的字节顺序。
·可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型。
·在代码与系统之间传递数据的各种规范。这些规范包括对齐限制,以及系统调用函数时如何使用堆栈和寄存器。
·运行时可用于机器代码的函数符号列表 - 通常来自非常具体的库集。

2. Android上可使用的ABI

  不同的 Android 手机使用不同的 CPU,而不同的 CPU 支持不同的指令集,CPU 与指令集的每种组合都有专属的ABI,下表列出了当前Android NDK开发中所支持的 ABI:

ABI 支持的指令集 备注
armeabi • ARMV5TE 和更高版本
• Thumb-1
Android NDK r17及之后版本以不再支持
armeabi-v7a • armeabi
• Thumb-2
• VFPv3-D16
• 其他(可选)
兼容armeabi指令集
2011年以后的生产的大部分Android设备都使用
arm64-v8a • AArch64 兼容armeabi-v7a、armeabi
x86 • x86 (IA-32)
• MMX
• SSE/2/3
• SSSE3
部分平板设备再使用
x86_64 • x86-64
• MMX
• SSE/2/3
• SSSE3
• SSE4.1、4.2
• POPCNT
兼容x86
mips • MIPS32r1 and later Android NDK r17及之后版本以不再支持
mips64 • MIPS64r6 Android NDK r17及之后版本以不再支持

3. 目前常见主流Android App所支持的ABI

  当我们需要使我们的 APP 支持尽可能多的不同 CPU 的时候,需要将支持不同ABI的 .so 文件放置在不同的目录下,APK 安装运行的时候会根据自己需要而自己选取。然而这样做却有两个问题:
1. 系统在加载 so 文件时会优先选择对应 ABI 文件夹下的 so,比如 ARMv7 的设备,会优先选择 armeabi-v7a 下的 so 进行加载。如果文件夹存在并且 so文件也存在,则正常加载;如果了文件夹存在,so不存在,则运行时崩溃;如果文件夹不存在,则会去 armeabi 文件夹下找 so 文件
2. 这些所有支持不同 ABI 的 .so 文件都会被打包进 apk 中,会造成 apk 体积增大
为了解决上面两个问题,一般我们会放弃支持部分 ABI 的设备,具体的放弃规则可以试项目情况、市面上支持某个特定 ABI 的设备的比例等因素而定,下表选取了当前市面上部分主流 APP 对ABI的支持情况:

应用名称 支持的ABI 备注
微信 仅 armeabi-v7a 使用了Flutter
QQ 仅 armeabi
淘宝 仅 armeabi-v7a
支付宝 仅 armeabi
微博 仅 armeabi
网易云音乐 仅 armeabi
哔哩哔哩 armeabi-v7a、x86 使用了Flutter
今日头条 仅 armeabi-v7a

  从这个表格中可用看出国内大部分主流App都支持了armeabi架构的CPU,而像微信、淘宝这类具有庞大用户群体的APP已经将最低支持ABI提升到了armeabi-v7a。又由于 armeabi-v7a 是向下兼容 armeabi 的,因此这么做只会导致在仅支持 armeabi 架构的设备上无法运行,但 2011年以后的生产的大部分Android设备都使用了armeabi-v7a,再加上现在市面上电子产品的更迭速度非常快,因此仅支持 armeabi CPU架构的设备基本可以忽略。

0x01. 问题的产生与初步解决

1. 背景及现状

  因项目改造需要将所支持的 ABI 由 armeabi 提高 armeabi-v7a,但是面临着一个问题:项目当前所使用的 so 库只有一部分同时支持了 armeabi 和 armeabi-v7a ,大部分的 so 库都仅仅支持了 armeabi 。如想要将所支持的 ABI 升级至 armeabi-v7a,则必须在 armeabi-v7a 文件夹中提供全套的 so 文件。

2. 初步的解决方案

  从上面Android上可使用的ABI表格可知 armeabi-v7a 是兼容 armeabi 的,所以ARMv7的设备也是支持 armeabi 的 so 的,所以可以把那些只提供了 armeabi 支持的 so库直接拷贝至 armeabi-v7a 文件夹下, 这样 ARMv7的设备在加载相应的 so 时会加载这些原本 armeabi 的 so文件,尽管这样做在性能上可能有所损耗,但终归是一种可行的方案。
对于 so 文件的迁移,其主要有两种来源,必须完整迁移每一种来源的 so 文件。

第一种是项目自身引用的相关 so 文件,这类文件一般放在 “项目 -> libs ” 、“项目 -> src ->main ->jniLibs”目录下,或者在build.gradle 中指定的其他目录,对于这类文件直接拷贝至新的 armeabi-v7a 目录中即可。

第二种是项目间接引入的相关 so 文件,比如 主工程所依赖的某个library 项目所使用的 so ,或者所依赖的某个第三方类库及其引用传递而来的 so 文件。这类 so 文件一般不容查找到,很容易遗漏,有个好的办法就是直接从 apk 文件中拷贝:可以先将项目编译,然后从将生成的 apk 文件解包,从中拿到 so 文件。
通过以上两步的操作,就可以完整的检出 apk 所需的所有 so 文件了,然后在将这些 so 文件放入项目的 armeabi-v7a 目录下,再在编译脚本 build.gradle 中配置生成的 apk 所支持的设备ABI为armeabi-v7a :

ndk {
     abiFilters 'armeabi-v7a' //仅打包v7a版本的so文件
    }

3. 隐患与风险

  通过以上方式处理的so类库缺失问题,在短期内缺失解决了问题,但是一旦需要引入新的 so 库文件,就必须按照原来的规则选取正确的ABI下的 so库文件,这个过程完全依赖于人工操作,并且在出现问题后还不利于定位 问题,这还不是最麻烦的情形,如果是通过第三方依赖引入的 so库文件,由于我们手动将其拷贝至了 armeabi-v7a 下,及时后面这个 so 库更新了,由于它只有 armeabi 类型的,所以在工程构建时合并 so库文件的时候新的 so库文件永远不会被合并进去,除非我们自己去一个个查询它的版本是否升级了、是否需要替换为更新后的so库文件。显然这个工作量和工作难度已经超出了我们的可控范围之内。

0x10. 自动化(工程化)的解决方案

1. gradle打包流程分析

  现在的Android应用都是采用Android Studio来开发的,AS默认是采用Gradle作为构建工具,更具体一点的来说就是,AndroidStudio中利用 android-gradle-plugin实现的打包,一般都会在项目有如下配置:

classpath "com.android.tools.build:gradle:3.2.1" //root build.gradle 中引入依赖
apply plugin: 'com.android.application' //主Module(如app)中应用该插件

通过如上的配置,就可以为该Module应用 android-gradle-plugin 中定义的打包流程,最终产出Apk文件。那么这个打包流程是如何工作的呢?这就不得不从Gradle的执行流程开始说起了。
  无论执行何种 gradle 命令,都会经历 gradle 的三个阶段,分别是: 初始化阶段、配置阶段、执行阶段;如下图所示:


gralde执行流程

对应的这三个阶段,gradle都为我们提供可供插入我们自己业务逻辑的地方,也就是图中的3个 “Hook”点。为了查看我们在执行打包时究竟执行了什么任务,可以在gradle完成配置阶段之后将本次需要执行的所有任务打印出来,在 主Module(app)的build.gradle中插入如下代码:

gradle.taskGraph.whenReady {
    TaskExecutionGraph taskGraph ->
        println("====================================================")
        println("即将要执行的 task 共 ${taskGraph.allTasks.size()} 个,与app工程相关的有:")
        int count = 0
        taskGraph.allTasks.each {
            if(it.project.name == "app"){
                println("\t${++count}. "+it)
            }
        }
    println("====================================================")
}

这段代码可以在执行 gradle命令的时候输出本次需要执行的所有task(由于在多Module的项目中,多个module的打包任务大部分都是相同的,因此这里只显示主module的任务):

输出信息为:
====================================================
即将要执行的 task 共 204 个,与app工程相关的有:
1. task ':app:checkReleaseClasspath'
2. task ':app:preBuild'
3. task ':app:extractProguardFiles'
4. task ':app:preReleaseBuild'
5. task ':app:compileReleaseAidl'
6. task ':app:compileReleaseRenderscript'
7. task ':app:checkReleaseManifest'
8. task ':app:generateReleaseBuildConfig'
9. task ':app:prepareLintJar'
10. task ':app:mainApkListPersistenceRelease'
11. task ':app:generateReleaseResValues'
12. task ':app:generateReleaseResources'
13. task ':app:mergeReleaseResources'
14. task ':app:createReleaseCompatibleScreenManifests'
15. task ':app:processReleaseManifest'
16. task ':app:splitsDiscoveryTaskRelease'
17. task ':app:processReleaseResources'
18. task ':app:generateReleaseSources'
19. task ':app:dataBindingExportBuildInfoRelease'
20. task ':app:dataBindingMergeDependencyArtifactsRelease'
21. task ':app:transformDataBindingBaseClassLogWithDataBindingMergeGenClassesForRelease'
22. task ':app:dataBindingGenBaseClassesRelease'
23. task ':app:dataBindingExportFeaturePackageIdsRelease'
24. task ':app:kaptGenerateStubsReleaseKotlin'
25. task ':app:kaptReleaseKotlin'
26. task ':app:compileReleaseKotlin'
27. task ':app:javaPreCompileRelease'
28. task ':app:compileReleaseJavaWithJavac'
29. task ':app:compileReleaseNdk'
30. task ':app:compileReleaseSources'
31. task ':app:lintVitalRelease'
32. task ':app:mergeReleaseShaders'
33. task ':app:compileReleaseShaders'
34. task ':app:generateReleaseAssets'
35. task ':app:mergeReleaseAssets'
36. task ':app:transformClassesWithMetricsForRelease'
37. task ':app:checkReleaseLibraries'
38. task ':app:processReleaseJavaRes'
39. task ':app:transformResourcesWithMergeJavaResForRelease'
40. task ':app:transformClassesAndResourcesWithProguardForRelease'
41. task ':app:transformClassesWithDexBuilderForRelease'
42. task ':app:transformClassesWithMultidexlistForRelease'
43. task ':app:transformDexArchiveWithDexMergerForRelease'
44. task ':app:transformClassesAndDexWithShrinkResForRelease'
45. task ':app:mergeReleaseJniLibFolders'
46. task ':app:transformNativeLibsWithMergeJniLibsForRelease'
47. task ':app:transformNativeLibsWithStripDebugSymbolForRelease'
48. task ':app:validateSigningRelease'
49. task ':app:packageRelease'
50. task ':app:assembleRelease'
====================================================

2. 特定切入点查找

通过上一步的分析可知在 apk 打包构建过程中共有3个 Task与 jniLibs相关:

  1. task ':app:mergeReleaseJniLibFolders'
  2. task ':app:transformNativeLibsWithMergeJniLibsForRelease'
  3. task ':app:transformNativeLibsWithStripDebugSymbolForRelease'

对于这个3个任务可以查看 android-gradle-plugin的源码:

task ':app:mergeReleaseJniLibFolders'(查看源码):
将项目中多个module中的 jniLibs 目录合并到 build/intermediates/jniLibs/ 下。

task ':app:transformNativeLibsWithMergeJniLibsForRelease'(查看源码):
按照一定规则将上一步中的 .so 和一些其他文件(如 .jar文件等)合并到 build/intermediates/incremental/debug-mergeJniLibs/zip-cachebuild/intermediates/transforms/mergeJniLibs/下。

task ':app:transformNativeLibsWithStripDebugSymbolForRelease'(查看源码):
去除 jnilibs so中的一些调试语法信息。

3. 自动化脚本实现

由上面的分析可知,我们只需要在 “task':app:transformNativeLibsWithStripDebugSymbolForRelease'” 之后将所缺失的 .so 库文件从 armeabi 下拷贝至 armeabi-v7a 即可:

 project.afterEvaluate {
            project.tasks.findAll {
                task ->
                    if (task.path.startsWith(":app:transformNativeLibsWithStripDebugSymbolFor")) {
                        task.doLast {
                            println("=================== fix-v7libs start ===================")
                            if(!task?.outputs?.files?.files?.isEmpty()){
                                task.outputs.files.files.each {
                                    File armeabiDirPath = searchForDirectory("armeabi", it)
                                    if (armeabiDirPath != null) {
                                        File armeabiv7DirPath = searchForDirectory("armeabi-v7a", it)
                                        if (armeabiv7DirPath == null) {
                                            armeabiv7DirPath = new File(armeabiDirPath.getParent() + File.separator + "armeabi-v7a" + File.separator)
                                        }
                                        if (!armeabiv7DirPath.isDirectory()) {
                                            throw new GradleException("JNILibs File Search Error !")
                                        }

                                        armeabiDirPath.listFiles().each {
                                            armeabiFile ->
                                                if (!(armeabiFile as File).exists() || armeabiFile.isDirectory()) {
                                                    throw new GradleException("JNILibs File Error !")
                                                }
                                                boolean skip = armeabiv7DirPath.list().contains(armeabiFile.name)
                                                if (!skip) {
                                                    File targetFile = new File(armeabiv7DirPath, (armeabiFile as File).name)

                                                    targetFile.withOutputStream { os ->
                                                        (armeabiFile as File).withInputStream {
                                                            is ->
                                                                os << is
                                                        }
                                                    }
                                                    println("COPY: from  ${(armeabiFile as File).absolutePath} \n\t to ${targetFile.absolutePath}")
                                                } else {
                                                    println("SKIP:${(armeabiFile as File).name}")
                                                }

                                        }
                                    }
                                }
                            }
                            println("=================== fix-v7libs finish ===================")
                        }
                    }
            }
        }

将以上脚本定义在 主Module的 build.gradle中,在打包过程中就可以实现自动拷贝 .so库文件了。

0x11. 插件化

  通过在build.gradle中增加脚本代码的方式就能实现自动化拷贝 so库文件了,但是这样做仍然不够优雅,因为需要直接将脚本代码定义在 build.gradle中,使得build.gradle显得很凌乱,同时如果有其他项目也需要这个功能,就需要将这段脚本代码拷贝过去,这种方式显得有些“原始”,其实我们还有更好的选择:可以将这个功能打包成一个 gradle-plugin 。这样做的话,如果项目需要配置这个功能,只需要引入插件、应用插件就可以了。
gradle-plugin的开发这里就不再赘述了,其核心代码就是上面这段脚本。目前这个gradle-plugin已经开发完成并发布到mavenCentral库中,可以通过如下方式使用该插件:

1、mavenCentral()  // 工程根build.gradle 的 repositories {}中加入mavenCentral仓库
2、classpath "com.github.zhangguoning.plugin:fix_v7libs_plugin:0.0.1" //工程根gradle文件中引入插件
3、apply plugin: 'fix_v7libs_plugin'//需要执行拷贝的模块加入此句,如app

maven库:https://search.maven.org/artifact/com.github.zhangguoning.plugin/fix_v7libs_plugin
插件源码:https://github.com/zhangguoning/FixArmeabiV7libsPlugin

0x101. 相关文献资料参照与源码库链接

Android ABI:https://developer.android.com/ndk/guides/abis
Gradle源码:https://docs.gradle.org/5.4.1/dsl/org.gradle.api.invocation.Gradle.html
Groovy API参考:http://www.groovy-lang.org/api.html
Android-Gradle-Plugin源码:https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle
Gradle版本 与 Android Gradle 插件 对应关系:https://developer.android.com/studio/releases/gradle-plugin.html#updating-plugin

你可能感兴趣的:(Android引入Flutter后缺失armeabi-v7a包问题解决)