背景简介
微信最近开源了mars,其中的xlog模块在兼顾安全性、流畅性、完整性和容错性的前提下,达到了:高性能高压缩率、不丢失任何一行日志、避免系统卡顿和CPU波峰。我们项目正在用react-native开发,也需要一个日志模块能够较好的处理JS端的日志,xlog的出现,是我们项目的不错选择,所以有了react-native-xlog的实现。
日志场景分析
从RN的视角来看,可以分为JS端日志和native端日志。
JS端日志
1.打到控制台的日志
调试RN项目,无论是通过adb logcat或是直接命令行执行react-natie log-android,都可以看到我们项目中调用console.trace/log/warn/error()的地方,都会有对应的日志输出,且包含字符串“ReactNativeJS”,这是在JNI层做的,其底层是直接用android的log实现,详细的此处不做分析。如果要在这边重定向日志到xlog,要么改jni层替代android的log调用,要么改js层的console.log的行为。前者,本人对c++的不熟,不在考虑范围。后者,改默认实现,很hack的行为,应该是可以实现,目前JS研究不够深,实现起来估计也不够直观。
//参见JSLogging.cpp
JSValueRef nativeLoggingHook(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[], JSValueRef *exception) {
android_LogPriority logLevel = ANDROID_LOG_DEBUG;
if (argumentCount > 1) {
int level = (int)Value(ctx, arguments[1]).asNumber();
// The lowest log level we get from JS is 0. We shift and cap it to be
// in the range the Android logging method expects.
logLevel = std::min(
static_cast<android_LogPriority>(level + ANDROID_LOG_DEBUG),
ANDROID_LOG_FATAL);
}
if (argumentCount > 0) {
String message = Value(ctx, arguments[0]).toString();
FBLOG_PRI(logLevel, "ReactNativeJS", "%s", message.str().c_str());// <-- 就在这边
}
return Value::makeUndefined(ctx);
}
2.传给native(java)的日志
调试的时候,JS端代码bug,就经常会遇到红色弹框。其实,JS会在每次和原生通信的时候都会捕获异常信息,传给native
//摘自MessageQueue.js
...
const guard = (fn) => {
try {
fn();
} catch (error) {
ErrorUtils.reportFatalError(error);
}
};
...
callFunctionReturnFlushedQueue(module: string, method: string, args: Array<any>) {
guard(() => {
this.__callFunction(module, method, args);
this.__callImmediates();
});
return this.flushedQueue();
}
...
这边的ErrorUtils.reportFatalError最终会调用ExceptionsManager.js中的reportException方法,可以看到在reportException方法,会根据isFatal会调用原生模块的ExceptionsManager.reportFatalException和ExceptionsManager.reportSoftException
//摘自ExceptionsManager.js
/**
* Handles the developer-visible aspect of errors and exceptions
*/
let exceptionID = 0;
function reportException(e: Error, isFatal: bool) {
const {ExceptionsManager} = require('NativeModules');
if (ExceptionsManager) {
const parseErrorStack = require('parseErrorStack');
const stack = parseErrorStack(e);
const currentExceptionID = ++exceptionID;
if (isFatal) {
ExceptionsManager.reportFatalException(e.message, stack, currentExceptionID);
} else {
ExceptionsManager.reportSoftException(e.message, stack, currentExceptionID);
}
if (__DEV__) {
const symbolicateStackTrace = require('symbolicateStackTrace');
symbolicateStackTrace(stack).then(
(prettyStack) => {
if (prettyStack) {
ExceptionsManager.updateExceptionMessage(e.message, prettyStack, currentExceptionID);
} else {
throw new Error('The stack is null');
}
}
).catch(
(error) => console.warn('Unable to symbolicate stack trace: ' + error.message)
);
}
}
}
...
/**
* Logs exceptions to the (native) console and displays them
*/
function handleException(e: Error, isFatal: boolean) {
// Workaround for reporting errors caused by `throw 'some string'`
// Unfortunately there is no way to figure out the stacktrace in this
// case, so if you ended up here trying to trace an error, look for
// `throw '<error message>'` somewhere in your codebase.
if (!e.message) {
e = new Error(e);
}
if (console._errorOriginal) {
console._errorOriginal(e.message);
} else {
console.error(e.message);
}
reportException(e, isFatal); //将console.error也传给native
}
从贴出来的ExceptionsManager.js源码片段,JS端调用console.error时,也会将错误信息传给native的。
native(java)端的日志
native端的日志,我们只关心java,jni层的不考虑,我们目前的项目没有具体使用场景。
java端的日志,我们可以暂且这么分,一种是RN的日志,一种是我们自己封装的模块的日志。
1.RN的日志
RN java端的日志接口类是FLog,官方提供了一个默认的实现类FLogDefaultLoggingDelegate,同时,也暴露了一个口子,让我们自己实现
public class FLog {
...
private static LoggingDelegate sHandler = FLogDefaultLoggingDelegate.getInstance();
/**
* Sets the logging delegate that overrides the default delegate.
*
* @param delegate the delegate to use
*/
public static void setLoggingDelegate(LoggingDelegate delegate) {
if (delegate == null) {
throw new IllegalArgumentException();
}
sHandler = delegate;
}
回顾下前面说到的JS端传递异常到native端的代码,我们看下native端的实现,可以发现reportFatalException会抛出JavascriptException,RN并未做处理,这就会导致crash,而crash也是我们关心的;而reportSoftException会调用FLog.e进行记录
public class ExceptionsManagerModule extends BaseJavaModule {
...
@ReactMethod
public void reportFatalException(String title, ReadableArray details, int exceptionId) {
showOrThrowError(title, details, exceptionId);
}
@ReactMethod
public void reportSoftException(String title, ReadableArray details, int exceptionId) {
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.showNewJSError(title, details, exceptionId);
} else {
FLog.e(ReactConstants.TAG, stackTraceToString(title, details));
}
}
private void showOrThrowError(String title, ReadableArray details, int exceptionId) {
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.showNewJSError(title, details, exceptionId);
} else {
throw new JavascriptException(stackTraceToString(title, details));
}
}
...
}
2.自己封装模块的日志
顾名思义,自己封装的,你可以自己选择各种实现。当然也有可能抛出各种异常的情况存在。
react-native-xlog设计
JS端
接口
- Xlog.open/close(), 开启/关闭xlog。
- 使用封装的方法Xlog.verbose/debug/info/warn/error/fatal('your custom tag','log message here'),基本和android系统的log级别一致。
实现
具体实现通过封装原生模块完成。
native端:
接口
- 两个初始化接口init/initWithNativeCrashInclude, 后者多包含了crash日志的记录
- 暴露给JS端的接口,和前面JS端的对应。
实现
- 自定义FLog的实现类FLogging2XLogDelegate,将Flog的日志打到xlog
- JS端对应的桥接方法,直接打到xlog
- 自定义UncaughtExceptionHandler的实现类XLogCustomCrashHandler,覆盖系统默认的hander