
一、背景
隨著公司應(yīng)用的逐漸增多,需要集中收集公司部分應(yīng)用線上運(yùn)行的一些崩潰數(shù)據(jù)和日志來(lái)進(jìn)行分析處理,在此實(shí)踐過(guò)程中了解到系統(tǒng)data/system/dropbox目錄會(huì)生成所有應(yīng)用的相關(guān)日志文件。
這個(gè)目錄是由Android系統(tǒng)服務(wù)之一DropBoxManagerService來(lái)管理,所以由此詳細(xì)閱讀了DropBoxManagerService相關(guān)的源碼,以下簡(jiǎn)稱DBMS。
DBMS可能是Android系統(tǒng)服務(wù)源碼較少的一個(gè),所以閱讀起來(lái)相對(duì)比較簡(jiǎn)單,閱讀之后發(fā)現(xiàn),其實(shí)這就是一個(gè)簡(jiǎn)易的日志文件管理服務(wù)。
我們?cè)趯?duì)應(yīng)用本地的部分日志文件進(jìn)行記錄和管理的時(shí)候,恰巧可以借鑒DBMS源碼對(duì)于文件管理的設(shè)計(jì)方案。
假設(shè)不讀源碼,如果我們自己設(shè)計(jì)日志文件管理系統(tǒng),應(yīng)該需要考慮哪些?
除了最基礎(chǔ)的獲取各類日志文件的方案,我們針對(duì)文件管理可以提出幾個(gè)需要考慮的點(diǎn):
- 存取日志采用何種策略
- 設(shè)計(jì)哪些防呆策略
- 是否需要對(duì)外提供接口,提供哪些接口
- 如何保證性能
- 多進(jìn)程的問(wèn)題如何解決
- 文件丟失該如何處理
- 文件變化如何通知使用方
我們帶著以上問(wèn)題來(lái)對(duì)DBMS進(jìn)行一個(gè)了解
二、DropBoxManagerService簡(jiǎn)介
DropBoxManagerService是Android系統(tǒng)的服務(wù)之一,采用C/S結(jié)構(gòu):
- Client端:DropboxManager,用于對(duì)應(yīng)用層提供接口。
- Server端:DropBoxManagerService,管理系統(tǒng)目錄(data/system/dropbox)的系統(tǒng)服務(wù)。
- 系統(tǒng)Setting數(shù)據(jù)庫(kù):負(fù)責(zé)管理DBMS的一些配置信息。
整體架構(gòu)關(guān)系如下圖所示:

2.1 DropBox目錄簡(jiǎn)介
這個(gè)目錄的目錄結(jié)構(gòu)如下圖所示:

