1. 介绍
xlog是微信官方的终端基础组件mars中可以独立使用的日志模块,是一个使用c++编写的业务行无关,平台性无关的基础组件。目前已接入微信Android、IOS、Mac、Window、WP等客户端。
1.1 优点
-
使用C++高性能记录日志,避免了使用Java在加密,压缩过程中产生的大量GC。
-
使用mmap,既有直接写内存的性能,又有直接写文件的可靠性。
-
使用mmap,避免了写文件时内核空间和用户空间频繁切换,以及dirty page回写时带来的不可控的CPU峰值。
-
写文件时,Dirty page的回写时机:
- 定时回写。
- 调用 write 的时候,发现 dirty page 占用内存超过系统内存一定比例。
- 内存不足。
-
mmap的回写时机:
- 不设置 MAP_NOSYNC 情况下 30s-60s(仅限FreeBSD,一款类UNIX操作系统)
- 调用 msync 或者 munmap
- 进程 crash
- 内存不足
-
-
使用mmap,避免了使用内存作为写文件的中间buffer时,当程序被系统杀死或发生crash,在内存中的日志信息丢失。
-
-
默认提供压缩功能。
- 先压缩再加密。效率比先加密再压缩高。因为压缩后数据量变少,并且加密前重复字符也比较多(时间等)。
- 流式压缩日志,避免多条日志集中压缩导致的cpu峰值导致程序卡顿。
- 一定大小作为一个压缩单位,即使压缩单位中有部分数据单位损坏,不影响这个单位中损坏部分之前的数据,只影响这个单位中损坏部分之后的数据。
-
默认提供 ECDH+TEA的混合加密算法。
- ECDH密钥协商层(安全通道建立)
- TEA数据加密层(高效传输)
-
启动时清理日志,避免占用户空间。
2. 如何引入
2.1 Gradle
dependencies {
implementation 'com.tencent.mars:mars-xlog:1.2.6'
}
与官方文档说明不同,该方式一样能使用加密
2.2 本地编译
2.2.1 安装环境
-
安装cmake,在安装过程中勾选添加到环境变量,或者手动把cmake的bin目录配置到环境变量中。
-
安装python,要求3.10版本以上。在安装过程中勾选添加到环境变量,或者手动配置环境变量指向python路径。
-
python从3.6版本开始,在命令行时输入的不是python、python3,而是py
-
如果是在Windows 11上,可能会出现运行时启动应用商店的情况。打开设置,在应用>高级应用设置>应用执行别名,关闭应用安装程序即可。
-
-
下载ndk-r20b,并配置环境变量 NDK_ROOT 指向 ndk 路径。
-
如果是Windows系统还要安装cygwin,要安装其中的make,gcc,gbd,然后把cygwin的bin目录配置到环境变量中。
2.2.2 编译
-
所有的编译脚本都在mars/mars 目录, 运行编译脚本之前也必须cd到此目录,在当前目录下运行,默认是编译 armeabi-v7a,arm64-v8a的,如果需要其他 CPU 架构,把编译脚本中的
aarchs = {'armeabi-v7a', 'arm64-v8a'}稍作修改即可。 -
执行命令
py build_android.py -
执行后,会让选择
Enter menu: 1. Clean && build mars. 2. Build incrementally mars. 3. Clean && build xlog. 4. Exit ``` -
选择3
-
输出结果全部在 mars/mars/libraries/mars_xlog_sdk 目录中,把mars_xlog_sdk/src目录下的java文件,以及mars_xlog_sdk/libs目录下的so文件复制到你的项目中。
3. 如何使用
3.1 初始化XLog
在Application或着别的地方初始化XLog
private void initXLog() {
final String logPath = this.getFilesDir() + "/xlog";
// this is necessary, or may crash for SIGBUS
final String cachePath = this.getFilesDir() + "/xlog_cache";
// privateKey = "145aa7717bf9745b91e9569b80bbf1eedaa6cc6cd0e26317d810e35710f44cf8"
String publicKey = "572d1e2710ae5fbca54c76a382fdd44050b3a675cb2bf39feebe85ef63d947aff0fa4943f1112e8b6af34bebebbaefa1a0aae055d9259b89a1858f7cc9af9df1";
String logFileNamePrefix = "xlog";
// debug包最低输出Verbose级别的日志,release包最低输出Info级别的日志
int logLevel = BuildConfig.DEBUG ? Xlog.LEVEL_VERBOSE : Xlog.LEVEL_INFO;
Xlog.open(true, logLevel, Xlog.AppednerModeAsync, cachePath, logPath, logFileNamePrefix, publicKey);
Xlog xlog = new Xlog();
// debug包输出日志到控制台,release包则不输出
xlog.setConsoleLogOpen(0, BuildConfig.DEBUG);
Log.setLogImp(xlog);
}
注意如果App使用了多进程,不要把多个进程的日志输出到同一个文件中,保证每个进程独享一个日志文件。而且保存 log 的目录请使用单独的目录,不要存放任何其他文件防止被 xlog 自动清理功能误删。
3.2 输出日志
像Android的Log一样使用XLog的Log,例如:
Log.d(TAG, Message)
3.3 关闭日志
在退出程序时关闭日志:
Log.appenderClose();
不要在Application的onTermincate()中调用,因为这个方法在模拟器上才会被回调
/**
* This method is for use in emulated process environments. It will
* never be called on a production Android device, where processes are
* removed by simply killing them; no user code (including this callback)
* is executed when doing so.
*/
@CallSuper
public void onTerminate() {
}
可以在例如App的MainActivity#onBackpress()这些退出App的方法中调用
4. 查看日志
4.1 从设备下载日志文件
如果按 3.1 的代码设置日志路径,getFilesDir()对应的路径是 /data/data/你的应用包名/files,这个目录在手机文件管理器是看不到的,可以通过Android Studio的Device Explorer,找到xlog目录下的日志文件
4.2 解密日志文件
4.2.1 仅压缩
如果不想使用加密模块,public key 参数设置为空字符即可,解密脚本使用 decode_mars_nocrypt_log_file.py, 但这样日志会只压缩不加密。
4.2 使用第三方解密软件
-
用软件打开日志文件,并设置 privateKey
-
解密日志
4.3 使用官方解密方式
参考 XLog加密使用指引 配置环境并解密日志
5. 源码分析
5.1 初始化
调用链:
Xlog.open()
->Java_com_tencent_mars_xlog_Xlog_appenderOpen()
->appender_open()
->XloggerAppender.NewInstance()
->XloggerAppender.open()
->xlogger_SetAppender()
->BOOT_RUN_EXIT()
各步骤代码:
// 在 Xlog.java 文件中
public static void open(boolean isLoadLib, int level, int mode, String cacheDir, String logDir, String nameprefix, String pubkey) {
if (isLoadLib) {
System.loadLibrary("c++_shared");
System.loadLibrary("marsxlog");
}
XLogConfig logConfig = new XLogConfig();
// ... 初始化XLogConfig的代码 ...
appenderOpen(logConfig);
}
private static native void appenderOpen(XLogConfig logConfig);
// 在 Java2C_Xlog.cc 文件中
JNIEXPORT void JNICALL Java_com_tencent_mars_xlog_Xlog_appenderOpen(JNIEnv* env, jclass clazz, jobject _log_config) {
// ... 将Java层数据转换为C++层数据 ...
appender_open(config);
xlogger_SetLevel((TLogLevel)level);
}
// 在 appender.cc 文件中
void appender_open(const XLogConfig& _config) {
// ... 非法情况判断 ...
sg_default_appender = XloggerAppender::NewInstance(_config, sg_max_byte_size);
// ... 初始化sg_default_appender ...
xlogger_SetAppender(&xlogger_appender);
BOOT_RUN_EXIT(appender_release_default_appender);
}
- XloggerAppender::NewInstance()
// 在 appender.cc 文件中
XloggerAppender* XloggerAppender::NewInstance(const XLogConfig& _config, uint64_t _max_byte_size) {
return new XloggerAppender(_config, _max_byte_size);
}
/**
* thread_async_ 的初始化使用了 boost::bind 来绑定成员函数 __AsyncLogThread ,
* 当线程启动时会执行 this->__AsyncLogThread(),
* __AsyncLogThread 是XloggerAppender的私有成员函数,用于异步日志写入的核心逻辑
*/
XloggerAppender::XloggerAppender(const XLogConfig& _config, uint64_t _max_byte_size)
: thread_async_(boost::bind(&XloggerAppender::__AsyncLogThread, this)), max_file_size_(_max_byte_size) {
Open(_config);
}
/**
* 该函数主要完成以下工作:
* 1. 创建缓存目录
* 2. 创建日志目录
* 3. 初始化mmap内存映射文件
* 4. 将上一次退出app时保留在缓存文件中的数据写入日志文件中
* 5. 写入初始化日志头信息
*/
void XloggerAppender::Open(const XLogConfig& _config){
// ... 其它代码 ...
// region 1. 创建缓存目录
// 当缓存路径是一个非空字符串,创建缓存目录并启动后台线程,删除过期的日志缓存文件、移动旧日志缓存文件
if (!config_.cachedir_.empty()) {
boost::filesystem::create_directories(config_.cachedir_);
thread_timeout_cache_ =
std::make_unique<comm::Thread>(boost::bind(&XloggerAppender::__DelTimeoutFile, this, config_.cachedir_));
thread_timeout_cache_->start_after(2 * 60 * 1000); // 两分钟后启动
thread_moveold_ = std::make_unique<comm::Thread>(boost::bind(&XloggerAppender::__MoveOldFiles,
this,
config_.cachedir_,
config_.logdir_,
config_.nameprefix_));
thread_moveold_->start_after(3 * 60 * 1000); // 三分钟后启动
// ... 其它代码 ...
}
// endregion
// region 2. 创建日志目录
// 启动后台线程,删除日志目录下的过期日志文件
thread_timeout_log_ =
std::make_unique<comm::Thread>(boost::bind(&XloggerAppender::__DelTimeoutFile, this, config_.logdir_));
thread_timeout_log_->start_after(2 * 60 * 1000); // 两分钟后启动
// 创建日志目录
boost::filesystem::create_directories(config_.logdir_);
// endregion
// ... 其它代码 ...
// region 3. 初始化mmap内存映射文件
char mmap_file_path[512] = {0};
// 初始化mmap内存映射文件的路径
snprintf(mmap_file_path,
sizeof(mmap_file_path),
"%s/%s.mmap3",
config_.cachedir_.empty() ? config_.logdir_.c_str() : config_.cachedir_.c_str(),
config_.nameprefix_.c_str());
bool use_mmap = false;
// 将磁盘文件映射到进程的虚拟内存地址空间,kBufferBlockLength是映射的大小(150KB),mmap_file_用于接收成功映射后的文件对象
if (OpenMmapFile(mmap_file_path, kBufferBlockLength, mmap_file_)) {
// 如果使用ZSTD压缩模式(由Facebook开发,采用有限状态熵(FSE)编码技术,在压缩率、速度和资源消耗之间实现更优平衡。)
if (_config.compress_mode_ == kZstd) {
log_buff_ = new LogZstdBuffer(mmap_file_.data(),
kBufferBlockLength,
true,
_config.pub_key_.c_str(),
_config.compress_level_);
} else {
// 如果使用ZLIB压缩模式(基于LZ77与哈夫曼编码结合的DEFLATE算法,压缩率中等,但压缩和解压速度较慢。)
log_buff_ = new LogZlibBuffer(mmap_file_.data(), kBufferBlockLength, true, _config.pub_key_.c_str());
}
use_mmap = true;
} else {
// 如果mmap文件映射失败,使用普通的内存分配方式创建日志缓冲区
char* buffer = new char[kBufferBlockLength];
if (_config.compress_mode_ == kZstd) {
log_buff_ =
new LogZstdBuffer(buffer, kBufferBlockLength, true, _config.pub_key_.c_str(), _config.compress_level_);
} else {
log_buff_ = new LogZlibBuffer(buffer, kBufferBlockLength, true, _config.pub_key_.c_str());
}
use_mmap = false;
}
if (nullptr == log_buff_->GetData().Ptr()) {
if (use_mmap && mmap_file_.is_open())
CloseMmapFile(mmap_file_);
return;
}
// endregion
// region 4. 将上一次退出app时保留在缓存文件中的数据写入日志文件中
AutoBuffer buffer;
// 将日志缓冲区中的数据刷新到buffer中
log_buff_->Flush(buffer);
// ... 其它代码 ...
// 检查buffer中是否有数据
if (buffer.Ptr()) {
// 写入提示信息,表示mmap数据的开始
WriteTips2File("~~~~~ begin of mmap ~~~~~\n");
// 将buffer中的数据写入日志文件
__Log2File(buffer.Ptr(), buffer.Length(), false);
// 写入提示信息,表示mmap数据的结束,并附带标记信息
WriteTips2File("~~~~~ end of mmap ~~~~~%s\n", mark_info);
}
// endregion
// region 5. 写入初始化日志头信息
// appender_info存储日志追加器信息
char appender_info[728] = {0};
// 格式化日志追加器的信息,包括日期、时间和标记信息。这是每次open的时候会打印的信息,方便定位问题。
snprintf(appender_info, sizeof(appender_info), "^^^^^^^^^^" __DATE__ "^^^" __TIME__ "^^^^^^^^^^%s", mark_info);
// 将日志追加器的信息写入mmap3日志缓存文件
Write(nullptr, appender_info);
char logmsg[256] = {0};
// 格式化获取mmap数据所花费的时间,并存储到logmsg数组中
snprintf(logmsg, sizeof(logmsg), "get mmap time: %" PRIu64, (int64_t)get_mmap_time);
// 将获取mmap数据所花费的时间写入mmap3日志缓存文件
Write(nullptr, logmsg);
// 写入MARS项目的URL信息
Write(nullptr, "MARS_URL: " MARS_URL);
// 写入MARS项目的路径信息
Write(nullptr, "MARS_PATH: " MARS_PATH);
// 写入MARS项目的版本信息
Write(nullptr, "MARS_REVISION: " MARS_REVISION);
// 写入MARS项目的构建时间信息
Write(nullptr, "MARS_BUILD_TIME: " MARS_BUILD_TIME);
// 写入MARS项目的构建标签信息
Write(nullptr, "MARS_BUILD_JOB: " MARS_TAG);
// 格式化日志追加器的模式和是否使用mmap的信息,并存储到logmsg数组中
snprintf(logmsg, sizeof(logmsg), "log appender mode:%d, use mmap:%d", (int)config_.mode_, use_mmap);
// 将日志追加器的模式和是否使用mmap的信息写入日志文件
Write(nullptr, logmsg);
// 检查缓存目录是否为空
if (!config_.cachedir_.empty()) {
// 获取缓存目录的空间信息
boost::filesystem::space_info info = boost::filesystem::space(config_.cachedir_);
// 格式化缓存目录的空间信息,包括总容量、可用空间和空闲空间,并存储到logmsg数组中
snprintf(logmsg,
sizeof(logmsg),
"cache dir space info, capacity:%" PRIuMAX " free:%" PRIuMAX " available:%" PRIuMAX,
info.capacity,
info.free,
info.available);
// 将缓存目录的空间信息写入日志文件
Write(nullptr, logmsg);
// endregion
}
6. 业务场景
6.1 如何捕获Java层的crash信息?
// 系统默认的Exception为 KillApplicationHandler
Thread.UncaughtExceptionHandler systemHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((thread, ex) ->
{
Log.e(TAG, android.util.Log.getStackTraceString(ex));
if (systemHandler != null) {
systemHandler.uncaughtException(thread, ex);
}
}
);