Android-UncaughtExceptionHandler

1. 什么是 UncaughtExceptionHandler?

在 Android 应用中,UncaughtExceptionHandler 是一个非常有用的工具,用于捕获未被捕获的异常(即未处理的运行时异常)。通过自定义 UncaughtExceptionHandler,开发者可以实现以下功能:

  • 记录崩溃日志。
  • 提供友好的用户体验(如显示错误提示)。
  • 收集设备信息以便后续分析。

接口定义如下:

 public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

以下是关于如何自定义 UncaughtExceptionHandler 的详细讲解。

UncaughtExceptionHandler 是 Java 提供的一个接口,用于捕获线程中未处理的异常。Android 中的应用程序运行在一个主线程(UI 线程)上,如果主线程发生未捕获的异常,默认情况下应用会崩溃并显示系统默认的崩溃界面。

通过自定义 UncaughtExceptionHandler,我们可以接管这个过程,并执行自定义逻辑。


2. 自定义 UncaughtExceptionHandler 的步骤

(1)创建自定义的 UncaughtExceptionHandler

首先,我们需要实现 Thread.UncaughtExceptionHandler 接口,并重写其 uncaughtException 方法。

public class CustomUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    // 保存默认的 UncaughtExceptionHandler
    private final Thread.UncaughtExceptionHandler defaultUEH;

    // 构造函数,保存默认的 UncaughtExceptionHandler
    public CustomUncaughtExceptionHandler(Thread.UncaughtExceptionHandler defaultUEH) {
        this.defaultUEH = defaultUEH;
    }

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        // 自定义逻辑:记录日志、发送崩溃报告等
        handleException(ex);

        // 调用默认的 UncaughtExceptionHandler(可选)
        if (defaultUEH != null) {
            defaultUEH.uncaughtException(thread, ex);
        } else {
            System.exit(1); // 如果没有默认处理器,直接退出应用
        }
    }

    private void handleException(Throwable ex) {
        if (ex == null) return;

        // 1. 记录日志
        Log.e("CustomExceptionHandler", "App crashed with exception: ", ex);

        // 2. 保存崩溃日志到文件
        saveCrashLogToFile(ex);

        // 3. 显示友好的错误界面
        showFriendlyErrorUI();
    }

    private void saveCrashLogToFile(Throwable ex) {
        // 实现将崩溃日志保存到文件的功能
        File crashLogFile = new File("/sdcard/crash_log.txt");
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(crashLogFile, true))) {
            writer.write("Crash Report:\n");
            writer.write("Time: " + new Date() + "\n");
            writer.write("Exception: " + ex.getMessage() + "\n");
            writer.write("Stack Trace:\n");
            ex.printStackTrace(writer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void showFriendlyErrorUI() {
        // 实现显示友好的错误界面的功能
        Intent intent = new Intent("com.example.ACTION_SHOW_ERROR_UI");
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        intent.putExtra("error_message", "Unfortunately, the app has crashed.");
        Application application = getApplicationContext(); // 需要获取全局上下文
        application.startActivity(intent);
    }
    
    void init() {
       // 获取默认的 UncaughtExceptionHandler
        Thread.UncaughtExceptionHandler defaultUEH = Thread.getDefaultUncaughtExceptionHandler();

        // 设置自定义的 UncaughtExceptionHandler
        Thread.setDefaultUncaughtExceptionHandler(new CustomUncaughtExceptionHandler(defaultUEH));
    }
}

(2)设置自定义的 UncaughtExceptionHandler

在应用启动时(通常是在 Application 类中),设置自定义的 UncaughtExceptionHandler

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //初始化自定义CustomUncaughtExceptionHandler即可
        CustomUncaughtExceptionHandler.getInstance().init()
    }
}

3. 注意事项

(1)确保线程安全

UncaughtExceptionHandler 是在线程级别工作的,因此需要确保对多线程的支持。如果需要捕获非主线程的异常,可以在每个线程中单独设置 UncaughtExceptionHandler

(2)不要阻塞主线程

uncaughtException 方法中,避免执行耗时操作(如网络请求或复杂的文件操作),否则可能导致应用无法正常退出。

(3)保留默认行为

调用默认的 UncaughtExceptionHandler 是一个良好的实践,因为它可能包含重要的系统行为(如生成崩溃报告)

4)收集设备信息

在捕获异常时,可以收集设备信息(如系统版本、设备型号、应用版本等),以便更好地分析崩溃原因。

java深色版本

