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);
关键说明:
ErrorUtils.setGlobalHandler(handler)
- 此方法用于替换 React Native 默认的全局错误处理器,将所有未被
try/catch
捕获的异常交给handler
处理。 handler
接收两个参数:error
(Error 对象)和isFatal
(布尔值)。其中isFatal = true
时,React Native 默认会显示红屏并终止 JS 执行;可以根据业务决定是否调用默认处理器。
- 此方法用于替换 React Native 默认的全局错误处理器,将所有未被
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)
。
- 在上报完成或等待一定时间后,可选择杀死进程(避免应用处于不稳定状态)。如果想保留“原生 Crash 弹窗”,可调用
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(是否已上报)
等。 - 优点:可灵活根据条件查询;缺点:实现较文件方案复杂、性能稍低(写入大量日志需注意批量插入优化)。
- 若需要复杂查询或聚合分析,可借助 SQLite 在本地维护一个
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 包裹或链式调用。
- 某些第三方库(如 Crashlytics)会在
上报接口频繁失败
- 当网络不可用时,上报会失败。建议在失败时保留本地日志,监听网络恢复后再重试。
- 使用 OkHttp 的拦截器或 WorkManager 进行持久化重试。
日志文件过多导致存储空间不足
- 定期(如应用启动时)扫描并删除超过一定时长(比如 7 天)的旧日志文件。
- 或在每次写入时检查存储总量是否超限(如 10MB),若超则删除最早的若干文件。
NDK 层 Crash 如何捕获
- NDK Crash 需要使用 native Crash 处理库,如 Google Breakpad 或 Tencent Bugly NDK。
- 这些库会在本地生成
.apk_crash
或.so_crash
日志,再配合 Java 上传逻辑上报。
七、总结
本文系统性地介绍了 React Native 中的错误采集原理与 Android 平台的实现细节,主要包括:
- JS 层错误捕获:通过
ErrorUtils.setGlobalHandler
拦截所有未捕获的 JS 异常,并借助NativeModules
将信息传递到 Android 端。 - Native 层错误捕获:使用
Thread.setDefaultUncaughtExceptionHandler
捕获 Java 未捕获异常,并写入本地文件和网络上报。 - 异步上报与本地存储:示例代码展示了如何将日志写入私有目录,并使用 OkHttp 在后台线程中将所有待上报日志逐条发送到服务器。
- 完整 Demo:整合各模块,提供一个可直接复制粘贴到项目中的错误采集库示例。
- 流程图解与最佳实践:帮助大家快速理解从 JS 到 Native 再到服务器的错误上报链路,以及实际落地时的注意事项。
通过本文,你应当能够在自己的 React Native 项目中快速集成错误采集功能,实时监控线上异常,并通过日志聚合与分析提升产品可靠性。
评论已关闭