Android异常监控

Android异常监控

Crash就是由于代码异常而导致App非正常退出现象,也就是我们常说的『崩溃

通常情况下会有以下两种类型Crash:

  • Java Crash
  • Native Crash

Java Crash

Java的Crash监控非常简单,Java中的Thread定义了一个接口: UncaughtExceptionHandler ;用于

处理未捕获的异常导致线程的终止(注意:****catch****了的是捕获不到的),当我们的应用crash的时候,就

会走 UncaughtExceptionHandler.uncaughtException ,在该方法中可以获取到异常的信息,我们通

过 Thread.setDefaultUncaughtExceptionHandler 该方法来设置线程的默认异常处理器,我们可以

将异常信息保存到本地或者是上传到服务器,方便我们快速的定位问题。

Android系统崩溃处理的流程:

图片来源:https://xiang-yu.blog.csdn.net/article/details/106647428

20200609193238913

具体的处理过程可参考:https://xiang-yu.blog.csdn.net/article/details/106647428

ZygoteInit每次fork出一个进程之后,会先初始化日志、crash监控,再利用反射调用 ActivityThread 的main()方法启动app。那么我们可以自己的app中重新注册Crash监控回调接口,然后在自己的app中处理Crash。

具体的实现类如下:

public class CrashHandler implements Thread.UncaughtExceptionHandler{

    private static final String FILE_NAME_SUFFIX = ".trace";
    private static Thread.UncaughtExceptionHandler mDefaultCrashHandler;
    private static Context mContext;
    private static CrashHandler crashHandler;
    private CrashHandler() {
    }
    public static CrashHandler getCrashHander(){
        if (crashHandler == null){
            synchronized (CrashHandler.class){
                if (crashHandler == null){
                    crashHandler = new CrashHandler();
                }
            }
        }
        return crashHandler;
    }

    public void init(@NonNull Context context) {
        //默认为:RuntimeInit#KillApplicationHandler
        mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
    }

    /**
     * 当程序中有未被捕获的异常,系统将会调用这个方法
     *
     * @param t 出现未捕获异常的线程
     * @param e 得到异常信息
     */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        try {
            //自行处理:保存本地
            File file = dealException(t, e);
            //上传服务器
            //......
        } catch (Exception e1) {
            e1.printStackTrace();
        } finally {
            //交给系统默认程序处理
            if (mDefaultCrashHandler != null) {
                mDefaultCrashHandler.uncaughtException(t, e);
            }
        }
    }

    /**
     * 导出异常信息到SD卡
     *
     * @param e
     */
    private File dealException(Thread t, Throwable e) throws Exception{
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        File f = new File(mContext.getExternalCacheDir().getAbsoluteFile(), "crash_info");
        if (!f.exists()) {
            f.mkdirs();
        }
        File crashFile = new File(f, time + FILE_NAME_SUFFIX);
        //往文件中写入数据
        PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
        pw.println(time);
        pw.println("Thread: " + t.getName());
        pw.println(getPhoneInfo());
        e.printStackTrace(pw); //写入crash堆栈
        pw.close();
        return crashFile;
    }


    private String getPhoneInfo() throws PackageManager.NameNotFoundException {
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
        StringBuilder sb = new StringBuilder();
        //App版本
        sb.append("App Version: ");
        sb.append(pi.versionName);
        sb.append("_");
        sb.append(pi.versionCode + "\n");

        //Android版本号sb.append("OS Version: ");
        sb.append(Build.VERSION.RELEASE);
        sb.append("_");
        sb.append(Build.VERSION.SDK_INT + "\n");

        //手机制造商sb.append("Vendor: ");
        sb.append(Build.MANUFACTURER + "\n");

        //手机型号sb.append("Model: ");
        sb.append(Build.MODEL + "\n");
        //CPU架构
        sb.append("CPU: ");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sb.append(Arrays.toString(Build.SUPPORTED_ABIS));
        } else {
            sb.append(Build.CPU_ABI);
        }
        return sb.toString();
    }

}

NDK异常