private String getDeviceInfo() {
    StringBuilder builder = new StringBuilder();
    builder.append("Device Model: ").append(Build.MODEL).append("\n");
    builder.append("Android Version: ").append(Build.VERSION.RELEASE).append("\n");
    builder.append("App Version: ").append(BuildConfig.VERSION_NAME).append("\n");
    return builder.toString();
}

4. 替代方案

除了自定义 UncaughtExceptionHandler,还可以使用第三方库来处理崩溃日志和异常监控,例如:

  • ACRA:一个流行的开源库,用于捕获崩溃日志并发送给开发者。
  • Crashlytics:Firebase 提供的崩溃报告工具,功能强大且易于集成。
  • Sentry:一个跨平台的错误跟踪工具,支持实时监控和分析。
  • Bugly: 腾讯出品的应用监控平台,用于捕获崩溃日志并发送给开发者,用于定位并分析

5. 这样设计的好处

为什么替换了Thread 的默认异常处理器,还要保留默认的异常处理器呢,这种设计机制有什么好处呢?

这种设计的思想主要基于以下几个核心原则和目标:


1. 遵循“责任链模式”

  • 思想:通过替换默认的异常处理器为自定义的异常处理器,实际上是将异常处理的逻辑扩展到一个“责任链”中。自定义处理器负责执行额外的逻辑(如记录日志、保存崩溃信息等),然后再将控制权交还给默认处理器。
  • 好处

    • 不破坏原有系统的默认行为。
    • 在不修改系统代码的情况下,扩展了功能。

2. 尊重默认行为

  • 思想:调用默认的异常处理器是为了保留系统原有的行为。例如,默认处理器可能会生成一些必要的崩溃报告或执行其他清理操作。
  • 原因

    • 默认处理器可能包含开发者未知的重要逻辑,直接忽略它可能导致不可预见的问题。
    • 如果应用运行在某些特定环境中(如测试设备或特殊 ROM),默认处理器的行为可能是必须的。

3. 扩展性与灵活性

  • 思想:通过替换默认处理器,可以在不影响原有逻辑的情况下,添加自定义的功能。例如:

    • 记录详细的崩溃日志。
    • 提供友好的用户界面(如显示错误提示)。
    • 收集设备信息以便后续分析。
  • 好处

    • 灵活地满足不同的需求。
    • 不需要修改系统代码或框架代码,只需在应用层进行扩展。

4. 防止重复劳动

  • 思想:默认的异常处理器已经实现了许多通用的功能(如终止进程、生成崩溃报告等)。通过调用默认处理器,可以避免重复实现这些功能。
  • 原因

    • 减少开发工作量。
    • 避免因手动实现通用功能而导致的潜在问题。

5. 兼容性和稳定性

  • 思想:在自定义处理器中调用默认处理器,可以确保兼容性。即使未来系统更新改变了默认处理器的行为,只要我们调用了它,就可以自动适配这些变化。
  • 好处

    • 提高代码的稳定性和兼容性。
    • 减少因系统更新导致的维护成本。

6. 责任分离

  • 思想:自定义处理器专注于执行额外的逻辑(如日志记录、用户界面展示等),而默认处理器负责执行核心的系统行为(如终止进程)。两者分工明确,职责分离。
  • 好处

    • 代码结构清晰。
    • 易于维护和扩展。

7. 示例场景

以下是一个具体的例子,说明这种设计的好处:

场景:捕获崩溃并发送日志

假设我们需要捕获应用的崩溃日志,并将其发送到服务器以供分析。同时,我们希望保留系统的默认行为(如显示崩溃提示或终止进程)。

假设我们需要捕获应用的崩溃日志,并将其发送到服务器以供分析。同时,我们希望保留系统的默认行为(如显示崩溃提示或终止进程)

public class CustomUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    private final Thread.UncaughtExceptionHandler defaultUEH;

    public CustomUncaughtExceptionHandler(Thread.UncaughtExceptionHandler defaultUEH) {
        this.defaultUEH = defaultUEH;
    }

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        // 自定义逻辑:记录日志并发送到服务器
        handleException(ex);

        // 调用默认处理器,保留系统行为
        if (defaultUEH != null) {
            defaultUEH.uncaughtException(thread, ex);
        } else {
            System.exit(1); // 如果没有默认处理器,直接退出应用
        }
    }

    private void handleException(Throwable ex) {
        if (ex == null) return;

        // 记录日志
        Log.e("CustomExceptionHandler", "App crashed with exception: ", ex);

        // 发送崩溃日志到服务器
        sendCrashLogToServer(ex);

        // 显示友好的错误界面
        showFriendlyErrorUI();
    }

    private void sendCrashLogToServer(Throwable ex) {
        // 实现将崩溃日志发送到服务器的功能
    }

    private void showFriendlyErrorUI() {
        // 实现显示友好的错误界面的功能
    }
}

