Android Crash 收集与异常处理实现详解
Android 应用稳定性至关重要,通过 UncaughtExceptionHandler 可实现全局异常捕获。如何自定义异常处理器,解析崩溃堆栈信息,并在崩溃时跳转界面展示。此外,还涵盖了长截图生成、动态权限申请(6.0+)以及 FileProvider 配置(7.0+)以支持图片分享功能,确保崩溃信息能准确上报并便于用户反馈。

Android 应用稳定性至关重要,通过 UncaughtExceptionHandler 可实现全局异常捕获。如何自定义异常处理器,解析崩溃堆栈信息,并在崩溃时跳转界面展示。此外,还涵盖了长截图生成、动态权限申请(6.0+)以及 FileProvider 配置(7.0+)以支持图片分享功能,确保崩溃信息能准确上报并便于用户反馈。

在 Android 应用开发中,应用的稳定性直接关系到用户体验和留存率。Crash(崩溃)是用户最常遇到的问题之一。为了及时发现并修复潜在问题,构建一套完善的 Crash 收集系统至关重要。本文将详细介绍如何基于 Java 语言实现全局异常捕获、解析崩溃信息、展示崩溃界面以及支持通过长截图分享崩溃详情。
要实现全局异常的捕获,我们需要利用 Thread 类中的内部接口 UncaughtExceptionHandler。该接口自 JDK 1.5 引入,允许开发者定义当线程发生未捕获异常时的处理逻辑。
我们需要自定义一个类来实现该接口,并将其设置为线程的默认异常处理器。
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static CrashHandler instance = new CrashHandler();
private CrashHandler() {
// 设置默认的全局异常处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
public static CrashHandler getInstance() {
return instance;
}
@Override
public void uncaughtException(Thread t, Throwable ex) {
// 在此处处理崩溃逻辑,如保存日志、上报服务器等
handleException(ex);
}
private void handleException(Throwable ex) {
// 具体处理逻辑
}
}
UncaughtExceptionHandler 会捕获代码中没有被 try-catch 包裹的异常,然后回调给 uncaughtException 方法。这是实现 Crash 收集的基础。
仅仅捕获异常是不够的,我们需要从 Throwable 对象中提取有价值的调试信息,包括崩溃发生的类名、行号、堆栈跟踪、异常消息以及发生时间。
private CrashModel parseCrash(Throwable ex) {
CrashModel model = new CrashModel();
try {
model.setEx(ex);
model.setTime(System.currentTimeMillis());
// 获取根本原因
if (ex.getCause() != null) {
ex = ex.getCause();
}
model.setExceptionMsg(ex.getMessage());
// 将堆栈信息转换为字符串
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
pw.flush();
String exceptionType = ex.getClass().getName();
// 提取第一行堆栈信息
StackTraceElement[] stackTrace = ex.getStackTrace();
if (stackTrace != null && stackTrace.length > 0) {
StackTraceElement element = stackTrace[0];
model.setLineNumber(element.getLineNumber());
model.setClassName(element.getClassName());
model.setFileName(element.getFileName());
model.setMethodName(element.getMethodName());
model.setExceptionType(exceptionType);
}
model.setFullException(sw.toString());
} catch (Exception e) {
// 防止解析过程中再次崩溃
e.printStackTrace();
}
return model;
}
通过上述代码,我们可以结构化地获取崩溃的关键数据,便于后续存储和分析。
在捕获到崩溃后,通常需要将相关信息展示给用户,以便用户反馈或确认。由于此时主线程可能已经处于不稳定状态,我们需要使用 Application Context 来启动 Activity。
Intent intent = new Intent(mContext, CrashActivity.class);
intent.putExtra(CrashActivity.CRASH_MODEL, model);
// 必须设置 FLAG_ACTIVITY_NEW_TASK,因为是在非 Activity 上下文中启动
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
这里 mContext 必须是 Application 的 Context,否则无法正确启动新的任务栈。同时,FLAG_ACTIVITY_NEW_TASK 是必须的标志位。
为了方便用户向开发者反馈,提供分享崩溃信息的功能是非常必要的。这包括分享纯文本信息和分享包含崩溃信息的长截图。
将解析后的崩溃信息拼接成字符串,调用系统的分享 Intent。
private void shareText(String text) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_SUBJECT, "崩溃信息:");
intent.putExtra(Intent.EXTRA_TEXT, text);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(Intent.createChooser(intent, "分享到"));
}
对于复杂的崩溃堆栈,文本形式可能不够直观。生成一张包含完整堆栈信息的长截图是更好的选择。这涉及到 ScrollView 的截图、权限申请以及 FileProvider 的配置。
ScrollView 截图的核心思想是创建一个与 ScrollView 内容区域大小一致的 Bitmap,然后将 ScrollView 的内容绘制到这个 Bitmap 上。
public Bitmap getBitmapByView(ScrollView view) {
if (view == null) return null;
int height = 0;
// 计算 ScrollView 的总高度
for (int i = 0; i < view.getChildCount(); i++) {
height += view.getChildAt(i).getHeight();
}
// 创建对应大小的 Bitmap
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
// 绘制白色背景
canvas.drawColor(Color.WHITE);
// 将 View 绘制到 Canvas 上
view.draw(canvas);
return bitmap;
}
需要注意的是,虽然遍历子 View 可以计算高度,但根据源码分析,ScrollView 实际上只能有一个直接子 View。因此,直接获取第一个子 View 的高度也是可行的优化方案。
Android 6.0 (API 23) 引入了运行时权限机制。敏感权限(如读写外部存储)需要在运行时动态申请。如果用户拒绝授权,应用将无法访问相应资源。
我们需要在需要权限的地方调用 requestPermissions 方法,并在 onRequestPermissionsResult 中处理结果。
// 申请权限
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 授权成功,执行保存图片操作
saveAndShareImage();
} else {
// 授权失败,提示用户
Toast.makeText(activity, "请授予 SD 卡权限才能分享图片", Toast.LENGTH_SHORT).show();
}
}
}
此外,也可以使用第三方库(如 PermissionX)来简化权限申请流程,避免重复编写判断逻辑。
将生成的 Bitmap 写入文件时,需要注意及时回收 Bitmap 以释放内存。
private File bitmapToFile(Bitmap bitmap) {
if (bitmap == null) return null;
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
File imageFile = new File(path, "crash-" + System.currentTimeMillis() + ".jpg");
try {
FileOutputStream out = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
// 及时回收 Bitmap
bitmap.recycle();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return imageFile;
}
从 Android 7.0 (API 24) 开始,Google 提高了安全性,禁止在应用间传递 file:// URI,否则会抛出 FileUriExposedException。为了解决这个问题,需要使用 FileProvider 来生成 content:// URI。
首先,在 res/xml 目录下新建 file_paths.xml 配置文件,声明可共享的目录。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="image_cache" path="Download" />
</paths>
其次,在 AndroidManifest.xml 中声明 Provider 组件。
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
最后,在分享 Intent 时根据系统版本判断使用哪种 URI。
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("image/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri contentUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
intent.putExtra(Intent.EXTRA_STREAM, contentUri);
} else {
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "分享图片"));
本文详细阐述了 Android 应用中 Crash 收集的完整实现流程。通过 UncaughtExceptionHandler 实现全局异常捕获,结合 Throwable 解析获取关键堆栈信息。在此基础上,实现了崩溃界面的展示以及文本和图片的分享功能。针对 Android 6.0 和 7.0+ 的系统特性,分别介绍了动态权限申请和 FileProvider 的安全配置方案。
在实际项目中,建议将收集到的崩溃信息异步上传至后端服务器进行分析,以便快速定位问题。同时,注意保护用户隐私,不要在崩溃日志中泄露用户的敏感个人信息。通过完善的 Crash 监控体系,可以显著提升应用的稳定性和用户体验。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online