本文介紹 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 的, 可以兼容
未完待續
還有坑的話繼續更新