- <app_basedir>:apk在/data/app下的安装路径,示例/data/app/~~07kR77IGzQfrbWwdbtkBJQ==/com.kos.launcher-6kugzuV2AqFjUibzOJm1eQ==
- <data_CE_dir>:表示apk在/data/data内部存储路径,示例/data/data/com.kos.launcher
工作中遇到这么个问题,com.kos.launcher是自写app,主要编程语言是C/C++,这些C/C++代码编译成数个*.so,然后打包进apk。在运行着android-12的firefly-rk3588s-pc上安装这apk,安装成功,之后运行也成功。但是,重启设备后,会随机出现这些*.so被置0,但文件长度保持不变。举个例子,图1显示了libhidapi.so正常时数据,图2是出现错误后这个so被清零后的数据。出问题后,此文件中字节都变成了0,但前、后两libhidapi.so文件长度保持不变。


把这apk安装到其它android设备上,像android7.1的aio-3399j,没有这问题。安装到同样是android12的华为手机,也没这问题。
但是,在这出问题的firefly-rk3588s-pc(android-12),安装带有*.so的其它产商apk,像bilibili的iBiliPlayer-bili.apk,好像也没这问题。
以下是更多出问题时现象。
- 一旦出了问题,在android启动创段,PackageManagerService开始执行scanDirLI安装/data/app下的app时,com.kos.launcher相关*.so已经被破坏了。
- 出问题后,如果把正确的so替换掉被破坏的so,再运行apk,能成功。
- 不论是32位的armeabi-v7a,还是64位的arm64-v8a,都有这问题。
- 用目前最新NDK-r25编译*.so,也有这问题。
- 不论是按reset键的热重启,还是拔、插电源线的冷重启,都有这问题。。
- 这问题好像只在安装后的第一次重启时出现。也就是说,要是第一次重启后,没出问题,那后面就不出了。
- 出问题的*.so存放在<app_basedir>/lib/arm64下,但出问题时,上级目录下的<app_basedir>/base.apk没有被破坏。
- [这点不知是出题时连带现象,还是android就有这特点]往<data_CE_dir>写入文件时,重启设备,这文件长度会随机变成0字节。举个例子,创建目录<data_CE_dir>/leagorlib,并把<app_basedir>/lib/arm64下的libhidapi.so复制这里。设备重启后,libhidapi.so变成了0字节。
这破坏发生在启动阶段的PackageManagerService之前,到现在没找到原因。这是目前猜测:1)自写的apk在处理*.so方面有问题,是编译有问题,还是android studio相关配置有问题,不知道。2)相比华为手机android-12,firefly-rk3588s-pc可能要求apk中的*.so较严格,发现不满足要求,就清零了。
一、临时解决办法
没找到原因,但问题总得解决,只能找个临时办法。要修改android源码,思路是基于出问题时一个特点:basek.apk没有被破坏。此次android启动时,一旦发现出问题了,就解压base.apk,提取出正确的so,用它们覆盖掉被破坏的so。
<aosp>/frameworks/native/cmds/installd/InstalldNativeService.cpp ------ binder::Status InstalldNativeService::createAppData(const std::optional<std::string>& uuid, const std::string& packageName, int32_t userId, int32_t flags, int32_t appId, const std::string& seInfo, int32_t targetSdkVersion, int64_t* _aidl_return) { ... if (flags & FLAG_STORAGE_CE) { ... if (packageName == "com.kos.launcher") { correct_so_zero_if_need("/data/app", packageName, path); } } ... }
在android启动阶段,PackageManagerService执行scanDirLI安装/data/app下的app时,会调用mInstaller.createAppData创建内部存储路径:/data/data/com.kos.launcher。对象mInstance的类型就是InstalldNativeService。当发现要安装的是“com.kos.launcher”时,调用correct_so_zero_if_need,以下是这函数逻辑。注:假设安装的是64位apk。
- 根据com.kos.launcher找到<app_basedir>,即:/data/app/~~07kR77IGzQfrbWwdbtkBJQ==/com.kos.launcher-6kugzuV2AqFjUibzOJm1eQ==
- 判断<app_basedir>/lib/arm64下的so是否有被破坏。目录下可能有多个so,只检查libmain.so。如果没有被破坏,结束此次操作,函数退出。否则认为须要执行修正了,执行步骤3。
- 在/data/data/com.kos.launcher下新建leagorlib目录,解压<app_basedir>/base.apk,结果存放在该目录。
- 把leagorlib/lib/arm64-v8a下的文件复制到<app_basedir>/lib/arm64,即用正确的so覆盖被破坏的。
- 删除leagorlib目录。
如果没发生破坏,correct_so_zero_if_need不会有“写”操作,像新建leagorlib等。有发生破坏,并修正后,除lib/arm64下的so被覆盖外,不会给文件系统留下其它东西。
为什么在“flags & FLAG_STORAGE_CE”时调用correct_so_zero_if_need?——在这if外也可以调用这函数。这么做是考虑到1)FLAG_STORAGE_CE对应/data/data下目录,correct_so_zero_if_need要用它存放base.apk的解压结果。2)com.kos.launcher是个用户级app,在创建AppData时,肯定会设置FLAG_STORAGE_CE标记。
correct_so_zero_if_need首先要根据com.kos.launcher计算<app_base_dir>,InstalldNativeService还有个成员函数linkNativeLibraryDirectory,它的参数nativeLibPath32指示了后跟“lib/arm”的<app_base_dir>,用这参数可以少计算<app_base_dir>,为什么不在那里调用correct_so_zero_if_need?——PackageManagerService只有安装32位的apk时才会调用linkNativeLibraryDirectory。
安装apk后,会在<app_basedir>/lib/arm64下存储着*.so,没重启前,这些so是正确的。为什么不把正确的so先存储到其它目录,像/data/data/com.kos.launcher,这样到时可免去解压apk,直接把这些so覆盖过去?——对/data/data/com.kos.launcher,见上面的第8点,它本身就可能清零,不稳定。对/sdcard中外部存储,PackageManagerService执行scanDirLI安装/data/app下的app时,即使是root,这目录没法访问,它指向的/storage/emulated/0,同样不行。
二、apk安装中的生成目录/文件
网上有不少关于描述APK安装的文章,像“Android 10.0 PackageManagerService(四)APK安装流程”。这里只大致说下APK安装中怎么生成目录/文件。一个apk可归为三种目录:安装目录、内部存储和外部存储。
- app程序目录。就是上面说的<app_basedir>:示例/data/app/~~07kR77IGzQfrbWwdbtkBJQ==/com.kos.launcher-6kugzuV2AqFjUibzOJm1eQ==。当中存储着apk文件,以及从apk提取出的*.so。
- 数据之data内部存储(AppData)。它包括两个目录:/data/data/com.kos.launcher、/data/user_de/0/com.kos.launcher,分别称为CE、DE。app调用ContextWrapper.getFilesDir得到的就是当中的/data/data/com.kos.launcher/files,但files子目录要到运行时才创建。注:/data/user_de/0/com.kos.launcher下没有files子目录。
- 数据之外部存储。/sdcard/Android/data/com.kos.launcher/files,app调用ContextWrapper.getExternalFilesDir可得到这目录,也就app存储私有数据的地方。目录要到运行时才创建。
综上所述,安装过程会创建app安装目录、除files子目录外的内部存储。
2.1 PackageInstaller解压launcher-release.apk,生成目录/data/app/vmdl1068530868.tmp
生成的vmdl1068530868.tmp是个目录,除没子目录oat,它和最后<app_basedir>目录内容是一样的。
:/data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA== # ls -l total 48580 -rw-r--r-- 1 system system 49736002 2022-09-03 10:49 base.apk drwxr-xr-x 3 system system 4096 2022-09-03 10:49 lib drwxrwx--x 3 system install 4096 2022-09-03 10:49 oat
以上是一个<app_basedir>示例,vmdl1068530868.tmp的区别就是没有oat子目录。base.apk是launcher-release.apk的原样复制,lib下存放着该apk的私有*.so。
PackageInstallerSession调用PackageManagerService.installStage,installStage有两个版本的,安装launcher-release.apk这种单apk用的是一个参数的版本。
<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java ------ private void installNonStaged() throws PackageManagerException { ... if (isMultiPackage()) { ... mPm.installStage(installingSession, installingChildSessions); } else { // launcher-release.apk这种单apk进的是这里一个参数版本的installStage mPm.installStage(installingSession); } }
2.2 /data/app/vmdl1068530868.tmp重命名为/data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA==
<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java ------ class FileInstallArgs extends InstallArgs { @Override boolean doRename(int status, ParsedPackage parsedPackage) { ... try { makeDirRecursive(afterCodeFile.getParentFile(), 0775); if (onIncremental) { // Just link files here. The stage dir will be removed when the installation // session is completed. mIncrementalManager.linkCodePath(beforeCodeFile, afterCodeFile); } else { // 进这个入口 Os.rename(beforeCodeFile.getAbsolutePath(), afterCodeFile.getAbsolutePath()); } } ... } } }
- beforeCodeFile.getAbsolutePath(): /data/app/vmdl1068530868.tmp
- afterCodeFile.getAbsolutePath(): /data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA==
onIncremental值是false,经过Os.rename后,beforeCodeFile被重命名为afterCodeFile,beforeCodeFile也就不存在了。
2.3 创建目录:/data/data/com.kos.launcher、/data/user_de/0/com.kos.launcher
这两个目录合称AppData,但一个apk的AppData可能是只有当中一个,可能两个都有,可能一个都没有。
<aosp>/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java ------ private void prepareAppDataAfterInstallLIF(AndroidPackage pkg) { ... // 对常见的单用户android,以下的for循环只有一次 for (UserInfo user : mUserManager.getUsers(false /*excludeDying*/)) { // 任务1:计算出flags final int flags; if (StorageManager.isUserKeyUnlocked(user.id) && smInternal.isCeStoragePrepared(user.id)) { // 对用户级apk来说,往往进入这里,即AppData同时有DE、CE。 flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE; } else if (umInternal.isUserRunning(user.id)) { flags = StorageManager.FLAG_STORAGE_DE; } else { continue; } ... // 任务2:根据flags创建AppData prepareAppData(batch, pkg, user.id, flags); ... } executeBatchLI(batch); }
prepareAppDataAfterInstallLIF负责创建该apk的AppData。它首先计算该apkAppData要包括哪些目录。或同时有DE、CE,或只有DE,或两个都没有。
FLAG_STORAGE_CE指的是/data/user/0/,即/data/data,该目录只能在解锁屏幕后才能访问。而FLAG_STORAGE_DE指的是/data/user_de/0,该目录在有屏幕锁的时候也能访问,参考Android官网关于DirectBoot的介绍。
PackageManagerService.prepareAppData根据flags创建AppData,它只是简单调用prepareAppDataLeaf,后者则调用mInstaller.createAppData、mInstaller.linkNativeLibraryDirectory创建出目录或文件。对象mInstance是个binder代理对象,对应binder本地对象是用C++写的InstalldNativeService。
以下是关于createAppData、linkNativeLibraryDirectory几点注释
- flags包含FLAG_STORAGE_CE时,InstalldNativeService::createAppData会在/data/data下创建com.kos.launcher目录,以及底下的cache、code_cache目录。一旦不包含FLAG_STORAGE_CE,连com.kos.launcher这个目录都不会创建。
- flags包含FLAG_STORAGE_DE时,InstalldNativeService::createAppData会创建/data/user_de/0下创建com.kos.launcher目录,以及底下的cache、code_cache。一旦不包含FLAG_STORAGE_DE,连com.kos.launcher这个目录都不会创建。
- linkNativeLibraryDirectory()只在安装32位apk时调用,它的作用是在/data/data/com.kos.launcher下创建个名称是lib的符号文件,指向/data/app/~~tCTRdRJndjsZ-ALFyKDC8g==/com.kos.launcher-ghfeDAyY71DQ5fv-tLAiUA==/lib/arm。
64位时的/data/data/com.kos.launcher目录
drwxrws--x 2 u0_a83 u0_a83_cache 4096 2022-09-08 02:50 cache drwxrws--x 2 u0_a83 u0_a83_cache 4096 2022-09-08 02:50 code_cache drwxrwx--x 2 u0_a83 u0_a83 4096 2022-09-08 02:50 files drwxrwx--x 2 u0_a83 u0_a83 4096 2022-09-08 02:50 shared_prefs
32位时的/data/data/com.kos.launcher目录
drwxrws--x 2 u0_a84 u0_a84_cache 4096 2022-09-08 07:11 cache drwxrws--x 2 u0_a84 u0_a84_cache 4096 2022-09-08 07:11 code_cache drwxrwx--x 2 u0_a84 u0_a84 4096 2022-09-08 07:11 files lrwxrwxrwx 1 root root 86 2022-09-08 07:11 lib -> /data/app/~~XXd3de4uF9KA3ZzpNqmG4Q==/com.kos.launcher-ieHwdJ4w1NuS5qOX_fft-A==/lib/arm drwxrwx--x 2 u0_a84 u0_a84 4096 2022-09-08 07:11 shared_prefs
在android启动阶段,PackageManagerService.scanDirLI会调用这里的PackageManagerService.prepareAppDataLeaf。也就是说,如果上次运行过程中/data/data/com.kos.launcher、/data/user_de/0/com.kos.launcher有被损坏,那这次启动会执行prepareAppDataLeaf进行修复。