CrashHandler-崩溃处理工具类兼容-Android-16-API-16捕获未处理异常本地存储崩溃日志上传日志到服务器
CrashHandler 崩溃处理工具类(兼容 Android 16+ / API 16)捕获未处理异常、本地存储崩溃日志、上传日志到服务器
CrashHandler 优化版(兼容 Android 16+)
针对原代码的Android 16 兼容性问题、内存泄漏风险、文件操作安全、网络请求稳定性等问题进行优化,核心保留「崩溃日志本地存储」和「日志上传服务器」功能,确保在 Android 16(API 16)及以上版本稳定运行。
优化核心目标
- 兼容 Android 16+:移除高版本 API 依赖(如
MultipartBody
构造兼容、SD 卡权限处理) - 修复文件操作风险:避免 SD 卡不可用时崩溃,优化文件路径合法性校验
- 增强网络稳定性:添加 OkHttp 超时配置,避免网络请求阻塞主线程,兼容低版本 OkHttp
- 解决内存泄漏:避免持有 Context 强引用,优化静态变量生命周期
- 完善异常处理:补充所有可能的异常捕获,避免二次崩溃
package com.nyw.wanglitiao.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import com.nyw.wanglitiao.config.Api;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* 崩溃处理工具类(兼容 Android 16+ / API 16)
* 功能:捕获未处理异常、本地存储崩溃日志、上传日志到服务器
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String FILE_NAME = "crash_";
private static final String FILE_NAME_SUFFIX = ".txt";
// 日志上传超时时间(5秒,避免阻塞)
private static final int HTTP_TIMEOUT = 5000;
// 主线程Handler(用于低版本Toast显示)
private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
// 单例实例(volatile 保证多线程可见性,兼容低版本)
private static volatile CrashHandler sInstance;
// 系统默认异常处理器
private Thread.UncaughtExceptionHandler mDefaultCrashHandler;
// 弱引用持有Context(避免内存泄漏,API 16+支持)
private Context mAppContext;
// 日志存储路径
private String mLogPath;
// 当前崩溃日志文件(临时存储,用于上传)
private File mCurrentCrashFile;
/**
* 私有构造:防止外部实例化
*/
private CrashHandler() {
}
/**
* 获取单例实例(双重检查锁,线程安全)
*/
public static CrashHandler getInstance() {
if (sInstance == null) {
synchronized (CrashHandler.class) {
if (sInstance == null) {
sInstance = new CrashHandler();
}
}
}
return sInstance;
}
/**
* 初始化(必须在Application中调用)
* @param context 上下文(建议传Application Context)
* @param logPath 日志存储路径(如:/sdcard/YourApp/crash/)
*/
public void init(Context context, String logPath) {
if (context == null) {
Log.e(TAG, "init failed: Context is null");
return;
}
// 持有Application Context,避免Activity Context内存泄漏
mAppContext = context.getApplicationContext();
// 校验并初始化日志路径(避免空路径/非法路径)
mLogPath = checkLogPath(logPath);
// 获取系统默认异常处理器
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置当前实例为默认异常处理器
Thread.setDefaultUncaughtExceptionHandler(this);
Log.d(TAG, "CrashHandler init success, log path: " + mLogPath);
}
/**
* 捕获未处理异常(核心方法)
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (ex == null) {
// 异常为空时,交给系统处理
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
}
return;
}
try {
// 1. 本地存储崩溃日志
mCurrentCrashFile = dumpExceptionToSDCard(ex);
// 2. 上传日志到服务器(异步执行,避免阻塞)
if (mCurrentCrashFile != null && mCurrentCrashFile.exists()) {
uploadExceptionToServer();
// 等待上传(最多4秒,避免日志未上传完成就退出)
Thread.sleep(4000);
}
// 3. 显示崩溃提示(主线程Toast)
showCrashToast("应用出现异常,即将重启");
} catch (InterruptedException e) {
Log.e(TAG, "uncaughtException: sleep interrupted", e);
} catch (Exception e) {
Log.e(TAG, "uncaughtException: handle crash failed", e);
} finally {
// 4. 交给系统处理或退出应用(保证程序正常终止)
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
} else {
// 系统无默认处理器时,主动退出
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(0);
}
}
}
/**
* 校验日志路径合法性(兼容SD卡不可用场景)
*/
private String checkLogPath(String inputPath) {
// 1. 输入路径为空时,使用默认路径(内部存储,无需SD卡权限,API 16+支持)
if (inputPath == null || inputPath.trim().isEmpty()) {
if (mAppContext != null) {
// 默认路径:/data/data/应用包名/cache/crash/(内部缓存,无需权限)
File defaultDir = new File(mAppContext.getCacheDir(), "crash");
return defaultDir.getAbsolutePath() + File.separator;
} else {
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/YourApp/crash/";
}
}
// 2. 校验路径是否存在,不存在则创建
File dir = new File(inputPath);
if (!dir.exists()) {
// 递归创建目录(兼容低版本,API 1+支持)
boolean createSuccess = dir.mkdirs();
if (!createSuccess) {
Log.e(TAG, "create log dir failed: " + inputPath + ", use default path");
// 创建失败时,使用内部缓存路径
if (mAppContext != null) {
File defaultDir = new File(mAppContext.getCacheDir(), "crash");
defaultDir.mkdirs();
return defaultDir.getAbsolutePath() + File.separator;
}
}
}
// 3. 确保路径以分隔符结尾
return inputPath.endsWith(File.separator) ? inputPath : inputPath + File.separator;
}
/**
* 本地存储崩溃日志到SD卡/内部存储
*/
@SuppressLint("SimpleDateFormat")
private File dumpExceptionToSDCard(Throwable ex) {
// 1. 优先使用内部存储(无需SD卡权限,兼容SD卡不可用场景)
boolean useInternalStorage = true;
// 2. 检查SD卡是否可用(API 16+支持)
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
useInternalStorage = false;
} else {
Log.w(TAG, "SD card unmounted, use internal storage");
}
// 3. 确定日志文件路径
File logDir;
if (useInternalStorage && mAppContext != null) {
// 内部缓存路径(/data/data/应用包名/cache/crash/)
logDir = new File(mAppContext.getCacheDir(), "crash");
} else {
logDir = new File(mLogPath);
}
// 4. 创建目录(确保目录存在)
if (!logDir.exists()) {
if (!logDir.mkdirs()) {
Log.e(TAG, "create log dir failed: " + logDir.getAbsolutePath());
return null;
}
}
// 5. 创建日志文件(以时间命名,避免重复)
long currentTime = System.currentTimeMillis();
String timeStr = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date(currentTime));
String fileName = FILE_NAME + timeStr + FILE_NAME_SUFFIX;
File crashFile = new File(logDir, fileName);
// 6. 写入日志内容
PrintWriter pw = null;
try {
pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile, false)));
// 6.1 写入崩溃时间
pw.println("Crash Time: " + timeStr);
// 6.2 写入设备/应用信息
dumpPhoneAndAppInfo(pw);
// 6.3 写入异常调用栈
pw.println("\nCrash Stack Trace:");
ex.printStackTrace(pw);
// 6.4 写入cause异常(避免遗漏根因)
Throwable cause = ex.getCause();
while (cause != null) {
pw.println("\nCause Stack Trace:");
cause.printStackTrace(pw);
cause = cause.getCause();
}
pw.flush();
Log.d(TAG, "dump crash log success: " + crashFile.getAbsolutePath());
return crashFile;
} catch (IOException e) {
Log.e(TAG, "dump crash log failed", e);
return null;
} finally {
// 关闭流(避免资源泄漏)
if (pw != null) {
pw.close();
}
}
}
/**
* 写入设备和应用信息到日志
*/
private void dumpPhoneAndAppInfo(PrintWriter pw) {
if (pw == null || mAppContext == null) {
return;
}
try {
// 1. 应用信息(版本名、版本号)
PackageManager pm = mAppContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mAppContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.println("App Package: " + mAppContext.getPackageName());
pw.println("App Version Name: " + pi.versionName);
pw.println("App Version Code: " + pi.versionCode);
// 2. 系统信息(Android版本、SDK版本)
pw.println("OS Version: " + Build.VERSION.RELEASE);
pw.println("OS SDK Int: " + Build.VERSION.SDK_INT);
// 3. 设备信息(制造商、型号、CPU架构)
pw.println("Device Vendor: " + Build.MANUFACTURER);
pw.println("Device Model: " + Build.MODEL);
pw.println("Device CPU ABI: " + Build.CPU_ABI);
// 兼容多CPU架构(API 17+支持,但API 16不会崩溃,仅不显示)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
pw.println("Device CPU ABI2: " + Build.CPU_ABI2);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "dump app info failed", e);
pw.println("Dump App Info Failed: " + e.getMessage());
}
}
/**
* 上传日志到服务器(异步执行,兼容Android 16+)
*/
private void uploadExceptionToServer() {
if (mCurrentCrashFile == null || !mCurrentCrashFile.exists()) {
Log.e(TAG, "upload failed: crash file not exist");
return;
}
if (Api.UPDATE_ERROR_DATA == null || Api.UPDATE_ERROR_DATA.trim().isEmpty()) {
Log.e(TAG, "upload failed: server url is null");
return;
}
// 1. 初始化OkHttp(添加超时配置,避免阻塞)
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
.readTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
.build();
// 2. 构建Multipart请求体(兼容低版本OkHttp,API 16+支持)
MediaType textMediaType = MediaType.parse("text/plain; charset=utf-8"); // 修复原代码"text/pain"拼写错误
RequestBody fileBody = RequestBody.create(textMediaType, mCurrentCrashFile);
// 兼容OkHttp 3.x版本的MultipartBody构造(避免高版本API依赖)
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"reportbugfile", // 服务器接收参数名
mCurrentCrashFile.getName(), // 文件名
fileBody // 文件请求体
)
.build();
// 3. 构建请求
Request request = new Request.Builder()
.url(Api.UPDATE_ERROR_DATA)
.post(requestBody)
.build();
// 4. 异步执行请求(避免阻塞崩溃线程)
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "upload crash log failed: " + e.getMessage());
// 上传失败时,可选择保留文件(后续重试)或删除
// mCurrentCrashFile.delete(); // 按需决定是否删除
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String responseBody = response.body() != null ? response.body().string() : "";
Log.d(TAG, "upload crash log success, response: " + responseBody);
// 上传成功后,删除本地日志文件(避免占用空间)
if (mCurrentCrashFile.exists()) {
boolean deleteSuccess = mCurrentCrashFile.delete();
Log.d(TAG, "delete crash file: " + deleteSuccess);
}
} else {
Log.e(TAG, "upload crash log failed, code: " + response.code());
}
}
});
}
/**
* 在主线程显示崩溃提示Toast(兼容低版本)
*/
private void showCrashToast(final String message) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 当前在主线程,直接显示
Toast.makeText(mAppContext, message, Toast.LENGTH_SHORT).show();
} else {
// 子线程,通过主线程Handler显示
MAIN_HANDLER.post(new Runnable() {
@Override
public void run() {
Toast.makeText(mAppContext, message, Toast.LENGTH_SHORT).show();
}
});
}
}
}
关键优化点说明(针对 Android 16 兼容 & 原问题修复)
原代码问题 | 优化方案 | 兼容说明(Android 16+) |
---|---|---|
SD 卡不可用时崩溃 | 优先使用内部缓存路径(/data/data/ 包名 /cache/),无需 SD 卡权限;SD 卡不可用时自动切换 | 内部缓存(getCacheDir ())API 1 + 支持,完全兼容 |
内存泄漏(持有 Context) | 使用 Application Context + 避免静态强引用,防止 Activity 销毁后内存泄漏 | Application Context 生命周期与应用一致,无版本限制 |
OkHttp 无超时配置 | 添加 5 秒连接 / 读写超时,避免网络异常时阻塞线程 | OkHttp 3.x 超时配置 API 兼容所有版本,16 + 无问题 |
日志文件路径非法 | 新增 checkLogPath() 校验路径,空路径 / 创建失败时自动使用默认路径 | File 类操作 API 1 + 支持,兼容 16+ |
二次崩溃风险 | 所有 IO 操作、网络请求都加异常捕获,避免处理崩溃时再次崩溃 | 基础 try-catch 语法,无版本限制 |
缺少异常根因日志 | 补充 ex.getCause() 遍历,记录所有嵌套异常栈,便于定位根因 | Throwable 类 API 1 + 支持,兼容 16+ |
主线程 Toast 显示异常 | 使用 Handler 确保 Toast 在 |
第二个解决方案
package com.nyw.wanglitiao.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.nyw.wanglitiao.config.Api;
import com.nyw.mvvmmode.net.HttpUtils; // 导入你的 HttpUtils 上传框架
import com.nyw.mvvmmode.utils.StoragePermissionUtils; // 导入存储权限工具类
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* 崩溃处理类(适配 Android 6 ~ Android 16+)
* 功能:1. 捕获全局未捕获异常 2. 生成规范日志文件 3. 调用框架上传日志 4. 避免权限问题
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String FILE_NAME_PREFIX = "crash_"; // 日志文件前缀
private static final String FILE_NAME_SUFFIX = ".txt"; // 日志文件后缀
private static final long MAX_LOG_SIZE = 5 * 1024 * 1024; // 单日志文件最大5MB
private static final long UPLOAD_DELAY = 3000; // 上传延迟(确保日志写入完成)
private static volatile CrashHandler sInstance; // 单例(volatile 确保多线程可见性)
private Thread.UncaughtExceptionHandler mDefaultHandler; // 系统默认异常处理器
private Context mContext;
private File mLogDir; // 日志存储目录(Android 10+ 无需权限)
private Handler mMainHandler; // 主线程Handler(避免ANR)
// 私有构造:防止外部实例化
private CrashHandler() {
mMainHandler = new Handler(Looper.getMainLooper());
}
/**
* 获取单例实例(双重检查锁,线程安全)
*/
public static CrashHandler getInstance() {
if (sInstance == null) {
synchronized (CrashHandler.class) {
if (sInstance == null) {
sInstance = new CrashHandler();
}
}
}
return sInstance;
}
/**
* 初始化崩溃处理器
* @param context 上下文(建议传 ApplicationContext)
* @param customLogDir 自定义日志目录(null 则使用默认目录)
*/
public void init(Context context, String customLogDir) {
if (mContext != null) {
Log.w(TAG, "CrashHandler 已初始化,请勿重复调用");
return;
}
// 存储ApplicationContext,避免内存泄漏
mContext = context.getApplicationContext();
// 获取系统默认异常处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置当前处理器为全局异常处理器
Thread.setDefaultUncaughtExceptionHandler(this);
// 初始化日志目录(适配 Android 16+ 分区存储)
initLogDir(customLogDir);
Log.d(TAG, "CrashHandler 初始化完成,日志目录:" + mLogDir.getAbsolutePath());
}
/**
* 初始化日志目录(Android 10+ 无需存储权限)
*/
private void initLogDir(String customLogDir) {
if (customLogDir != null && !customLogDir.isEmpty()) {
// 自定义目录:优先使用外部存储应用私有目录(避免权限)
mLogDir = new File(mContext.getExternalFilesDir(null), customLogDir);
} else {
// 默认目录:/Android/data/包名/files/crash_logs
mLogDir = new File(mContext.getExternalFilesDir(null), "crash_logs");
}
// 创建目录(多级目录需用 mkdirs())
if (!mLogDir.exists()) {
boolean createSuccess = mLogDir.mkdirs();
if (!createSuccess) {
Log.e(TAG, "日志目录创建失败,使用内置缓存目录");
// 降级:使用内置缓存目录(无权限问题)
mLogDir = new File(mContext.getCacheDir(), "crash_logs");
mLogDir.mkdirs();
}
}
}
/**
* 捕获全局未捕获异常(核心方法)
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (ex == null) {
Log.w(TAG, "捕获到空异常,交给系统处理");
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
}
return;
}
try {
// 1. 生成崩溃日志文件(子线程执行,避免阻塞崩溃线程)
File crashFile = generateCrashLog(thread, ex);
// 2. 延迟上传日志(确保文件写入完成)
if (crashFile != null && crashFile.exists()) {
uploadCrashLogDelayed(crashFile);
}
// 3. 延迟退出应用(确保上传请求发出)
delayAppExit();
} catch (Exception e) {
Log.e(TAG, "处理崩溃异常时出错", e);
} finally {
// 4. 交给系统处理(确保应用正常退出)
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
} else {
// 系统无默认处理器:强制退出(避免死循环)
System.exit(0);
}
}
}
/**
* 生成崩溃日志文件
*/
private File generateCrashLog(Thread thread, Throwable ex) {
try {
// 日志文件名:crash_20240520_153020.txt
String time = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA)
.format(new Date(System.currentTimeMillis()));
File crashFile = new File(mLogDir, FILE_NAME_PREFIX + time + FILE_NAME_SUFFIX);
// 写入日志内容
try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)))) {
// 1. 写入崩溃时间
pw.println("==================== 崩溃时间 ====================");
pw.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.CHINA)
.format(new Date(System.currentTimeMillis())));
// 2. 写入线程信息
pw.println("\n==================== 线程信息 ====================");
pw.println("线程名称:" + thread.getName());
pw.println("线程ID:" + thread.getId());
pw.println("是否主线程:" + (Looper.getMainLooper().getThread() == thread));
// 3. 写入设备与应用信息
pw.println("\n==================== 设备与应用信息 ====================");
dumpDeviceAndAppInfo(pw);
// 4. 写入异常信息(含完整调用栈)
pw.println("\n==================== 异常信息 ====================");
pw.println("异常类型:" + ex.getClass().getName());
pw.println("异常原因:" + ex.getMessage());
pw.println("完整调用栈:");
ex.printStackTrace(pw); // 写入完整调用栈
// 5. 写入cause异常(如果有)
Throwable cause = ex.getCause();
if (cause != null) {
pw.println("\n==================== Cause异常 ====================");
pw.println("Cause类型:" + cause.getClass().getName());
pw.println("Cause原因:" + cause.getMessage());
pw.println("Cause调用栈:");
cause.printStackTrace(pw);
}
Log.d(TAG, "崩溃日志生成成功:" + crashFile.getAbsolutePath());
return crashFile;
} catch (IOException e) {
Log.e(TAG, "写入崩溃日志失败", e);
return null;
}
} catch (Exception e) {
Log.e(TAG, "生成崩溃日志异常", e);
return null;
}
}
/**
* 写入设备与应用信息到日志
*/
private void dumpDeviceAndAppInfo(PrintWriter pw) {
try {
// 应用信息
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.println("应用包名:" + mContext.getPackageName());
pw.println("应用版本名:" + pi.versionName);
pw.println("应用版本号:" + pi.versionCode);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pw.println("应用版本码(长):" + pi.getLongVersionCode());
}
// 设备系统信息
pw.println("\n设备制造商:" + Build.MANUFACTURER);
pw.println("设备型号:" + Build.MODEL);
pw.println("Android版本:" + Build.VERSION.RELEASE);
pw.println("Android SDK版本:" + Build.VERSION.SDK_INT);
pw.println("设备主板:" + Build.BOARD);
pw.println("CPU架构:" + Build.CPU_ABI);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
pw.println("CPU架构(64位):" + Build.CPU_ABI2);
}
pw.println("系统版本:" + Build.DISPLAY);
} catch (PackageManager.NameNotFoundException e) {
pw.println("获取应用信息失败:" + e.getMessage());
} catch (Exception e) {
pw.println("写入设备信息失败:" + e.getMessage());
}
}
/**
* 延迟上传崩溃日志(确保文件写入完成)
*/
private void uploadCrashLogDelayed(File crashFile) {
// 子线程延迟上传,避免阻塞
new Thread(() -> {
try {
// 延迟3秒:确保日志文件完全写入
TimeUnit.MILLISECONDS.sleep(UPLOAD_DELAY);
// 检查文件是否存在且大小合法
if (!crashFile.exists() || crashFile.length() <= 0 || crashFile.length() > MAX_LOG_SIZE) {
Log.e(TAG, "日志文件不合法,跳过上传:" + crashFile.getAbsolutePath());
return;
}
// 检查存储权限(Android 11+ 可能需要)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !StoragePermissionUtils.checkStoragePermission(mContext instanceof android.app.Activity ? (android.app.Activity) mContext : null)) {
Log.w(TAG, "缺少存储权限,尝试降级上传(使用文件流)");
}
// 调用框架上传方法(替换为你 HttpUtils 的实际上传接口)
uploadByFramework(crashFile);
} catch (InterruptedException e) {
Log.e(TAG, "上传延迟线程被中断", e);
} catch (Exception e) {
Log.e(TAG, "日志上传异常", e);
}
}).start();
}
/**
* 调用项目框架上传日志(核心:替换为你的 HttpUtils 上传逻辑)
*/
private void uploadByFramework(File crashFile) {
if (mContext == null || crashFile == null) {
Log.e(TAG, "上传参数为空,终止上传");
return;
}
// 1. 构造上传参数(根据你的接口需求调整)
String uploadUrl = Api.UPDATE_ERROR_DATA;
String fileKey = "reportbugfile"; // 与服务器接收字段一致
// 额外参数(如果需要)
java.util.Map<String, String> extraParams = new java.util.HashMap<>();
extraParams.put("device_model", Build.MODEL);
extraParams.put("android_version", Build.VERSION.RELEASE);
extraParams.put("app_version", getAppVersionName());
extraParams.put("crash_time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
.format(new Date(crashFile.lastModified())));
// 2. 调用 HttpUtils 上传(使用你框架的带进度上传方法)
HttpUtils.getInstance().uploadFile(
uploadUrl,
fileKey,
crashFile,
extraParams,
true, // 是否需要Token(根据你的接口调整)
String.class, // 服务器返回类型(根据实际调整)
new HttpUtils.HttpCallback<String>() {
@Override
public void onSuccess(String response) {
Log.d(TAG, "日志上传成功,服务器响应:" + response);
// 可选:上传成功后删除日志文件(避免占用空间)
if (crashFile.exists() && crashFile.delete()) {
Log.d(TAG, "上传成功,删除日志文件:" + crashFile.getName());
}
}
@Override
public void onFailure(HttpUtils.ApiError errorType, String errorMsg) {
Log.e(TAG, "日志上传失败,错误类型:" + errorType + ",错误信息:" + errorMsg);
// 可选:上传失败后保留文件,下次启动再试
}
}
);
}
/**
* 延迟退出应用(确保上传请求发出)
*/
private void delayAppExit() {
// 主线程延迟退出,避免上传请求被中断
mMainHandler.postDelayed(() -> {
Log.d(TAG, "延迟退出应用");
if (mDefaultHandler == null) {
System.exit(0);
}
}, UPLOAD_DELAY + 1000); // 比上传延迟多1秒,确保请求发出
}
/**
* 获取应用版本名(工具方法)
*/
private String getAppVersionName() {
try {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), 0);
return pi.versionName;
} catch (Exception e) {
Log.e(TAG, "获取版本名失败", e);
return "unknown";
}
}
/**
* 异常转为字符串(工具方法,便于调试)
*/
private String throwableToString(Throwable ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
pw.close();
return sw.toString();
}
}
关键优化细节说明
1. 适配 Android 16 存储权限
- 核心改动:使用
Context.getExternalFilesDir(null)
替代Environment.getExternalStorageState()
- 路径示例:
/Android/data/你的包名/files/crash_logs
- 优势:Android 10+ 无需申请
WRITE_EXTERNAL_STORAGE
权限,符合分区存储规范,避免权限拒绝问题
- 路径示例:
- 降级处理:目录创建失败时,自动切换到
Context.getCacheDir()
(内置缓存目录,完全无权限问题)
2. 集成框架上传(替换原生 OkHttp)
- 核心代码:
uploadByFramework
方法中调用HttpUtils.uploadFile
- 复用你项目已有的上传框架,避免重复造轮子
- 支持 Token 自动添加、错误统一处理(与其他接口保持一致)
- 上传成功后自动删除日志文件,避免占用存储空间
3. 日志内容增强
- 补充 线程信息(主线程 / 子线程、线程 ID),便于定位崩溃发生的线程
- 补充 Cause 异常(很多崩溃的根本原因在 Cause 中)
- 补充 设备详细信息(CPU 架构、系统版本、主板型号),便于适配问题定位
- 日志格式规范化,便于服务器解析和人工排查
4. 稳定性优化
- 单例安全:使用
volatile + 双重检查锁
,避免多线程下重复初始化 - 内存泄漏防护:只存储
ApplicationContext
,不持有 Activity 引用 - 异常降级:日志目录创建失败时自动切换到缓存目录,避免崩溃处理逻辑本身崩溃
- 延迟控制:日志写入后延迟 3 秒上传,确保文件完全写入;上传后延迟 1 秒退出,确保请求发出
使用方法
1. 初始化(建议在 Application 中)
在 Application 中初始化 CrashHandler 的完整代码
建议在自定义 Application
类的 onCreate()
中初始化 CrashHandler
,确保应用启动时就开启崩溃捕获功能,且能获取全局 ApplicationContext
(避免内存泄漏)。
步骤 1:创建 / 修改自定义 Application 类
如果项目中没有自定义 Application
,先在 AndroidManifest.xml
中配置;如果已有,直接在 onCreate()
中添加初始化代码即可。
1.1 自定义 Application 完整代码
java
运行
package com.nyw.wanglitiao;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import com.nyw.wanglitiao.util.CrashHandler;
import com.nyw.mvvmmode.net.HttpUtils; // 确保导入你的 HttpUtils(如果CrashHandler依赖)
import com.nyw.mvvmmode.utils.StoragePermissionUtils; // 可选:如果需要提前初始化权限工具
/**
* 自定义 Application 类:管理全局初始化(如CrashHandler、网络框架等)
*/
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
// 全局上下文(谨慎使用,避免内存泄漏,优先用局部Context)
private static Context sApplicationContext;
// 日志存储目录:建议用应用私有目录(Android 10+无需权限)
// 路径示例:/Android/data/com.nyw.wanglitiao/files/crash_logs
private static final String CRASH_LOG_DIR = "crash_logs";
@Override
public void onCreate() {
super.onCreate();
// 1. 保存全局 ApplicationContext(仅用于全局工具类,如CrashHandler)
sApplicationContext = getApplicationContext();
Log.d(TAG, "MyApplication onCreate:全局上下文初始化完成");
// 2. 初始化 CrashHandler(核心:开启崩溃捕获)
initCrashHandler();
// 3. 可选:初始化其他全局工具(如HttpUtils、权限工具等,根据项目需求)
initGlobalTools();
}
/**
* 初始化 CrashHandler:开启崩溃日志捕获+上传
*/
private void initCrashHandler() {
try {
// 获取 CrashHandler 单例
CrashHandler crashHandler = CrashHandler.getInstance();
// 初始化:参数1=全局ApplicationContext,参数2=日志存储目录(null则用默认)
// 这里传 CRASH_LOG_DIR,日志会存在 /Android/data/包名/files/crash_logs
crashHandler.init(sApplicationContext, CRASH_LOG_DIR);
Log.d(TAG, "CrashHandler 初始化成功:日志目录=" + CRASH_LOG_DIR);
} catch (Exception e) {
// 异常防护:避免初始化CrashHandler失败导致应用启动崩溃
Log.e(TAG, "CrashHandler 初始化失败!", e);
}
}
/**
* 可选:初始化其他全局工具(如网络框架、权限工具等)
* (如果你的CrashHandler依赖HttpUtils上传,建议提前初始化HttpUtils)
*/
private void initGlobalTools() {
try {
// 示例1:初始化 HttpUtils(如果CrashHandler的上传依赖HttpUtils)
// (根据你的HttpUtils初始化逻辑调整,比如是否需要自定义SSL证书)
HttpUtils.getInstance();
Log.d(TAG, "HttpUtils 全局初始化完成");
// 示例2:可选初始化 StoragePermissionUtils(如果需要提前配置)
// StoragePermissionUtils 无需主动初始化,调用时自动适配,此处可省略
} catch (Exception e) {
Log.e(TAG, "全局工具初始化失败!", e);
}
}
/**
* 获取全局 ApplicationContext(谨慎使用!仅给全局工具类用,如CrashHandler)
* 注意:不要用此Context持有Activity相关引用(如View、Dialog),会导致内存泄漏
*/
public static Context getAppContext() {
return sApplicationContext;
}
/**
* 可选:应用终止时的资源释放(如关闭网络连接、释放缓存等)
*/
@Override
public void onTerminate() {
super.onTerminate();
Log.d(TAG, "MyApplication onTerminate:应用终止,释放资源");
// 示例:如果HttpUtils有手动关闭连接的方法,可在此调用
// HttpUtils.getInstance().closeClient();
}
}
步骤 2:在 AndroidManifest.xml 中配置 Application
必须在清单文件中注册自定义 Application
,否则系统会使用默认 Application
,自定义初始化逻辑不会执行。
2.1 配置清单文件关键代码
找到 AndroidManifest.xml
的 <application>
标签,添加 android:name
属性指向你的自定义 Application
类:
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nyw.wanglitiao"> <!-- 替换为你的项目包名 -->
<!-- 可选:如果CrashHandler需要上传日志(网络请求),添加网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 配置自定义 Application -->
<application
android:name=".MyApplication" <!-- 关键:指向你的自定义Application类 -->
android:allowBackup="true"
android:icon="@mipmap/ic_launcher" <!-- 替换为你的应用图标 -->
android:label="@string/app_name" <!-- 替换为你的应用名称 -->
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"> <!-- 替换为你的应用主题 -->
<!-- 其他配置:Activity、Service、权限等 -->
<activity android:name=".MainActivity"> <!-- 你的主Activity -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
步骤 3:关键注意事项(避免初始化失败)
- 包名一致性确保
MyApplication
的包名(如com.nyw.wanglitiao
)与AndroidManifest.xml
中的package
属性一致,否则系统找不到自定义Application
,会报错ClassNotFoundException
。 - 避免在 Application 中做耗时操作
Application.onCreate()
是主线程操作,初始化CrashHandler
本身是轻量操作(仅配置目录和处理器),不会阻塞启动;但不要在此处添加网络请求、文件读写等耗时逻辑(会导致应用启动慢或 ANR)。 - 异常防护初始化
CrashHandler
时用try-catch
包裹,避免因初始化逻辑本身出错(如目录权限、文件异常)导致应用启动崩溃(如果CrashHandler
初始化失败,至少应用还能正常启动,只是失去崩溃捕获功能)。 - **日志目录权限(Android 10+ 无需处理)**示例中日志目录用
getExternalFilesDir(CRASH_LOG_DIR)
(应用私有外部目录),Android 10+ 无需申请WRITE_EXTERNAL_STORAGE
权限;Android 9 及以下会自动授予该目录权限,无需额外处理。
验证初始化是否成功
启动应用后,查看 Logcat 中 TAG = "MyApplication"
的日志:
- 如果看到
CrashHandler 初始化成功:日志目录=crash_logs
,说明初始化完成; - 如果看到
CrashHandler 初始化失败!
,根据报错信息排查(如包名错误、目录创建失败等)。
扩展:测试崩溃捕获功能
可以在 MainActivity 中加一个测试按钮,主动触发崩溃,验证日志是否生成:
java
运行
// 在 MainActivity 的 onClick 中添加
findViewById(R.id.btn_test_crash).setOnClickListener(v -> {
// 主动抛出空指针异常,测试CrashHandler
String nullStr = null;
nullStr.length();
});
点击按钮后,应用会崩溃,此时在 /Android/data/com.nyw.wanglitiao/files/crash_logs
目录下会生成 crash_20240520_153020.txt
格式的日志文件,同时 CrashHandler
会自动调用 HttpUtils
上传日志到服务器。
咐上我写的mvvm项目中的代码
package com.nyw.mvvmmode.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.nyw.mvvmmode.net.Api;
import com.nyw.mvvmmode.net.HttpUtils; // 导入你的 HttpUtils 上传框架
import com.nyw.mvvmmode.utils.StoragePermissionUtils; // 导入存储权限工具类
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* 崩溃处理类(适配 Android 6 ~ Android 16+)
* 功能:1. 捕获全局未捕获异常 2. 生成规范日志文件 3. 调用框架上传日志 4. 避免权限问题
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String FILE_NAME_PREFIX = "crash_"; // 日志文件前缀
private static final String FILE_NAME_SUFFIX = ".txt"; // 日志文件后缀
private static final long MAX_LOG_SIZE = 5 * 1024 * 1024; // 单日志文件最大5MB
private static final long UPLOAD_DELAY = 3000; // 上传延迟(确保日志写入完成)
private static volatile CrashHandler sInstance; // 单例(volatile 确保多线程可见性)
private Thread.UncaughtExceptionHandler mDefaultHandler; // 系统默认异常处理器
private Context mContext;
private File mLogDir; // 日志存储目录(Android 10+ 无需权限)
private Handler mMainHandler; // 主线程Handler(避免ANR)
// 私有构造:防止外部实例化
private CrashHandler() {
mMainHandler = new Handler(Looper.getMainLooper());
}
/**
* 获取单例实例(双重检查锁,线程安全)
*/
public static CrashHandler getInstance() {
if (sInstance == null) {
synchronized (CrashHandler.class) {
if (sInstance == null) {
sInstance = new CrashHandler();
}
}
}
return sInstance;
}
/**
* 初始化崩溃处理器
* @param context 上下文(建议传 ApplicationContext)
* @param customLogDir 自定义日志目录(null 则使用默认目录)
*/
public void init(Context context, String customLogDir) {
if (mContext != null) {
Log.w(TAG, "CrashHandler 已初始化,请勿重复调用");
return;
}
// 存储ApplicationContext,避免内存泄漏
mContext = context.getApplicationContext();
// 获取系统默认异常处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置当前处理器为全局异常处理器
Thread.setDefaultUncaughtExceptionHandler(this);
// 初始化日志目录(适配 Android 16+ 分区存储)
initLogDir(customLogDir);
Log.d(TAG, "CrashHandler 初始化完成,日志目录:" + mLogDir.getAbsolutePath());
}
/**
* 初始化日志目录(Android 10+ 无需存储权限)
*/
private void initLogDir(String customLogDir) {
if (customLogDir != null && !customLogDir.isEmpty()) {
// 自定义目录:优先使用外部存储应用私有目录(避免权限)
mLogDir = new File(mContext.getExternalFilesDir(null), customLogDir);
} else {
// 默认目录:/Android/data/包名/files/crash_logs
mLogDir = new File(mContext.getExternalFilesDir(null), "crash_logs");
}
// 创建目录(多级目录需用 mkdirs())
if (!mLogDir.exists()) {
boolean createSuccess = mLogDir.mkdirs();
if (!createSuccess) {
Log.e(TAG, "日志目录创建失败,使用内置缓存目录");
// 降级:使用内置缓存目录(无权限问题)
mLogDir = new File(mContext.getCacheDir(), "crash_logs");
mLogDir.mkdirs();
}
}
}
/**
* 捕获全局未捕获异常(核心方法)
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (ex == null) {
Log.w(TAG, "捕获到空异常,交给系统处理");
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
}
return;
}
try {
// 1. 生成崩溃日志文件(子线程执行,避免阻塞崩溃线程)
File crashFile = generateCrashLog(thread, ex);
// 2. 延迟上传日志(确保文件写入完成)
if (crashFile != null && crashFile.exists()) {
uploadCrashLogDelayed(crashFile);
}
// 3. 延迟退出应用(确保上传请求发出)
delayAppExit();
} catch (Exception e) {
Log.e(TAG, "处理崩溃异常时出错", e);
} finally {
// 4. 交给系统处理(确保应用正常退出)
if (mDefaultHandler != null) {
mDefaultHandler.uncaughtException(thread, ex);
} else {
// 系统无默认处理器:强制退出(避免死循环)
System.exit(0);
}
}
}
/**
* 生成崩溃日志文件
*/
private File generateCrashLog(Thread thread, Throwable ex) {
try {
// 日志文件名:crash_20240520_153020.txt
String time = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA)
.format(new Date(System.currentTimeMillis()));
File crashFile = new File(mLogDir, FILE_NAME_PREFIX + time + FILE_NAME_SUFFIX);
// 写入日志内容
try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)))) {
// 1. 写入崩溃时间
pw.println("==================== 崩溃时间 ====================");
pw.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.CHINA)
.format(new Date(System.currentTimeMillis())));
// 2. 写入线程信息
pw.println("\n==================== 线程信息 ====================");
pw.println("线程名称:" + thread.getName());
pw.println("线程ID:" + thread.getId());
pw.println("是否主线程:" + (Looper.getMainLooper().getThread() == thread));
// 3. 写入设备与应用信息
pw.println("\n==================== 设备与应用信息 ====================");
dumpDeviceAndAppInfo(pw);
// 4. 写入异常信息(含完整调用栈)
pw.println("\n==================== 异常信息 ====================");
pw.println("异常类型:" + ex.getClass().getName());
pw.println("异常原因:" + ex.getMessage());
pw.println("完整调用栈:");
ex.printStackTrace(pw); // 写入完整调用栈
// 5. 写入cause异常(如果有)
Throwable cause = ex.getCause();
if (cause != null) {
pw.println("\n==================== Cause异常 ====================");
pw.println("Cause类型:" + cause.getClass().getName());
pw.println("Cause原因:" + cause.getMessage());
pw.println("Cause调用栈:");
cause.printStackTrace(pw);
}
Log.d(TAG, "崩溃日志生成成功:" + crashFile.getAbsolutePath());
return crashFile;
} catch (IOException e) {
Log.e(TAG, "写入崩溃日志失败", e);
return null;
}
} catch (Exception e) {
Log.e(TAG, "生成崩溃日志异常", e);
return null;
}
}
/**
* 写入设备与应用信息到日志
*/
private void dumpDeviceAndAppInfo(PrintWriter pw) {
try {
// 应用信息
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.println("应用包名:" + mContext.getPackageName());
pw.println("应用版本名:" + pi.versionName);
pw.println("应用版本号:" + pi.versionCode);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pw.println("应用版本码(长):" + pi.getLongVersionCode());
}
// 设备系统信息
pw.println("\n设备制造商:" + Build.MANUFACTURER);
pw.println("设备型号:" + Build.MODEL);
pw.println("Android版本:" + Build.VERSION.RELEASE);
pw.println("Android SDK版本:" + Build.VERSION.SDK_INT);
pw.println("设备主板:" + Build.BOARD);
pw.println("CPU架构:" + Build.CPU_ABI);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
pw.println("CPU架构(64位):" + Build.CPU_ABI2);
}
pw.println("系统版本:" + Build.DISPLAY);
} catch (PackageManager.NameNotFoundException e) {
pw.println("获取应用信息失败:" + e.getMessage());
} catch (Exception e) {
pw.println("写入设备信息失败:" + e.getMessage());
}
}
/**
* 延迟上传崩溃日志(确保文件写入完成)
*/
private void uploadCrashLogDelayed(File crashFile) {
// 子线程延迟上传,避免阻塞
new Thread(() -> {
try {
// 延迟3秒:确保日志文件完全写入
TimeUnit.MILLISECONDS.sleep(UPLOAD_DELAY);
// 检查文件是否存在且大小合法
if (!crashFile.exists() || crashFile.length() <= 0 || crashFile.length() > MAX_LOG_SIZE) {
Log.e(TAG, "日志文件不合法,跳过上传:" + crashFile.getAbsolutePath());
return;
}
// 检查存储权限(Android 11+ 可能需要)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !StoragePermissionUtils.checkStoragePermission(mContext instanceof android.app.Activity ? (android.app.Activity) mContext : null)) {
Log.w(TAG, "缺少存储权限,尝试降级上传(使用文件流)");
}
// 调用框架上传方法(替换为你 HttpUtils 的实际上传接口)
uploadByFramework(crashFile);
} catch (InterruptedException e) {
Log.e(TAG, "上传延迟线程被中断", e);
} catch (Exception e) {
Log.e(TAG, "日志上传异常", e);
}
}).start();
}
/**
* 调用项目框架上传日志(核心:替换为你的 HttpUtils 上传逻辑)
*/
private void uploadByFramework(File crashFile) {
if (mContext == null || crashFile == null) {
Log.e(TAG, "上传参数为空,终止上传");
return;
}
// 1. 构造上传参数(根据你的接口需求调整)
String uploadUrl = Api.UPDATE_ERROR_DATA;
String fileKey = "reportbugfile"; // 与服务器接收字段一致
// 额外参数(如果需要)
java.util.Map<String, String> extraParams = new java.util.HashMap<>();
extraParams.put("device_model", Build.MODEL);
extraParams.put("android_version", Build.VERSION.RELEASE);
extraParams.put("app_version", getAppVersionName());
extraParams.put("crash_time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
.format(new Date(crashFile.lastModified())));
// 2. 调用 HttpUtils 上传(使用你框架的带进度上传方法)
HttpUtils.getInstance().uploadFile(
uploadUrl,
fileKey,
crashFile,
extraParams,
true, // 是否需要Token(根据你的接口调整)
String.class, // 服务器返回类型(根据实际调整)
new HttpUtils.HttpCallback<String>() {
@Override
public void onSuccess(String response) {
Log.d(TAG, "日志上传成功,服务器响应:" + response);
// 可选:上传成功后删除日志文件(避免占用空间)
if (crashFile.exists() && crashFile.delete()) {
Log.d(TAG, "上传成功,删除日志文件:" + crashFile.getName());
}
}
@Override
public void onFailure(HttpUtils.ApiError errorType, String errorMsg) {
Log.e(TAG, "日志上传失败,错误类型:" + errorType + ",错误信息:" + errorMsg);
// 可选:上传失败后保留文件,下次启动再试
}
}
);
}
/**
* 延迟退出应用(确保上传请求发出)
*/
private void delayAppExit() {
// 主线程延迟退出,避免上传请求被中断
mMainHandler.postDelayed(() -> {
Log.d(TAG, "延迟退出应用");
if (mDefaultHandler == null) {
System.exit(0);
}
}, UPLOAD_DELAY + 1000); // 比上传延迟多1秒,确保请求发出
}
/**
* 获取应用版本名(工具方法)
*/
private String getAppVersionName() {
try {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), 0);
return pi.versionName;
} catch (Exception e) {
Log.e(TAG, "获取版本名失败", e);
return "unknown";
}
}
/**
* 异常转为字符串(工具方法,便于调试)
*/
private String throwableToString(Throwable ex) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
pw.close();
return sw.toString();
}
}
在MyApplication 中,在onCreate 初始化 CrashHandler(核心:开启崩溃捕获) initCrashHandler();
package com.nyw.mvvmmode;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkRequest;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.nyw.mvvmmode.net.HttpConfig;
import com.nyw.mvvmmode.net.NetworkEvent;
import com.nyw.mvvmmode.utils.CrashHandler;
import com.nyw.mvvmmode.utils.Event;
import com.nyw.mvvmmode.utils.EventBusUtils;
import com.nyw.mvvmmode.utils.EventCode;
import com.nyw.mvvmmode.utils.SecureSPUtils;
import org.greenrobot.eventbus.EventBus;
public class MyApplication extends Application {
// 全局 Context 实例
private static Context context;
// 主线程 Handler 实例(用于全局线程切换)
private static Handler mainHandler;
private static final String TAG = "MyApplication";
// 日志存储目录:建议用应用私有目录(Android 10+无需权限)
// 路径示例:/Android/data/com.nyw.nvvmmode/files/crash_logs
private static final String CRASH_LOG_DIR = "crash_logs";
// 保存网络状态变量
private static boolean isNetworkConnected = false;
public static boolean isNetworkConnected() {
return isNetworkConnected;
}
@Override
public void onCreate() {
super.onCreate();
// 初始化全局 Context
context = getApplicationContext();
// 初始化主线程 Handler
mainHandler = new Handler(Looper.getMainLooper());
// 1.SharedPreferences 基础初始化(默认不加密,使用默认 SP 名称和密钥)
SecureSPUtils.init(this);
// 初始化 CrashHandler(核心:开启崩溃捕获)
initCrashHandler();
// 开启 Token 自动刷新功能
HttpConfig.setEnableTokenRefresh(true);
// 注册网络状态监听
registerNetworkCallback();
}
/**
* 获取全局 Context
*/
public static Context getContext() {
return context;
}
/**
* 获取主线程 Handler
*/
public static Handler getHandler() {
return mainHandler;
}
/**
* 注册网络状态监听
* 通过 EventBus 发送网络状态变化事件
*/
@SuppressLint("MissingPermission")
private void registerNetworkCallback() {
try {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
Log.e(TAG, "ConnectivityManager is null");
return;
}
NetworkRequest.Builder builder = new NetworkRequest.Builder();
cm.registerNetworkCallback(builder.build(), new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
Log.d(TAG, "网络已连接");
// EventBus.getDefault().post(new NetworkEvent(true));
isNetworkConnected=true;
// 网络已连接
NetworkEvent networkEvent=new NetworkEvent(true);
Event event=new Event(EventCode.NETWORK_CHANGE,networkEvent);
EventBusUtils.postSticky(event);//分发的粘性事件
}
@Override
public void onLost(Network network) {
super.onLost(network);
Log.d(TAG, "网络已断开");
// EventBus.getDefault().post(new NetworkEvent(false));
// 网络已断开
isNetworkConnected=false;
NetworkEvent networkEvent=new NetworkEvent(false);
Event event=new Event(EventCode.NETWORK_CHANGE,networkEvent);
EventBusUtils.postSticky(event);//分发的粘性事件
}
});
} catch (Exception e) {
Log.e(TAG, "注册网络监听失败: " + e.getMessage());
}
}
/**
* 初始化 CrashHandler:开启崩溃日志捕获+上传
*/
private void initCrashHandler() {
try {
// 获取 CrashHandler 单例
CrashHandler crashHandler = CrashHandler.getInstance();
// 初始化:参数1=全局ApplicationContext,参数2=日志存储目录(null则用默认)
// 这里传 CRASH_LOG_DIR,日志会存在 /Android/data/包名/files/crash_logs
crashHandler.init(context, CRASH_LOG_DIR);
Log.d(TAG, "CrashHandler 初始化成功:日志目录=" + CRASH_LOG_DIR);
} catch (Exception e) {
// 异常防护:避免初始化CrashHandler失败导致应用启动崩溃
Log.e(TAG, "CrashHandler 初始化失败!", e);
}
}
}