應(yīng)用背景:
本應(yīng)用日志使用的是xlog組件,在app初始化時初始化xlog組件栈妆,傳入了緩存日志文件路徑初肉、日志文件路徑、日志文件名三個關(guān)鍵參數(shù)荆责,xlog會根據(jù)這三個參數(shù)生成緩存日志文件和日志文件滥比。緩存日志文件類型為.mmap3,日志文件類型為.xlog做院,app可以調(diào)用xlog的flush函數(shù)將緩存日志文件內(nèi)容寫入日志文件盲泛;日志文件名是以當前進程的名字為基礎(chǔ),將進程名中的"."替換為了"_"键耕,這樣做的目的是后臺的日志系統(tǒng)對于文件命名有要求寺滚,不允許文件名在除后綴名之外出現(xiàn)".",由此傳入的兩個日志文件名為:
package_name:service
package_name
之前考慮到為了在app卸載后日志文件依然存在屈雄,使用的日志存儲路徑為手機的外部存儲的公有空間村视,地址為
/storage/emulated/0/xxx/appname/logs/
android存儲空間背景:
這里使用了知乎老哥的一個圖簡單介紹一下背景
內(nèi)部存儲:
/data目錄,對用戶不可見酒奶,即使使用adb依然不可查看蚁孔,只有root過的手機可以查看⊥锖浚或者是debug版本的app可以在androidstudio上使用Device File Explorer查看杠氢。內(nèi)部存儲空間本身就是為了保護應(yīng)用本身的隱私而設(shè)計的,所有設(shè)備上的內(nèi)部存儲空間都是始終可用的瘸彤,在存儲應(yīng)用所依賴的數(shù)據(jù)時更為可靠修然;app訪問自己的內(nèi)部存儲空間不需要權(quán)限笛钝,其他應(yīng)用無法訪問质况,Shared Preferences和SQLite數(shù)據(jù)庫就是存儲在內(nèi)部存儲空間;在app卸載時內(nèi)部存儲空間的所有文件都會被刪掉玻靡,緩存性文件會在設(shè)備存儲空間不足時被刪除结榄,所以文件放在緩存性目錄下隨時可能被刪除。獲取內(nèi)部存儲空間的路徑為:
context.filesDir()囤捻,/data/data/包名/files/ // 持久性文件根目錄 路徑在不同的手機上可能會不同
context.cacheDir(), /data/data/包名/cache/ // 緩存性文件根目錄
外部存儲:
/storage 目錄臼朗,外部存儲也分為公有空間和私有空間。
私有空間:
存儲應(yīng)用私有數(shù)據(jù),外部存儲應(yīng)用私有目錄對應(yīng)Android/data/包名视哑。此路徑文件在android 11前可以直接在手機的文件管理下查看绣否,在android 11中不可見,但可以使用adb查看挡毅。和內(nèi)部存儲空間一樣也分為持久性目錄和緩存性目錄蒜撮,需要注意的是在android 10 開啟分區(qū)存儲后,應(yīng)用只能訪問自身的私有空間跪呈,即使獲得了存儲權(quán)限也不能訪問其他應(yīng)用的私有空間段磨。在應(yīng)用卸載后系統(tǒng)會自動移除這些目錄釋放空間。獲取私有空間的路徑為:
getExternalFilesDirs(@NonNull Context context, @Nullable String type) // 持久性文件 /storage/emulated/0/Android/data/包名/files 根據(jù)傳入的type不同返回路徑不同 傳入null則返回files路徑
getExternalCacheDirs(context) // 緩存性文件 /storage/emulated/0/Android/data/包名/cache
公有空間:
如果應(yīng)用的數(shù)據(jù)可供其他應(yīng)用訪問耗绿,則使用共享存儲空間苹支。具體的訪問方法變化如下:
//外部存儲公有目錄的訪問方法
//分區(qū)存儲開啟前
getExternalStorageDirectory() // 分區(qū)存儲開啟之前 獲得讀寫權(quán)限后 獲取路徑使用File直接操作
//android 10 開啟分區(qū)存儲
MediaStore // 訪問媒體集合 訪問其他應(yīng)用文件需要權(quán)限 READ_EXTERNAL_STORAGE
Storage Access Framework // 存儲訪問框架 訪問文檔和其他文件 使用時會出現(xiàn)系統(tǒng)提供的文檔列表頁面提供給用戶操作 關(guān)鍵在于獲取文件的Uri
//android 11 應(yīng)用targetSdkVersion >= 30 強制開啟分區(qū)存儲
File API // android 11重新開啟了使用file訪問媒體文件的方法 不過還是會重定向到MediaStore 造成性能影響
fopen() // 加入了原生庫的訪問 MANAGE_EXTERNAL_STORAGE // 針對手機管家、文件管理器等app提供的外部存儲管理權(quán)限
在分區(qū)存儲開啟之前可以使用getExternalStorageDirectory()獲取路徑進行操作误阻,而在android 10開啟分區(qū)存儲后债蜜,關(guān)閉了使用獲取路徑讀取文件的方法,官方推薦的方法為使用MediaStore訪問媒體資源究反,使用Storage Access Framework訪問文檔和其他文件策幼。android 11更新后又重新加入了使用File訪問文件路徑的方法,但是會重定向到MediaStore奴紧。
問題:
在android 11的手機中主進程可以寫入日志文件特姐,但是服務(wù)進程不會寫入日志文件,包括mmap文件和xlog文件黍氮,導致日志選擇上傳的時候失敗唐含。
問題分析:
- 由于android 11開啟了強制分區(qū)存儲導致的文件讀寫問題?
android 11雖然開啟了強制分區(qū)存儲沫浆,但是只是針對targetSdk >= 30的情況捷枯,而本應(yīng)用的targetSdk為28,并未開啟強制分區(qū)存儲专执;
主進程可以正常寫入文件說明文件寫入沒有問題淮捆;
經(jīng)過試驗也可以在服務(wù)進程在外部存儲公有空間創(chuàng)建文件,無論是使用File API還是通過JNI的fopen方法均可完成本股;
由此可以判斷與分區(qū)存儲無關(guān)攀痊。
但是將日志存儲路徑更換為外部存儲應(yīng)用的私有空間后,兩個進程的日志文件都創(chuàng)建成功了拄显。所以真的和分區(qū)存儲無關(guān)嗎苟径?
- xlog的mmap打開失敗躬审?
為了排除xlog版本老舊的問題棘街,我從gradle引入了新版本的xlog蟆盐,依然存在上述問題;通過查看xlog的原理文檔和xlog的github源碼可知遭殉,Xlog初始化經(jīng)過了以下步驟
Xlog.open // 應(yīng)用初始化Xlog命令 傳入多個參數(shù)
appenderOpen(level, mode, cacheDir, logDir, nameprefix, 0, pubkey) // Xlog.class 一個JNI方法 JNIEXPORT void JNICALL Java_com_tencent_mars_xlog_Xlog_appenderOpen // Java2C_Xlog.cc 獲取傳入的參數(shù)構(gòu)造一個XlogConfig對象
appender_open(const XLogConfig& _config) // appender.cc 初始化一個XloggerAppender對象
XloggerAppender::NewInstance(const XLogConfig& _config) // appender.cc 調(diào)用構(gòu)造函數(shù)
XloggerAppender::XloggerAppender(const XLogConfig& _config) // appender.cc 調(diào)用Open函數(shù)
XloggerAppender::Open(const XLogConfig& _config) // appender.cc 調(diào)用openMmapFile打開mmap
OpenMmapFile(const char* _filepath, unsigned int _size, boost::iostreams::mapped_file& _mmmap_file) // mmap_util.cc 調(diào)用mmap的open函數(shù) _mmmap_file.open(param) // mmap_util.cc 再向下深入就進入了boost iostream庫使用mmap寫入映射文件的流程
IsMmapFileOpenSucc(const boost::iostreams::mapped_file& _mmmap_file) // mmap_util.cc 判斷mmap是否open成功
通過以上流程分析可以發(fā)現(xiàn)石挂,問題最大可能是出現(xiàn)在了mmap.open函數(shù)中,如果打開失敗就是創(chuàng)建mmap文件失敗险污,那么程序自然就不可能將緩存通過mmap寫入映射文件誊稚。那么為了驗證open的結(jié)果,需要獲取到IsMmapFileOpenSucc的結(jié)果罗心,而為了獲取這個結(jié)果就需要加入打印日志代碼將xlog重新打包測試了里伯。為了達成這個目的:
- 在github下載mars源碼,自己編譯xlog庫渤闷。
分析mars的源碼疾瓮,可以發(fā)現(xiàn)使用的是cmake編譯,可以實現(xiàn)很好的跨平臺效果飒箭,在代碼主目錄下可以看到編譯的腳本文件如下狼电。
將代碼下載后執(zhí)行腳本文件,出現(xiàn)的問題有
(1)找不到NDK_ROOT路徑弦蹂,在電腦的.bash_profile文件中配置肩碟;和之前配置的是NDK_HOME路徑一樣
(2)ifaddrs.h類報錯,找不到結(jié)構(gòu)體ifaddrs凸椿;查看ifaddrs.h代碼里面沒有聲明ifaddrs結(jié)構(gòu)體削祈,但是查看文件的提交可以發(fā)現(xiàn)在之前的版本是有這個結(jié)構(gòu)體的,只是在一次更新中替換成了如下include代碼脑漫,在電腦的ndk文件中搜索這個類髓抑,顯示的結(jié)果如下。所以理論上編譯不應(yīng)該出現(xiàn)問題优幸,為了解決這個問題我嘗試將ndk ifaddrs結(jié)構(gòu)體粘貼到mars ifaddrs.h文件中吨拍,編譯通過生成so庫,但是這里還是留下了疑問网杆。
// mars/comm/jni/ifaddrs.h
#include <ifaddrs.h>
//ndk搜索ifaddrs.h 里面包含了提示缺失的結(jié)構(gòu)體
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層代碼刹碾,放入應(yīng)用的lib中即可燥撞。
一開始引入了一個mars-xlog-1.x-source.jar文件座柱,一直引入失敗迷帜。。
- 插入日志打印mmap open結(jié)果是否成功
為了打印日志色洞,在Java_com_tencent_mars_xlog_Xlog_appenderOpen函數(shù)中加入了打印日志方法測試戏锹,分別嘗試了mars自帶的兩種方法和jni打印android日志的方法如下,全部失敗火诸。
// mars里使用的打印日志方法 本質(zhì)也是調(diào)用 __android_log_print 失敗
xerror2(TSF"hello from JNI");
LOGD("testxlog", "-------user define:%s--------", "hello from JNI"); // 有個開關(guān)恒為false改為了true
// 自己加入的 __android_log_print 失敗
printf
LOGDD("LOG from JNI");
可以看到mars的日志是可以正常輸出的锦针,但是上面的四種方法卻失敗了,所以會不會是日志打印位置的原因置蜀?此時日志上未初始化奈搜?又留下了一個疑問
最后嘗試將日志放入xlog的寫入JNI函數(shù)JNICALL Java_com_tencent_mars_xlog_Xlog_logWrite2中打印,日志打印成功盯荤。
LOGDD("LOG from JNI");
LOGDD("is use mmap %d", XloggerAppender::is_use_mmap);
最后在appender.cc中加入了全局靜態(tài)變量保存mmap open結(jié)果馋吗,在write函數(shù)中獲取結(jié)果打印∏锍樱可以看到有兩個進程一個進程open成功一個進程open失敗宏粤,那就一定是mmap在android 11的適配問題了嗎?
問題原因:
最終在導師的幫忙下定位到了問題灼卢,拉取了全部的日志绍哎,發(fā)現(xiàn)在打印mmap日志前有一個奇怪的系統(tǒng)日志如下(由于我之前過濾日志沒注意過,也沒有把全部日志輸出到一個文件里仔細查看)鞋真,MediaProvider報錯如下崇堰。
查看android 11 MediaProvider源碼,發(fā)現(xiàn)了如下流程
MediaProvider getAbsoluteSanitizedPath // 處理后的path和原path不相同 報錯
FileUtils getAbsoluteSanitizedPath sanitizePath // 以 / 作為分隔符 將路徑分成一塊塊的數(shù)組 數(shù)組項調(diào)用下面處理
sanitizeDisplayName // 以點開頭的轉(zhuǎn)為下劃線返回名字(這里考慮的是會不會點開頭的是隱藏文件涩咖?)繼續(xù)調(diào)用 buildValidFatFilename
buildValidFatFilename // 會修改文件名將所有的無效字符替換為 "_" 使得對FAT文件系統(tǒng)有效 遍歷每個字符赶袄,其中特殊字符包括 ":" 都被替換為了 "_" 特殊字符如下
最終返回的路徑與原路徑不匹配,自然就會文將創(chuàng)建失敗。為了驗證這個問題,打開android 11 的文件管理器汗洒,嘗試在手機外部存儲公共空間修改文件名加入":"商佑,提示特殊字符無效。
查看android 10的代碼滤祖,未發(fā)現(xiàn)如上流程,也說明了為什么這個bug只和android 11有關(guān)。
總結(jié):
問題的本質(zhì)是我們使用進程名作為文件名溉跃,而進程名中帶有":",導致文件創(chuàng)建失敗告抄。并且不只是":"撰茎,許多其他的字符如上圖都屬于公共空間文件名的不合法字符,但是在應(yīng)用的私有空間是不存在這個問題的打洼,這可能是由于在分區(qū)存儲后公有空間和私有空間文件訪問走的是兩套機制龄糊,訪問公有空間要通過MediaStore逆粹,但是訪問私有空間并不需要,自然就沒有了這個文件名稱的校驗炫惩。
對于這個問題的解決方案一開始就很清晰僻弹,就是將日志路徑設(shè)置到應(yīng)用的私有空間,但是在為了找尋問題出現(xiàn)的原因他嚷,確實費了一番力氣蹋绽。
還存在的問題:
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/