NDK异常指的是app在使用c和c++ so库的时候,如果库运行引起app崩溃,我们同样可以采用类似的方式来收集和处理异常,用找出代码问题的具体位置。

Linux信号机制了解

信号机制是Linux进程间通信的一种重要方式,Linux信号一方面用于正常的进程间通信和同步,另一方

面它还负责监控系统异常及中断。当应用程序运行异常时,Linux内核将产生错误信号并通知当前进

程。当前进程在接收到该错误信号后,可以有三种不同的处理方式。

忽略该信号;

捕捉该信号并执行对应的信号处理函数(信号处理程序);

执行该信号的缺省操作(如终止进程);

当Linux应用程序在执行时发生严重错误,一般会导致程序崩溃。其中,Linux专门提供了一类crash信

号,在程序接收到此类信号时,缺省操作是将崩溃的现场信息记录到核心文件,然后终止进程。

常见崩溃信号列表:

SIGSEGV

内存引用无效。

SIGBUS

访问内存对象的未定义部分。

SIGFPE

算术运算错误,除以零。

SIGILL

非法指令,如执行垃圾或特权指令

SIGSYS

糟糕的系统调用

SIGXCPU

超过CPU时间限制。

SIGXFSZ

文件大小限制。

一般的出现崩溃信号,Android系统默认缺省操作是直接退出我们的程序。但是系统允许我们给某一个

进程的某一个特定信号注册一个相应的处理函数(signal),即对该信号的默认处理动作进行修改。因

此NDK Crash的监控可以采用这种信号机制,捕获崩溃信号执行我们自己的信号处理函数从而捕获NDK

Crash。

BreakPad

Google breakpad是一个跨平台的崩溃转储和分析框架和工具集合,其开源地址是:https://github.co

m/google/breakpad。breakpad在Linux中的实现就是借助了Linux信号捕获机制实现的。因为其实现

为C++,因此在Android中使用,必须借助NDK工具。

引入项目

将Breakpad源码下载解压,首先查看README.ANDROID文件。

If you're using the ndk-build build system, you can follow
these simple steps:

  1/ Include android/google_breakpad/Android.mk from your own
     project's Android.mk

     This can be done either directly, or using ndk-build's
     import-module feature.

  2/ Link the library to one of your modules by using:

     LOCAL_STATIC_LIBRARIES += breakpad_client

NOTE: The client library requires a C++ STL implementation,
      which you can select with APP_STL in your Application.mk

      It has been tested succesfully with both STLport and GNU libstdc++
      
      使用步骤:

1.将

android/google_breakpad/Android.mk 导入到我们自己项目中;

2.在自己的Android.mk中依赖breakpad_client

第一步:我们先新建一个module命名为breakpad;

第二步:在该module下的main目录新建cpp文件夹;

第三步:将Breakpad下载解压后的src拷贝至cpp包下面;

image-20201109014337179

第四步:在Breakpad下与src同级目录下新建CMakeLists.txt,并编写编译内容,该内容来源于android/google_breakpad/Android.mk文件,但是需要用cmake语法来完成编译脚本,具体内容如下:

cmake_minimum_required(VERSION 3.4.1)


include_directories(src src/common/android/include)

enable_language(ASM)

add_library(breakpad STATIC
        src/client/linux/crash_generation/crash_generation_client.cc
        src/client/linux/dump_writer_common/thread_info.cc
        src/client/linux/dump_writer_common/ucontext_reader.cc
        src/client/linux/handler/exception_handler.cc
        src/client/linux/handler/minidump_descriptor.cc
        src/client/linux/log/log.cc
        src/client/linux/microdump_writer/microdump_writer.cc
        src/client/linux/minidump_writer/linux_dumper.cc
        src/client/linux/minidump_writer/linux_ptrace_dumper.cc
        src/client/linux/minidump_writer/minidump_writer.cc
        src/client/minidump_file_writer.cc
        src/common/convert_UTF.cc
        src/common/md5.cc
        src/common/string_conversion.cc
        src/common/linux/breakpad_getcontext.S
        src/common/linux/elfutils.cc
        src/common/linux/file_id.cc
        src/common/linux/guid_creator.cc
        src/common/linux/linux_libc_support.cc
        src/common/linux/memory_mapped_file.cc
        src/common/linux/safe_readlink.cc)


