Compare commits
6 Commits
appbase-v1
...
appbase-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9db3b3b703 | |||
| 744fb23291 | |||
| bd01220892 | |||
| 4b2b5acc99 | |||
| 1274bc7c05 | |||
| f67c57108a |
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon May 11 14:45:14 HKT 2026
|
||||
stageCount=5
|
||||
#Mon May 11 16:56:19 HKT 2026
|
||||
stageCount=7
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.4
|
||||
publishVersion=15.20.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.20.5
|
||||
baseBetaVersion=15.20.7
|
||||
|
||||
@@ -26,6 +26,8 @@ public class App extends GlobalApplication {
|
||||
if (isDebugging() != true) {
|
||||
setIsDebugging(BuildConfig.DEBUG);
|
||||
}
|
||||
// release 版调试码
|
||||
//setIsDebugging(!BuildConfig.DEBUG);
|
||||
|
||||
// 初始化 Toast 工具类(传入应用全局上下文,确保 Toast 可在任意地方调用)
|
||||
ToastUtils.init(getApplicationContext());
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#Created by .winboll/winboll_app_build.gradle
|
||||
#Mon May 11 14:45:14 HKT 2026
|
||||
stageCount=5
|
||||
#Mon May 11 16:56:19 HKT 2026
|
||||
stageCount=7
|
||||
libraryProject=libappbase
|
||||
baseVersion=15.20
|
||||
publishVersion=15.20.4
|
||||
publishVersion=15.20.6
|
||||
buildCount=0
|
||||
baseBetaVersion=15.20.5
|
||||
baseBetaVersion=15.20.7
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
/**
|
||||
* 应用崩溃保险丝内部类(单例)
|
||||
* 核心作用:限制短时间内重复崩溃,通过「熔断等级」控制崩溃页面启动策略
|
||||
* 等级范围:MINI(1)~ MAX(2),每次崩溃等级-1,熔断后启动基础版崩溃页面
|
||||
*/
|
||||
public final class AppCrashSafetyWire {
|
||||
|
||||
public static final String TAG = "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(CrashHandler._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(CrashHandler._CrashCountFilePath);
|
||||
if (f.exists()) {
|
||||
// 反序列化从文件读取等级
|
||||
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(CrashHandler._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);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,9 @@ 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;
|
||||
@@ -37,523 +39,292 @@ import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/11 20:14
|
||||
* @Describe * 应用全局崩溃处理类(单例逻辑)
|
||||
* 应用全局崩溃处理类(单例逻辑)
|
||||
* 核心功能:捕获应用未捕获异常,记录崩溃日志到文件,启动崩溃报告页面,
|
||||
* 并通过「崩溃保险丝」机制防止重复崩溃,保障基础功能可用
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025/11/11 20:14:00
|
||||
* @EditTime 2026/05/11 15:36:45
|
||||
*/
|
||||
public final class CrashHandler {
|
||||
|
||||
/** 日志标签,用于当前类的日志输出标识 */
|
||||
public static final String TAG = "CrashHandler";
|
||||
// ====================== 常量定义 ======================
|
||||
/** 日志标签 */
|
||||
public static final String TAG = "CrashHandler";
|
||||
/** 崩溃报告页面标题 */
|
||||
public static final String TITTLE = "CrashReport";
|
||||
/** Intent 传递崩溃信息键 */
|
||||
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
||||
/** SharedPreferences 存储键 */
|
||||
static final String PREFS = CrashHandler.class.getName() + "PREFS";
|
||||
/** 标记是否发生崩溃键 */
|
||||
static final String PREFS_CRASHHANDLER_ISCRASHHAPPEN = "PREFS_CRASHHANDLER_ISCRASHHAPPEN";
|
||||
|
||||
/** 崩溃报告页面标题 */
|
||||
public static final String TITTLE = "CrashReport";
|
||||
// ====================== 成员变量 ======================
|
||||
/** 崩溃保险丝状态文件路径 */
|
||||
public static String _CrashCountFilePath;
|
||||
/** 系统默认异常处理器兜底 */
|
||||
public static final UncaughtExceptionHandler DEFAULT_UNCAUGHT_EXCEPTION_HANDLER
|
||||
= Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
/** Intent 传递崩溃信息的键(用于向崩溃页面传递日志) */
|
||||
public static final String EXTRA_CRASH_LOG = "crashInfo";
|
||||
// ====================== 对外初始化方法 ======================
|
||||
/**
|
||||
* 初始化崩溃处理器(默认存储路径)
|
||||
* @param app 全局Application实例
|
||||
*/
|
||||
public static void init(final Application app) {
|
||||
_CrashCountFilePath = app.getExternalFilesDir("CrashHandler") + "/IsCrashHandlerCrashHappen.dat";
|
||||
LogUtils.d(TAG, "init _CrashCountFilePath = " + _CrashCountFilePath);
|
||||
init(app, null);
|
||||
}
|
||||
|
||||
/** 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() {
|
||||
/**
|
||||
* 初始化崩溃处理器(自定义日志目录)
|
||||
* @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) {
|
||||
public void uncaughtException(final Thread thread, final Throwable throwable) {
|
||||
try {
|
||||
// 尝试处理崩溃(捕获内部异常,避免 CrashHandler 自身崩溃)
|
||||
tryUncaughtException(thread, throwable);
|
||||
tryUncaughtException(thread, throwable, crashDir, app);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
// 处理失败时,交给系统默认处理器兜底
|
||||
LogUtils.e(TAG, "uncaughtException error", e);
|
||||
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 {
|
||||
// ====================== 内部崩溃处理核心 ======================
|
||||
/**
|
||||
* 执行崩溃信息收集、日志写入、跳转崩溃页面
|
||||
*/
|
||||
private static void tryUncaughtException(final Thread thread,
|
||||
final Throwable throwable,
|
||||
final String crashDir,
|
||||
final Application app) {
|
||||
// 触发崩溃保险丝
|
||||
AppCrashSafetyWire.getInstance().burnSafetyWire();
|
||||
|
||||
/** 单例实例(volatile 保证多线程可见性) */
|
||||
private static volatile AppCrashSafetyWire _AppCrashSafetyWire;
|
||||
// 格式化时间
|
||||
final String time = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss",
|
||||
Locale.getDefault()).format(new Date());
|
||||
|
||||
/** 当前熔断等级(1:最低防护;2:最高防护;≤0:熔断) */
|
||||
private volatile Integer currentSafetyLevel;
|
||||
/** 最低熔断等级(1,再崩溃则熔断) */
|
||||
private static final int _MINI = 1;
|
||||
/** 最高熔断等级(2,初始状态) */
|
||||
private static final int _MAX = 2;
|
||||
// 创建日志文件
|
||||
File logParent = TextUtils.isEmpty(crashDir)
|
||||
? new File(app.getExternalFilesDir(null), "crash")
|
||||
: new File(crashDir);
|
||||
final File crashFile = new File(logParent, "crash_" + time + ".txt");
|
||||
|
||||
/**
|
||||
* 私有构造方法(单例模式,禁止外部实例化)
|
||||
* 初始化时加载本地存储的熔断等级
|
||||
*/
|
||||
private AppCrashSafetyWire() {
|
||||
LogUtils.d(TAG, "AppCrashSafetyWire()");
|
||||
currentSafetyLevel = loadCurrentSafetyLevel();
|
||||
}
|
||||
// 获取应用版本信息
|
||||
String versionName = "unknown";
|
||||
long versionCode = 0;
|
||||
try {
|
||||
final PackageInfo packageInfo = app.getPackageManager()
|
||||
.getPackageInfo(app.getPackageName(), 0);
|
||||
versionName = packageInfo.versionName;
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
versionCode = packageInfo.getLongVersionCode();
|
||||
} else {
|
||||
versionCode = packageInfo.versionCode;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
LogUtils.e(TAG, "get package info fail");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例(双重检查锁定,线程安全)
|
||||
* @return AppCrashSafetyWire 单例
|
||||
*/
|
||||
public static synchronized AppCrashSafetyWire getInstance() {
|
||||
if (_AppCrashSafetyWire == null) {
|
||||
_AppCrashSafetyWire = new AppCrashSafetyWire();
|
||||
}
|
||||
return _AppCrashSafetyWire;
|
||||
}
|
||||
// 抓取异常堆栈
|
||||
String fullStackTrace;
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
throwable.printStackTrace(pw);
|
||||
fullStackTrace = sw.toString();
|
||||
pw.close();
|
||||
|
||||
/**
|
||||
* 设置当前熔断等级(内存中)
|
||||
* @param currentSafetyLevel 目标等级(1~2)
|
||||
*/
|
||||
public void setCurrentSafetyLevel(int currentSafetyLevel) {
|
||||
this.currentSafetyLevel = currentSafetyLevel;
|
||||
}
|
||||
// 拼接崩溃头部信息
|
||||
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");
|
||||
sb.append("Android SDK : ").append(Build.VERSION.SDK_INT).append("\n");
|
||||
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);
|
||||
|
||||
/**
|
||||
* 获取当前熔断等级(内存中)
|
||||
* @return 当前等级(1~2 或 null)
|
||||
*/
|
||||
public int getCurrentSafetyLevel() {
|
||||
return currentSafetyLevel;
|
||||
}
|
||||
final String errorLog = sb.toString();
|
||||
|
||||
/**
|
||||
* 保存熔断等级到本地文件(持久化,重启应用生效)
|
||||
* @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());
|
||||
}
|
||||
}
|
||||
// 写入日志文件
|
||||
try {
|
||||
writeFile(crashFile, errorLog);
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "write crash log file fail");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地文件加载熔断等级(应用启动时初始化)
|
||||
* @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;
|
||||
}
|
||||
// 跳转崩溃页面
|
||||
gotoCrashActivity(errorLog, app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 熔断保险丝(每次崩溃调用,降低防护等级)
|
||||
* @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;
|
||||
}
|
||||
/**
|
||||
* 写入文本到文件
|
||||
*/
|
||||
private static void writeFile(final File file, final String content) throws IOException {
|
||||
final File parentFile = file.getParentFile();
|
||||
if (parentFile != null && !parentFile.exists()) {
|
||||
parentFile.mkdirs();
|
||||
}
|
||||
file.createNewFile();
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(content.getBytes());
|
||||
fos.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查熔断等级是否在有效范围内(1~2)
|
||||
* @param safetyLevel 待检查的等级
|
||||
* @return true:在范围内(防护有效);false:超出范围(已熔断)
|
||||
*/
|
||||
boolean isSafetyWireWorking(int safetyLevel) {
|
||||
LogUtils.d(TAG, "isSafetyWireOK()");
|
||||
LogUtils.d(TAG, String.format("SafetyLevel %d", safetyLevel));
|
||||
/**
|
||||
* 根据保险丝状态跳转对应崩溃页面
|
||||
*/
|
||||
private static void gotoCrashActivity(final String errorLog, final Application app) {
|
||||
final Intent intent = new Intent();
|
||||
if (AppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||||
intent.setClass(app, GlobalCrashActivity.class);
|
||||
} else {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
try {
|
||||
if (GlobalApplication.isDebugging()) {
|
||||
app.startActivity(intent);
|
||||
} else {
|
||||
CrashHandleNotifyUtils.handleUncaughtException(app, intent, GlobalCrashActivity.class);
|
||||
}
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
LogUtils.e(TAG, "CrashActivity not found");
|
||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
||||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "start CrashActivity error");
|
||||
if (DEFAULT_UNCAUGHT_EXCEPTION_HANDLER != null) {
|
||||
DEFAULT_UNCAUGHT_EXCEPTION_HANDLER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即恢复熔断等级到最高(2)
|
||||
* 用于重启应用后重置防护状态
|
||||
*/
|
||||
void resumeToMaximumImmediately() {
|
||||
LogUtils.d(TAG, "resumeToMaximumImmediately() call saveCurrentSafetyLevel(_MAX)");
|
||||
AppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
|
||||
}
|
||||
// ====================== 内部Activity页面 ======================
|
||||
/**
|
||||
* 基础极简崩溃页面
|
||||
* 保险丝熔断时启动,避免复杂布局二次崩溃
|
||||
*/
|
||||
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
||||
private static final int MENUITEM_COPY = 0;
|
||||
private static final int MENUITEM_RESTART = 1;
|
||||
|
||||
/**
|
||||
* 关闭防护(设置等级为最低(1))
|
||||
* 下次崩溃直接熔断
|
||||
*/
|
||||
void off() {
|
||||
LogUtils.d(TAG, "off()");
|
||||
saveCurrentSafetyLevel(_MINI);
|
||||
}
|
||||
private String mLog;
|
||||
|
||||
/**
|
||||
* 检查当前保险丝是否有效(防护未熔断)
|
||||
* @return true:有效(等级 1~2);false:已熔断
|
||||
*/
|
||||
boolean isAppCrashSafetyWireOK() {
|
||||
LogUtils.d(TAG, "isAppCrashSafetyWireOK()");
|
||||
currentSafetyLevel = loadCurrentSafetyLevel();
|
||||
return isSafetyWireWorking(currentSafetyLevel);
|
||||
}
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||
initLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟恢复保险丝到最高等级(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);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 动态初始化布局
|
||||
*/
|
||||
private void initLayout() {
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
|
||||
/**
|
||||
* 基础版崩溃报告页面(保险丝熔断时启动)
|
||||
* 极简实现:仅展示崩溃日志,提供复制、重启功能,避免复杂布局导致二次崩溃
|
||||
*/
|
||||
public static final class CrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
||||
/** 菜单标识:复制崩溃日志 */
|
||||
private static final int MENUITEM_COPY = 0;
|
||||
/** 菜单标识:重启应用 */
|
||||
private static final int MENUITEM_RESTART = 1;
|
||||
HorizontalScrollView hw = new HorizontalScrollView(this);
|
||||
hw.setBackgroundColor(0xFFF5F5F5);
|
||||
|
||||
/** 崩溃日志文本(从 CrashHandler 传递过来) */
|
||||
private String mLog;
|
||||
TextView message = new TextView(this);
|
||||
final int padding = dp2px(16);
|
||||
message.setPadding(padding, padding, padding, padding);
|
||||
message.setText(mLog);
|
||||
message.setTextColor(0xFF000000);
|
||||
message.setTextIsSelectable(true);
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// 初始化崩溃保险丝延迟恢复机制
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(getApplicationContext());
|
||||
hw.addView(message);
|
||||
contentView.addView(hw, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
setContentView(contentView);
|
||||
|
||||
// 获取传递的崩溃日志
|
||||
mLog = getIntent().getStringExtra(EXTRA_CRASH_LOG);
|
||||
// 设置系统默认主题(避免自定义主题冲突)
|
||||
setTheme(android.R.style.Theme_DeviceDefault_Light_DarkActionBar);
|
||||
getActionBar().setTitle(TITTLE);
|
||||
getActionBar().setSubtitle(GlobalApplication.class.getSimpleName() + " Error");
|
||||
}
|
||||
|
||||
// 动态创建布局(避免 XML 布局加载异常)
|
||||
setContentView: {
|
||||
// 垂直滚动视图(处理日志过长)
|
||||
ScrollView contentView = new ScrollView(this);
|
||||
contentView.setFillViewport(true);
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
restartApp();
|
||||
}
|
||||
|
||||
// 水平滚动视图(处理日志行过长)
|
||||
HorizontalScrollView hw = new HorizontalScrollView(this);
|
||||
hw.setBackgroundColor(0xFFF5F5F5); // 深色模式灰色背景
|
||||
/**
|
||||
* 重启应用
|
||||
*/
|
||||
private void restartApp() {
|
||||
final Intent intent = getPackageManager()
|
||||
.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);
|
||||
}
|
||||
|
||||
// 日志显示文本框
|
||||
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); // 支持文本选择(便于手动复制)
|
||||
}
|
||||
/**
|
||||
* dp转px
|
||||
*/
|
||||
private int dp2px(final float dpValue) {
|
||||
final float scale = Resources.getSystem().getDisplayMetrics().density;
|
||||
return (int) (dpValue * scale + 0.5f);
|
||||
}
|
||||
|
||||
// 组装布局: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")
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
menu.add(0, MENUITEM_COPY, 0, "Copy")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
// 添加「重启」菜单:同上
|
||||
menu.add(0, MENUITEM_RESTART, 0, "Restart")
|
||||
|
||||
menu.add(0, MENUITEM_RESTART, 0, "Restart")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(final 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();
|
||||
restartApp();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
package cc.winboll.studio.libappbase;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2026-05-11 13:28:00
|
||||
* @EditTime 2026-05-11 13:30:45
|
||||
* @Describe 应用崩溃保险丝单例管理类
|
||||
* 限制短时间内应用重复崩溃,通过分级熔断机制控制崩溃页面跳转策略;
|
||||
* 防护等级区间 1~2,每次崩溃自动降级,等级溢出则判定为彻底熔断;
|
||||
* 支持本地文件持久化等级、延迟自动恢复、手动重置防护等级能力。
|
||||
*/
|
||||
public final class GlobalAppCrashSafetyWire {
|
||||
|
||||
/** 日志标识标签 */
|
||||
public static final String TAG = "GlobalAppCrashSafetyWire";
|
||||
/** 最低防护等级阈值 */
|
||||
private static final int _MINI = 1;
|
||||
/** 最高防护等级阈值 */
|
||||
private static final int _MAX = 2;
|
||||
|
||||
/** 单例实例对象 */
|
||||
private static volatile GlobalAppCrashSafetyWire _AppCrashSafetyWire;
|
||||
/** 当前防护熔断等级 */
|
||||
private volatile Integer currentSafetyLevel;
|
||||
|
||||
/**
|
||||
* 私有构造方法
|
||||
* 单例禁止外部实例化,初始化加载本地持久化防护等级
|
||||
*/
|
||||
private GlobalAppCrashSafetyWire() {
|
||||
LogUtils.d(TAG, "构造方法执行:初始化崩溃保险丝实例");
|
||||
currentSafetyLevel = loadCurrentSafetyLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例对象
|
||||
* @return GlobalAppCrashSafetyWire 单例
|
||||
*/
|
||||
public static synchronized GlobalAppCrashSafetyWire getInstance() {
|
||||
if (_AppCrashSafetyWire == null) {
|
||||
_AppCrashSafetyWire = new GlobalAppCrashSafetyWire();
|
||||
}
|
||||
return _AppCrashSafetyWire;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前防护等级
|
||||
* @param currentSafetyLevel 待设置的防护等级
|
||||
*/
|
||||
public void setCurrentSafetyLevel(final int currentSafetyLevel) {
|
||||
this.currentSafetyLevel = currentSafetyLevel;
|
||||
LogUtils.d(TAG, "setCurrentSafetyLevel:设置等级 = " + currentSafetyLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前内存中防护等级
|
||||
* @return 当前防护等级
|
||||
*/
|
||||
public int getCurrentSafetyLevel() {
|
||||
return currentSafetyLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存防护等级到本地文件持久化
|
||||
* @param currentSafetyLevel 待保存防护等级
|
||||
*/
|
||||
public void saveCurrentSafetyLevel(final int currentSafetyLevel) {
|
||||
LogUtils.d(TAG, "saveCurrentSafetyLevel:开始持久化等级 = " + currentSafetyLevel);
|
||||
this.currentSafetyLevel = currentSafetyLevel;
|
||||
|
||||
ObjectOutputStream oos = null;
|
||||
try {
|
||||
oos = new ObjectOutputStream(new FileOutputStream(CrashHandler._CrashCountFilePath));
|
||||
oos.writeInt(currentSafetyLevel);
|
||||
oos.flush();
|
||||
LogUtils.d(TAG, "saveCurrentSafetyLevel:写入文件成功,等级 = " + currentSafetyLevel);
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "saveCurrentSafetyLevel:写入文件异常", e);
|
||||
} finally {
|
||||
if (oos != null) {
|
||||
try {
|
||||
oos.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "saveCurrentSafetyLevel:关闭流异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地文件读取防护等级
|
||||
* @return 加载后的防护等级
|
||||
*/
|
||||
public int loadCurrentSafetyLevel() {
|
||||
LogUtils.d(TAG, "loadCurrentSafetyLevel:加载本地防护等级");
|
||||
File file = new File(CrashHandler._CrashCountFilePath);
|
||||
|
||||
if (!file.exists()) {
|
||||
currentSafetyLevel = _MAX;
|
||||
LogUtils.d(TAG, "loadCurrentSafetyLevel:文件不存在,初始化为最高等级 = " + _MAX);
|
||||
saveCurrentSafetyLevel(currentSafetyLevel);
|
||||
return currentSafetyLevel;
|
||||
}
|
||||
|
||||
ObjectInputStream ois = null;
|
||||
try {
|
||||
ois = new ObjectInputStream(new FileInputStream(CrashHandler._CrashCountFilePath));
|
||||
currentSafetyLevel = ois.readInt();
|
||||
LogUtils.d(TAG, "loadCurrentSafetyLevel:读取文件成功,当前等级 = " + currentSafetyLevel);
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "loadCurrentSafetyLevel:读取文件异常", e);
|
||||
currentSafetyLevel = _MAX;
|
||||
} finally {
|
||||
if (ois != null) {
|
||||
try {
|
||||
ois.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(TAG, "loadCurrentSafetyLevel:关闭流异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentSafetyLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行一次保险丝熔断降级
|
||||
* @return 降级后是否仍在有效防护区间
|
||||
*/
|
||||
boolean burnSafetyWire() {
|
||||
LogUtils.d(TAG, "burnSafetyWire:执行一次熔断降级");
|
||||
final int safeLevel = loadCurrentSafetyLevel();
|
||||
|
||||
if (isSafetyWireWorking(safeLevel)) {
|
||||
saveCurrentSafetyLevel(safeLevel - 1);
|
||||
return isSafetyWireWorking(safeLevel - 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验指定等级是否在有效防护区间
|
||||
* @param safetyLevel 待校验防护等级
|
||||
* @return true=防护有效 false=已熔断
|
||||
*/
|
||||
boolean isSafetyWireWorking(final int safetyLevel) {
|
||||
LogUtils.d(TAG, "isSafetyWireWorking:校验等级 = " + safetyLevel);
|
||||
if (safetyLevel >= _MINI && safetyLevel <= _MAX) {
|
||||
LogUtils.d(TAG, "isSafetyWireWorking:等级在合法区间内");
|
||||
return true;
|
||||
}
|
||||
LogUtils.d(TAG, "isSafetyWireWorking:等级超出合法区间,已熔断");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即恢复防护等级为最高等级
|
||||
*/
|
||||
void resumeToMaximumImmediately() {
|
||||
LogUtils.d(TAG, "resumeToMaximumImmediately:重置为最高防护等级");
|
||||
GlobalAppCrashSafetyWire.getInstance().saveCurrentSafetyLevel(_MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭防护,设置为临界最低等级
|
||||
*/
|
||||
void off() {
|
||||
LogUtils.d(TAG, "off:设置为临界最低防护等级");
|
||||
saveCurrentSafetyLevel(_MINI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前整体保险丝是否处于可用防护状态
|
||||
* @return true=防护正常 false=已熔断
|
||||
*/
|
||||
boolean isAppCrashSafetyWireOK() {
|
||||
LogUtils.d(TAG, "isAppCrashSafetyWireOK:检测当前防护状态");
|
||||
currentSafetyLevel = loadCurrentSafetyLevel();
|
||||
return isSafetyWireWorking(currentSafetyLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟异步检测并自动恢复防护等级
|
||||
* @param context 上下文对象
|
||||
*/
|
||||
void postResumeCrashSafetyWireHandler(final Context context) {
|
||||
LogUtils.d(TAG, "postResumeCrashSafetyWireHandler:开启延迟自动恢复任务");
|
||||
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final int nextLevel = currentSafetyLevel - 1;
|
||||
if (!GlobalAppCrashSafetyWire.getInstance().isSafetyWireWorking(nextLevel)) {
|
||||
GlobalAppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||||
LogUtils.d(TAG, "postResumeCrashSafetyWireHandler:临近熔断,自动重置最高等级");
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
public static void testGlobalAppCrashSafetyWire(Context context) {
|
||||
if (GlobalAppCrashSafetyWire.getInstance().isAppCrashSafetyWireOK()) {
|
||||
GlobalAppCrashSafetyWire.getInstance().burnSafetyWire();
|
||||
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
|
||||
context.getString(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,69 +10,119 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
import cc.winboll.studio.libappbase.R;
|
||||
|
||||
import cc.winboll.studio.libappbase.utils.CrashHandleNotifyUtils;
|
||||
|
||||
/**
|
||||
* 应用异常报告观察活动窗口类
|
||||
* 核心功能:应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情,
|
||||
* 并提供「复制日志」「重启应用」操作入口,便于开发者定位问题和用户恢复应用
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025-11-11 19:58:00
|
||||
* @EditTime 2025-11-11 20:15:32
|
||||
* @Describe 应用异常报告观察活动窗口类
|
||||
* 应用发生未捕获崩溃时,由 CrashHandler 启动此页面,展示崩溃日志详情,
|
||||
* 提供复制日志、重启应用操作入口,便于开发者定位问题及用户恢复应用;
|
||||
* 页面初始化异常时捕获错误并输出调试日志,提升崩溃页面自身稳定性。
|
||||
* @CreateTime 2025/11/11 19:58:00
|
||||
* @EditTime 2026/05/11 15:40:12
|
||||
*/
|
||||
public final class GlobalCrashActivity extends Activity
|
||||
implements MenuItem.OnMenuItemClickListener {
|
||||
public final class GlobalCrashActivity extends Activity implements MenuItem.OnMenuItemClickListener {
|
||||
|
||||
/** 日志标签 */
|
||||
// ====================== 常量定义 ======================
|
||||
public static final String TAG = "GlobalCrashActivity";
|
||||
|
||||
/** 菜单标识:复制崩溃日志 */
|
||||
private static final int MENU_ITEM_COPY = 0;
|
||||
/** 菜单标识:重启应用 */
|
||||
private static final int MENU_ITEM_RESTART = 1;
|
||||
|
||||
/** 崩溃报告自定义视图 */
|
||||
// ====================== 成员变量 ======================
|
||||
/** 崩溃报告展示自定义视图 */
|
||||
private GlobalCrashReportView mCrashReportView;
|
||||
/** 崩溃日志文本 */
|
||||
/** 崩溃日志文本内容 */
|
||||
private String mCrashLog;
|
||||
|
||||
// ====================== 生命周期方法 ======================
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
LogUtils.d(TAG, "onCreate: 初始化崩溃展示页面");
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
LogUtils.d(TAG, "onCreate 方法进入");
|
||||
try {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Context appContext = getApplicationContext();
|
||||
// 初始化崩溃安全防护机制
|
||||
AppCrashSafetyWire.getInstance().postResumeCrashSafetyWireHandler(appContext);
|
||||
|
||||
final Context appContext = getApplicationContext();
|
||||
GlobalAppCrashSafetyWire.getInstance()
|
||||
.postResumeCrashSafetyWireHandler(appContext);
|
||||
// 获取传递的崩溃日志
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
LogUtils.d(TAG, "获取到崩溃日志,长度:" + (mCrashLog != null ? mCrashLog.length() : 0));
|
||||
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
setContentView(R.layout.activity_globalcrash);
|
||||
setContentView(R.layout.activity_globalcrash);
|
||||
mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
|
||||
mCrashReportView.setReport(mCrashLog);
|
||||
|
||||
mCrashReportView = findViewById(R.id.activityglobalcrashGlobalCrashReportView1);
|
||||
if (mCrashReportView != null) {
|
||||
mCrashReportView.setReport(mCrashLog);
|
||||
setActionBar(mCrashReportView.getToolbar());
|
||||
}
|
||||
setActionBar(mCrashReportView.getToolbar());
|
||||
if (getActionBar() != null) {
|
||||
getActionBar().setTitle(CrashHandler.TITTLE);
|
||||
getActionBar().setSubtitle(GlobalApplication.getAppName(appContext));
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LogUtils.e(TAG, "GlobalCrashActivity onCreate 发生异常", e);
|
||||
AppCrashSafetyWire.getInstance().burnSafetyWire();
|
||||
|
||||
if (getActionBar() != null) {
|
||||
getActionBar().setTitle(CrashHandler.TITTLE);
|
||||
final String appName = GlobalApplication.getAppName(appContext);
|
||||
getActionBar().setSubtitle(appName);
|
||||
}
|
||||
LogUtils.d(TAG, "onCreate: 崩溃页面初始化完成");
|
||||
mCrashLog = getIntent().getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
final Intent intent = new Intent();
|
||||
intent.putExtra(CrashHandler.EXTRA_CRASH_LOG, mCrashLog);
|
||||
CrashHandleNotifyUtils.handleUncaughtException(GlobalApplication.getInstance(), intent, CrashHandler.CrashActivity.class);
|
||||
|
||||
StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
|
||||
StringBuilder sb = new StringBuilder("GlobalCrashActivity onCreate StackTrace");
|
||||
for (StackTraceElement item : stackElements) {
|
||||
sb.append("\n").append(item.toString());
|
||||
}
|
||||
LogUtils.d(TAG, sb.toString());
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
LogUtils.d(TAG, "onBackPressed 触发重启应用");
|
||||
restartApp();
|
||||
}
|
||||
|
||||
// ====================== 菜单相关回调 ======================
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
LogUtils.d(TAG, "onCreateOptionsView 初始化菜单");
|
||||
menu.add(0, MENU_ITEM_COPY, 0, "Copy")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
|
||||
menu.add(0, MENU_ITEM_RESTART, 0, "Restart")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
|
||||
mCrashReportView.updateMenuStyle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(final MenuItem item) {
|
||||
LogUtils.d(TAG, "菜单项被点击,ID:" + item.getItemId());
|
||||
switch (item.getItemId()) {
|
||||
case MENU_ITEM_COPY:
|
||||
copyCrashLogToClipboard();
|
||||
break;
|
||||
case MENU_ITEM_RESTART:
|
||||
AppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||||
restartApp();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ====================== 内部私有工具方法 ======================
|
||||
/**
|
||||
* 重启当前应用
|
||||
*/
|
||||
private void restartApp() {
|
||||
LogUtils.d(TAG, "restartApp: 执行应用重启操作");
|
||||
LogUtils.d(TAG, "开始执行应用重启逻辑");
|
||||
final PackageManager packageManager = getPackageManager();
|
||||
final Intent launchIntent = packageManager.getLaunchIntentForPackage(getPackageName());
|
||||
|
||||
@@ -82,52 +132,17 @@ implements MenuItem.OnMenuItemClickListener {
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(launchIntent);
|
||||
}
|
||||
|
||||
finish();
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
switch (itemId) {
|
||||
case MENU_ITEM_COPY:
|
||||
copyCrashLogToClipboard();
|
||||
break;
|
||||
case MENU_ITEM_RESTART:
|
||||
GlobalAppCrashSafetyWire.getInstance().resumeToMaximumImmediately();
|
||||
restartApp();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(0, MENU_ITEM_COPY, 0, "Copy")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
|
||||
menu.add(0, MENU_ITEM_RESTART, 0, "Restart")
|
||||
.setOnMenuItemClickListener(this)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||
|
||||
if (mCrashReportView != null) {
|
||||
mCrashReportView.updateMenuStyle();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制崩溃日志到系统剪贴板
|
||||
* 将崩溃日志复制到系统剪贴板
|
||||
*/
|
||||
private void copyCrashLogToClipboard() {
|
||||
LogUtils.d(TAG, "copyCrashLogToClipboard: 复制崩溃日志到剪贴板");
|
||||
final ClipboardManager clipboardManager = (ClipboardManager)
|
||||
getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
LogUtils.d(TAG, "执行复制崩溃日志到剪贴板");
|
||||
final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
final ClipData clipData = ClipData.newPlainText(getPackageName(), mCrashLog);
|
||||
clipboardManager.setPrimaryClip(clipData);
|
||||
Toast.makeText(getApplication(), "The text is copied.", Toast.LENGTH_SHORT).show();
|
||||
|
||||
@@ -10,24 +10,20 @@ import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import cc.winboll.studio.libappbase.CrashHandler;
|
||||
import cc.winboll.studio.libappbase.GlobalCrashActivity;
|
||||
import cc.winboll.studio.libappbase.LogUtils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* @Author ZhanGSKen&豆包大模型<zhangsken@qq.com>
|
||||
* @Date 2025/11/29 21:12
|
||||
* @Describe 应用崩溃处理通知实用工具集(类库兼容版)
|
||||
* 应用崩溃处理通知实用工具集(类库兼容版)
|
||||
* 核心功能:作为独立类库使用,发送崩溃通知,点击跳转宿主应用的 GlobalCrashActivity 并传递日志
|
||||
* 适配说明:移除固定包名依赖,通过外部传入宿主包名,支持任意应用集成使用
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @CreateTime 2025/11/29 21:12:00
|
||||
* @EditTime 2026/05/11 15:38:21
|
||||
*/
|
||||
public class CrashHandleNotifyUtils {
|
||||
|
||||
// ====================== 常量定义 ======================
|
||||
public static final String TAG = "CrashHandleNotifyUtils";
|
||||
|
||||
/** 通知渠道ID(Android 8.0+ 必须) */
|
||||
private static final String CRASH_NOTIFY_CHANNEL_ID = "crash_notify_channel";
|
||||
/** 通知渠道名称(用户可见) */
|
||||
@@ -38,202 +34,186 @@ public class CrashHandleNotifyUtils {
|
||||
private static final int API_LEVEL_ANDROID_12 = 31;
|
||||
/** PendingIntent.FLAG_IMMUTABLE 常量值(API 31+) */
|
||||
private static final int FLAG_IMMUTABLE = 0x00000040;
|
||||
|
||||
/** 通知内容最大行数(控制在3行,超出部分省略) */
|
||||
private static final int NOTIFICATION_MAX_LINES = 3;
|
||||
|
||||
|
||||
// ====================== 对外核心方法 ======================
|
||||
/**
|
||||
* 处理未捕获异常(核心方法,类库入口)
|
||||
* 改进点:新增宿主包名参数,移除类库对固定包名的依赖
|
||||
* @param hostApp 宿主应用的 Application 实例(用于获取宿主上下文)
|
||||
* @param hostPackageName 宿主应用的包名(关键:用于绑定意图、匹配 Activity)
|
||||
* @param errorLog 崩溃日志(从宿主 CrashHandler 传递过来)
|
||||
* 处理未捕获异常(类库入口核心方法)
|
||||
* @param hostApp 宿主Application实例
|
||||
* @param hostPackageName 宿主应用包名
|
||||
* @param errorLog 崩溃日志内容
|
||||
* @param reportCrashActivity 崩溃详情跳转Activity类
|
||||
*/
|
||||
public static void handleUncaughtException(Application hostApp, String hostPackageName, String errorLog) {
|
||||
// 1. 校验核心参数(类库场景必须严格校验,避免空指针)
|
||||
public static void handleUncaughtException(final Application hostApp,
|
||||
final String hostPackageName,
|
||||
final String errorLog,
|
||||
final Class<?> reportCrashActivity) {
|
||||
LogUtils.d(TAG, "handleUncaughtException 进入方法");
|
||||
// 校验入参
|
||||
if (hostApp == null || TextUtils.isEmpty(hostPackageName) || TextUtils.isEmpty(errorLog)) {
|
||||
LogUtils.e(TAG, "发送崩溃通知失败:参数为空(hostApp=" + hostApp + ", hostPackageName=" + hostPackageName + ", errorLog=" + errorLog + ")");
|
||||
LogUtils.e(TAG, "handleUncaughtException 参数为空校验不通过");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取宿主应用名称(使用宿主上下文,避免类库包名混淆)
|
||||
String hostAppName = getHostAppName(hostApp, hostPackageName);
|
||||
|
||||
// 3. 发送崩溃通知到宿主通知栏(点击跳转宿主的 GlobalCrashActivity)
|
||||
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog);
|
||||
final String hostAppName = getHostAppName(hostApp, hostPackageName);
|
||||
sendCrashNotification(hostApp, hostPackageName, hostAppName, errorLog, reportCrashActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载方法:兼容原有调用逻辑(避免宿主集成时改动过大)
|
||||
* 从 Intent 中提取崩溃日志和宿主包名,适配原有 CrashHandler 调用方式
|
||||
* @param hostApp 宿主应用的 Application 实例
|
||||
* @param intent 存储崩溃信息的意图(extra 中携带崩溃日志)
|
||||
* 重载兼容方法:适配原有CrashHandler调用方式
|
||||
* @param hostApp 宿主Application实例
|
||||
* @param intent 携带崩溃信息Intent
|
||||
* @param reportCrashActivity 崩溃详情Activity
|
||||
*/
|
||||
public static void handleUncaughtException(Application hostApp, Intent intent) {
|
||||
// 从意图中提取宿主包名(优先使用意图中携带的包名,无则用宿主 Application 包名)
|
||||
public static void handleUncaughtException(final Application hostApp,
|
||||
final Intent intent,
|
||||
final Class<?> reportCrashActivity) {
|
||||
LogUtils.d(TAG, "handleUncaughtException 重载方法进入");
|
||||
String hostPackageName = intent.getStringExtra("EXTRA_HOST_PACKAGE_NAME");
|
||||
if (TextUtils.isEmpty(hostPackageName)) {
|
||||
hostPackageName = hostApp.getPackageName();
|
||||
LogUtils.w(TAG, "意图中未携带宿主包名,使用 Application 包名:" + hostPackageName);
|
||||
LogUtils.w(TAG, "未携带宿主包名,默认使用应用自身包名");
|
||||
}
|
||||
|
||||
// 从意图中提取崩溃日志(与 CrashHandler.EXTRA_CRASH_INFO 保持一致)
|
||||
String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
|
||||
// 调用核心方法处理
|
||||
handleUncaughtException(hostApp, hostPackageName, errorLog);
|
||||
final String errorLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG);
|
||||
handleUncaughtException(hostApp, hostPackageName, errorLog, reportCrashActivity);
|
||||
}
|
||||
|
||||
// ====================== 内部工具方法 ======================
|
||||
/**
|
||||
* 获取宿主应用名称(类库场景适配:使用宿主包名获取,避免类库包名干扰)
|
||||
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @return 宿主应用名称(读取失败返回 "未知应用")
|
||||
* 获取宿主应用名称
|
||||
* @param hostContext 宿主上下文
|
||||
* @param hostPackageName 宿主包名
|
||||
* @return 应用名称,失败返回未知应用
|
||||
*/
|
||||
private static String getHostAppName(Context hostContext, String hostPackageName) {
|
||||
private static String getHostAppName(final Context hostContext, final String hostPackageName) {
|
||||
try {
|
||||
// 用宿主包名获取宿主应用信息,确保获取的是宿主的应用名称(类库关键改进)
|
||||
return hostContext.getPackageManager().getApplicationLabel(
|
||||
hostContext.getPackageManager().getApplicationInfo(hostPackageName, 0)
|
||||
).toString();
|
||||
return hostContext.getPackageManager()
|
||||
.getApplicationLabel(hostContext.getPackageManager()
|
||||
.getApplicationInfo(hostPackageName, 0)).toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "获取宿主应用名称失败(包名:" + hostPackageName + ")", e);
|
||||
LogUtils.e(TAG, "获取宿主应用名称失败", e);
|
||||
return "未知应用";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送崩溃通知到宿主系统通知栏(类库兼容版)
|
||||
* 改进点:全程使用宿主上下文和宿主包名,避免类库包名依赖
|
||||
* @param hostContext 宿主应用的上下文(Application 实例)
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @param hostAppName 宿主应用的名称(用于通知标题)
|
||||
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||
* 发送崩溃系统通知
|
||||
* @param hostContext 宿主上下文
|
||||
* @param hostPackageName 宿主包名
|
||||
* @param hostAppName 宿主应用名
|
||||
* @param errorLog 崩溃日志
|
||||
* @param reportCrashActivity 跳转Activity
|
||||
*/
|
||||
private static void sendCrashNotification(Context hostContext, String hostPackageName, String hostAppName, String errorLog) {
|
||||
// 1. 获取宿主的通知管理器(使用宿主上下文,确保通知归属宿主应用)
|
||||
NotificationManager notificationManager = (NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
private static void sendCrashNotification(final Context hostContext,
|
||||
final String hostPackageName,
|
||||
final String hostAppName,
|
||||
final String errorLog,
|
||||
final Class<?> reportCrashActivity) {
|
||||
final NotificationManager notificationManager =
|
||||
(NotificationManager) hostContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (notificationManager == null) {
|
||||
LogUtils.e(TAG, "获取宿主 NotificationManager 失败(包名:" + hostPackageName + ")");
|
||||
LogUtils.e(TAG, "获取NotificationManager失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 适配 Android 8.0+(API 26+):创建宿主的通知渠道(归属宿主,避免类库渠道冲突)
|
||||
// 8.0以上创建通知渠道
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createCrashNotifyChannel(hostContext, notificationManager);
|
||||
}
|
||||
|
||||
// 3. 构建通知意图(核心改进:绑定宿主包名,跳转宿主的 GlobalCrashActivity)
|
||||
PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext, hostPackageName, errorLog);
|
||||
final PendingIntent jumpIntent = getGlobalCrashPendingIntent(hostContext,
|
||||
hostPackageName, errorLog, reportCrashActivity);
|
||||
if (jumpIntent == null) {
|
||||
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")");
|
||||
LogUtils.e(TAG, "构建跳转PendingIntent失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 构建通知实例(使用宿主上下文,确保通知资源归属宿主)
|
||||
Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
|
||||
|
||||
// 5. 发送通知(归属宿主应用,避免类库与宿主通知混淆)
|
||||
final Notification notification = buildNotification(hostContext, hostAppName, errorLog, jumpIntent);
|
||||
notificationManager.notify(CRASH_NOTIFY_ID, notification);
|
||||
LogUtils.d(TAG, "崩溃通知发送成功(宿主包名:" + hostPackageName + ",标题:" + hostAppName + ",日志长度:" + errorLog.length() + "字符)");
|
||||
LogUtils.d(TAG, "崩溃通知发送成功,宿主包名:" + hostPackageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建宿主应用的崩溃通知渠道(类库场景:渠道归属宿主,避免类库与宿主渠道冲突)
|
||||
* @param hostContext 宿主应用的上下文
|
||||
* @param notificationManager 宿主的通知管理器
|
||||
* 创建通知渠道(适配Android O及以上)
|
||||
* @param hostContext 宿主上下文
|
||||
* @param notificationManager 通知管理器
|
||||
*/
|
||||
private static void createCrashNotifyChannel(Context hostContext, NotificationManager notificationManager) {
|
||||
// 仅 Android 8.0+ 执行(避免低版本报错)
|
||||
private static void createCrashNotifyChannel(final Context hostContext,
|
||||
final NotificationManager notificationManager) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// 构建通知渠道(归属宿主应用,描述明确类库用途)
|
||||
android.app.NotificationChannel channel = new android.app.NotificationChannel(
|
||||
CRASH_NOTIFY_CHANNEL_ID,
|
||||
CRASH_NOTIFY_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
CRASH_NOTIFY_CHANNEL_ID,
|
||||
CRASH_NOTIFY_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
channel.setDescription("应用崩溃通知(由 WinBoLL Studio 类库提供,点击查看详情)");
|
||||
// 注册渠道到宿主的通知管理器,确保渠道归属宿主
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
LogUtils.d(TAG, "宿主崩溃通知渠道创建成功(宿主包名:" + hostContext.getPackageName() + ",渠道ID:" + CRASH_NOTIFY_CHANNEL_ID + ")");
|
||||
LogUtils.d(TAG, "通知渠道创建完成");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心改进:构建跳转宿主 GlobalCrashActivity 的意图(类库关键)
|
||||
* 1. 绑定宿主包名,确保类库能正确启动宿主的 Activity;
|
||||
* 2. 传递崩溃日志,与宿主 GlobalCrashActivity 日志接收逻辑匹配;
|
||||
* 3. 使用宿主上下文,避免类库上下文导致的适配问题。
|
||||
* @param hostContext 宿主应用的上下文
|
||||
* @param hostPackageName 宿主应用的包名
|
||||
* @param errorLog 崩溃日志(传递给宿主的 GlobalCrashActivity)
|
||||
* @return 跳转崩溃详情页的 PendingIntent
|
||||
* 构建跳转崩溃详情页PendingIntent
|
||||
* @param hostContext 宿主上下文
|
||||
* @param hostPackageName 宿主包名
|
||||
* @param errorLog 崩溃日志
|
||||
* @param reportCrashActivity 目标Activity
|
||||
* @return PendingIntent实例
|
||||
*/
|
||||
private static PendingIntent getGlobalCrashPendingIntent(Context hostContext, String hostPackageName, String errorLog) {
|
||||
private static PendingIntent getGlobalCrashPendingIntent(final Context hostContext,
|
||||
final String hostPackageName,
|
||||
final String errorLog,
|
||||
final Class<?> reportCrashActivity) {
|
||||
try {
|
||||
// 1. 构建跳转宿主 GlobalCrashActivity 的显式意图(类库场景必须显式指定宿主包名)
|
||||
Intent crashIntent = new Intent(hostContext, GlobalCrashActivity.class);
|
||||
// 关键:绑定宿主包名,确保意图能正确匹配宿主的 Activity(避免类库包名干扰)
|
||||
final Intent crashIntent = new Intent(hostContext, reportCrashActivity);
|
||||
crashIntent.setPackage(hostPackageName);
|
||||
// 传递崩溃日志(键:EXTRA_CRASH_INFO,与宿主 GlobalCrashActivity 完全匹配)
|
||||
crashIntent.putExtra(CrashHandler.EXTRA_CRASH_LOG, errorLog);
|
||||
// 设置意图标志:确保在宿主应用中正常启动,避免重复创建和任务栈混乱
|
||||
crashIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
// 2. 构建 PendingIntent(使用宿主上下文,适配高版本)
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= API_LEVEL_ANDROID_12) {
|
||||
flags |= FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
hostContext,
|
||||
CRASH_NOTIFY_ID, // 用通知ID作为请求码,确保唯一(避免意图复用)
|
||||
crashIntent,
|
||||
flags
|
||||
hostContext,
|
||||
CRASH_NOTIFY_ID,
|
||||
crashIntent,
|
||||
flags
|
||||
);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "构建跳转意图失败(宿主包名:" + hostPackageName + ")", e);
|
||||
LogUtils.e(TAG, "构建跳转Intent异常", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通知实例(类库兼容版)
|
||||
* 改进点:使用宿主上下文加载资源,确保通知样式适配宿主应用
|
||||
* @param hostContext 宿主应用的上下文
|
||||
* @param hostAppName 宿主应用的名称(通知标题)
|
||||
* @param errorLog 崩溃日志(通知内容)
|
||||
* @param jumpIntent 通知点击跳转意图(跳转宿主的 GlobalCrashActivity)
|
||||
* @return 构建完成的 Notification 对象
|
||||
* 构建Notification通知实例
|
||||
* @param hostContext 宿主上下文
|
||||
* @param hostAppName 宿主应用名
|
||||
* @param errorLog 崩溃日志
|
||||
* @param jumpIntent 点击跳转意图
|
||||
* @return 构建好的Notification
|
||||
*/
|
||||
private static Notification buildNotification(Context hostContext, String hostAppName, String errorLog, PendingIntent jumpIntent) {
|
||||
// 兼容 Android 8.0+:指定宿主的通知渠道ID
|
||||
@SuppressWarnings("deprecation")
|
||||
private static Notification buildNotification(final Context hostContext,
|
||||
final String hostAppName,
|
||||
final String errorLog,
|
||||
final PendingIntent jumpIntent) {
|
||||
Notification.Builder builder = new Notification.Builder(hostContext);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setChannelId(CRASH_NOTIFY_CHANNEL_ID);
|
||||
}
|
||||
|
||||
// 核心:用BigTextStyle控制“默认3行省略,下拉显示完整”(使用宿主上下文构建)
|
||||
Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();
|
||||
bigTextStyle.setSummaryText("日志已省略,下拉查看完整内容");
|
||||
bigTextStyle.bigText(errorLog);
|
||||
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃"); // 标题明确标识宿主和崩溃状态
|
||||
bigTextStyle.setBigContentTitle(hostAppName + " 崩溃");
|
||||
builder.setStyle(bigTextStyle);
|
||||
|
||||
// 配置通知核心参数(全程使用宿主上下文,确保资源归属宿主)
|
||||
builder
|
||||
// 关键:使用宿主应用的小图标(避免类库图标显示异常)
|
||||
.setSmallIcon(hostContext.getApplicationInfo().icon)
|
||||
.setContentTitle(hostAppName + " 崩溃")
|
||||
.setContentText(getShortContent(errorLog)) // 3行内缩略文本
|
||||
.setContentIntent(jumpIntent) // 点击跳转宿主的 GlobalCrashActivity
|
||||
.setAutoCancel(true) // 点击后自动关闭
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setPriority(Notification.PRIORITY_DEFAULT);
|
||||
builder.setSmallIcon(hostContext.getApplicationInfo().icon)
|
||||
.setContentTitle(hostAppName + " 崩溃")
|
||||
.setContentText(getShortContent(errorLog))
|
||||
.setContentIntent(jumpIntent)
|
||||
.setAutoCancel(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setPriority(Notification.PRIORITY_DEFAULT);
|
||||
|
||||
// 适配 Android 4.1+:确保在宿主应用中正常显示
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
return builder.build();
|
||||
} else {
|
||||
@@ -242,23 +222,24 @@ public class CrashHandleNotifyUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:截取日志文本,确保显示在3行内(通用逻辑,无包名依赖)
|
||||
* @param content 完整崩溃日志
|
||||
* @return 3行内的缩略文本
|
||||
* 截取缩略日志文本
|
||||
* @param content 原始日志
|
||||
* @return 缩略文案
|
||||
*/
|
||||
private static String getShortContent(String content) {
|
||||
private static String getShortContent(final String content) {
|
||||
if (content == null || content.isEmpty()) {
|
||||
return "无崩溃日志";
|
||||
}
|
||||
int maxLength = 80; // 估算3行字符数(可根据需求调整)
|
||||
final int maxLength = 80;
|
||||
return content.length() <= maxLength ? content : content.substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源(类库场景:空实现,避免宿主调用时报错,预留扩展)
|
||||
* @param hostContext 宿主应用的上下文(显式传入,避免类库上下文依赖)
|
||||
* 资源释放预留方法
|
||||
* @param hostContext 宿主上下文
|
||||
*/
|
||||
public static void release(Context hostContext) {
|
||||
LogUtils.d(TAG, "CrashHandleNotifyUtils 资源释放完成(宿主包名:" + (hostContext != null ? hostContext.getPackageName() : "未知") + ")");
|
||||
public static void release(final Context hostContext) {
|
||||
LogUtils.d(TAG, "CrashHandleNotifyUtils 执行资源释放");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user