# 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);
关键说明:
- ErrorUtils.setGlobalHandler(handler)
 - 此方法用于替换 React Native 默认的全局错误处理器,将所有未被 try/catch捕获的异常交给handler处理。
- handler接收两个参数:- error(Error 对象)和- isFatal(布尔值)。其中- isFatal = true时,React Native 默认会显示红屏并终止 JS 执行;可以根据业务决定是否调用默认处理器。
 
- error.stack
 - 包含了多行堆栈信息,包括文件名、行号和函数名,有助于精确定位问题。
 
- 上报到 Native - 通过 NativeModules.ErrorReportModule.sendJSException(...)将错误信息传到 Android 端,后续由 Native 统一存储与上报。
 
- 生产/开发环境差异 - 在开发模式(__DEV__ === true)下,通常保留默认红屏提示以便调试;在生产模式下可选择静默或展示自定义错误页面。
 
3.1.2 示例代码:捕获 JS 错误并上报
在应用入口(如 index.js 或 App.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(...) 接口,用于设置全局未捕获异常处理器。典型流程如下:
- 在自定义的 Application子类中实现Thread.UncaughtExceptionHandler接口。
- 在 onCreate()方法中,通过Thread.setDefaultUncaughtExceptionHandler(...)注册该处理器。
- 当任何未捕获的 Java 异常(如 NullPointerException)抛出时,系统会调用我们的uncaughtException(Thread t, Throwable e)方法。
- 在 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();
    }
}
关键说明:
- 记录并调用默认处理器 - 在 onCreate()中,使用Thread.getDefaultUncaughtExceptionHandler()获取系统默认的异常处理器,并在自定义捕获完成后,可选择调用默认处理器以展示系统弹窗。
 
- 本地写日志 - 将异常堆栈写入应用私有目录下的 crash_logs文件夹中,文件名包含时间戳便于后续查找。
 
- 异步上报 - 利用 OkHttp 在新线程中以 JSON 形式 POST 到后端 REST 接口。
- 上报内容通常包含:时间戳、异常类名与消息、堆栈信息、App 版本、设备信息、系统版本、网络状态等。
 
- 延迟退出 - 在上报完成或等待一定时间后,可选择杀死进程(避免应用处于不稳定状态)。如果想保留“原生 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.setGlobalHandler 的 globalErrorHandler 中,我们只需调用:
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表,字段包括:id、timestamp、type(js/native)、message、stack、deviceInfo、sentStatus(是否已上报)等。
- 优点:可灵活根据条件查询;缺点:实现较文件方案复杂、性能稍低(写入大量日志需注意批量插入优化)。
 
- 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 主要功能模块说明
- JS 全局错误捕获 (- jsExceptionHandler.js)
 - 使用 ErrorUtils.setGlobalHandler捕获所有 JS 未捕获异常,调用NativeModules.ErrorReportModule.sendJSException(...)。
 
- 原生模块 - ErrorReportModule(- ErrorReportModule.java)
 - 暴露 sendJSException(String jsonStr, Promise promise)方法给 JS。
- 将接收到的 jsonStr写入本地文件夹js_error_logs/,并调用统一上报接口。
 
- Java Crash 捕获 (- CrashHandler.java)
 - 实现 Thread.UncaughtExceptionHandler,在uncaughtException(Thread t, Throwable ex)中将异常写入crash_logs/,并上报。
 
- 应用生命周期与注册 (- MyApplication.java)
 - 在 onCreate()中注册Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(...)),并向 React Native 注册ErrorReportModule。
 
- 异步上报逻辑 - 使用 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.js 与 index.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,写文件并上报。
- 所有日志先被写入本地文件,再通过异步线程逐条上传后删除,保证“至少一次”上报。
六、常见问题与最佳实践
- JS 异常捕获不到 - 确认是否已在入口文件(index.js)最早阶段就引入了jsExceptionHandler.js。
- 避免使用第三方框架(如 Redux-Saga)导致的异步错误没有抛出到全局。可在每个 saga 中添加 .catch()。
 
- Native 层异常 handler 被覆盖 - 某些第三方库(如 Crashlytics)会在 Application.onCreate()中设置自己的UncaughtExceptionHandler,导致我们的 Handler 无效。
- 解决办法:在 MyApplication.onCreate()中先获取系统默认 Handler,再将 Crashlytics 的 Handler 包裹或链式调用。
 
- 上报接口频繁失败 - 当网络不可用时,上报会失败。建议在失败时保留本地日志,监听网络恢复后再重试。
- 使用 OkHttp 的拦截器或 WorkManager 进行持久化重试。
 
- 日志文件过多导致存储空间不足 - 定期(如应用启动时)扫描并删除超过一定时长(比如 7 天)的旧日志文件。
- 或在每次写入时检查存储总量是否超限(如 10MB),若超则删除最早的若干文件。
 
- NDK 层 Crash 如何捕获 
七、总结
本文系统性地介绍了 React Native 中的错误采集原理与 Android 平台的实现细节,主要包括:
- JS 层错误捕获:通过 ErrorUtils.setGlobalHandler拦截所有未捕获的 JS 异常,并借助NativeModules将信息传递到 Android 端。
- Native 层错误捕获:使用 Thread.setDefaultUncaughtExceptionHandler捕获 Java 未捕获异常,并写入本地文件和网络上报。
- 异步上报与本地存储:示例代码展示了如何将日志写入私有目录,并使用 OkHttp 在后台线程中将所有待上报日志逐条发送到服务器。
- 完整 Demo:整合各模块,提供一个可直接复制粘贴到项目中的错误采集库示例。
- 流程图解与最佳实践:帮助大家快速理解从 JS 到 Native 再到服务器的错误上报链路,以及实际落地时的注意事项。
通过本文,你应当能够在自己的 React Native 项目中快速集成错误采集功能,实时监控线上异常,并通过日志聚合与分析提升产品可靠性。