‌React Native错误采集原理及Android平台实现详解‌

# ‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌‌React Native 错误采集原理及 Android 平台实现详解

在移动应用开发中,**错误采集**(Error Reporting)能帮助我们在第一时间发现并定位线上问题,极大提升产品质量与用户体验。本文将从错​误采集的整体原理出发,结合 React Native 框架的特点,详细讲解如何在**Android 平台**实现完整的错误采集方案。文章包含架构原理、关键代码示例、ASCII 图解与详细说明,帮助你快速上手并构建自己的错误采集系统。

---

## 目录

1. [前言](#一-前言)  
2. [错误采集原理概览](#二-错误采集原理概览)  
   1. [JS 层错误捕获](#21-js-层错误捕获)  
   2. [Native 层错误捕获(Android)](#22-native-层错误捕获android)  
   3. [React Native 桥与异常传递](#23-react-native-桥与异常传递)  
3. [Android 平台实现详解](#三-android-平台实现详解)  
   1. [JavaScript 层面采集](#31-javascript-层面采集)  
      - [全局异常捕获:ErrorUtils](#311-全局异常捕获errorutils)  
      - [示例代码:捕获 JS 错误并上报](#312-示例代码捕获-js-错误并上报)  
   2. [Native(Java)层面采集](#32-nativejava-层面采集)  
      - [UncaughtExceptionHandler 介绍](#321-uncaughtexceptionhandler-介绍)  
      - [示例代码:在 Application 中设置全局捕获](#322-示例代码在-application-中设置全局捕获)  
   3. [JS 错误向 Native 传递](#33-js-错误向-native-传递)  
      - [使用 NativeModules 传递错误信息](#331-使用-nativemodules-传递错误信息)  
      - [示例代码:JS 调用 Native 上报接口](#332-示例代码js-调用-native-上报接口)  
   4. [错误存储与网络上报](#34-错误存储与网络上报)  
      - [本地存储方案:文件、SQLite 或 SharedPreferences](#341-本地存储方案文件sqlite-或-sharedpreferences)  
      - [网络上报方案:RESTful 接口调用](#342-网络上报方案restful-接口调用)  
      - [示例代码:保存本地并异步上报](#343-示例代码保存本地并异步上报)  
4. [错误上报流程图解](#四-错误上报流程图解)  
5. [集成示例:自定义错误采集库](#五-集成示例自定义错误采集库)  
   1. [代码结构](#51-代码结构)  
   2. [主要功能模块说明](#52-主要功能模块说明)  
   3. [完整 Demo](#53-完整-demo)  
6. [常见问题与最佳实践](#六-常见问题与最佳实践)  
7. [总结](#七-总结)  

---

## 一、前言

React Native 混合了 JavaScript 与原生代码,既有 JS 引擎执行的逻辑错误,也可能因原生模块或第三方库引发的崩溃(Crash)。线上应用若无法及时捕获并上报这些错误,就很难定位问题根源、快速迭代。  

- **JS 层错误**:诸如 `undefined is not an object`、Promise 未捕获的异常、UI 组件渲染出错等,均会在 JS 引擎中抛出异常。  
- **Native 层错误**(Android):Java/Kotlin 抛出的 `NullPointerException`、`IndexOutOfBoundsException`、甚至由于 NDK 引发的 native crash,都需要在原生层进行捕获。  

React Native 提供了 JS 与 Native 互通的“桥”(Bridge)机制,我们可以将 JS 层捕获到的异常传递到 Native,再由 Native 统一进行存储与上报。接下来,本文先从原理层面概述捕获流程,然后深入 Android 平台实现细节。

---

## 二、错误采集原理概览

在 React Native 中,错误采集通常分为两个阶段:  
1. **捕获阶段**:捕获 JS 及 Native 层抛出的异常;  
2. **上报阶段**:将异常信息持久化并发送到服务器,用于后续分析。

主要原理如下:

┌────────────────────────────────────────────────────────────────┐
│ React Native 应用 │
│ ┌───────────┐ ┌────────────┐ ┌───────────────┐ │
│ │ JS 层 │──捕获──▶│ 错误处理 │──Native─▶│ 错误上传组件 │ │
│ │ (ErrorUtils)│ │ (Native Module)│ │ (Retrofit) │ │
│ └───────────┘ └────────────┘ └───────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ 错误抛出 (TypeError, etc.) 错误抛出 (NPE, etc.) │
└────────────────────────────────────────────────────────────────┘


### 2.1 JS 层错误捕获

- 利用 React Native 内置的 [`ErrorUtils`](https://reactnative.dev/docs/javascript-environment#errorutils) 全局对象,拦截未捕获的 JS 异常。  
- 也可在组件中使用 `try/catch` 捕获同步 / 异步异常,或重写 `console.error`、`window.onerror` 来捕获。  
- 捕获后,将关键信息(错误消息、堆栈、设备信息、应用版本号等)封装后,调用 Native 模块进行上报或持久化。  

### 2.2 Native 层错误捕获(Android)

- **Java 异常**:在 `Application` 或某个 `Activity` 中通过 `Thread.setDefaultUncaughtExceptionHandler(...)` 设​置全局的 `UncaughtExceptionHandler`,捕获所有未处理的 Java 异常。  
- **NDK 异常**(Native Crash):若涉及 native 代码,可借助如 [NDK Crash Handler](https://source.android.com/devices/tech/debug) 或第三方库(如 Breakpad、Bugly NDK)进行捕获。  
- 捕获到异常后,同样将信息(`Throwable` 堆栈、设备信息)传入错误采集模块,统一处理。  

### 2.3 React Native 桥与异常传递

- React Native 的桥(Bridge)允许 JS 与 Native 互相调用。JS 捕获到异常后,通过 `NativeModules.ErrorReportModule.sendJSException(...)` 将错误信息传递到 Android Native 端;  
- 对于 Native 层捕获的异常,可直接在 `UncaughtExceptionHandler` 中调用网络请求或存储逻辑;也可以通过 RN 的 `DevSupportManager` 触发 RN 的红屏(仅开发模式)。  
- 最终,所有异常信息都会汇总到同一个“错误采集中心”进行存储(本地缓存)和网络上报。  

---

## 三、Android 平台实现详解

下面我们重点围绕 Android 平台,分层次详细讲解如何捕获并上报 React Native 中的各种异常。

### 3.1 JavaScript 层面采集

#### 3.1.1 全局异常捕获:ErrorUtils

React Native 在 JS 环境中提供了一个全局对象 `ErrorUtils`,可以用来替换默认的错误处理器,从而捕获所有未被 `try/catch` 包裹的异常。典型用法如下:

```js
// src/jsExceptionHandler.js

import { NativeModules } from 'react-native';
const { ErrorReportModule } = NativeModules;

/**
 * 自定义 JS 全局异常处理器
 * @param {Error} error 捕获到的 Error 对象
 * @param {boolean} isFatal 表示是否为致命异常(RN 默认认为部分异常会触发红屏)
 */
function globalErrorHandler(error, isFatal) {
  // 1. 格式化错误信息
  const errorMessage = error.message;
  const stackTrace = error.stack; // 多行堆栈信息

  // 2. 构建上报参数
  const errorInfo = {
    message: errorMessage,
    stack: stackTrace,
    isFatal,
    ts: Date.now(),
    // 可加入更多业务字段,如 React 版本、App 版本、用户 ID 等
  };

  // 3. 调用 Native 模块上报到 Android 端
  ErrorReportModule.sendJSException(JSON.stringify(errorInfo));

  // 4. 若是开发模式,可调用默认处理以显示红屏提示;生产环境可静默处理
  if (__DEV__) {
    // 如果想保留 RN 红屏,可调用默认处理器
    // ErrorUtils.getGlobalHandler()(error, isFatal);
    console.warn('开发环境下,调用默认红屏处理');
  } else {
    // 生产环境:静默或展示自定义错误页面
    console.log('生产环境下,已将错误上报,建议重启应用或跳转到安全页面');
  }
}

// 注册全局异常处理器
ErrorUtils.setGlobalHandler(globalErrorHandler);

关键说明:

  1. ErrorUtils.setGlobalHandler(handler)

    • 此方法用于替换 React Native 默认的全局错误处理器,将所有未被 try/catch 捕获的异常交给 handler 处理。
    • handler 接收两个参数:error(Error 对象)和 isFatal(布尔值)。其中 isFatal = true 时,React Native 默认会显示红屏并终止 JS 执行;可以根据业务决定是否调用默认处理器。
  2. error.stack

    • 包含了多行堆栈信息,包括文件名、行号和函数名,有助于精确定位问题。
  3. 上报到 Native

    • 通过 NativeModules.ErrorReportModule.sendJSException(...) 将错误信息传到 Android 端,后续由 Native 统一存储与上报。
  4. 生产/开发环境差异

    • 在开发模式(__DEV__ === true)下,通常保留默认红屏提示以便调试;在生产模式下可选择静默或展示自定义错误页面。

3.1.2 示例代码:捕获 JS 错误并上报

在应用入口(如 index.jsApp.js)中,需在最早阶段安装全局异常处理器:

// index.js

import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import './jsExceptionHandler'; // 引入全局异常处理模块

AppRegistry.registerComponent(appName, () => App);

此时,任何 JS 运行期间抛出的未捕获异常都会被 globalErrorHandler 捕获,并立即调用 Native 方法进行上报。


3.2 Native(Java)层面采集

在 Android 平台,除了 JS 层可能发生的错误,还需要在 Native 层捕获 Java 层或 NDK 层抛出的异常。

3.2.1 UncaughtExceptionHandler 介绍

Java 提供了 Thread.setDefaultUncaughtExceptionHandler(...) 接口,用于设置全局未捕获异常处理器。典型流程如下:

  1. 在自定义的 Application 子类中实现 Thread.UncaughtExceptionHandler 接口。
  2. onCreate() 方法中,通过 Thread.setDefaultUncaughtExceptionHandler(...) 注册该处理器。
  3. 当任何未捕获的 Java 异常(如 NullPointerException)抛出时,系统会调用我们的 uncaughtException(Thread t, Throwable e) 方法。
  4. uncaughtException 中,可进行日志收集、设备信息采集,并通过网络上报或写入本地文件;也可选择重启应用或直接杀进程。

3.2.2 示例代码:在 Application 中设置全局捕获

// android/app/src/main/java/com/myapp/MyApplication.java

package com.myapp;

import android.app.Application;
import android.content.Context;
import android.os.Looper;
import android.os.Handler;
import android.util.Log;

import androidx.annotation.NonNull;

import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;

import okhttp3.*; // 使用 OkHttp 进行网络上报

public class MyApplication extends Application implements Thread.UncaughtExceptionHandler {

    private static final String TAG = "CrashHandler";
    private Thread.UncaughtExceptionHandler defaultHandler;

    @Override
    public void onCreate() {
        super.onCreate();

        // 1. 记录系统默认的异常处理器
        defaultHandler = Thread.getDefaultUncaughtExceptionHandler();

        // 2. 设置当前 CrashHandler 为默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 全局未捕获异常处理
     *
     * @param thread 抛出异常的线程
     * @param ex     Throwable 对象
     */
    @Override
    public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
        // 1. 将异常信息写入本地文件
        writeExceptionToFile(ex);

        // 2. 异步上报到服务器
        postExceptionToServer(ex);

        // 3. 延迟一段时间后杀进程或调用默认处理器
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            // 若希望保留默认系统弹窗,可调用:
            // defaultHandler.uncaughtException(thread, ex);

            // 否则直接杀死进程
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        }, 2000);
    }

    /**
     * 将 Throwable 信息写入本地文件
     */
    private void writeExceptionToFile(Throwable ex) {
        try {
            File dir = new File(getFilesDir(), "crash_logs");
            if (!dir.exists()) dir.mkdirs();
            String fileName = "crash_" + System.currentTimeMillis() + ".log";
            File logFile = new File(dir, fileName);

            FileWriter fw = new FileWriter(logFile);
            PrintWriter pw = new PrintWriter(fw);
            ex.printStackTrace(pw);
            pw.close();
            fw.close();

            Log.d(TAG, "Exception written to file: " + logFile.getAbsolutePath());
        } catch (Exception e) {
            Log.e(TAG, "Failed to write exception file", e);
        }
    }

    /**
     * 异步上报到服务器
     */
    private void postExceptionToServer(Throwable ex) {
        new Thread(() -> {
            try {
                // 1. 构建 JSON payload
                JSONObject json = new JSONObject();
                json.put("timestamp", System.currentTimeMillis());
                json.put("exception", ex.toString());
                json.put("stack", getStackString(ex));
                json.put("appVersion", "1.0.0");
                json.put("deviceModel", android.os.Build.MODEL);
                // 可根据业务需求添加更多字段

                // 2. 使用 OkHttp 发送 POST 请求
                OkHttpClient client = new OkHttpClient();
                RequestBody body = RequestBody.create(
                    json.toString(),
                    MediaType.parse("application/json; charset=utf-8")
                );
                Request request = new Request.Builder()
                    .url("https://api.example.com/reportCrash")
                    .post(body)
                    .build();

                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    Log.d(TAG, "Crash report sent successfully");
                } else {
                    Log.e(TAG, "Crash report failed: " + response.code());
                }
            } catch (Exception e) {
                Log.e(TAG, "Error posting exception to server", e);
            }
        }).start();
    }

    /**
     * 获取 Throwable 的堆栈字符串
     */
    private String getStackString(Throwable ex) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : ex.getStackTrace()) {
            sb.append(element.toString()).append("\n");
        }
        return sb.toString();
    }
}

关键说明:

  1. 记录并调用默认处理器

    • onCreate() 中,使用 Thread.getDefaultUncaughtExceptionHandler() 获取系统默认的异常处理器,并在自定义捕获完成后,可选择调用默认处理器以展示系统弹窗。
  2. 本地写日志

    • 将异常堆栈写入应用私有目录下的 crash_logs 文件夹中,文件名包含时间戳便于后续查找。
  3. 异步上报

    • 利用 OkHttp 在新线程中以 JSON 形式 POST 到后端 REST 接口。
    • 上报内容通常包含:时间戳、异常类名与消息、堆栈信息、App 版本、设备信息、系统版本、网络状态等。
  4. 延迟退出

    • 在上报完成或等待一定时间后,可选择杀死进程(避免应用处于不稳定状态)。如果想保留“原生 Crash 弹窗”,可调用 defaultHandler.uncaughtException(thread, ex)

3.3 JS 错误向 Native 传递

很多时候我们更关心的是 JS 端的业务逻辑错误,因此需要将 JS 捕获到的异常传递到 Native 层进行统一处理或持久化。

3.3.1 使用 NativeModules 传递错误信息

在 Android 端,需要先创建一个原生模块 ErrorReportModule,暴露给 JS 调用:

// android/app/src/main/java/com/myapp/ErrorReportModule.java

package com.myapp;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import android.util.Log;

import org.json.JSONObject;

/**
 * ErrorReportModule 用于接收 JS 端传过来的异常信息,并进行本地保存或上报
 */
public class ErrorReportModule extends ReactContextBaseJavaModule {
    private static final String TAG = "ErrorReportModule";

    public ErrorReportModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "ErrorReportModule";
    }

    /**
     * JS 端调用该方法上报异常
     *
     * @param jsonStr 包含异常信息的 JSON 字符串
     * @param promise 回调 Promise,用于通知 JS 端是否成功
     */
    @ReactMethod
    public void sendJSException(String jsonStr, Promise promise) {
        try {
            // 1. 解析 JSON
            JSONObject json = new JSONObject(jsonStr);
            String message = json.optString("message");
            String stack = json.optString("stack");
            boolean isFatal = json.optBoolean("isFatal", false);
            long ts = json.optLong("ts");

            // 2. 将异常信息写到本地文件或数据库
            writeJSErrorToFile(message, stack, isFatal, ts);

            // 3. 异步上报到服务器(可与 Java Crash 上报合并接口)
            postJSErrorToServer(json);

            // 成功后返回
            promise.resolve(true);
        } catch (Exception e) {
            Log.e(TAG, "Failed to send JS exception", e);
            promise.reject("ErrorReportFail", e);
        }
    }

    private void writeJSErrorToFile(String message, String stack, boolean isFatal, long ts) {
        // 参考 Java Crash 写文件逻辑,将 JS 错误写入独立目录
        // e.g., getReactApplicationContext().getFilesDir() + "/js_error_logs/"
    }

    private void postJSErrorToServer(JSONObject json) {
        // 直接复用上文 Java Crash 的 postExceptionToServer 方法
        // 或者在这里再构建一个 HTTP 请求上报 JS 错误
    }
}

MainApplication.java 中注册该模块:

// android/app/src/main/java/com/myapp/MainApplication.java

import com.myapp.ErrorReportModule; // 引入

@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
        new MainReactPackage(),
        new ErrorReportPackage() // 自定义 package,返回 ErrorReportModule
    );
}

然后创建 ErrorReportPackage

// android/app/src/main/java/com/myapp/ErrorReportPackage.java

package com.myapp;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class ErrorReportPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Arrays.<NativeModule>asList(new ErrorReportModule(reactContext));
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

3.3.2 示例代码:JS 调用 Native 上报接口

在前文注册了 ErrorUtils.setGlobalHandlerglobalErrorHandler 中,我们只需调用:

import { NativeModules } from 'react-native';
const { ErrorReportModule } = NativeModules;

// 假设已在 globalErrorHandler 中调用
ErrorReportModule.sendJSException(JSON.stringify(errorInfo))
  .then(() => console.log('JS exception reported successfully'))
  .catch((err) => console.error('Failed to report JS exception', err));

当 JS 端捕获到错误时,会将 errorInfo 以 JSON 字符串形式传给 Native,再由 Native 统一写文件或上报。


3.4 错误存储与网络上报

3.4.1 本地存储方案:文件、SQLite 或 SharedPreferences

  • 文件方案

    • 最简单也是最常用的方式:将异常日志写入应用私有目录(getFilesDir())下的 crash_logs/js_error_logs/ 文件夹,每次写一个新文件,文件名可包含时间戳,示例如:

      /data/data/com.myapp/files/crash_logs/crash_1625078400000.log
      /data/data/com.myapp/files/js_error_logs/js_error_1625078400000.log
    • 优点:实现简单、易区分;缺点:文件数量多时需定期清理,可自行在写入时检查旧日志并删除超过一定条数或时间的文件。
  • SQLite 方案

    • 若需要复杂查询或聚合分析,可借助 SQLite 在本地维护一个 errors 表,字段包括:idtimestamptype(js/native)messagestackdeviceInfosentStatus(是否已上报) 等。
    • 优点:可灵活根据条件查询;缺点:实现较文件方案复杂、性能稍低(写入大量日志需注意批量插入优化)。
  • SharedPreferences 方案

    • 一般只适用于保存少量最后一次错误信息,可用于应用重启后显示上次崩溃原因,但不适合长期存储大量日志。

3.4.2 网络上报方案:RESTful 接口调用

  • 统一上报接口

    • 后端可以提供一个 POST /api/v1/reportError 接口,接收 JSON 格式错误信息,包括:

      {
        "type": "js" | "native",
        "timestamp": 1625078400000,
        "message": "TypeError: undefined is not an object",
        "stack": "...",
        "deviceModel": "Pixel 5",
        "osVersion": "Android 11",
        "appVersion": "1.0.0",
        "network": "WIFI",
        "userId": "12345"
      }
    • Android 使用 OkHttp 或 Retrofit 进行异步 POST;iOS 可用 Alamofire;JS 可用 fetch()
  • 批量上报

    • 当网络恢复时(监听网络变化),可一次性将本地缓存中的多条日志批量上报,以减少网络请求次数并保证严格的“至少一次”上报语义。
  • 失败重试与幂等

    • 若上报失败(如网络中断),可保存到本地并在下一次网络可达时重试;后端可根据设备 ID + 时间戳做幂等去重。

3.4.3 示例代码:保存本地并异步上报

ErrorReportModule.sendJSException 中,我们可以先将 JSON 字符串写入本地文件,再调用一个统一的 uploadPendingLogs() 方法,将所有未上报的日志文件逐个发送至服务器并删除:

// android/app/src/main/java/com/myapp/ErrorReportModule.java

private void writeJSErrorToFile(String message, String stack, boolean isFatal, long ts) {
    try {
        File dir = new File(getReactApplicationContext().getFilesDir(), "js_error_logs");
        if (!dir.exists()) dir.mkdirs();
        String fileName = "js_" + ts + ".log";
        File logFile = new File(dir, fileName);

        FileWriter fw = new FileWriter(logFile);
        PrintWriter pw = new PrintWriter(fw);
        pw.println("timestamp:" + ts);
        pw.println("isFatal:" + isFatal);
        pw.println("message:" + message);
        pw.println("stack:");
        pw.println(stack);
        pw.close();
        fw.close();
        Log.d(TAG, "JS Exception written to file: " + logFile.getAbsolutePath());

        // 保存完文件后,尝试上报所有待上传日志
        uploadPendingLogs("js_error_logs");
    } catch (Exception e) {
        Log.e(TAG, "Failed to write JS exception file", e);
    }
}

private void uploadPendingLogs(String subDir) {
    new Thread(() -> {
        try {
            File dir = new File(getReactApplicationContext().getFilesDir(), subDir);
            if (!dir.exists() || !dir.isDirectory()) return;
            File[] files = dir.listFiles();
            if (files == null || files.length == 0) return;

            OkHttpClient client = new OkHttpClient();
            for (File logFile : files) {
                // 1. 读取文件内容
                StringBuilder sb = new StringBuilder();
                java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(logFile));
                String line;
                while ((line = br.readLine()) != null) {
                    sb.append(line).append("\n");
                }
                br.close();

                // 2. 构建 JSON 对象
                JSONObject payload = new JSONObject();
                payload.put("type", subDir.startsWith("js") ? "js" : "native");
                payload.put("log", sb.toString());
                // 可加入额外字段:App 版本、设备信息等

                // 3. 发送 POST 请求
                RequestBody body = RequestBody.create(
                    payload.toString(),
                    MediaType.parse("application/json; charset=utf-8")
                );
                Request request = new Request.Builder()
                    .url("https://api.example.com/reportError")
                    .post(body)
                    .build();

                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    // 删除已成功上报的文件
                    logFile.delete();
                    Log.d(TAG, "Uploaded and deleted log file: " + logFile.getName());
                } else {
                    Log.e(TAG, "Upload failed for file: " + logFile.getName() + ", code: " + response.code());
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Error uploading pending logs", e);
        }
    }).start();
}

四、错误上报流程图解

下面用 ASCII 图示展示从 JS 抛出异常到 Android 层捕获并上报的完整流程:

┌───────────────────────────────────────────────────────────────────┐
│                           React Native JS                        │
│  ┌───────────────────────────────────────────────────────────────┐│
│  │        1. 代码执行抛出未捕获异常 (e.g., TypeError)             ││
│  │  ┌─────────────────────────────────────────────────────────┐  ││
│  │  │ ErrorUtils.setGlobalHandler 捕获 (globalErrorHandler)   │  ││
│  │  │ - 格式化错误信息 (message, stack, ts, isFatal)          │  ││
│  │  │ - 调用: ErrorReportModule.sendJSException(jsonString)   │─┐│
│  │  └─────────────────────────────────────────────────────────┘  ││
│  │              ▲                                                ││
│  │              │  (Bridge: JS → Native)                          ││
│  └──────────────┴─────────────────────────────────────────────────┘│
│              │                                                       │
│              ▼                                                       │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │                    Android Native (ErrorReportModule)         │  │
│  │  ┌──────────────────────────────────────────────────────────┐  │  │
│  │  │ sendJSException(jsonString)                             │  │  │
│  │  │ - 解析 JSON                                              │  │  │
│  │  │ - writeJSErrorToFile 写入 /files/js_error_logs/          │  │  │
│  │  │ - uploadPendingLogs 上传到 https://.../reportError     │  │  │
│  │  └──────────────────────────────────────────────────────────┘  │  │
│  │            ▲                                                  │  │
│  │            │ (异步上报,如成功则删文件; 失败则留待下次重试)     │  │
│  └────────────┴──────────────────────────────────────────────────┘  │
│              │                                                       │
│              ▼                                                       │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  2. Native 层 (Java CrashHandler) 捕获 Java 未捕获异常           │  │
│  │  Thread.setDefaultUncaughtExceptionHandler 监听 NPE, etc.       │  │
│  │  ┌──────────────────────────────────────────────────────────┐  │  │
│  │  │ uncaughtException(Thread t, Throwable ex)                │  │  │
│  │  │ - writeExceptionToFile 写 /files/crash_logs/              │  │  │
│  │  │ - postExceptionToServer 上传到 https://.../reportError   │  │  │
│  │  └──────────────────────────────────────────────────────────┘  │  │
│  │                  ▲                                            │  │
│  │                  │ (同样采用异步网络上报并删除已上报文件)   │  │
│  └──────────────────┴────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────────────────┘
  • JS 层:通过 ErrorUtils.setGlobalHandler 捕获未处理的 JS 异常,并调用 Native 模块上报。
  • Bridge:React Native 桥负责将 sendJSException 调用转给 Android 原生 ErrorReportModule
  • Native 层 JS 上报ErrorReportModule 将信息写入 /files/js_error_logs/,并尝试上传到服务器。
  • Native 层 Java Crash:通过 Thread.setDefaultUncaughtExceptionHandler 捕获所有 Java 未捕获异常,同样写入 /files/crash_logs/ 并异步上传。

五、集成示例:自定义错误采集库

下面我们以一个完整的自定义错误采集库为示例,演示如何将上述各模块结合起来,快速集成到 React Native 项目中。

5.1 代码结构

myapp/
├── android/
│   ├── app/
│   │   ├── src/
│   │   │   ├── main/
│   │   │   │   ├── java/com/myapp/
│   │   │   │   │   ├── ErrorReportModule.java
│   │   │   │   │   ├── ErrorReportPackage.java
│   │   │   │   │   ├── MyApplication.java
│   │   │   │   │   └── CrashHandler.java
│   │   │   └── ...
│   │   └── AndroidManifest.xml
│   └── build.gradle
├── src/
│   ├── jsExceptionHandler.js   // JS 全局异常捕获
│   └── App.js
├── index.js                    // 应用入口
└── package.json
  • ErrorReportModule.java:负责接收 JS 异常并存储/上报。
  • CrashHandler.java:实现 Thread.UncaughtExceptionHandler,负责捕获 Java 异常。
  • MyApplication.java:在 Application 中注册 CrashHandler,并导入 ErrorReportModule
  • jsExceptionHandler.js:安装 ErrorUtils 全局异常处理。
  • App.js / index.js:应用入口,加载全局异常处理器并启动主界面。

5.2 主要功能模块说明

  1. JS 全局错误捕获 (jsExceptionHandler.js)

    • 使用 ErrorUtils.setGlobalHandler 捕获所有 JS 未捕获异常,调用 NativeModules.ErrorReportModule.sendJSException(...)
  2. 原生模块 ErrorReportModule (ErrorReportModule.java)

    • 暴露 sendJSException(String jsonStr, Promise promise) 方法给 JS。
    • 将接收到的 jsonStr 写入本地文件夹 js_error_logs/,并调用统一上报接口。
  3. Java Crash 捕获 (CrashHandler.java)

    • 实现 Thread.UncaughtExceptionHandler,在 uncaughtException(Thread t, Throwable ex) 中将异常写入 crash_logs/,并上报。
  4. 应用生命周期与注册 (MyApplication.java)

    • onCreate() 中注册 Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(...)),并向 React Native 注册 ErrorReportModule
  5. 异步上报逻辑

    • 使用 OkHttp 在新线程里将所有待上报的日志文件逐条发送至后端 REST 接口,并在成功后删除对应文件。

5.3 完整 Demo

下文给出各模块的完整代码示例,帮助你快速复制到自己的项目中使用。

5.3.1 CrashHandler.java

// android/app/src/main/java/com/myapp/CrashHandler.java

package com.myapp;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;

import okhttp3.*;

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = "CrashHandler";
    private Context mContext;
    private Thread.UncaughtExceptionHandler defaultHandler;

    public CrashHandler(Context context) {
        mContext = context;
        defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
    }

    @Override
    public void uncaughtException(Thread t, Throwable ex) {
        // 写入本地
        writeExceptionToFile(ex);

        // 上报到服务器
        postExceptionToServer(ex);

        // 延迟退出
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            // 可调用默认处理器(系统弹窗),或直接杀进程
            // defaultHandler.uncaughtException(t, ex);
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        }, 2000);
    }

    private void writeExceptionToFile(Throwable ex) {
        try {
            File dir = new File(mContext.getFilesDir(), "crash_logs");
            if (!dir.exists()) dir.mkdirs();
            String fileName = "native_" + System.currentTimeMillis() + ".log";
            File logFile = new File(dir, fileName);

            FileWriter fw = new FileWriter(logFile);
            PrintWriter pw = new PrintWriter(fw);
            ex.printStackTrace(pw);
            pw.close();
            fw.close();
            Log.d(TAG, "Native exception written to: " + logFile.getAbsolutePath());
        } catch (Exception e) {
            Log.e(TAG, "Failed to write native exception", e);
        }
    }

    private void postExceptionToServer(Throwable ex) {
        new Thread(() -> {
            try {
                JSONObject json = new JSONObject();
                json.put("type", "native");
                json.put("timestamp", System.currentTimeMillis());
                json.put("message", ex.toString());
                json.put("stack", getStackString(ex));
                json.put("appVersion", "1.0.0");
                json.put("deviceModel", android.os.Build.MODEL);

                OkHttpClient client = new OkHttpClient();
                RequestBody body = RequestBody.create(
                    json.toString(),
                    MediaType.parse("application/json; charset=utf-8")
                );
                Request request = new Request.Builder()
                    .url("https://api.example.com/reportError")
                    .post(body)
                    .build();

                Response response = client.newCall(request).execute();
                if (response.isSuccessful()) {
                    Log.d(TAG, "Native crash report sent");
                    // 可根据业务删除本地文件
                } else {
                    Log.e(TAG, "Native crash report failed: " + response.code());
                }
            } catch (Exception e) {
                Log.e(TAG, "Error sending native crash to server", e);
            }
        }).start();
    }

    private String getStackString(Throwable ex) {
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : ex.getStackTrace()) {
            sb.append(element.toString()).append("\n");
        }
        return sb.toString();
    }
}

5.3.2 ErrorReportModule.java

// android/app/src/main/java/com/myapp/ErrorReportModule.java

package com.myapp;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import android.util.Log;

import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;

import okhttp3.*;

public class ErrorReportModule extends ReactContextBaseJavaModule {
    private static final String TAG = "ErrorReportModule";

    public ErrorReportModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "ErrorReportModule";
    }

    @ReactMethod
    public void sendJSException(String jsonStr, Promise promise) {
        try {
            JSONObject json = new JSONObject(jsonStr);
            String message = json.optString("message");
            String stack = json.optString("stack");
            boolean isFatal = json.optBoolean("isFatal", false);
            long ts = json.optLong("ts");

            writeJSErrorToFile(message, stack, isFatal, ts);
            uploadPendingLogs("js_error_logs");

            promise.resolve(true);
        } catch (Exception e) {
            Log.e(TAG, "Failed to send JS exception", e);
            promise.reject("ErrorReportFail", e);
        }
    }

    private void writeJSErrorToFile(String message, String stack, boolean isFatal, long ts) {
        try {
            File dir = new File(getReactApplicationContext().getFilesDir(), "js_error_logs");
            if (!dir.exists()) dir.mkdirs();
            String fileName = "js_" + ts + ".log";
            File logFile = new File(dir, fileName);

            FileWriter fw = new FileWriter(logFile);
            PrintWriter pw = new PrintWriter(fw);
            pw.println("timestamp:" + ts);
            pw.println("isFatal:" + isFatal);
            pw.println("message:" + message);
            pw.println("stack:");
            pw.println(stack);
            pw.close();
            fw.close();
            Log.d(TAG, "JS exception written to: " + logFile.getAbsolutePath());
        } catch (Exception e) {
            Log.e(TAG, "Failed to write JS exception file", e);
        }
    }

    private void uploadPendingLogs(String subDir) {
        new Thread(() -> {
            try {
                File dir = new File(getReactApplicationContext().getFilesDir(), subDir);
                if (!dir.exists() || !dir.isDirectory()) return;
                File[] files = dir.listFiles();
                if (files == null || files.length == 0) return;

                OkHttpClient client = new OkHttpClient();
                for (File logFile : files) {
                    StringBuilder sb = new StringBuilder();
                    java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(logFile));
                    String line;
                    while ((line = br.readLine()) != null) {
                        sb.append(line).append("\n");
                    }
                    br.close();

                    JSONObject payload = new JSONObject();
                    payload.put("type", "js");
                    payload.put("log", sb.toString());
                    payload.put("appVersion", "1.0.0");
                    payload.put("deviceModel", android.os.Build.MODEL);

                    RequestBody body = RequestBody.create(
                        payload.toString(),
                        MediaType.parse("application/json; charset=utf-8")
                    );
                    Request request = new Request.Builder()
                        .url("https://api.example.com/reportError")
                        .post(body)
                        .build();

                    Response response = client.newCall(request).execute();
                    if (response.isSuccessful()) {
                        logFile.delete();
                        Log.d(TAG, "Uploaded and deleted JS log: " + logFile.getName());
                    } else {
                        Log.e(TAG, "Upload failed for JS log: " + logFile.getName());
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Error uploading pending JS logs", e);
            }
        }).start();
    }
}

5.3.3 MyApplication.java

// android/app/src/main/java/com/myapp/MyApplication.java

package com.myapp;

import android.app.Application;
import android.util.Log;

import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;

import java.util.Arrays;
import java.util.List;

public class MyApplication extends Application implements ReactApplication {
    private static final String TAG = "MyApplication";

    private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Override
        public boolean getUseDeveloperSupport() {
            return BuildConfig.DEBUG;
        }

        @Override
        protected List<ReactPackage> getPackages() {
            return Arrays.<ReactPackage>asList(
                new MainReactPackage(),
                new ErrorReportPackage()
            );
        }

        @Override
        protected String getJSMainModuleName() {
            return "index";
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        // 注册 Java 全局异常捕获
        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this));
        Log.d(TAG, "CrashHandler registered");
    }

    @Override
    public ReactNativeHost getReactNativeHost() {
        return mReactNativeHost;
    }
}

5.3.4 jsExceptionHandler.js

// src/jsExceptionHandler.js

import { NativeModules } from 'react-native';
const { ErrorReportModule } = NativeModules;

/**
 * 全局 JS 错误处理器
 */
function globalErrorHandler(error, isFatal) {
  const errorMessage = error.message;
  const stackTrace = error.stack;
  const errorInfo = {
    message: errorMessage,
    stack: stackTrace,
    isFatal,
    ts: Date.now(),
  };

  ErrorReportModule.sendJSException(JSON.stringify(errorInfo))
    .then(() => console.log('JS exception reported'))
    .catch((err) => console.error('Failed to report JS exception', err));

  if (__DEV__) {
    // 保留红屏提示
    console.warn('开发模式:调用默认红屏处理');
    ErrorUtils.getGlobalHandler()(error, isFatal);
  } else {
    // 生产模式:静默处理或显示自定义页面
    console.log('生产模式:JS 错误已上报,建议重启应用');
  }
}

// 安装全局错误处理器
ErrorUtils.setGlobalHandler(globalErrorHandler);

5.3.5 App.jsindex.js

// App.js

import React from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';

export default function App() {
  // 故意抛出一个未捕获异常用于测试
  const throwError = () => {
    // 下面这一行将触发 JS 错误
    const a = undefined;
    console.log(a.b.c);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>React Native 错误采集 Demo</Text>
      <Button title="触发 JS 错误" onPress={throwError} />
      <Button
        title="触发本地 Crash"
        onPress={() => {
          throw new Error('模拟本地 Crash'); // 可触发 Native Java Crash
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 24 },
});
// index.js

import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import './src/jsExceptionHandler'; // 引入全局异常捕获模块

AppRegistry.registerComponent(appName, () => App);

至此,一个完整的React Native 错误采集库已集成完毕:

  • JS 层:未捕获异常会触发 globalErrorHandler,调用 ErrorReportModule.sendJSException
  • Native(Java)层:未捕获的 Java Exception 会触发 CrashHandler.uncaughtException,写文件并上报。
  • 所有日志先被写入本地文件,再通过异步线程逐条上传后删除,保证“至少一次”上报。

六、常见问题与最佳实践

  1. JS 异常捕获不到

    • 确认是否已在入口文件(index.js)最早阶段就引入了 jsExceptionHandler.js
    • 避免使用第三方框架(如 Redux-Saga)导致的异步错误没有抛出到全局。可在每个 saga 中添加 .catch()
  2. Native 层异常 handler 被覆盖

    • 某些第三方库(如 Crashlytics)会在 Application.onCreate() 中设置自己的 UncaughtExceptionHandler,导致我们的 Handler 无效。
    • 解决办法:在 MyApplication.onCreate() 中先获取系统默认 Handler,再将 Crashlytics 的 Handler 包裹或链式调用。
  3. 上报接口频繁失败

    • 当网络不可用时,上报会失败。建议在失败时保留本地日志,监听网络恢复后再重试。
    • 使用 OkHttp 的拦截器或 WorkManager 进行持久化重试。
  4. 日志文件过多导致存储空间不足

    • 定期(如应用启动时)扫描并删除超过一定时长(比如 7 天)的旧日志文件。
    • 或在每次写入时检查存储总量是否超限(如 10MB),若超则删除最早的若干文件。
  5. NDK 层 Crash 如何捕获

    • NDK Crash 需要使用 native Crash 处理库,如 Google BreakpadTencent Bugly NDK
    • 这些库会在本地生成 .apk_crash.so_crash 日志,再配合 Java 上传逻辑上报。

七、总结

本文系统性地介绍了 React Native 中的错误采集原理与 Android 平台的实现细节,主要包括:

  1. JS 层错误捕获:通过 ErrorUtils.setGlobalHandler 拦截所有未捕获的 JS 异常,并借助 NativeModules 将信息传递到 Android 端。
  2. Native 层错误捕获:使用 Thread.setDefaultUncaughtExceptionHandler 捕获 Java 未捕获异常,并写入本地文件和网络上报。
  3. 异步上报与本地存储:示例代码展示了如何将日志写入私有目录,并使用 OkHttp 在后台线程中将所有待上报日志逐条发送到服务器。
  4. 完整 Demo:整合各模块,提供一个可直接复制粘贴到项目中的错误采集库示例。
  5. 流程图解与最佳实践:帮助大家快速理解从 JS 到 Native 再到服务器的错误上报链路,以及实际落地时的注意事项。

通过本文,你应当能够在自己的 React Native 项目中快速集成错误采集功能,实时监控线上异常,并通过日志聚合与分析提升产品可靠性。

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日