目录

CrashHandler-崩溃处理工具类兼容-Android-16-API-16捕获未处理异常本地存储崩溃日志上传日志到服务器

CrashHandler 崩溃处理工具类(兼容 Android 16+ / API 16)捕获未处理异常、本地存储崩溃日志、上传日志到服务器

CrashHandler 优化版(兼容 Android 16+)

针对原代码的Android 16 兼容性问题、内存泄漏风险、文件操作安全、网络请求稳定性等问题进行优化,核心保留「崩溃日志本地存储」和「日志上传服务器」功能,确保在 Android 16(API 16)及以上版本稳定运行。

优化核心目标

  1. 兼容 Android 16+:移除高版本 API 依赖(如 MultipartBody 构造兼容、SD 卡权限处理)
  2. 修复文件操作风险:避免 SD 卡不可用时崩溃,优化文件路径合法性校验
  3. 增强网络稳定性:添加 OkHttp 超时配置,避免网络请求阻塞主线程,兼容低版本 OkHttp
  4. 解决内存泄漏:避免持有 Context 强引用,优化静态变量生命周期
  5. 完善异常处理:补充所有可能的异常捕获,避免二次崩溃

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:关键注意事项(避免初始化失败)

  1. 包名一致性确保 MyApplication 的包名(如 com.nyw.wanglitiao)与 AndroidManifest.xml 中的 package 属性一致,否则系统找不到自定义 Application,会报错 ClassNotFoundException
  2. 避免在 Application 中做耗时操作Application.onCreate() 是主线程操作,初始化 CrashHandler 本身是轻量操作(仅配置目录和处理器),不会阻塞启动;但不要在此处添加网络请求、文件读写等耗时逻辑(会导致应用启动慢或 ANR)。
  3. 异常防护初始化 CrashHandler 时用 try-catch 包裹,避免因初始化逻辑本身出错(如目录权限、文件异常)导致应用启动崩溃(如果 CrashHandler 初始化失败,至少应用还能正常启动,只是失去崩溃捕获功能)。
  4. **日志目录权限(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);
        }
    }


}