Android日志收集:
日志的收集一直有个痛点,就是性能与日志完整性无法兼得。
保证性能:
要实现高性能的日志收集,势必要使用大量内存,先将日志写入内存中,然后在合适的时机将内存里的日志写入到文件系统中(flush), 如果在 flush 之前用户强杀了进程,那么内存里的内容会因此而丢失 。
保证完整:
日志实时写入文件可以保证日志的完整性,但是写文件是 IO 操作,涉及到用户态与内核态的切换,而且这种开销是开启线程都无法避免的,也就是说即使开启一个新线程实时写入也是相对耗时的。
mmap概念
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
特点:实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
image.png
mmap 内存映射文件之后,可以直接通过操作内存来读写文件,性能上接近直接读写内存。针对一次写文件,节省了用户态到内核态切换的开销,也减少了数据拷贝的次数。
mmap代码
地址:android/platform/bionic/libc/bionic/mmap.cpp:
mmap原型函数:
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
- 参数start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
- 参数length:代表将文件中多大的部分映射到内存。
- 参数prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_NONE无权限,基本没有用
PROT_READ读权限
PROT_WRITE写权限
PROT_EXEC执行权限 - 参数flags: 描述了映射的类型。
MAP_FIXED开启这个选项,则 addr 参数指定的地址是作为必须而不是建议。如果由于空间不足等问题无法映射则调用失败。不建议使用。
MAP_PRIVATE表明这个映射不是共享的。文件使用 copy on write 机制映射,任何内存中的改动并不反映到文件之中。也不反映到其他映射了这个文件的进程之中。如果只需要读取某个文件而不改变文件内容,可以使用这种模式。
MAP_SHARED和其他进程共享这个文件。往内存中写入相当于往文件中写入。会影响映射了这个文件的其他进程。与 MAP_PRIVATE冲突。 - 参数fd: 文件描述符。进行 map 之后,文件的引用计数会增加。因此,我们可以在 map 结束后关闭 fd,进程仍然可以访问它。当我们 unmap 或者结束进程,引用计数会减少。
- 参数offset: 文件偏移,从文件起始算起。
应用
Android中也有不少地方用到,比如匿名共享内存,Binder机制等
这里记录下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个参数,分别是缓存文件的路径,缓存文件的大小,日志的路径
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 的第一个字节会用于存储日志路径名称长度,后面紧跟日志路径,之后才是日志信息
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 失败,则降级使用内存缓存
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内的数据清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) {
// 写脏数据
writeDirtyLogToFile(buffer_fd);
// 根据 buffer size 调整 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;
}
- 回写上次因断电(泛指,包括强杀进程)来不及写到日志文件中的脏数据
- 根据 buffer size, 使用 ftruncate 调整 buffer 文件大小
- 使用 mmap 创建文件内存映射
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);
// 缓存写不下时异步刷新
if (log_size >= logBuffer->emptySize()) {
logBuffer->async_flush(fileFlush);
}
logBuffer->append(log);
env->ReleaseStringUTFChars(log_, log);
}
写文件是 LogBuffer 和 AsyncFileFlush的async_flush方法 协作完成的
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();
}
}










网友评论