里面存放的都是系統(tǒng)的一些日志文件,針對(duì)不同類型的文件,文件名稱和后綴也有所不同。
2.1.1 文件格式
- tag:代表日志類型,常見(jiàn)的tag:data_App_anr,system_app_crash,data_app_nativecrash,其中data_app表示普通應(yīng)用,system_app表示系統(tǒng)應(yīng)用
- timeStampMillis:日志的時(shí)間戳,一般情況下等于崩潰的時(shí)間,有些情況下系統(tǒng)會(huì)做一些調(diào)整
- extentions:后綴名,常見(jiàn)的文件后綴名:.txt,.lost,.txt.gz,.tmp,一般的日志文件都是.txt或者.txt.gz,文件被刪除后的記錄會(huì)以.lost命名
這種文件命名方式優(yōu)點(diǎn)是可以一眼看出這是什么類型的文件。
2.1.2 常見(jiàn)的文件
- JE文件:
[email protected] - NE文件:
[email protected] - ANR文件:
[email protected]
還包括一些系統(tǒng)其它的錯(cuò)誤日志,內(nèi)存,重啟相關(guān)的等等。
2.2 提供的接口
2.2.1 添加文件
addData/addFile/addEntry
2.2.2 獲取文件
getNextEntry,根據(jù)tag和時(shí)間戳來(lái)獲取想要的文件。
2.2.3 dump目錄信息
獲取DropBox目錄的一些信息:文件個(gè)數(shù),文件列表,文件詳細(xì)信息等,可以通過(guò)命令行操作(dumpsys dropbox)。
$ dumpsys dropbox
Drop box contents: 131 entries
Max entries: 1000
// 以下省略......
2.2.4 其它CMD命令
提供其他一些CMD操作的命令,如set-rate-limit,add-low-priority等等。
2.3 目錄管控配置
2.3.1 默認(rèn)基礎(chǔ)配置及文件清除策略
這些配置存在系統(tǒng)的setting數(shù)據(jù)庫(kù)里面,可以通過(guò)settings.global來(lái)訪問(wèn)配置。
文件存儲(chǔ)的配置主要包括以下幾個(gè)維度:
- 文件存活時(shí)長(zhǎng)(默認(rèn)3天);
- 最大存儲(chǔ)文件數(shù)量(默認(rèn)1000個(gè));
- 低內(nèi)存情況下最大文件數(shù)量(默認(rèn)300個(gè));
- DropBox目錄所能使用的空間(默認(rèn)10MB);
- DropBox目錄最多占可用存儲(chǔ)(可用存儲(chǔ)=系統(tǒng)可用存儲(chǔ)-系統(tǒng)總存儲(chǔ)*預(yù)留比例)的比例(10%);
- DropBox使用需要預(yù)留的存儲(chǔ)占總存儲(chǔ)的比例(10%);
- 清除空間時(shí)掃描磁盤空間的時(shí)間間隔;
- 需要壓縮的最小文件大小。
根據(jù)以上配置,我們可以知道該目錄下的日志文件清除策略,觸發(fā)配置上限后會(huì)及時(shí)的刪除文件。
在以下三種情況會(huì)執(zhí)行文件清除策略,防止DropBox占用太多的空間:
- 設(shè)備低內(nèi)存;
- setting配置發(fā)生變更;
- 添加文件。
同時(shí)在添加文件的時(shí)候,超過(guò)配置的可占用空間,會(huì)被丟棄。
/**
* Trims the files on disk to make sure they aren't using too much space.
* @return the overall quota for storage (in bytes)
*/
private synchronized long trimToFit() throws IOException {
return mCachedQuotaBlocks * mBlockSize;
}
2.3.2 文件刪除及標(biāo)記處理策略
在上述策略不滿足后,部分文件會(huì)被刪除,刪除后,會(huì)在DropBox添加一個(gè).lost的空文件標(biāo)記被刪除的文件。
2.3.3 文件類型管控
DropBoxMangerService對(duì)于可存儲(chǔ)的文件類型也有控制,主要是對(duì)于TAG的控制。
public boolean isTagEnabled(String tag) {}
2.3.4 權(quán)限管控
使用DropBox需要READ_LOGS權(quán)限和PACKAGE_USAGE_STATS兩個(gè)權(quán)限。
2.4 讀寫策略
這塊涉及到DBMS幾個(gè)關(guān)鍵方法和屬性,主要涉及到初始化(init),添加文件(addEntry),獲取文件(getNextEntry),文件類型(EntryFile)。
DBMS作為系統(tǒng)服務(wù)會(huì)由SystemServer啟動(dòng),添加文件(addEntry)和獲取文件(getNextEntry)在調(diào)用時(shí)會(huì)先進(jìn)行初始化(init)。
其中每個(gè)文件都會(huì)轉(zhuǎn)換成一個(gè)EntryFile類來(lái)管理,關(guān)系見(jiàn)下圖:

