Android日志收集:
日志的收集一直有個(gè)痛點(diǎn)烁涌,就是性能與日志完整性無法兼得撮执。
保證性能:
要實(shí)現(xiàn)高性能的日志收集,勢必要使用大量內(nèi)存舷丹,先將日志寫入內(nèi)存中,然后在合適的時(shí)機(jī)將內(nèi)存里的日志寫入到文件系統(tǒng)中(flush)颜凯, 如果在 flush 之前用戶強(qiáng)殺了進(jìn)程,那么內(nèi)存里的內(nèi)容會(huì)因此而丟失 症概。
保證完整:
日志實(shí)時(shí)寫入文件可以保證日志的完整性,但是寫文件是 IO 操作穴豫,涉及到用戶態(tài)與內(nèi)核態(tài)的切換,而且這種開銷是開啟線程都無法避免的精肃,也就是說即使開啟一個(gè)新線程實(shí)時(shí)寫入也是相對(duì)耗時(shí)的。
mmap概念
mmap是一種內(nèi)存映射文件的方法司抱,即將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對(duì)映關(guān)系习柠。
特點(diǎn):實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存资溃,而系統(tǒng)會(huì)自動(dòng)回寫臟頁面到對(duì)應(yīng)的文件磁盤上,即完成了對(duì)文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)溶锭。相反宝恶,內(nèi)核空間對(duì)這段區(qū)域的修改也直接反映用戶空間垫毙,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。
mmap 內(nèi)存映射文件之后综芥,可以直接通過操作內(nèi)存來讀寫文件,性能上接近直接讀寫內(nèi)存猎拨。針對(duì)一次寫文件征峦,節(jié)省了用戶態(tài)到內(nèi)核態(tài)切換的開銷消请,也減少了數(shù)據(jù)拷貝的次數(shù)类腮。
mmap代碼
地址:android/platform/bionic/libc/bionic/mmap.cpp:
mmap原型函數(shù):
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
- 參數(shù)start:指向欲映射的內(nèi)存起始地址,通常設(shè)為 NULL蚜枢,代表讓系統(tǒng)自動(dòng)選定地址,映射成功后返回該地址厂抽。
- 參數(shù)length:代表將文件中多大的部分映射到內(nèi)存。
- 參數(shù)prot:映射區(qū)域的保護(hù)方式筷凤。可以為以下幾種方式的組合:
PROT_NONE
無權(quán)限藐守,基本沒有用
PROT_READ
讀權(quán)限
PROT_WRITE
寫權(quán)限
PROT_EXEC
執(zhí)行權(quán)限 - 參數(shù)flags: 描述了映射的類型。
MAP_FIXED
開啟這個(gè)選項(xiàng)卢厂,則 addr 參數(shù)指定的地址是作為必須而不是建議乾蓬。如果由于空間不足等問題無法映射則調(diào)用失敗慎恒。不建議使用。
MAP_PRIVATE
表明這個(gè)映射不是共享的融柬。文件使用 copy on write 機(jī)制映射,任何內(nèi)存中的改動(dòng)并不反映到文件之中粒氧。也不反映到其他映射了這個(gè)文件的進(jìn)程之中。如果只需要讀取某個(gè)文件而不改變文件內(nèi)容靠欢,可以使用這種模式。
MAP_SHARED
和其他進(jìn)程共享這個(gè)文件门怪。往內(nèi)存中寫入相當(dāng)于往文件中寫入。會(huì)影響映射了這個(gè)文件的其他進(jìn)程掷空。與 MAP_PRIVATE沖突肋殴。 - 參數(shù)fd: 文件描述符。進(jìn)行 map 之后护锤,文件的引用計(jì)數(shù)會(huì)增加。因此烙懦,我們可以在 map 結(jié)束后關(guān)閉 fd,進(jìn)程仍然可以訪問它氯析。當(dāng)我們 unmap 或者結(jié)束進(jìn)程,引用計(jì)數(shù)會(huì)減少掩缓。
- 參數(shù)offset: 文件偏移,從文件起始算起你辣。
應(yīng)用
Android中也有不少地方用到巡通,比如匿名共享內(nèi)存
舍哄,Binder機(jī)制
等
這里記錄下log4a中如何使用
java端:
public void init(String bufferPath, int capacity, String logPath) {
try {
ptr = initNative(bufferPath, capacity, logPath);
}catch (Exception e) {
Log.e(TAG, Log4a.getStackTraceString(e));
}
}
public void write(String log) {
if (ptr != 0) {
try {
writeNative(ptr, log);
}catch (Exception e) {
Log.e(TAG, Log4a.getStackTraceString(e));
}
}
}
public void flushAsync() {
if (ptr != 0) {
try {
flushAsyncNative(ptr);
}catch (Exception e) {
Log.e(TAG, Log4a.getStackTraceString(e));
}
}
}
public void release() {
if (ptr != 0) {
try {
releaseNative(ptr);
}catch (Exception e) {
Log.e(TAG, Log4a.getStackTraceString(e));
}
ptr = 0;
}
}
initNative
初始化方法 initNative 接受3個(gè)參數(shù),分別是緩存文件的路徑跪解,緩存文件的大小,日志的路徑
static jlong initNative(JNIEnv *env, jclass type, jstring buffer_path_,
jint capacity, jstring log_path_) {
const char *buffer_path = env->GetStringUTFChars(buffer_path_, 0);
const char *log_path = env->GetStringUTFChars(log_path_, 0);
const size_t buffer_size = static_cast(capacity);
// 打開緩存文件
int buffer_fd = open(buffer_path, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
// 打開日志文件
int log_fd = open(log_path, O_RDWR|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
// buffer 的第一個(gè)字節(jié)會(huì)用于存儲(chǔ)日志路徑名稱長度叉讥,后面緊跟日志路徑,之后才是日志信息
if (strlen(log_path) > CHAR_MAX / 2) {
jclass je = env->FindClass("java/lang/IllegalArgumentException");
std::ostringstream oss;
oss << "The length of log path must be less than " << CHAR_MAX / 2;
env -> ThrowNew(je, oss.str().c_str());
return 0;
}
// 初始化異步文件刷新
if (fileFlush == nullptr) {
fileFlush = new AsyncFileFlush(log_fd);
}
char *buffer_ptr = openMMap(buffer_fd, buffer_size);
bool map_buffer = true;
//如果打開 mmap 失敗图仓,則降級(jí)使用內(nèi)存緩存
if(buffer_ptr == nullptr) {
buffer_ptr = new char[capacity];
map_buffer = false;
}
env->ReleaseStringUTFChars(buffer_path_, buffer_path);
env->ReleaseStringUTFChars(log_path_, log_path);
LogBuffer* logBuffer = new LogBuffer(buffer_ptr, buffer_size);
//將buffer內(nèi)的數(shù)據(jù)清0, 并寫入日志文件路徑
logBuffer->initData(log_path);
logBuffer->map_buffer = map_buffer;
return reinterpret_cast(logBuffer);
}
static char* openMMap(int buffer_fd, size_t buffer_size) {
char* map_ptr = nullptr;
if (buffer_fd != -1) {
// 寫臟數(shù)據(jù)
writeDirtyLogToFile(buffer_fd);
// 根據(jù) buffer size 調(diào)整 buffer 文件大小
ftruncate(buffer_fd, static_cast(buffer_size));
lseek(buffer_fd, 0, SEEK_SET);
map_ptr = (char *) mmap(0, buffer_size, PROT_WRITE | PROT_READ, MAP_SHARED, buffer_fd, 0);
if (map_ptr == MAP_FAILED) {
map_ptr = nullptr;
}
}
return map_ptr;
}
- 回寫上次因斷電(泛指救崔,包括強(qiáng)殺進(jìn)程)來不及寫到日志文件中的臟數(shù)據(jù)
- 根據(jù) buffer size捏顺, 使用 ftruncate 調(diào)整 buffer 文件大小
- 使用 mmap 創(chuàng)建文件內(nèi)存映射
writeNative
static void writeNative(JNIEnv *env, jobject instance, jlong ptr,
jstring log_) {
const char *log = env->GetStringUTFChars(log_, 0);
LogBuffer* logBuffer = reinterpret_cast(ptr);
size_t log_size = strlen(log);
// 緩存寫不下時(shí)異步刷新
if (log_size >= logBuffer->emptySize()) {
logBuffer->async_flush(fileFlush);
}
logBuffer->append(log);
env->ReleaseStringUTFChars(log_, log);
}
寫文件是 LogBuffer 和 AsyncFileFlush的async_flush方法 協(xié)作完成的
Android本身Api是否有提供mmap功能呢:
MappedByteBuffer
使用sample
static void writeDemo() {
File dir = new File(logFileDir);
if (!dir.exists()) {
boolean mk = dir.mkdirs();
Log.d(defTag, "make dir " + mk);
}
File eFile = new File(logFileDir + File.separator + fileName);
byte[] strBytes = logContent.getBytes();
try {
RandomAccessFile randomAccessFile = new RandomAccessFile(eFile, "rw");
MappedByteBuffer mappedByteBuffer;
final int inputLen = strBytes.length;
if (!eFile.exists()) {
boolean nf = eFile.createNewFile();
Log.d(defTag, "new log file " + nf);
mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE);
} else {
mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, inputLen);
}
if (mappedByteBuffer.remaining() < inputLen) {
mappedByteBuffer = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE, gCurrentLogPos, LOG_FILE_GROW_SIZE + inputLen);
}
mappedByteBuffer.put(strBytes);
gCurrentLogPos += inputLen;
} catch (Exception e) {
Log.e(defTag, "WriteRunnable run: ", e);
if (!eFile.exists()) {
boolean nf = eFile.createNewFile();
Log.d(defTag, "new log file " + nf);
}
FileOutputStream os = new FileOutputStream(eFile, true);
os.write(logContent.getBytes());
os.flush();
os.close();
}
}