本文介绍 MARS xlog 使用以及使用过程中踩过的坑
xlog 是什么
xlog 是微信开源框架 MARS 的一部分, 处理应用日志
微信的对 xlog 的介绍文档--「微信终端跨平台组件 mars 系列(一) - 高性能日志模块xlog)」
总结出来就是
xlog 方案总结
使用流式方式对单行日志进行压缩,压缩加密后写进作为 log 中间 buffer的 mmap 中
虽然使用流式压缩并没有达到最理想的压缩率,但和 mmap 一起使用能兼顾流畅性 完整性 容错性 的前提下,83.7%的压缩率也是能接受的。使用这个方案,除非 IO 损坏或者磁盘没有可用空间,基本可以保证不会丢失任何一行日志。
一个优秀的日志模块必须做到:
- 不能把用户的隐私信息打印到日志文件里,不能把日志明文打到日志文件里。
- 不能影响程序的性能。最基本的保证是使用了日志不会导致程序卡顿。
- 不能因为程序被系统杀掉,或者发生了 crash,crash 捕捉模块没有捕捉到导致部分时间点没有日志, 要保证程序整个生命周期内都有日志。
- 不能因为部分数据损坏就影响了整个日志文件,应该最小化数据损坏对日志文件的影响。
上面这几点也即安全性 流畅性 完整性 容错性, 它们之间存在着矛盾关系:
- 如果直接写文件会卡顿,但如果使用内存做中间 buffer 又可能丢日志
- 如果不对日志内容进行压缩会导致 IO 卡顿影响性能,但如果压缩,部分损坏可能会影响整个压缩块,而且为了增大压缩率集中压缩又可能导致 CPU 短时间飙高。
mars 的日志模块 xlog 就是在兼顾这四点的前提下做到:高性能高压缩率、不丢失任何一行日志、避免系统卡顿和 CPU 波峰。
xlog 使用
MARS 的 GitHub 上介绍比较详细,
xlog 背景知识
先跑起来一个 Demo 之后, 需要深入了解一下
mmap
mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
正如微信的介绍文章中所说的:
mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换。
mmap几乎和直接写内存一样的性能,而且 mmap 既不会丢日志,回写时机对我们来说又基本可控。
System.loadLibrary()
上文中有关于该方法的源码分析, 总结来说
- 该方法用来加载 'xxx.so' 文件, 一些
native
方法的具体实现 - 该方法会从以下位置加载 so 文件:
/vendor/lib
,/system/lib
,/data/app/com.xxxxx.xxx-1
so 文件
因为 Android 手机 CPU 架构的差异, 可能会有很多版本的 so 文件, 如果你是使用本地编译 xlog 的, 你应该注意对应不同 CPU 架构编译不同的 so 文件
本地编译的 so 文件放在 src/jniLibs
目录下, AS 可以自动编译到 apk 中
xlog 踩坑
我的坑主要是因为 xposed 的原因, 刚开始 Demo 很顺利, 接入到项目中问题就一个个的
couldn't find "libstlport_shared.so
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/io.communet.ichater-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]] couldn't find "libc++_shared.so"
at java.lang.Runtime.loadLibrary(Runtime.java:385)
at java.lang.System.loadLibrary(System.java:993)
at io.communet.ichater.main.util.LogUtils.initXLog(LogUtils.java:38)
at io.communet.ichater.wx.hook.WxHook.dealWx(WxHook.java:163)
at io.communet.ichater.wx.hook.WxHook.access$000(WxHook.java:48)
at io.communet.ichater.wx.hook.WxHook$1.afterHookedMethod(WxHook.java:152)
at de.robv.android.xposed.XposedBridge.handleHookedMethod(XposedBridge.java:374)
at android.content.ContextWrapper.attachBaseContext(<Xposed>)
at android.app.Service.attach(Service.java:702)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:2759)
at android.app.ActivityThread.access$1800(ActivityThread.java:151)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1386)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5254)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:905)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:700)
at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:107)
上文以及提到会在哪里加载 so 文件, 但是由于 xposed 的原因, Classloader 指向的文件为 /data/app/io.communet.ichater-2/base.apk
, 不能找到指定的 so 文件, 所以需要指定绝对路径
解决:
String nativeLibraryDir = getNativeLibraryDir(context);
System.load(nativeLibraryDir + "/libc++_shared.so");
System.load(nativeLibraryDir + "/libmarsxlog.so");
/**
* 获取本地支持库
*/
private static String getNativeLibraryDir(Context context) throws PackageManager.NameNotFoundException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo("io.communet.ichater", 0);
return applicationInfo.nativeLibraryDir;
}
日志存储位置
微信有提到关于日志同步和异步两种写入方式以及日志文件的存储位置
mode : 文件写入模式,分异步和同步,变量定义见 Xlog.java 里 AppednerModeXX, Release版本一定要用 AppednerModeAsync, Debug 版本两个都可以,但是使用 AppednerModeSync 可能会有卡顿。
cacheDir : 缓存目录,当 logDir 不可写时候会写进这个目录,可选项,不选用请给 "", 如若要给,建议给应用的 /data/data/packname/files/log 目录。
logDir : 日志写入目录,请给单独的目录,除了日志文件不要把其他文件放入该目录,不然可能会被日志的自动清理功能清理掉。
实际运行中发现, 当同步写入时, 日志文件开始会被存放在 cacheDir, 一段时间后, 会被放到 logDir, 但是异步模式下, 文件一直放在 cacheDir, 即便调用 appenderFlush
方法, 日志会从 mmap 中写入文件, 但是文件的位置还是在 cacheDir, 当然, 应用有读写 SDCard 的权限
解决:
该问题还未查明原因, 目前的解决方法是不给 cacheDir, 文件会被直接放到 logDir, 但是, 官方说如果不给 cacheDir, 可能出现 SIGBUS, 参见 issue#249
2019/4/17更新: 解决了, 说起来都惭愧, 还有一个参数
cacheDays : 一般情况下填0即可。非0表示会在 _cachedir 目录下存放几天的日志。
将该值设置为 0 即可, 之前以为这个值表示的是缓存日志保存的天数, 设置了 7, 实际上保留缓存日志的天数默认 10 天, 清理逻辑如下
每次启动时会删除过期文件,只保留十天内的日志文件(该值定义在appender.cc中的 kMaxLogAliveTime ),所以给 Xlog 的目录请使用单独目录,防止误删其他文件。目前不会根据文件大小进行清理。如若想自定义清理逻辑请自行更改appender.cc中的 __del_timeout_file 函数。 #Android
couldn't find "xxx.so" is 32-bit instead of 64-bit
注意和上文中的那个 BUG 区分, 这里是因为用 32 位的 so 代替 64 位的 so 导致的
解决:
jniLibs 下面不要放 64 位的, 只放 32 的, 可以兼容
未完待续
还有坑的话继续更新