react-native-xlog开发回顾

背景简介

微信最近开源了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(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) {
   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 ''` 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

你可能感兴趣的:(react-native-xlog开发回顾)