target_link_libraries(breakpad log)

第五步:在cpp目录下新建breakpad.cpp文件,编写breakpad crash监控代码,具体内容如下;

//
// Created by Administrator on 2020/11/5.
//


#include 
#include 
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"
#include "breakpad/src/client/linux/handler/exception_handler.h"

bool DumpCallback(const google_breakpad::MinidumpDescriptor &descriptor,
                  void *context,
                  bool succeeded) {
    __android_log_print(ANDROID_LOG_ERROR, "native", "native crash:%s", descriptor.path());
    return false;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_breakpad_crash_BreakpadCrash_initNativeCrash(JNIEnv *env, jclass clazz, jstring path_) {

    const char *path = env->GetStringUTFChars(path_, 0);

    __android_log_print(ANDROID_LOG_INFO, "native", "===> %s", path);
    google_breakpad::MinidumpDescriptor descriptor(path);
    static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback,
                                                NULL, true, -1);
    env->ReleaseStringUTFChars(path_, path);

}

第六步:在cpp目录新建CMakeLists.txt来将breakpad库和crash 监控编译为可移植的so;

cmake_minimum_required(VERSION 3.4.1)

include_directories(breakpad/src breakpad/src/common/android/include)
# libbugly.so
add_library(breakpadcrash-lib SHARED breakpad.cpp)

add_subdirectory(breakpad)

# 链接ndk中的log库
target_link_libraries(breakpadcrash-lib  breakpad log)

第七步:新建java文件调用so 的native方法

public class BreakpadCrash {
    static {
        System.loadLibrary("breakpadcrash-lib");
    }

    public static void init(Context context) {
        Context applicationContext = context.getApplicationContext();
        File file = new File(applicationContext.getExternalCacheDir(), "native_crash");
        if (!file.exists()) {
            file.mkdirs();
        }
        initNativeCrash(file.getAbsolutePath());
    }

    public static native void initNativeCrash(String file);
}

第八步:在build.gradle文件中添加编译配置

apply plugin: 'com.android.library'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"


    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'

        externalNativeBuild {
            cmake {
//                cppFlags "-std=c++11"
                cppFlags ""
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.2.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

第九步:之后build该模块就可以在build\intermediates\cmake目录中看到生成的so

使用:在Application的oncreate中调用

image-20201109015041781

另外我们需要新建一个moudle来模拟第三方的jni so库,然后在activity中使用该测试库实现崩溃;

image-20201109015240136

image-20201109015501092

应用崩溃之后可以在data.包名.cache.native下面找到日志文件,改文件为dmp文件,需要使用minidump_stackwalk 工具来编写为正常的日志文件,在 Android Studio 的安装目录下的 bin\lldb\bin 里面就存在一

个对应平台的 minidump_stackwalk,进入该目录或者配置该目录到环境变量,然后执行:

minidump_stackwalk xxxx.dump > crash.txt

打开txt文件可以看到:

image-20201109015903606

有对应的错误类型和堆栈信息。

接下来使用 Android NDK 里面提供的 addr2line 工具将寄存器地址转换为对应符号。addr2line 要用和

自己 so 的 ABI 匹配的目录,同时需要使用有符号信息的so(一般debug的就有)。

因为我使用的是模拟器x86架构,因此addr2line位于:

E:\sdk\ndk\21.1.6352462\toolchains\x86_64-4.9\prebuilt\windows-x86_64\bin

image-20201109020201718
x86_64-linux-android-addr2line.exe -f -C -e E:\workspace\AndroidCrash\jnibug\build\intermediates\cmake\debug\obj\x86\libbugly-lib.so 0x5a4
image-20201109020241444

image-20201109020311629

项目源码:https://github.com/heezier/AndroidCrash

你可能感兴趣的:(Android异常监控)