下面了解一下初始化,EntryFile,添加文件和獲取文件的具體內(nèi)容:
2.4.1 初始化
初始化會(huì)將DropBox文件列表緩存到內(nèi)存中。
/** If never run before, scans disk contents to build in-memory tracking data. */
private synchronized void init() throws IOException {
// 省略代碼......
File[] files = mDropBoxDir.listFiles(); // 列出所有文件
for (File file : files) {
EntryFile entry = new EntryFile(file, mBlockSize); // 一個(gè)日志文件對(duì)應(yīng)一個(gè)EntryFile對(duì)象
enrollEntry(entry); // 加入到mAllFiles
}
}
初始化的時(shí)機(jī):
- 設(shè)備存儲(chǔ)容量低廣播回調(diào)
- 設(shè)置配置項(xiàng)修改
- 添加日志文件
- 獲取日志文件
- dump 命令行列出DropBox的一些內(nèi)容
2.4.2 EntryFile文件屬性
每個(gè)文件對(duì)應(yīng)一個(gè)EntryFile,用block數(shù)來(lái)統(tǒng)計(jì)大小,DBMS涉及的讀寫都是根據(jù)磁盤的blockSize來(lái)進(jìn)行,效率會(huì)更高。
static final class EntryFile implements Comparable<EntryFile> {
public final String tag; // 日志文件的tag,類型
public final long timestampMillis; // 日志文件的時(shí)間戳
public final int flags; // 日志文件的flag,標(biāo)志TEXT,EMPTY,GZIPPED
public final int blocks; // 存放文件的塊數(shù)
}
2.4.3 添加文件
添加一個(gè)日志文件,常見(jiàn)的在Ams中的addErrorToDropBox方法調(diào)用。
添加文件管控策略
① .lost的文件格式不允許添加。
// 如果添加.lost的文件,拋異常
if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
② 配置不允許記錄的TAG,不會(huì)被添加。
// 從設(shè)置里面讀取這個(gè)tag是否被允許記錄
if (!isTagEnabled(tag)) return;
③ 根據(jù)系統(tǒng)設(shè)置的磁盤塊大小進(jìn)行寫入,提高寫入效率。
int bufferSize = mBlockSize;
④ 異常時(shí)間戳文件矯正:寫入文件前會(huì)將超過(guò)當(dāng)前時(shí)間10s的文件修改時(shí)間后重新命名并加入到緩存文件列表中。
// 找出當(dāng)前時(shí)間10s之后的所有文件
SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000));
EntryFile[] future = null;
if (!tail.isEmpty()) {
future = tail.toArray(new EntryFile[tail.size()]);
tail.clear(); // 從文件列表中mAllFiles清除掉超過(guò)當(dāng)前時(shí)間的
}
// 省略代碼......
for (EntryFile late : future) {
if ((late.flags & DropBoxManager.IS_EMPTY) == 0) { // 將這些超過(guò)當(dāng)前時(shí)間的文件重命名,時(shí)間戳依次+1,并且重新加入到mAllFiles中
enrollEntry(new EntryFile());
}
}
⑤ 添加文件的順序,先創(chuàng)建臨時(shí)文件,然后使用文件的rename方法,rename方法是原子操作,保證并發(fā)操作的安全。
// 通過(guò)rename方法保存文件,保證并發(fā)操作的安全
temp.renameTo(file))
⑥ 文件添加完成之后通過(guò)發(fā)送廣播通知,廣播分為實(shí)時(shí)廣播和延遲廣播,延遲廣播用來(lái)通知優(yōu)先級(jí)較低的文件。
//低優(yōu)先級(jí)的可以發(fā)送延時(shí)廣播
mHandler.maybeDeferBroadcast(tag, time);
//高優(yōu)先級(jí)的發(fā)送實(shí)時(shí)廣播
mHandler.sendBroadcast(tag, time);
2.4.4 獲取文件
DBMS獲取文件的邏輯比較簡(jiǎn)單,根據(jù)方法名getNextEntry(String tag, long millis,...)我們可以見(jiàn)名知意,主要根據(jù)使用者傳入的時(shí)間戳,找出這個(gè)時(shí)間戳往后的第一個(gè)文件。
for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) {
return new DropBoxManager.Entry(entry.tag, entry.timestampMillis, file, entry.flags);
}
2.5 源碼閱讀總結(jié)
2.5.1 回答我們閱讀前提出的問(wèn)題
① 存取日志的策略
- 會(huì)在低存儲(chǔ),添加獲取文件等時(shí)機(jī)將文件列表初始化到內(nèi)存中。
② 設(shè)計(jì)哪些防呆策略
- 提供了文件大小,存儲(chǔ)占比等限制。
- 會(huì)在低存儲(chǔ),配置更改的時(shí)候清除文件。
- 配置保存在setting中,然后通過(guò)ContentObserver來(lái)監(jiān)聽(tīng)配置變化。
③ 對(duì)外提供哪些接口
- 提供添加獲取,以及cmd命令相關(guān)的接口,開發(fā)調(diào)試都能兼顧。
④ 如何保證性能
- 從源碼的注解可以看出,目前每個(gè)Entry無(wú)論大小都對(duì)應(yīng)一個(gè)文件效率是比較低,源碼也列出了TODO,考慮用單文件隊(duì)列來(lái)優(yōu)化。
// TODO: This implementation currently uses one file per entry, which is
// inefficient for smallish entries -- consider using a single queue file
// per tag (or even globally) instead.
- 采用文件系統(tǒng)塊大小來(lái)讀寫來(lái)提高效率。
⑤ 多進(jìn)程的問(wèn)題如何解決
- 文件操作都是先寫temp,然后采用rename的方案來(lái)保證原子操作從而保證并發(fā)操作的安全。
- addEntry和getNextEntry都做了加鎖處理。
⑥ 文件丟失該如何處理
- 文件被刪除后,會(huì)用一個(gè)同名的空文件來(lái)替代,從而標(biāo)記有文件被刪除了。
⑦ 文件變化如何通知使用方
- 通過(guò)發(fā)廣播的方式來(lái)通知外界,針對(duì)不同優(yōu)先級(jí)的文件又設(shè)置實(shí)時(shí)和延時(shí)廣播。
2.5.2 其它點(diǎn)
- 文件存儲(chǔ)不光限制大小,也會(huì)限制文件類型
- 文件不是全部壓縮的,超過(guò)一定大小的文件會(huì)進(jìn)行壓縮
- 文件命名有講究,包含了應(yīng)用類型,崩潰信息,發(fā)生時(shí)間等相關(guān)信息
- 文件獲取是根據(jù)時(shí)間戳先后來(lái)獲取的,對(duì)于時(shí)間戳異常的文件會(huì)進(jìn)行時(shí)間上的調(diào)整
2.5.3 作為使用者的看法
當(dāng)然,我在使用源碼的過(guò)程中,也發(fā)現(xiàn)我個(gè)人覺(jué)得可以優(yōu)化的點(diǎn)。
- 在使用中,部分文件命名應(yīng)該加上包名,類似應(yīng)用產(chǎn)生的崩潰文件,可以按包名區(qū)分文件,對(duì)使用更友好,當(dāng)然這個(gè)設(shè)計(jì)的初衷是給系統(tǒng)統(tǒng)一使用,可能不對(duì)外開放。
- 權(quán)限管控過(guò)于單一,對(duì)于業(yè)務(wù)本身的一些異常日志,應(yīng)當(dāng)支持自由查看。
- 這些文件的信息應(yīng)該用數(shù)據(jù)庫(kù)維護(hù)起來(lái)更好,方便使用者用,當(dāng)然可能設(shè)計(jì)可能會(huì)變得更復(fù)雜,不夠簡(jiǎn)約。
三、源碼閱讀應(yīng)用–日志文件管理&上報(bào)設(shè)計(jì)
3.1 概述
背景:
部分應(yīng)用希望上報(bào)應(yīng)用運(yùn)行時(shí)的一些日志,包括運(yùn)行時(shí)log,崩潰log,Hprof內(nèi)存快照,捕獲異常等等
需求:
需要設(shè)計(jì)一套客戶端的日志文件收集、管理及上報(bào)一個(gè)功能
參考:
- 日志保存管理方案可以參考DBMS中的一些策略
- 日志上傳方案參考業(yè)內(nèi)已有的一些優(yōu)秀模型
3.2 方案
整體方案方案采用生產(chǎn)者-消費(fèi)者模型,其中幾個(gè)關(guān)鍵節(jié)點(diǎn)
- 生產(chǎn)者:應(yīng)用的多個(gè)進(jìn)程,他們可能會(huì)生成不同類型的日志,并寫入到指定的文件目錄
- 臨時(shí)文件目錄:根據(jù)文件類型、優(yōu)先級(jí)設(shè)置不同目錄來(lái)存放臨時(shí)文件
- 上報(bào)數(shù)據(jù)目錄:臨時(shí)文件目錄中的文件會(huì)通過(guò)rename方案寫到上報(bào)數(shù)據(jù)目錄
- 消費(fèi)者:上報(bào)進(jìn)程,上報(bào)進(jìn)程會(huì)通過(guò)FileObserver監(jiān)聽(tīng)變化,從而來(lái)上報(bào)文件
整體的流程圖如下:

3.3 確定對(duì)外接口
- 獲取文件的接口
- 存文件的接口
- 統(tǒng)計(jì)文件(類型,數(shù)量)的接口
- 更改部分配置策略的接口
- 主動(dòng)上報(bào)的接口
- 其它自定義參數(shù)的接口
3.4 確定收集管控策略
- 是否允許收集:該配置關(guān)閉后,本地不會(huì)執(zhí)行任何收集行為
- 日志存儲(chǔ)目錄:私有目錄固化出一個(gè)空間
- 文件命名方式:參照DBMS,進(jìn)程名_日志類型_前后臺(tái)@時(shí)間戳.txt.gz
- 日志類型開關(guān):每個(gè)日志類型設(shè)置是否允許手機(jī)
- 收集日志類型:崩潰日志,運(yùn)行時(shí)日志,內(nèi)存快照,捕獲日志,其它自定義日志等
- 日志存活時(shí)長(zhǎng):參照DBMS,超過(guò)一定時(shí)間,則刪除文件
- 日志存儲(chǔ)空間:參照DBMS,設(shè)置一個(gè)手機(jī)可用存儲(chǔ)的比例·
- 日志文件數(shù)量:超過(guò)指定數(shù)量,則刪除部分文件;參照DBMS,當(dāng)可用存儲(chǔ)較低的情況,應(yīng)該存儲(chǔ)更少的文件數(shù)量
- 其余初始化的一些時(shí)機(jī),同樣參考DBMS
3.5 確定上報(bào)管控策略
- 是否允許上報(bào),該配置關(guān)閉后,不允許上報(bào)行為
- 是否允許在流量情況下上報(bào),該配置設(shè)置不允許后,只允許在wifi情況下上報(bào)
- 流量情況下單次、單日、單月最多可上報(bào)的文件大小,該配置控制流量情況下,應(yīng)用在上報(bào)時(shí)可以上報(bào)的文件大小
- wifi情況下單次、單日、單月最多可上報(bào)的文件大小,該配置控制wifi情況下,應(yīng)用在上報(bào)時(shí)可以上報(bào)的文件大小
- 上報(bào)間隔時(shí)間,該配置控制低優(yōu)先級(jí)的文件上報(bào)時(shí)間間隔
- 上報(bào)失敗次數(shù)限制,該配置控制在失敗一定次數(shù)以后,不再允許上報(bào)
- 上報(bào)優(yōu)先級(jí)(低優(yōu)先級(jí)的日志無(wú)需頻繁上報(bào))
- 弱網(wǎng)絡(luò)情況本次上報(bào)的文件大小
- 單次、單日、單月允許使用的流量大小,該配置控制應(yīng)用在上報(bào)時(shí)可以使用的流量大小
- 可上報(bào)的最低電量限制,該配置控制上報(bào)情況下最小電量限制
3.6 收集日志方案
- DropBox日志:先讀取到本地,然后存儲(chǔ)上報(bào)
- 運(yùn)行時(shí)日志:利用adb logcat命令輸出日志到本地儲(chǔ)存上
- 內(nèi)存快照:dump Hprof文件,然后進(jìn)行一些裁剪,以便于能夠以更小的體積上傳
- 其它日志:實(shí)時(shí)輸出記錄到本地,按需上報(bào)
以上具體方案不作為本次重點(diǎn),不再詳述。
3.7 寫入日志方案

