Android O 源码中编译程序, 关于32/64位so库兼容问题的记载
阅读原文时间:2021年04月20日阅读:1

由于项目需求,需要在Android O系统中加入第三方库, Android O 默认编译的是64位操作系统, 第三方公司提供了32位和64位的库, 但是在实际应用过程中还是遇到了种种问题, 在此做个记录, 希望遇到同样问题的小伙伴们不要掉入相同的坑.

具体分为以下几个问题:

  1. 系统是怎样判断一个应用是32/64位架构
  2. 如何在源码中将自己写的应用编译成32/64位
  3. 系统级应用如何使用apk自身的库文件
  4. android默认的压缩优化引发的问题

坑1:

项目一开始, 客户只提供了32位的库文件, 将自己写的应用和库文件集成到系统, Android.mk文件如下:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_APPS)

LOCAL_PACKAGE_NAME := OemScanDemoTest
#LOCAL_CERTIFICATE := platform

#LOCAL_JACK_ENABLED := disabled
LOCAL_MULTILIB := 32

include $(BUILD_PACKAGE)

include $(CLEAR_VARS)
LOCAL_MODULE := libHHPScanInterface
LOCAL_SRC_FILES := libHHPScanInterface.so
LOCAL_MULTILIB := 32
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_SUFFIX := .so
include $(BUILD_PREBUILT)

include $(CLEAR_VARS)
LOCAL_MODULE := libHSMDecoderAPI
LOCAL_SRC_FILES := libHSMDecoderAPI.so
LOCAL_MULTILIB := 32
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_SUFFIX := .so
include $(BUILD_PREBUILT)

include $(CLEAR_VARS)
LOCAL_MODULE := libHsmKil
LOCAL_SRC_FILES := libHsmKil.so
LOCAL_MULTILIB := 32
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_SUFFIX := .so
include $(BUILD_PREBUILT)

在代码中肯定是会load这些库文件的, 编译好之后烧录到系统验证, 结果报错如下:

这就很奇怪, 明明我的Android.mk中配置了LOCAL_MULTILIB := 32属性, 按理来说不应该是32位的应用吗? 为什么在lib64文件夹下找库文件? 于是查看了下zygote进程, 我们的程序果然是zygote64派生的, 说明系统采用了64位的架构来解析我们的应用

msm8953_64:/ # ps -A | grep zygote
root           862     1 4246428  75192 poll_schedule_timeout 736113afa8 S zygote64
root           865     1 1577004  67052 poll_schedule_timeout f398c778 S zygote
webview_zygote 2022    1 1339532  32908 poll_schedule_timeout f2612778 S webview_zygote32
msm8953_64:/ # 
msm8953_64:/ # ps -A | grep 862                                                                                                                                                  
root           862     1 4246428  75192 poll_schedule_timeout 736113afa8 S zygote64
.....
u0_a78        3765   862 4327188  50488 binder_thread_read 736113af78 S com.example.oemscandemo

使用ps -A | grep 865 查看32位zygote派生的所有进程, 发现还是有程序是32位的, 说明并不是所有的系统应用都强制要求是64位的. 那么, 系统是根据什么条件判断一个应用该采用哪种架构运行呢? 这里有一篇博客写的很好:

https://blog.csdn.net/weixin_40107510/article/details/78138874

借用博客中的总结:

  1. 如果apk包中lib文件夹下有.so库,就根据这个.so库的架构模式,确定app的primaryCpuAbi的值
  2. 对于system app, 如果没法通过第一步确定primaryCpuAbi的值,PKMS会根据/system/app/${APP_NAME}/lib和/system/app/${APP_NAME}/lib64这两个文件夹是否存在,来确定它的primaryCpuAbi的值
  3. 对于还没有确定的app, 在最后还会将自己的primaryCpuAbi值与和他使用相同UID的package的值设成一样
  4. 对于到这里还没有确认primaryCpuAbi的app,就会在启动进程时使用ro.product.cpu.abilist这个property的值的第一项作为它关联的ABI

由于我们的是系统级的app, 库文件都是编译到system/lib/ 或 vendor/lib等目录下, apk自身不存在lib文件夹, 导致最终会走到上面提到的四点中的最后一点, 系统属性ro.product.cpu.abilist 第一项为 arm64-v8a, 所以我们的程序就光荣的成为了64位应用.

坑2

既然你一定要跟我对着干, 一定要使用64位的库, 那就使用64位的库好了, 还好客户提供了相应的64位的库文件. 于是将32位的库替换成64位的, 满以为问题可以得到圆满解决, 但人生中很多时候, 愿望和显示是相悖的, 运行后还是崩溃!

提示找不到的这个库是系统中camera的底层库文件, 系统自带的, 因为客户提供的库中需要load这个库文件(扫码库, 需要打开相机). 而且! 它只能编译成32位看库文件! Oh shit!

