Android高性能日志模块-Xlog
前言
日志可以帮助我们定位问题,记录当前程序的运行状态。与后端开发不同的是,Android中的Log原生支持的仅是本地调试和信息记录,并不能很方便地定位远程问题。当有用户反馈时,通常是给用户重新编一个打开日志的安装包或通过远程的开关给特定用户开启日志,别无他法。
原生的Log方案主要是为程序发布前服务的,实际工程中日志应该是随着Release包上线的。
微信的xlog
微信团队开源了一个基础组件项目——Mars, Xlog是其中日志部分的开源代码。微信团队利用Xlog支撑了微信iOS、Android两端的教亿个客户端,我们可以采用这个技术方案。
为啥采用Xlog,下面引用一段选自《Android工程化最佳实践》来说明。
Xlog 是 Mars组件库中的一个模块。一般的打日志方式仅仅用于调试,在远程用户出现问题的时候,最常用的做法是通过后端的日志和网络日志定位错误。而微信希望客户端可以像服务端一样保证日志的长久存储(Release版本中也会打日志),这样在远端用户出现异常的时候可以自动将日志上报给分析系统进行行为分析,让日志功能发挥更大的用处。
为了更好地说明其设计思路和方案,下面将通过问答的方式进行讲述。
Q:远端用户出错的时候移动端怎么分析出错原因?A:在正式版App中也打印日志。
Q:如果正式版中开启了日志,日志的性能问题怎么解决?A:用C++写核心功能,减少Java的GC损耗。
Q:日志是要写入磁盘的,写入磁盘的性能一向很低,用C++也未必能提高多少效率吧?A:那就用MMAP来减少内核空间和用户空间的切换,操作内存就等于操作文件,提高写入磁盘效率。
Q:日志怎么存储呢,可以建立一个内存中的日志缓冲区吗?A:建立缓冲区是可以的,但是什么时候写入磁盘就是一个问题。
Q:如何保证写入的成功率?A:尽力保证每一条Log 都立刻写入磁盘,有一条写一条。没写入的放入缓存,缓存做得小点,可以增加写入频率。
Q:写入磁盘的Log是否是加密的?A:应该加密,不能在磁盘中存放明文。
Q:会不会出现加密后文件损坏的情况?A:肯定会。这也是单条日志加密的优点,即使出现数据损坏,对后续数据的恢复影响也很小。
Q:加密的过程是如何的,为什么不用批量加密?A:批量加密是完全可行的,但是批量加密会出现 CPU 波峰,引起CPU波动,所以采用单行日志加密。而且单行日志加密出错也仅仅会损坏某一条日志信息,不会影响到其他的日志。
Q:每一行日志都进行一次加密,会不会性能太低了?A:通常采用先压缩再加密的方案,压缩后的String长度会明显减小,可以减少加密的数据量,从而提升性能。
Q:每行日志都进行一次压缩,这个性能如何?A:因为打印日志一般都有一段时间间隔,采用的方案是流式压缩,属于微秒级别的开销。
Q:日志系统设计的关键是什么?A,日志系统设计的关键是安全性、流畅性、完整性、容错性。
总之,微信会强制开发者清空Debug大田到的日志。对于存活在线上的日志会进行分级。
动手
首先根据前篇的Xlog的编译,我们可以获取到Xlog需要的so库文件,将其放入我们的我们的项目之中。
修改我们的build.gradle文件
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
minSdkVersion 23
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
ndk {
abiFilters "armeabi"
abiFilters "armeabi-v7a"
abiFilters "arm64-v8a"
abiFilters "x86"
abiFilters "x86_64"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
jniLibs.srcDirs = ["src/main/jniLibs/jni"]
}
}
}
dependencies {
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation "com.orhanobut:logger:2.2.0"
}
将前篇中下载的项目的demo文件中与Xlog相关的java代码复制进项目中。
既如上图所见的Log.java与Xlog.java这两个文件。
抽取XLog的配置,独立成类,方便日后初始化。
package com.tao.lib_log.adapetr;
import java.io.File;
/**
* @Author: tao
* @ClassName: LogConfigBuilder
* @Time: 2021/5/11 21:29
* @E-mail: 1462320178@qq.com
* @version: 1.0
* @Description: java类作用描述
* @Exception: 无
*/
public class LogConfigBuilder {
/**日志文件路径*/
private final String logPath;
/**日志缓存文件路径*/
private String cachePath;
/**日志文件名称*/
private String logFileName;
/**日志加密公钥*/
private String publicKey;
/**日志缓存天数*/
private int cacheDays;
/**
* 构造器
* 根据日志路径设置默认值
* @param logPath 日志路径
*/
public LogConfigBuilder(String logPath) {
this.logPath = logPath;
this.cachePath = logPath + File.separator + "cache";
this.logFileName = "xLog";
this.publicKey = "";
this.cacheDays = 1;
}
/**
* 设置缓存路径
* @param cachePath 缓存路径
* @return LogConfigBuilder
*/
public LogConfigBuilder setCachePath(String cachePath){
this.cachePath = cachePath;
return this;
}
/**
* 设置日志的文件名
* @param logFileName 日志文件名
* @return LogConfigBuilder
*/
public LogConfigBuilder setLogFileName(String logFileName){
this.logFileName = logFileName;
return this;
}
/**
* 设置加密公钥
* @param publicKey 加密公钥
* @return LogConfigBuilder
*/
public LogConfigBuilder setPublicKey(String publicKey){
this.publicKey = publicKey;
return this;
}
/**
* 设置日志的缓存天数
* @param cacheDays 缓存天数
* @return LogConfigBuilder
*/
public LogConfigBuilder setCacheDays(int cacheDays){
this.cacheDays = cacheDays;
return this;
}
public String getLogPath() {
return logPath;
}
public String getCachePath() {
return cachePath;
}
public String getLogFileName() {
return logFileName;
}
public String getPublicKey() {
return publicKey;
}
public int getCacheDays() {
return cacheDays;
}
}
编辑与logger相关的adapter
package com.tao.lib_log.adapetr;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.orhanobut.logger.LogAdapter;
import com.tao.lib_log.BuildConfig;
import com.tencent.mars.xlog.Log;
import com.tencent.mars.xlog.Xlog;
import static com.orhanobut.logger.Logger.DEBUG;
import static com.orhanobut.logger.Logger.ERROR;
import static com.orhanobut.logger.Logger.INFO;
import static com.orhanobut.logger.Logger.WARN;
/**
* @Author: tao
* @ClassName: XLogAdapter
* @Time: 2021/5/11 21:23
* @E-mail: 1462320178@qq.com
* @version: 1.0
* @Description: java类作用描述
* @Exception: 无
*/
public class XLogAdapter implements LogAdapter {
static {
System.loadLibrary("c++_shared");
System.loadLibrary("marsxlog");
}
public XLogAdapter(final LogConfigBuilder builder) {
Log.setLogImp(new Xlog());
Xlog.appenderOpen(Xlog.LEVEL_ALL,Xlog.AppednerModeAsync,builder.getCachePath(),builder.getLogPath(),
builder.getLogFileName(),builder.getCacheDays(),builder.getPublicKey());
}
@Override
public boolean isLoggable(int priority, @Nullable String tag) {
return !BuildConfig.DEBUG;
}
@Override
public void log(int priority, @Nullable String tag, @NonNull String message) {
switch (priority) {
case DEBUG:
Log.d(tag,message);
break;
case INFO:
Log.i(tag,message);
break;
case WARN:
Log.w(tag,message);
break;
case ERROR:
Log.e(tag,message);
break;
default:
Log.v(tag,message);
break;
}
}
}
LogUtils
package com.tao.lib_log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.orhanobut.logger.AndroidLogAdapter;
import com.orhanobut.logger.BuildConfig;
import com.orhanobut.logger.FormatStrategy;
import com.orhanobut.logger.Logger;
import com.orhanobut.logger.PrettyFormatStrategy;
import com.tao.lib_log.adapetr.LogConfigBuilder;
import com.tao.lib_log.adapetr.XLogAdapter;
import com.tencent.mars.xlog.Log;
/**
* @Author: tao
* @ClassName: LogUtils
* @Time: 2021/5/11 21:16
* @E-mail: 1462320178@qq.com
* @version: 1.0
* @Description: java类作用描述
* @Exception: 无
*/
public class LogUtils {
private LogUtils() {
throw new ClassCastException("LogUtils 不能实例化");
}
/**
* 日志模块初始化
* debug模式下打印日志到logcat
* 非debug模式下将日志输出至指定文件目录
* @param configBuilder 非debug模式日志的文件的配置,为null则不进行日志加载
*/
public static void init(@Nullable LogConfigBuilder configBuilder){
if (com.tao.lib_log.BuildConfig.DEBUG){
FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
.tag("LOG")
.build();
Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));
}else {
if (configBuilder != null){
Logger.addLogAdapter(new XLogAdapter(configBuilder));
}
}
}
/**
* 非debug模式下
* 应用退出关闭日志打印
*/
public static void logClose(){
if (!BuildConfig.DEBUG){
Log.appenderClose();
}
}
/**
* 日志输出 D
* @param message 信息
* @param args 参数
*/
public static void d(@NonNull String message, @Nullable Object... args) {
Logger.d(message, args);
}
/**
* 日志输出 D
* @param object 信息
*/
public static void d(@Nullable Object object) {
Logger.d(object);
}
/**
* 日志输出 E
* @param message 信息
* @param args 参数
*/
public static void e(@NonNull String message, @Nullable Object... args) {
Logger.e(null, message, args);
}
/**
* 日志输出 E
* @param throwable 异常
* @param message 信息
* @param args 参数
*/
public static void e(@Nullable Throwable throwable, @NonNull String message, @Nullable Object... args) {
Logger.e(throwable, message, args);
}
/**
* 日志输出 I
* @param message 信息
* @param args 参数
*/
public static void i(@NonNull String message, @Nullable Object... args) {
Logger.i(message, args);
}
/**
* 日志输出 V
* @param message 信息
* @param args 参数
*/
public static void v(@NonNull String message, @Nullable Object... args) {
Logger.v(message, args);
}
/**
* 日志输出 W
* @param message 信息
* @param args 参数
*/
public static void w(@NonNull String message, @Nullable Object... args) {
Logger.w(message, args);
}
/**
* 意外错误 的 日志输出
* @param message 信息
* @param args 参数
*/
public static void wtf(@NonNull String message, @Nullable Object... args) {
Logger.wtf(message, args);
}
/**
* json 的日志输出
* @param json json
*/
public static void json(@Nullable String json) {
Logger.json(json);
}
/**
* xml 的日志输出
* @param xml xml
*/
public static void xml(@Nullable String xml) {
Logger.xml(xml);
}
}
使用方式
/**初始化*/
LogConfigBuilder logConfigBuilder = new LogConfigBuilder(getFilesDir().getAbsolutePath() + File.pathSeparator + "log");
LogUtils.init(logConfigBuilder);
/**退出*/
LogUtils.logClose();
如果是debug模式则直接在logcat中输出日志。
如果是正式包,则在你设置的目录下输出日志文件。
由于输出的日志是需要解密(即使没有设置密钥),也是需要用Mars源码log/crypt下的解密文件进行解析。
如果加密了则使用decode_mars_crpty_log_file文件,反之则使用decode_mars_nocrpty_log_file文件。
python decode_mars_crpty_log_file.py [对应的文件]
python decode_mars_nocrpty_log_file.py [对应的文件]