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<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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容