Files
WinBoLL/libappbase/src/main/java/cc/winboll/studio/libappbase/CrashHandler.java

559 lines
21 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cc.winboll.studio.libappbase;
import android.app.Activity;
import android.app.Application;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
* @Date 2025/11/11 20:14
* @Describe * 应用全局崩溃处理类(单例逻辑)
* 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
* 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
*/
public final class CrashHandler {
/** 日志标签,用于当前类的日志输出标识 */
public static final String TAG = "CrashHandler";
/** 崩溃报告页面标题 */
public static final String TITTLE = "CrashReport";
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
public static final String EXTRA_CRASH_LOG = "crashInfo";
/** SharedPreferences 存储键(用于记录崩溃状态) */
final static String PREFS = CrashHandler.class.getName() + "PREFS";
/** SharedPreferences 中存储「是否发生崩溃」的键 */
final static String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
/** 崩溃保险丝状态文件路径(存储当前熔断等级) */
public static String _CrashCountFilePath;
/** 系统默认的未捕获异常处理器(用于降级处理,避免 CrashHandler 自身崩溃) */
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER = Thread.getDefaultUncaughtExceptionHandler();
/**
* 初始化崩溃处理器(默认存储路径)
* 调用重载方法,崩溃日志默认存储在应用外部私有目录的 crash 文件夹下
* @param app 全局 Application 实例(用于获取存储目录、包信息等)
*/
public static void init(Application app) {
// 初始化崩溃保险丝状态文件路径(外部存储/CrashHandler/IsCrashHandlerCrashHappen.dat
_CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
LogUtils.d(TAG, String.format("_CrashCountFilePath %s", _CrashCountFilePath));
// 调用带目录参数的初始化方法,传入 null 使用默认路径
init(app, null);
}
/**
* 初始化崩溃处理器(指定日志存储目录)
* 替换系统默认的未捕获异常处理器,自定义崩溃处理逻辑
* @param app 全局 Application 实例
* @param crashDir 崩溃日志存储目录null 则使用默认路径)
*/
public static void init(final Application app, final String crashDir) {
// 设置自定义未捕获异常处理器
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
// 尝试处理崩溃(捕获内部异常,避免 CrashHandler 自身崩溃)
tryUncaughtException(thread, throwable);
} catch (Throwable e) {
e.printStackTrace();
// 处理失败时,交给系统默认处理器兜底
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
}
/**
* 实际处理崩溃的核心方法
* 1. 熔断保险丝记录崩溃次数2. 收集崩溃信息3. 写入日志文件4. 启动崩溃报告页面
* @param thread 发生崩溃的线程
* @param throwable 崩溃异常对象(包含堆栈信息)
*/
private void tryUncaughtException(Thread thread, Throwable throwable) {
// 触发崩溃保险丝(每次崩溃熔断一次,降低防护等级)
AppCrashSafetyWire.getInstance().burnSafetyWire();
// 格式化崩溃发生时间(用于日志文件名和内容)
final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss", Locale.getDefault()).format(new Date());
// 创建崩溃日志文件(默认路径:外部存储/crash/[时间].txt
File crashFile = new File(
TextUtils.isEmpty(crashDir) ? new File(app.getExternalFilesDir(null), "crash") : new File(crashDir),
"crash_" + time + ".txt"
);
// 获取应用版本信息(版本名、版本号)
String versionName = "unknown";
long versionCode = 0;
try {
PackageInfo packageInfo = app.getPackageManager().getPackageInfo(app.getPackageName(), 0);
versionName = packageInfo.versionName;
// 适配 Android 9.0+API 28的版本号获取方式
versionCode = Build.VERSION.SDK_INT >= 28 ? packageInfo.getLongVersionCode() : packageInfo.versionCode;
} catch (PackageManager.NameNotFoundException ignored) {}
// 将异常堆栈信息转换为字符串
String fullStackTrace;
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
throwable.printStackTrace(pw); // 将异常堆栈写入 PrintWriter
fullStackTrace = sw.toString();
pw.close();
}
// 拼接崩溃信息(设备信息 + 应用信息 + 堆栈信息)
StringBuilder sb = new StringBuilder();
sb.append("************* Crash Head ****************\n");
sb.append("Time Of Crash : ").append(time).append("\n");
sb.append("Device Manufacturer : ").append(Build.MANUFACTURER).append("\n"); // 设备厂商
sb.append("Device Model : ").append(Build.MODEL).append("\n"); // 设备型号
sb.append("Android Version : ").append(Build.VERSION.RELEASE).append("\n"); // Android 版本
sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n"); // SDK 版本
sb.append("App VersionName : ").append(versionName).append("\n"); // 应用版本名
sb.append("App VersionCode : ").append(versionCode).append("\n"); // 应用版本号
sb.append("************* Crash Head ****************\n");
sb.append("\n").append(fullStackTrace); // 拼接异常堆栈
final String errorLog = sb.toString();
// 将崩溃日志写入文件(忽略写入失败)
try {
writeFile(crashFile, errorLog);
} catch (IOException ignored) {}
// 启动崩溃报告页面(标签用于代码块折叠)
gotoCrashActiviy: {
Intent intent = new Intent();
LogUtils.d(TAG, "gotoCrashActiviy: ");
// 根据保险丝状态选择启动的崩溃页面
if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
LogUtils.d(TAG, "gotoCrashActiviy: isAppCrashSafetyWireOK");
// 保险丝正常启动自定义样式的崩溃报告页面GlobalCrashActivity
intent.setClass(app, GlobalCrashActivity.class);
intent.putExtra(EXTRA_CRASH_LOG, errorLog); // 传递崩溃日志
} else {
LogUtils.d(TAG, "gotoCrashActiviy: else");
// 保险丝熔断启动基础版崩溃页面CrashActivity避免复杂页面再次崩溃
intent.setClass(app, CrashActivity.class);
intent.putExtra(EXTRA_CRASH_LOG, errorLog);
}
// 设置意图标志:清除原有任务栈,创建新任务(避免回到崩溃前页面)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
);
try {
if (GlobalApplication.isDebugging()&&AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
// 如果是 debug 版,启动崩溃页面窗口
app.startActivity(intent);
} else {
// 如果是 release 版,就只发送一个通知
CrashHandleNotifyUtils.handleUncaughtException(app, intent);
}
// 终止当前进程(确保完全重启)
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
} catch (ActivityNotFoundException e) {
// 未找到崩溃页面(如未在 Manifest 注册),交给系统默认处理器
e.printStackTrace();
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
} catch (Exception e) {
// 其他异常,兜底处理
e.printStackTrace();
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(thread, throwable);
}
}
}
}
/**
* 将字符串内容写入文件(创建父目录、覆盖写入)
* @param file 目标文件(包含路径)
* @param content 待写入的内容(崩溃日志)
* @throws IOException 文件创建或写入失败时抛出
*/
private void writeFile(File file, String content) throws IOException {
File parentFile = file.getParentFile();
// 父目录不存在则创建
if (parentFile != null && !parentFile.exists()) {
parentFile.mkdirs();
}
file.createNewFile(); // 创建文件
FileOutputStream fos = new FileOutputStream(file);
fos.write(content.getBytes()); // 写入内容(默认 UTF-8 编码)
try {
fos.close(); // 关闭流
} catch (IOException e) {}
}
});
}
/**
* 应用崩溃保险丝内部类(单例)
* 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略
* 等级范围MINI1~ MAX2每次崩溃等级-1熔断后启动基础版崩溃页面
*/
public static final class AppCrashSafetyWire {
/** 单例实例volatile 保证多线程可见性) */
private static volatile AppCrashSafetyWire _AppCrashSafetyWire;
/** 当前熔断等级1最低防护2最高防护≤0熔断 */
private volatile Integer currentSafetyLevel;
/** 最低熔断等级1再崩溃则熔断 */
private static final int _MINI = 1;
/** 最高熔断等级2初始状态 */
private static final int _MAX = 2;
/**
* 私有构造方法(单例模式,禁止外部实例化)
* 初始化时加载本地存储的熔断等级
*/
private AppCrashSafetyWire() {
LogUtils.d(TAG, "AppCrashSafetyWire()");
currentSafetyLevel = loadCurrentSafetyLevel();
}
/**
* 获取单例实例(双重检查锁定,线程安全)
* @return AppCrashSafetyWire 单例
*/
public static synchronized AppCrashSafetyWire getInstance() {
if (_AppCrashSafetyWire == null) {
_AppCrashSafetyWire = new AppCrashSafetyWire();
}
return _AppCrashSafetyWire;
}
/**
* 设置当前熔断等级(内存中)
* @param currentSafetyLevel 目标等级1~2
*/
public void setCurrentSafetyLevel(int currentSafetyLevel) {
this.currentSafetyLevel = currentSafetyLevel;
}
/**
* 获取当前熔断等级(内存中)
* @return 当前等级1~2 或 null
*/
public int getCurrentSafetyLevel() {
return currentSafetyLevel;
}
/**
* 保存熔断等级到本地文件(持久化,重启应用生效)
* @param currentSafetyLevel 待保存的等级
*/
public void saveCurrentSafetyLevel(int currentSafetyLevel) {
LogUtils.d(TAG, "saveCurrentSafetyLevel()");
this.currentSafetyLevel = currentSafetyLevel;
try {
// 序列化等级到文件ObjectOutputStream 写入 int
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(_CrashCountFilePath));
oos.writeInt(currentSafetyLevel);
oos.flush();
oos.close();
LogUtils.d(TAG, String.format("saveCurrentSafetyLevel writeInt currentSafetyLevel %d", currentSafetyLevel));
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
}
/**
* 从本地文件加载熔断等级(应用启动时初始化)
* @return 加载的等级(文件不存在则初始化为 MAX2
*/
public int loadCurrentSafetyLevel() {
LogUtils.d(TAG, "loadCurrentSafetyLevel()");
try {
File f = new File(_CrashCountFilePath);
if (f.exists()) {
// 反序列化从文件读取等级
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(_CrashCountFilePath));
currentSafetyLevel = ois.readInt();
LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() readInt currentSafetyLevel %d", currentSafetyLevel));
} else {
// 文件不存在初始化等级为最高2并保存
currentSafetyLevel = _MAX;
LogUtils.d(TAG, String.format("loadCurrentSafetyLevel() currentSafetyLevel init to _MAX->%d", _MAX));
saveCurrentSafetyLevel(currentSafetyLevel);
}
} catch (IOException e) {
LogUtils.d(TAG, e, Thread.currentThread().getStackTrace());
}
return currentSafetyLevel;
}
/**
* 熔断保险丝(每次崩溃调用,降低防护等级)
* @return 熔断后是否仍在防护范围内truefalse已熔断
*/
boolean burnSafetyWire() {
LogUtils.d(TAG, "burnSafetyWire()");
// 加载当前等级
int safeLevel = loadCurrentSafetyLevel();
// 若在防护范围内1~2等级-1 并保存
if (isSafetyWireWorking(safeLevel)) {
LogUtils.d(TAG, "burnSafetyWire() use");
saveCurrentSafetyLevel(safeLevel - 1);
// 返回熔断后的状态
return isSafetyWireWorking(safeLevel - 1);
}
return false;
}
/**
* 检查熔断等级是否在有效范围内1~2
* @param safetyLevel 待检查的等级
* @return true在范围内防护有效false超出范围已熔断
*/
boolean isSafetyWireWorking(int safetyLevel) {
LogUtils.d(TAG, "isSafetyWireOK()");
LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel));
if (safetyLevel >= _MINI && safetyLevel <= _MAX) {
LogUtils.d(TAG, String.format("In Safety Level"));
return true;
}
LogUtils.d(TAG, String.format("Out of Safety Level"));
return false;
}
/**
* 立即恢复熔断等级到最高2
* 用于重启应用后重置防护状态
*/
void resumeToMaximumImmediately() {
LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)");
AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
}
/**
* 关闭防护设置等级为最低1
* 下次崩溃直接熔断
*/
void off() {
LogUtils.d(TAG, "off()");
saveCurrentSafetyLevel(_MINI);
}
/**
* 检查当前保险丝是否有效(防护未熔断)
* @return true有效等级 1~2false已熔断
*/
boolean isAppCrashSafetyWireOK() {
LogUtils.d(TAG, "isAppCrashSafetyWireOK()");
currentSafetyLevel = loadCurrentSafetyLevel();
return isSafetyWireWorking(currentSafetyLevel);
}
/**
* 延迟恢复保险丝到最高等级500ms 后)
* 核心作用:崩溃页面启动后,若下次即将熔断,提前恢复防护等级,避免持续崩溃
* @param context 上下文(用于获取主线程 Handler
*/
void postResumeCrashSafetyWireHandler(final Context context) {
// 主线程延迟 500ms 执行(避免页面启动时阻塞)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
LogUtils.d(TAG, "Handler run()");
// 检查:若当前等级-1 后超出防护范围(即将熔断),则恢复到最高等级
if (!AppCrashSafetyWire.getInstance().isSafetyWireWorking(currentSafetyLevel - 1)) {
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
LogUtils.d(TAG, "postResumeCrashSafetyWireHandler: 恢复保险丝到最高等级");
}
}
}, 500);
}
}
/**
* 基础版崩溃报告页面(保险丝熔断时启动)
* 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃
*/
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
/** 菜单标识:复制崩溃日志 */
private static final int MENUITEM_COPY = 0;
/** 菜单标识:重启应用 */
private static final int MENUITEM_RESTART = 1;
/** 崩溃日志文本(从 CrashHandler 传递过来) */
private String mLog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化崩溃保险丝延迟恢复机制
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
// 获取传递的崩溃日志
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
// 设置系统默认主题(避免自定义主题冲突)
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
// 动态创建布局(避免 XML 布局加载异常)
setContentView: {
// 垂直滚动视图(处理日志过长)
ScrollView contentView = new ScrollView(this);
contentView.setFillViewport(true);
// 水平滚动视图(处理日志行过长)
HorizontalScrollView hw = new HorizontalScrollView(this);
hw.setBackgroundColor(Color.GRAY); // 背景色设为灰色
// 日志显示文本框
TextView message = new TextView(this);
{
int padding = dp2px(16); // 内边距 16dp适配不同屏幕
message.setPadding(padding, padding, padding, padding);
message.setText(mLog); // 设置崩溃日志
message.setTextColor(Color.BLACK); // 文字黑色
message.setTextIsSelectable(true); // 支持文本选择(便于手动复制)
}
// 组装布局TextView -> HorizontalScrollView -> ScrollView
hw.addView(message);
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
// 设置当前 Activity 布局
setContentView(contentView);
// 配置 ActionBar 标题和副标题
getActionBar().setTitle(TITTLE);
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
}
}
/**
* 重写返回键逻辑:点击返回键直接重启应用
*/
@Override
public void onBackPressed() {
restart();
}
/**
* 重启当前应用(与 GlobalCrashActivity 逻辑一致)
* 清除任务栈,启动主 Activity终止当前进程
*/
private void restart() {
PackageManager pm = getPackageManager();
// 获取应用启动意图(默认启动主 Activity
Intent intent = pm.getLaunchIntentForPackage(getPackageName());
if (intent != null) {
// 设置意图标志:清除原有任务栈,创建新任务
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
);
startActivity(intent);
}
// 关闭当前页面,终止进程,确保完全重启
finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
/**
* dp 转 px适配不同屏幕密度
* @param dpValue dp 值
* @return 转换后的 px 值
*/
private int dp2px(final float dpValue) {
final float scale = Resources.getSystem().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f); // 四舍五入确保精度
}
/**
* 菜单点击事件回调(处理复制、重启)
* @param item 被点击的菜单项
* @return false不消费事件保持默认行为
*/
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case MENUITEM_COPY:
// 复制日志到剪贴板
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
cm.setPrimaryClip(ClipData.newPlainText(getPackageName(), mLog));
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
break;
case MENUITEM_RESTART:
// 恢复保险丝到最高等级,然后重启应用
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
restart();
break;
}
return false;
}
/**
* 创建 ActionBar 菜单(添加复制、重启项)
* @param menu 菜单容器
* @return true显示菜单
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 添加「复制」菜单:有空间时显示在 ActionBar否则放入溢出菜单
menu.add(0, MENUITEM_COPY, 0, "Copy")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
// 添加「重启」菜单:同上
menu.add(0, MENUITEM_RESTART, 0, "Restart")
.setOnMenuItemClickListener(this)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
return true;
}
}
}