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);}/*** 捕获未处理异常(核心方法)*/@Overridepublic 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() {@Overridepublic void onFailure(Call call, IOException e) {Log.e(TAG, "upload crash log failed: " + e.getMessage());// 上传失败时,可选择保留文件(后续重试)或删除// mCurrentCrashFile.delete(); // 按需决定是否删除}@Overridepublic 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() {@Overridepublic 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; // 单日志文件最大5MBprivate 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_logsmLogDir = 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();}}}/*** 捕获全局未捕获异常(核心方法)*/@Overridepublic 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.txtString 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>() {@Overridepublic void onSuccess(String response) {Log.d(TAG, "日志上传成功,服务器响应:" + response);// 可选:上传成功后删除日志文件(避免占用空间)if (crashFile.exists() && crashFile.delete()) {Log.d(TAG, "上传成功,删除日志文件:" + crashFile.getName());}}@Overridepublic 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_logsprivate static final String CRASH_LOG_DIR = "crash_logs";@Overridepublic 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_logscrashHandler.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;}/*** 可选:应用终止时的资源释放(如关闭网络连接、释放缓存等)*/@Overridepublic 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 --><applicationandroid: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 -> {// 主动抛出空指针异常,测试CrashHandlerString 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; // 单日志文件最大5MBprivate 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_logsmLogDir = 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();}}}/*** 捕获全局未捕获异常(核心方法)*/@Overridepublic 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.txtString 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>() {@Overridepublic void onSuccess(String response) {Log.d(TAG, "日志上传成功,服务器响应:" + response);// 可选:上传成功后删除日志文件(避免占用空间)if (crashFile.exists() && crashFile.delete()) {Log.d(TAG, "上传成功,删除日志文件:" + crashFile.getName());}}@Overridepublic 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_logsprivate static final String CRASH_LOG_DIR = "crash_logs";
// 保存网络状态变量private static boolean isNetworkConnected = false;public static boolean isNetworkConnected() {return isNetworkConnected;}@Overridepublic void onCreate() {super.onCreate();// 初始化全局 Contextcontext = getApplicationContext();// 初始化主线程 HandlermainHandler = 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() {@Overridepublic 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);//分发的粘性事件}@Overridepublic 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_logscrashHandler.init(context, CRASH_LOG_DIR);Log.d(TAG, "CrashHandler 初始化成功:日志目录=" + CRASH_LOG_DIR);} catch (Exception e) {// 异常防护:避免初始化CrashHandler失败导致应用启动崩溃Log.e(TAG, "CrashHandler 初始化失败!", e);}}}