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 <jni.h>
#include <android/log.h>
#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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 原文[https://www.jianshu.com/p/4278915847b6]源码[https://down...
    WaterYuan阅读 1,882评论 1 2
  • Crash(应用崩溃)是由于代码异常而导致 App 非正常退出,导致应用程序无法继续使用,所有工作都 停止的现象。...
    zcwfeng阅读 611评论 0 2
  • 背景: 支付SDK面向游戏提供支付服务,高效的游戏引擎一般都会C++编写的,通过NDK编译成so文件在Androi...
    jpchen_hn阅读 4,813评论 0 6
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,472评论 2 59
  • 做为程序员,最不愿意看到的就是自己写的程序崩溃,特别是遇到没有错误信息的崩溃的时候,往往程序员自己也就随之一起崩溃...
    点融黑帮阅读 1,852评论 2 2