应用背景:
本应用日志使用的是xlog组件,在app初始化时初始化xlog组件,传入了缓存日志文件路径、日志文件路径、日志文件名三个关键参数,xlog会根据这三个参数生成缓存日志文件和日志文件。缓存日志文件类型为.mmap3,日志文件类型为.xlog,app可以调用xlog的flush函数将缓存日志文件内容写入日志文件;日志文件名是以当前进程的名字为基础,将进程名中的"."替换为了"_",这样做的目的是后台的日志系统对于文件命名有要求,不允许文件名在除后缀名之外出现".",由此传入的两个日志文件名为:
package_name:service
package_name
之前考虑到为了在app卸载后日志文件依然存在,使用的日志存储路径为手机的外部存储的公有空间,地址为
/storage/emulated/0/xxx/appname/logs/
android存储空间背景:
这里使用了知乎老哥的一个图简单介绍一下背景
内部存储:
/data目录,对用户不可见,即使使用adb依然不可查看,只有root过的手机可以查看。或者是debug版本的app可以在androidstudio上使用Device File Explorer查看。内部存储空间本身就是为了保护应用本身的隐私而设计的,所有设备上的内部存储空间都是始终可用的,在存储应用所依赖的数据时更为可靠;app访问自己的内部存储空间不需要权限,其他应用无法访问,Shared Preferences和SQLite数据库就是存储在内部存储空间;在app卸载时内部存储空间的所有文件都会被删掉,缓存性文件会在设备存储空间不足时被删除,所以文件放在缓存性目录下随时可能被删除。获取内部存储空间的路径为:
context.filesDir(),/data/data/包名/files/ // 持久性文件根目录 路径在不同的手机上可能会不同
context.cacheDir(), /data/data/包名/cache/ // 缓存性文件根目录
外部存储:
/storage 目录,外部存储也分为公有空间和私有空间。
私有空间:
存储应用私有数据,外部存储应用私有目录对应Android/data/包名。此路径文件在android 11前可以直接在手机的文件管理下查看,在android 11中不可见,但可以使用adb查看。和内部存储空间一样也分为持久性目录和缓存性目录,需要注意的是在android 10 开启分区存储后,应用只能访问自身的私有空间,即使获得了存储权限也不能访问其他应用的私有空间。在应用卸载后系统会自动移除这些目录释放空间。获取私有空间的路径为:
getExternalFilesDirs(@NonNull Context context, @Nullable String type) // 持久性文件 /storage/emulated/0/Android/data/包名/files 根据传入的type不同返回路径不同 传入null则返回files路径
getExternalCacheDirs(context) // 缓存性文件 /storage/emulated/0/Android/data/包名/cache
公有空间:
如果应用的数据可供其他应用访问,则使用共享存储空间。具体的访问方法变化如下:
//外部存储公有目录的访问方法
//分区存储开启前
getExternalStorageDirectory() // 分区存储开启之前 获得读写权限后 获取路径使用File直接操作
//android 10 开启分区存储
MediaStore // 访问媒体集合 访问其他应用文件需要权限 READ_EXTERNAL_STORAGE
Storage Access Framework // 存储访问框架 访问文档和其他文件 使用时会出现系统提供的文档列表页面提供给用户操作 关键在于获取文件的Uri
//android 11 应用targetSdkVersion >= 30 强制开启分区存储
File API // android 11重新开启了使用file访问媒体文件的方法 不过还是会重定向到MediaStore 造成性能影响
fopen() // 加入了原生库的访问 MANAGE_EXTERNAL_STORAGE // 针对手机管家、文件管理器等app提供的外部存储管理权限
在分区存储开启之前可以使用getExternalStorageDirectory()获取路径进行操作,而在android 10开启分区存储后,关闭了使用获取路径读取文件的方法,官方推荐的方法为使用MediaStore访问媒体资源,使用Storage Access Framework访问文档和其他文件。android 11更新后又重新加入了使用File访问文件路径的方法,但是会重定向到MediaStore。
问题:
在android 11的手机中主进程可以写入日志文件,但是服务进程不会写入日志文件,包括mmap文件和xlog文件,导致日志选择上传的时候失败。
问题分析:
- 由于android 11开启了强制分区存储导致的文件读写问题?
android 11虽然开启了强制分区存储,但是只是针对targetSdk >= 30的情况,而本应用的targetSdk为28,并未开启强制分区存储;
主进程可以正常写入文件说明文件写入没有问题;
经过试验也可以在服务进程在外部存储公有空间创建文件,无论是使用File API还是通过JNI的fopen方法均可完成;
由此可以判断与分区存储无关。
但是将日志存储路径更换为外部存储应用的私有空间后,两个进程的日志文件都创建成功了。所以真的和分区存储无关吗?
- xlog的mmap打开失败?
为了排除xlog版本老旧的问题,我从gradle引入了新版本的xlog,依然存在上述问题;通过查看xlog的原理文档和xlog的github源码可知,Xlog初始化经过了以下步骤
Xlog.open // 应用初始化Xlog命令 传入多个参数
appenderOpen(level, mode, cacheDir, logDir, nameprefix, 0, pubkey) // Xlog.class 一个JNI方法 JNIEXPORT void JNICALL Java_com_tencent_mars_xlog_Xlog_appenderOpen // Java2C_Xlog.cc 获取传入的参数构造一个XlogConfig对象
appender_open(const XLogConfig& _config) // appender.cc 初始化一个XloggerAppender对象
XloggerAppender::NewInstance(const XLogConfig& _config) // appender.cc 调用构造函数
XloggerAppender::XloggerAppender(const XLogConfig& _config) // appender.cc 调用Open函数
XloggerAppender::Open(const XLogConfig& _config) // appender.cc 调用openMmapFile打开mmap
OpenMmapFile(const char* _filepath, unsigned int _size, boost::iostreams::mapped_file& _mmmap_file) // mmap_util.cc 调用mmap的open函数 _mmmap_file.open(param) // mmap_util.cc 再向下深入就进入了boost iostream库使用mmap写入映射文件的流程
IsMmapFileOpenSucc(const boost::iostreams::mapped_file& _mmmap_file) // mmap_util.cc 判断mmap是否open成功
通过以上流程分析可以发现,问题最大可能是出现在了mmap.open函数中,如果打开失败就是创建mmap文件失败,那么程序自然就不可能将缓存通过mmap写入映射文件。那么为了验证open的结果,需要获取到IsMmapFileOpenSucc的结果,而为了获取这个结果就需要加入打印日志代码将xlog重新打包测试了。为了达成这个目的:
- 在github下载mars源码,自己编译xlog库。
分析mars的源码,可以发现使用的是cmake编译,可以实现很好的跨平台效果,在代码主目录下可以看到编译的脚本文件如下。
将代码下载后执行脚本文件,出现的问题有
(1)找不到NDK_ROOT路径,在电脑的.bash_profile文件中配置;和之前配置的是NDK_HOME路径一样
(2)ifaddrs.h类报错,找不到结构体ifaddrs;查看ifaddrs.h代码里面没有声明ifaddrs结构体,但是查看文件的提交可以发现在之前的版本是有这个结构体的,只是在一次更新中替换成了如下include代码,在电脑的ndk文件中搜索这个类,显示的结果如下。所以理论上编译不应该出现问题,为了解决这个问题我尝试将ndk ifaddrs结构体粘贴到mars ifaddrs.h文件中,编译通过生成so库,但是这里还是留下了疑问。
// mars/comm/jni/ifaddrs.h
#include
//ndk搜索ifaddrs.h 里面包含了提示缺失的结构体
struct ifaddrs {
/** Pointer to the next element in the linked list. */
struct ifaddrs* ifa_next;
/** Interface name. */
char* ifa_name;
/** Interface flags (like `SIOCGIFFLAGS`). */
unsigned int ifa_flags;
/** Interface address. */
struct sockaddr* ifa_addr;
/** Interface netmask. */
struct sockaddr* ifa_netmask;
union {
/** Interface broadcast address (if IFF_BROADCAST is set). */
struct sockaddr* ifu_broadaddr;
/** Interface destination address (if IFF_POINTOPOINT is set). */
struct sockaddr* ifu_dstaddr;
} ifa_ifu;
/** Unused. */
void* ifa_data;
};
/** Synonym for `ifa_ifu.ifu_broadaddr` in `struct ifaddrs`. */
#define ifa_broadaddr ifa_ifu.ifu_broadaddr
/** Synonym for `ifa_ifu.ifu_dstaddr` in `struct ifaddrs`. */
#define ifa_dstaddr ifa_ifu.ifu_dstaddr
- 获得xlog的java层代码
为了获得xlog的java层jar包,先找到了引入gradle加载的xlog arr库,虽然不能在androidstudio的External libraries中直接查看,但是可以获取到包的路径,复制后更改后缀名为zip打开,取出可以看到的文件目录如下,classes.jar就是我们需要的java层代码,放入应用的lib中即可。
一开始引入了一个mars-xlog-1.x-source.jar文件,一直引入失败。。
- 插入日志打印mmap open结果是否成功
为了打印日志,在Java_com_tencent_mars_xlog_Xlog_appenderOpen函数中加入了打印日志方法测试,分别尝试了mars自带的两种方法和jni打印android日志的方法如下,全部失败。
// mars里使用的打印日志方法 本质也是调用 __android_log_print 失败
xerror2(TSF"hello from JNI");
LOGD("testxlog", "-------user define:%s--------", "hello from JNI"); // 有个开关恒为false改为了true
// 自己加入的 __android_log_print 失败
printf
LOGDD("LOG from JNI");
可以看到mars的日志是可以正常输出的,但是上面的四种方法却失败了,所以会不会是日志打印位置的原因?此时日志上未初始化?又留下了一个疑问
最后尝试将日志放入xlog的写入JNI函数JNICALL Java_com_tencent_mars_xlog_Xlog_logWrite2中打印,日志打印成功。
LOGDD("LOG from JNI");
LOGDD("is use mmap %d", XloggerAppender::is_use_mmap);
最后在appender.cc中加入了全局静态变量保存mmap open结果,在write函数中获取结果打印。可以看到有两个进程一个进程open成功一个进程open失败,那就一定是mmap在android 11的适配问题了吗?
问题原因:
最终在导师的帮忙下定位到了问题,拉取了全部的日志,发现在打印mmap日志前有一个奇怪的系统日志如下(由于我之前过滤日志没注意过,也没有把全部日志输出到一个文件里仔细查看),MediaProvider报错如下。
查看android 11 MediaProvider源码,发现了如下流程
MediaProvider getAbsoluteSanitizedPath // 处理后的path和原path不相同 报错
FileUtils getAbsoluteSanitizedPath sanitizePath // 以 / 作为分隔符 将路径分成一块块的数组 数组项调用下面处理
sanitizeDisplayName // 以点开头的转为下划线返回名字(这里考虑的是会不会点开头的是隐藏文件?)继续调用 buildValidFatFilename
buildValidFatFilename // 会修改文件名将所有的无效字符替换为 "_" 使得对FAT文件系统有效 遍历每个字符,其中特殊字符包括 ":" 都被替换为了 "_" 特殊字符如下
最终返回的路径与原路径不匹配,自然就会文将创建失败。为了验证这个问题,打开android 11 的文件管理器,尝试在手机外部存储公共空间修改文件名加入":",提示特殊字符无效。
查看android 10的代码,未发现如上流程,也说明了为什么这个bug只和android 11有关。
总结:
问题的本质是我们使用进程名作为文件名,而进程名中带有":",导致文件创建失败。并且不只是":",许多其他的字符如上图都属于公共空间文件名的不合法字符,但是在应用的私有空间是不存在这个问题的,这可能是由于在分区存储后公有空间和私有空间文件访问走的是两套机制,访问公有空间要通过MediaStore,但是访问私有空间并不需要,自然就没有了这个文件名称的校验。
对于这个问题的解决方案一开始就很清晰,就是将日志路径设置到应用的私有空间,但是在为了找寻问题出现的原因,确实费了一番力气。
还存在的问题:
mars源码中为什么不能在Java_com_tencent_mars_xlog_Xlog_appenderOpen中打印日志?
ifaddrs类为什么会引入失败?
mmap的原理?
这些可能只有等以后自己懂得更多才能研究了。。
链接如下:
mars代码
https://github.com/Tencent/mars
android mediaprovider代码
https://android.googlesource.com/platform/packages/providers/MediaProvider/