如果强行将libcamera_interface.so编译成64位的, 很有可能引起系统异常, 这也不符合编程的基本思想: 可以扩充添加代码, 尽量不要修改原有代码. 更何况这是系统的底层代码库啊, 再退一步讲, 如果这个库又引用了其它的库文件呢? 这样改下去将变成一个巨大的工程.

绕来绕去还是绕回来了, 还得使用32位的库. 那么如何将一个系统级应用编成一个32位程序呢? 根据坑1 的介绍, 只要在应用的目录下出现lib/armeabi-v7a而不是lib/arm64-v8a的文件夹, 应该就可以让系统认为是32位架构的程序了, 于是修改了Android.mk:

include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_APPS)

LOCAL_PACKAGE_NAME := OemScanDemoTest
LOCAL_CERTIFICATE := platform
LOCAL_MULTILIB := 32

<!-- 修改了这里 -->
LOCAL_JNI_SHARED_LIBRARIES := libHHPScanInterface libHSMDecoderAPI libHsmKil libmmcamera_interface libbinder libcamera_client libcutils libutils libc++

include $(BUILD_PACKAGE)

编译生成的产物目录下产生了lib/armeabi-v7a文件夹, 按照坑1中的理论, 这个应用会被zygote派生, 而不是zygote64. 烧录验证, 果然不再报找不到库文件的错误, 使用ps -A | grep zygote命令查看, 我们的应用进程也确实是32位zygote派生, 这就说明我们的应用彻彻底底的成为了一个32位应用.

其中LOCAL_MULTILIB := 32 的作用是在应用目录下生成lib/armeabi-v7a, LOCAL_JNI_SHARED_LIBRARIES 作用是将.so文件打包到lib/armeabi-v7a文件夹下, 这样一来, 虽然是系统应用, 也不用去system/lib 或者 vendor/lib下去找库文件, 优先使用自身库文件.

坑3

问题到了这儿还没完, 坑永远填不完, 这回不再报找不到库的错误, 运行报错如下:

库是找的到了, 调用库里面的方法出问题了! 不得不怀疑是这个库给的有问题, 但是用Android Studio编译, 并且把selinux权限临时关闭掉(adb shell setenforce 0), 安装到系统是可以正常使用的. (使用IDE编译应用能够使用为什么还要在系统里去编译呢? 其一, 由于需求原因, 我们需要将代码内置到系统, 作为一个系统服务, 方便其它进程调用; 其二, 第三方进程SELinux权限也不好解决, 有的甚至无法解决, 所以我们必须要将代码在源码中编译, 并添加好相应的SELinux权限. 关于权限问题, 不是本篇主题, 不做介绍).  这样看来不是库的问题, 那会不会是编译器的不同导致的呢?

我们知道android8.x的源码使用的是jack编译器, android studio 使用的什么编译器我没有了解过, 但是Android.mk中有一条属性可供配置: LOCAL_JACK_ENABLED := disabled, 配置这个属性是告诉系统这个程序不采用jack编译器编译, 使用老编译器编译. 烧录系统测试, 还是一样的错误.

熟悉C的人看到这种错误肯定很熟悉, 是因为库的参数不兼容或者调用这传入的参数和.so中的参数不匹配导致的. 网上找了很久的资料结果无所获. 猛然记起, Android有一个压缩优化的属性可供配置(貌似以前遇到过类似的坑, 由于没做笔记时间一长又被坑一次):

LOCAL_PROGUARD_ENABLED := disabled  禁用混淆
LOCAL_DEX_PREOPT := false   关闭优化

而android源码中默认是打开LOCAL_DEX_PREOPT属性的, 优化了so文件, 导致so库里面的jni函数接口产生了变化, 于是就出现了上面的错误. 关闭优化, 将SELinux权限问题处理掉, 终于可以正常使用了.

还有些细节也记录一下, 原本打算将应用编译到system/app/目录下的, 但是有些库文件, 比如libmmcamera_interface.so, 只存在vendor/lib/目录下. 这个时候LOCAL_JNI_SHARED_LIBRARIES := libmmcamera_interface 会报错, 说找不到这个库文件. 也就是说system下的进程不能引用vendor下的资源. 然后将应用编译进了vendor/app/目录下, libHHPScanInterface libHSMDecoderAPI libHsmKil libmmcamera_interface libbinder libcamera_client libcutils libutils libc++ 这个属性却不会报错了, 而事实上有些库是存在system/lib/下的. 于是得出了一个来不及细想的结论:

vendor/是Google用来作为运营商和厂商做私有定制的目录,  但也是系统级别的, 所以它可以调用到system下的资源; 而反过来, Android O 之后的系统, system由Google完成, 按照Google的预想, 客户只需要定制vendor即可, 不需要厂商再来操心system. 这么一想, system不能使用vendor下的资源也就可以理解了, 因为Google根本不需要使用你一个第三方的东西.

当然, 对于这个问题我没有去仔细研究,  以上只是我的推测, 如有研究过的朋友, 可以分享.