在这个例子中:

  • handleException 方法负责执行自定义逻辑(如记录日志、发送崩溃报告)。
  • defaultUEH.uncaughtException 方法负责保留系统的默认行为(如终止进程)。

8. 总结

通过替换默认的异常处理器为自定义的异常处理器,并在自定义处理器中调用默认处理器,这种设计的核心思想是:

  • 扩展性:在不破坏原有逻辑的情况下,添加自定义功能。
  • 兼容性:保留系统的默认行为,确保兼容性和稳定性。
  • 责任分离:自定义处理器专注于扩展功能,而默认处理器专注于核心逻辑。

这种设计不仅提高了代码的灵活性和可维护性,还能更好地满足实际开发中的多样化需求。

6. 总结

通过自定义 UncaughtExceptionHandler,开发者可以更好地控制应用崩溃时的行为,提供更好的用户体验并收集崩溃数据。然而,在实际开发中,建议结合第三方工具(如 Crashlytics 或 Sentry)以获得更全面的崩溃监控能力。

Native崩溃

UncaughtExceptionHandler 是 Java 层的机制,主要用于捕获和处理 Java 层未捕获的异常(即 Throwable 的子类)。然而,对于 Native 崩溃(例如 C/C++ 代码中的段错误、非法内存访问等),UncaughtExceptionHandler 并不能直接捕获和处理。


1. 为什么 UncaughtExceptionHandler 无法捕获 Native 崩溃?

  • Java 和 Native 层的区别

    • UncaughtExceptionHandler 是 Java 虚拟机(JVM)的一部分,只能捕获发生在 JVM 中的异常。
    • Native 崩溃通常是由 C/C++ 代码中的问题(如空指针解引用、数组越界等)引起的,这些崩溃发生在底层操作系统或运行时环境中,超出了 JVM 的控制范围。
  • 崩溃类型不同

    • Java 层崩溃通常是通过抛出 Throwable 对象来表示的。
    • Native 崩溃则是由操作系统生成的信号(如 SIGSEGVSIGABRT 等)触发的,这些信号不会被 JVM 捕获。

2. 如何捕获和处理 Native 崩溃?

虽然 UncaughtExceptionHandler 无法捕获 Native 崩溃,但可以通过以下方法捕获和处理:

(1)使用信号处理器(Signal Handler)

在 C/C++ 代码中,可以注册信号处理器来捕获 Native 崩溃。常见的信号包括:

  • SIGSEGV:段错误(非法内存访问)。
  • SIGABRT:程序调用 abort()
  • SIGBUS:总线错误。
  • SIGILL:非法指令。

示例代码(C/C++):

#include <signal.h>
#include <execinfo.h>
#include <iostream>

void signalHandler(int sig) {
    void* array[10];
    size_t size = backtrace(array, 10);

    // 打印堆栈信息
    std::cerr << "Caught signal: " << sig << std::endl;
    backtrace_symbols_fd(array, size, STDERR_FILENO);
    exit(1); // 终止程序
}

void setupSignalHandler() {
    struct sigaction sa;
    sa.sa_handler = signalHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGSEGV, &sa, nullptr); // 捕获段错误
    sigaction(SIGABRT, &sa, nullptr); // 捕获 abort()
    sigaction(SIGBUS, &sa, nullptr);  // 捕获总线错误
    sigaction(SIGILL, &sa, nullptr);  // 捕获非法指令
}

int main() {
    setupSignalHandler();
    // 故意制造一个崩溃
    int* ptr = nullptr;
    *ptr = 42; // 段错误
    return 0;
}
(2)使用 Android NDK 提供的工具

Android NDK 提供了一些工具和库,可以帮助捕获和分析 Native 崩溃:

  • liblog:用于记录日志。
  • backtrace 函数:获取崩溃时的调用堆栈。
  • sigaction 函数:注册信号处理器。

示例代码(NDK):

#include <android/log.h>
#include <signal.h>
#include <execinfo.h>

#define LOG_TAG "NativeCrashHandler"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