通過(guò)網(wǎng)絡(luò)課程的學(xué)習(xí),了解到mmap的性能非常高,所以最終采用“多進(jìn)程寫+mmap”的方案,并且避免了跨進(jìn)程的調(diào)用堆積,效率很高
3.8 上報(bào)日志方案
參照DBMS添加文件的實(shí)時(shí)和延時(shí)通知方案,上報(bào)也分為實(shí)時(shí)上報(bào)和延時(shí)上報(bào)
- 實(shí)時(shí)上報(bào):出現(xiàn)一份日志,就直接上報(bào),針對(duì)重要性較高的日志
- 延時(shí)上報(bào):達(dá)到一定數(shù)量,或者達(dá)到一定時(shí)間進(jìn)行上報(bào)

3.9 數(shù)據(jù)監(jiān)控
3.9.1 質(zhì)量監(jiān)控

3.9.2 容災(zāi)監(jiān)控

四、總結(jié)
本文主要講了兩塊內(nèi)容:
1、DropBoxManagerService源碼閱讀與解析,包括接口設(shè)計(jì)、文件存儲(chǔ)的管控機(jī)制和策略,多進(jìn)程的處理,異常防呆機(jī)制
2、應(yīng)用日志收集與上報(bào)方案,主要參考DropBoxManagerService源碼的設(shè)計(jì)
我們經(jīng)常強(qiáng)調(diào)源碼閱讀,源碼究竟能給我們帶來(lái)什么呢?我認(rèn)為主要有以下幾點(diǎn):
- 編碼技術(shù)的提升
- 分析問(wèn)題的思路
- 解決方案的設(shè)計(jì)
- 設(shè)計(jì)模式的應(yīng)用
本文拋磚引玉,借助以上案例簡(jiǎn)單地講了一下DBMS源碼以及源碼閱讀的應(yīng)用,希望在源碼閱讀方面能夠帶給大家一些啟發(fā),同時(shí)對(duì)Android系統(tǒng)一些不常見(jiàn)的服務(wù)有一個(gè)了解。






