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.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&豆包大模型 * @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()) { // 如果是 debug 版,启动崩溃页面窗口 app.startActivity(intent); } // 发送一个通知 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) {} } }); } /** * 应用崩溃保险丝内部类(单例) * 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略 * 等级范围:MINI(1)~ MAX(2),每次崩溃等级-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 加载的等级(文件不存在则初始化为 MAX(2)) */ 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 熔断后是否仍在防护范围内(true:是;false:已熔断) */ 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~2);false:已熔断 */ public 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(0xFFF5F5F5); // 深色模式灰色背景 // 日志显示文本框 TextView message = new TextView(this); { int padding = dp2px(16); // 内边距 16dp(适配不同屏幕) message.setPadding(padding, padding, padding, padding); message.setText(mLog); // 设置崩溃日志 message.setTextColor(0xFF000000); // 深色模式灰色文字,普通模式黑色文字 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; } } }