void signalHandler(int sig) {
    void* array[10];
    size_t size = backtrace(array, 10);

    LOGE("Caught signal: %d", sig);
    backtrace_symbols_fd(array, size, STDERR_FILENO);
    exit(1);
}

extern "C" void Java_com_example_NativeLib_setupSignalHandler(JNIEnv*, jobject) {
    struct sigaction sa;
    sa.sa_handler = signalHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGSEGV, &sa, nullptr);
    sigaction(SIGABRT, &sa, nullptr);
    sigaction(SIGBUS, &sa, nullptr);
    sigaction(SIGILL, &sa, nullptr);
}

在 Java 层调用:

public class NativeLib {
    static {
        System.loadLibrary("native-lib");
    }

    public native void setupSignalHandler();

    public void init() {
        setupSignalHandler();
    }
}
(3)使用第三方崩溃监控工具

许多第三方崩溃监控工具支持捕获和分析 Native 崩溃,例如:

  • Sentry:支持多平台崩溃监控,包括 Native 崩溃。
  • ACRA:虽然主要针对 Java 崩溃,但也有一些扩展支持 Native 崩溃。
  • Firebase Crashlytics:支持 Java 和 Native 崩溃报告。

3. 总结

  • UncaughtExceptionHandler 的局限性:它只能捕获 Java 层的未捕获异常,无法捕获 Native 崩溃。
  • 捕获 Native 崩溃的方法

    • 使用信号处理器(sigaction)捕获 Native 崩溃。
    • 利用 Android NDK 提供的工具记录崩溃信息。
    • 使用第三方崩溃监控工具(如 Firebase Crashlytics 或 Sentry)。
  • 最佳实践:结合 Java 层和 Native 层的崩溃处理机制,全面监控应用的稳定性。

通过上述方法,你可以有效地捕获和处理 Native 崩溃,从而提高应用的稳定性和用户体验。

Android-UncaughtExceptionHandler的初始设置

那么Android系统的主线程的defaultUncaughtExceptionHandler是在哪里设置的呢?

全局搜索有两处Thread.setDefaultUncaughtExceptionHandler()方法调用,分别在
Android_Exception

此时就体现刚才说的默认异常处理器设计的优雅之处了,通过责任链模式,前后设置的DefaultUncaughtExceptionHandler并不会彼此覆盖,以为设置之前获取线程当前的默认处理器并放置到内部的异常处理器中,defaultUncaughtExceptionHandler这个属性是Thread的一个static属性。

RuntimeInit 是 Android 系统中一个非常重要的类,主要用于初始化 Android 应用的运行时环境。它在应用启动的早期阶段运行,是每个 Android 应用启动过程中不可或缺的一部分。


1. RuntimeInit 的运行时机

RuntimeInit 类主要在以下场景中运行:

(1)应用启动时

当用户启动一个 Android 应用时,系统会通过 Zygote 进程创建一个新的应用进程。在这个过程中,RuntimeInit 被用来初始化应用的运行时环境。

  • Zygote 进程:Zygote 是 Android 系统中的一个特殊进程,负责 fork 出新的应用进程。Zygote 本身已经加载了基础的 Android 框架库和运行时环境。
  • fork 子进程:当需要启动一个新的应用时,Zygote 会 fork 出一个子进程,并在这个子进程中调用 RuntimeInit 来完成应用的初始化。
(2)执行 am start 命令时

当你通过命令行使用 adb shell am start 启动某个 Activity 时,系统也会调用 RuntimeInit 来初始化目标应用的运行时环境。


2. RuntimeInit 的主要作用

RuntimeInit 的核心任务是为 Android 应用的运行时环境做好准备,主要包括以下几个方面:

(1)设置异常处理器
  • RuntimeInit 会设置全局的未捕获异常处理器(UncaughtExceptionHandler),用于处理应用崩溃时的异常。
  • 它还实现了默认的异常处理逻辑,例如生成崩溃日志并终止应用。

3. 总结

RuntimeInit 是 Android 应用启动过程中的一个重要组件,它的主要职责是初始化应用的运行时环境,包括设置异常处理器、加载框架代码、启动主线程等。它在 Zygote 进程 fork 出的新进程中运行,并为后续的应用启动奠定了基础。

Android 应用的启动流程的具体流程, 可以进一步研究 ZygoteInitActivityThread 的实现细节,它们与 RuntimeInit 共同构成了 Android 应用启动的核心机制。


philadelphia
17 声望4 粉丝

雪山千古冷,独照峨眉峰


下一篇 »
Linux 日志处理