Android实现异常捕获并API上报:UncaughtExceptionHandler问题咨询
我之前在做Android异常上报的时候也遇到过完全一样的问题,给你分享几个经过实践验证的方案,应该能解决你的痛点:
方案一:结合全局异常处理器 + 手动上报工具(最稳妥的基础方案)
这个方案既能覆盖未捕获的全局异常,又能处理try-catch里的异常,同时解决和现有HTTP请求的冲突:
步骤1:实现全局UncaughtExceptionHandler
核心思路是不直接在崩溃时发起网络请求(避免应用退出前网络请求不稳定,或者和现有请求冲突),而是先把异常信息缓存到本地,等下次应用启动时再上报。同时要保留系统默认的异常处理器,确保应用能正常崩溃退出。
代码示例:
public class GlobalCrashHandler implements Thread.UncaughtExceptionHandler { private final Thread.UncaughtExceptionHandler defaultHandler; private final Context appContext; public GlobalCrashHandler(Context context) { this.appContext = context.getApplicationContext(); this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); } @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { // 1. 收集异常信息(堆栈、设备型号、系统版本等) String crashDetails = collectCrashInfo(throwable); // 2. 保存到本地文件(内部存储,避免权限问题) saveCrashToLocal(crashDetails); // 3. 交给系统默认处理器处理,保证应用正常崩溃 if (defaultHandler != null) { defaultHandler.uncaughtException(thread, throwable); } else { Process.killProcess(Process.myPid()); System.exit(1); } } private String collectCrashInfo(Throwable throwable) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); throwable.printStackTrace(pw); // 遍历所有cause,收集完整堆栈 Throwable cause = throwable.getCause(); while (cause != null) { cause.printStackTrace(pw); cause = cause.getCause(); } pw.close(); // 可以在这里添加设备信息,比如Build.MODEL、Build.VERSION.RELEASE等 return "设备型号:" + Build.MODEL + "\n系统版本:" + Build.VERSION.RELEASE + "\n异常堆栈:\n" + sw.toString(); } private void saveCrashToLocal(String crashDetails) { File crashFile = new File(appContext.getFilesDir(), "pending_crash.log"); try (FileWriter writer = new FileWriter(crashFile, true)) { writer.write("===== " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " =====\n"); writer.write(crashDetails + "\n\n"); } catch (IOException e) { e.printStackTrace(); } } }
步骤2:实现通用异常上报工具类
这个工具类负责发起HTTP POST请求,使用独立的网络客户端(不要复用应用内全局的OkHttpClient,避免拦截器、Cookie等配置冲突),并且在后台线程执行上报:
public class ExceptionReporter { private static final String REPORT_API_URL = "你的上报API端点"; // 独立的OkHttpClient,避免和应用内其他请求冲突 private static final OkHttpClient REPORT_CLIENT = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); // 上报异常(支持手动调用和全局崩溃后启动上报) public static void reportException(String exceptionDetails) { // 用协程或者线程池执行后台任务 CoroutineScope(Dispatchers.IO).launch { try { RequestBody body = RequestBody.create( MediaType.parse("text/plain; charset=utf-8"), exceptionDetails ); Request request = new Request.Builder() .url(REPORT_API_URL) .post(body) .build(); Response response = REPORT_CLIENT.newCall(request).execute(); if (response.isSuccessful()) { // 上报成功,删除本地缓存的异常文件 deletePendingCrashFile(); } else { // 上报失败,保留文件下次再试 Log.e("ExceptionReporter", "上报失败,状态码:" + response.code()); } } catch (IOException e) { e.printStackTrace(); // 网络异常,保留文件下次再试 } } } // 手动上报单个异常(用于try-catch块) public static void reportException(Exception e) { String details = collectExceptionDetails(e); reportException(details); } private static String collectExceptionDetails(Exception e) { // 和collectCrashInfo逻辑类似,收集单个异常的信息 StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); pw.close(); return "手动捕获异常:\n" + sw.toString(); } private static void deletePendingCrashFile() { File crashFile = new File(App.getInstance().getFilesDir(), "pending_crash.log"); if (crashFile.exists()) { crashFile.delete(); } } }
步骤3:注册处理器并启动时检查上报
在自定义Application的onCreate方法里注册全局处理器,同时检查本地是否有未上报的异常:
public class App extends Application { private static App instance; @Override public void onCreate() { super.onCreate(); instance = this; // 注册全局异常处理器 Thread.setDefaultUncaughtExceptionHandler(new GlobalCrashHandler(this)); // 检查并上报上次崩溃的异常 checkPendingCrashReport(); } private void checkPendingCrashReport() { File crashFile = new File(getFilesDir(), "pending_crash.log"); if (crashFile.exists() && crashFile.length() > 0) { try (BufferedReader reader = new BufferedReader(new FileReader(crashFile))) { StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line).append("\n"); } ExceptionReporter.reportException(sb.toString()); } catch (IOException e) { e.printStackTrace(); } } } public static App getInstance() { return instance; } }
步骤4:在try-catch块中手动上报
对于现有代码里的try-catch,只需要在catch块里添加一行代码:
try { // 你的业务代码,比如发起HTTP请求 } catch (IOException e) { // 原来的异常处理逻辑 e.printStackTrace(); // 新增:上报异常 ExceptionReporter.reportException(e); }
方案二:用AOP自动拦截所有try-catch异常(减少代码侵入)
如果你的项目里try-catch块很多,不想手动修改每一处,可以用AspectJ实现切面编程,自动拦截所有catch块中的异常并上报,完全不需要修改现有业务代码。
步骤1:添加AspectJ依赖
在项目根目录的build.gradle中添加:
buildscript { dependencies { classpath 'org.aspectj:aspectjtools:1.9.9.1' } }
在模块的build.gradle中添加:
plugins { id 'com.android.application' } import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main android { // 你的项目配置 } dependencies { implementation 'org.aspectj:aspectjrt:1.9.9.1' } // 配置AspectJ编译流程 final def log = project.logger final def variants = project.android.applicationVariants variants.all { variant -> if (!variant.buildType.isDebuggable()) { log.debug("跳过非Debug构建:${variant.buildType.name}") return } JavaCompile javaCompile = variant.javaCompile javaCompile.doLast { String[] args = [ "-showWeaveInfo", "-1.8", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator) ] log.debug("AspectJ编译参数:${Arrays.toString(args)}") MessageHandler handler = new MessageHandler(true) new Main().run(args, handler) // 打印编译日志 for (IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT, IMessage.ERROR, IMessage.FAIL: log.error(message.message, message.thrown) break case IMessage.WARNING: log.warn(message.message, message.thrown) break case IMessage.INFO: log.info(message.message, message.thrown) break case IMessage.DEBUG: log.debug(message.message, message.thrown) break } } } }
步骤2:编写切面类
@Aspect public class ExceptionCatchAspect { // 定义切点:拦截所有包下的Exception捕获 @Pointcut("handler(java.lang.Exception) && within(com.your.package..*)") public void allExceptionCatch() {} // 切点触发后执行上报 @AfterThrowing(pointcut = "allExceptionCatch()", throwing = "e") public void afterExceptionCaught(Exception e) { ExceptionReporter.reportException(e); } }
这样所有try-catch块捕获的Exception都会自动调用上报方法,完全不需要手动修改业务代码。
方案三:补充说明:解决HTTP请求冲突的关键
不管用哪种方案,要避免和现有HTTP请求冲突,核心注意两点:
- 使用独立的网络客户端:上报请求不要复用应用内全局的OkHttpClient(尤其是带有自定义拦截器、CookieJar、证书配置的),单独创建一个极简的客户端,避免互相干扰。
- 在后台线程执行上报:永远不要在主线程发起上报请求,用协程、线程池或者IntentService(不推荐)执行,避免阻塞主线程或者ANR。
内容的提问来源于stack exchange,提问作者wap
相关产品推荐
相关产品推荐

