Android 平台的Crash崩溃捕获

上层-java/kotlin:

Android应用层java/kotlin的crash捕获相对容易。直接实现Thread.UncaughtExceptionHandler即可处理收集。Thread.UncaughtExceptionHandler:当某一线程因未捕获的异常而即将终止时,Java 虚拟机将使用 Thread.getUncaughtExceptionHandler() 查询该线程以获得其 UncaughtExceptionHandler 的线程,并调用处理程序的 uncaughtException 方法,将线程和异常作为参数传递。如果某一线程没有明确设置其 UncaughtExceptionHandler,则将它的 ThreadGroup 对象作为其 UncaughtExceptionHandler。如果 ThreadGroup 对象对处理异常没有什么特殊要求,那么它可以将调用转发给 默认的未捕获异常处理程序。

我们需要实现此接口,并注册为程序中默认未捕获异常处理。这样当未捕获异常发生时,就可以做一些个性化的异常处理操作。

    <pre name="code" class="java">package com.example.mycrashtest;
     
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.FilenameFilter;
    import java.io.PrintWriter;
    import java.io.StringWriter;
    import java.io.Writer;
    import java.lang.Thread.UncaughtExceptionHandler;
    import java.lang.reflect.Field;
    import java.text.DateFormat;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
     
    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.content.Intent;
    import android.content.pm.PackageInfo;
    import android.content.pm.PackageManager;
    import android.content.pm.PackageManager.NameNotFoundException;
    import android.os.Build;
    import android.os.Environment;
    import android.os.Looper;
    import android.os.SystemClock;
    import android.util.Log;
    import android.widget.Toast;
     
    /**
     * <h3>全局捕获异常</h3>
     * <br>
     * 当程序发生Uncaught异常的时候,有该类来接管程序,并记录错误日志
     *
     */
    @SuppressLint("SimpleDateFormat")
    public class CrashHandler implements UncaughtExceptionHandler {
     
        public static String TAG = "MyCrash";
        // 系统默认的UncaughtException处理类
        private Thread.UncaughtExceptionHandler mDefaultHandler;
     
        private static CrashHandler instance = new CrashHandler();
        private Context mContext;
     
        // 用来存储设备信息和异常信息
        private Map<String, String> infos = new HashMap<String, String>();
     
        // 用于格式化日期,作为日志文件名的一部分
        private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
     
        /** 保证只有一个CrashHandler实例 */
        private CrashHandler() {
        }
     
        /** 获取CrashHandler实例 ,单例模式 */
        public static CrashHandler getInstance() {
            return instance;
        }
     
        /**
         * 初始化
         *
         * @param context
         */
        public void init(Context context) {
            mContext = context;
            // 获取系统默认的UncaughtException处理器
            mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
            // 设置该CrashHandler为程序的默认处理器
            Thread.setDefaultUncaughtExceptionHandler(this);
            autoClear(5);
        }
     
        /**
         * 当UncaughtException发生时会转入该函数来处理
         */
        @Override
        public void uncaughtException(Thread thread, Throwable ex) {
            if (!handleException(ex) && mDefaultHandler != null) {
                // 如果用户没有处理则让系统默认的异常处理器来处理
                mDefaultHandler.uncaughtException(thread, ex);
            } else {
                SystemClock.sleep(3000);
                // 退出程序
                android.os.Process.killProcess(android.os.Process.myPid());
                System.exit(1);
            }
        }
     
        /**
         * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
         *
         * @param ex
         * @return true:如果处理了该异常信息; 否则返回false.
         */
        private boolean handleException(Throwable ex) {
            if (ex == null)
                return false;
     
            try {
                // 使用Toast来显示异常信息
                new Thread() {
     
                    @Override
                    public void run() {
                        Looper.prepare();
                        Toast.makeText(mContext, "很抱歉,程序出现异常,即将重启.",
                                Toast.LENGTH_LONG).show();
                        Looper.loop();
                    }
                }.start();
                // 收集设备参数信息
                collectDeviceInfo(mContext);
                // 保存日志文件
                saveCrashInfoFile(ex);
                SystemClock.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
     
            return true;
        }
     
        /**
         * 收集设备参数信息
         *
         * @param ctx
         */
        public void collectDeviceInfo(Context ctx) {
            try {
                PackageManager pm = ctx.getPackageManager();
                PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(),
                        PackageManager.GET_ACTIVITIES);
                if (pi != null) {
                    String versionName = pi.versionName + "";
                    String versionCode = pi.versionCode + "";
                    infos.put("versionName", versionName);
                    infos.put("versionCode", versionCode);
                }
            } catch (NameNotFoundException e) {
                Log.e(TAG, "an error occured when collect package info", e);
            }
            Field[] fields = Build.class.getDeclaredFields();
            for (Field field : fields) {
                try {
                    field.setAccessible(true);
                    infos.put(field.getName(), field.get(null).toString());
                } catch (Exception e) {
                    Log.e(TAG, "an error occured when collect crash info", e);
                }
            }
        }
     
        /**
         * 保存错误信息到文件中
         * @param ex
         * @return 返回文件名称,便于将文件传送到服务器
         * @throws Exception
         */
        private String saveCrashInfoFile(Throwable ex) throws Exception {
            StringBuffer sb = new StringBuffer();
            try {
                SimpleDateFormat sDateFormat = new SimpleDateFormat(
                        "yyyy-MM-dd HH:mm:ss");
                String date = sDateFormat.format(new java.util.Date());
                sb.append("\r\n" + date + "\n");
                for (Map.Entry<String, String> entry : infos.entrySet()) {
                    String key = entry.getKey();
                    String value = entry.getValue();
                    sb.append(key + "=" + value + "\n");
                }
     
                Writer writer = new StringWriter();
                PrintWriter printWriter = new PrintWriter(writer);
                ex.printStackTrace(printWriter);
                Throwable cause = ex.getCause();
                while (cause != null) {
                    cause.printStackTrace(printWriter);
                    cause = cause.getCause();
                }
                printWriter.flush();
                printWriter.close();
                String result = writer.toString();
                sb.append(result);
     
                String fileName = writeFile(sb.toString());
                return fileName;
            } catch (Exception e) {
                Log.e(TAG, "an error occured while writing file...", e);
                sb.append("an error occured while writing file...\r\n");
                writeFile(sb.toString());
            }
            return null;
        }
     
        private String writeFile(String sb) throws Exception {
            String time = formatter.format(new Date());
            String fileName = "crash-" + time + ".log";
            if (FileUtil.hasSdcard()) {
                String path = getGlobalpath();
                File dir = new File(path);
                if (!dir.exists())
                    dir.mkdirs();
     
                FileOutputStream fos = new FileOutputStream(path + fileName, true);
                fos.write(sb.getBytes());
                fos.flush();
                fos.close();
            }
            return fileName;
        }
     
        public static String getGlobalpath() {
            return Environment.getExternalStorageDirectory().getAbsolutePath()
                    + File.separator + "crash" + File.separator;
        }
        
        public static void setTag(String tag) {
            TAG = tag;
        }
        
        /**
         * 文件删除
         * @param day 文件保存天数
         */
        public void autoClear(final int autoClearDay) {
            FileUtil.delete(getGlobalpath(), new FilenameFilter() {
     
                @Override
                public boolean accept(File file, String filename) {
                    String s = FileUtil.getFileNameWithoutExtension(filename);
                    int day = autoClearDay < 0 ? autoClearDay : -1 * autoClearDay;
                    String date = "crash-" + DateUtil.getOtherDay(day);
                    return date.compareTo(s) >= 0;
                }
            });
            
        }
     
    }

    package com.example.mycrashtest;
     
    import android.app.Application;
     
    public class MyAppcation extends Application{
        
        @Override
        public void onCreate() {
            super.onCreate();
            CrashHandler.getInstance().init(this);
        }
     
    }

如此一来,java层的crash就能捕获了。
Native-C/C++层:

native crash具有上下文不全、出错信息模糊、难以捕捉等特点,比java crash更难修复。Android中JNI/NDK一般都是将c/c++生成so文件引入在java层调用c/c++的方法。当然也可以不这样。先看看so文件吧:

so 组成

一个完整的 so 由C代码加一些 debug 信息组成,这些debug信息会记录 so 中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表,这种 so 也叫做未 strip 的,通常体积会比较大。一般release的 so 都是需要经过一个strip操作的,这样strip之后的 so 中的debug信息会被剥离,整个 so 的体积也会缩小。

如下图所示:

如下可以看到strip之前和之后的大小对比。

可以简单将这个debug信息理解为Java代码混淆中的mapping文件,只有拥有这个mapping文件才能进行堆栈分析。

如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。

所以,这些debug信息尤为重要,是我们分析NE问题的关键信息,那么我们在编译 so 时候务必保留一份未被strip的so 或者剥离后的符号表信息,以供后面问题分析,并且每次编译的so 都需要保存,一旦产生代码修改重新编译,那么修改前后的符号表信息会无法对应,也无法进行分析。

获取 strip 和未被 strip 的 so

目前Android Studio无论是使用mk或者Cmake编译的方式都会同时输出strip和未strip的so,如下图是Cmake编译so产生的两个对应的so。

strip之前的so路径:build/intermediates/transforms/mergeJniLibs

strip之后的so路径:build/intermediates/transforms/stripDebugSymbol

另外也可以通过Android SDK提供的工具aarch64-linux-android-strip手动进行strip,aarch64-linux-android-strip这个工具位于/Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains目录下。

现在看看具体的几种捕获:

logcat捕获

顾名思义,就是通过logcat进行捕获,我们通过Android Studio打开logcat,制造一个NE(NativeException),只能看到很多类似#00 pc 00000000000161a0的符号,并没有一个可以直接阅读的日志,我们想通过logcat直接输出一份可以直接阅读的log。

可以使用Android/SDK/NDK下面提供的一个工具ndk-stack,它可以直接将NE输出的log解析为可阅读的日志。

ndk-stack一般是位于ndk的工具下面,Mac下的地址为

/Users/XXXX/Library/Android/sdk/ndk/21.3.6528147/ndk-stack

然后在该目录下执行控制台命令,或者在 Android Studio的terminal中执行也可

adb shell logcat | androidsdk绝对路径/ndk-stack -sym so所在目录

如此控制台在应用发生NE的时候便会输出如下日志,由日志可以看出,崩溃对应的so以及对应的方法名,如果有c的源码,那么就很容易定位问题。

    promote:~ njvivo$ adb shell logcat | ndk-stack -sym libbreakpad-core.so
    ********** Crash dump: **********
    Build fingerprint: 'vivo/PD1809/PD1809:8.1.0/OPM1.171019.026/compil04252203:user/release-keys'
    #00 0x00000000000161a0 /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/lib/arm64/libbreakpad-core.so (Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo+16)
    #01 0x00000000000090cc /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/oat/arm64/base.odex (offset 0x9000)
    Crash dump is completed

其实ndk-stack这个工具原理就是内部集成利用了addr2line来实时解析堆栈并且显示在控制台中。

看到这里有的小伙伴就觉得那这个不是很简单,但是实际的崩溃场景一是不容易复现,二是用户的场景有时候很难模拟,那么线上的NE崩溃又该如何监测和定位呢,有两种方式。

通过DropBox日志解析--适用于系统应用

这个很简单,DropBox会记录JE,NE,ANR的各种日志,只需要将DropBox下面的日志传上来即可进行分析解决,下面贴上一份日志示例。

 解析方案1:

借助上述的ndk-stack工具,可以直接将DropBox下面的日志解析成堆栈,从中可以看出,崩溃在breakpad.cpp第111行的Crash()方法中。

    ndk-stack -sym /Users/njvivo/Desktop/NE -dump data_app_native_crash@1605531663898.txt
    ********** Crash dump: **********
    Build fingerprint: 'vivo/PD1809/PD1809:8.1.0/OPM1.171019.026/compil04252203:user/release-keys'
    #00 0x00000000000161a0 /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/lib/arm64/libbreakpad-core.so (Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo+16)
    Crash()
    /Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:111:8
    Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo
    /Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:122:0
    #01 0x00000000000090cc /data/app/com.android.necase-lEp0warh8FqicyY1YqGXXA==/oat/arm64/base.odex (offset 0x9000)
    Crash dump is completed

解析方案2:

还是利用Android/SDK/NDK提供的工具linux-android-addr2line,这个工具位于/Users/njvivo/Library/Android/sdk/ndk目录下,有两个版本。

    aarch64架构
    /Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
    arm架构
    /Users/njvivo/Library/Android/sdk/ndk/21.3.6528147/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line

命令使用方法如下,结合未被strip的so以及日志里面出现的堆栈符号00000000000161a0,同样可以解析出崩溃地址和方法。

    aarch64-linux-android-addr2line -f -C -e libbreakpad-core.so 00000000000161a0
     
    Crash()
    /Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:111

 基于以上,看似也很简单,但是有一个致命的问题就是DropBox只有系统应用能访问,非系统应用根本拿不到日志,那么,非系统应用该怎么办呢?

通过BreakPad捕获解析--适用于所有应用

非系统应用可以通过google提供的开源工具BreakPad进行监测分析,CrashSDK也是采用的此种方式,可以实时监听到NE的发生,并且记录相关的文件, 从而可以将崩溃和相应的应用崩溃时的启动、场景等结合起来上报。

下面简单介绍一下BreakPad的使用方式。

BreakPad的实现功能

BreakPad主要提供两个个功能,NE的监听和回调,生成minidump文件,也就是dmp结尾的文件,另外提供两个工具,符号表工具和堆栈还原工具。

    符号表工具:用于从so中提取出debug信息,获取到堆栈对应的符号表。
    堆栈还原工具:用于将BreakPad生成的dump文件还原成符号,也就是堆栈偏移值。

这两个工具会在编译BreakPad源码的时候产生。

编译完之后会产生minidump_stackwalk工具,有些同学不想编译的话,Android Studio本身也提供了这个工具。

这个minidump_stackwalk程序在Android Studio的目录下面也存在,可以拿出来直接使用,如果不想编译的话,直接到该目录下面取即可,Mac路径为:

/Applications/Android Studio.app/Contents/bin/lldb/bin/minidump_stackwalk

BreakPad的捕获原理

由上述可以得知,BreakPad在应用发生NE崩溃时,可以将NE对应的minidump文件写入到本地,同时会回调给应用层,应用层可以针对本次崩溃做一些处理,达到捕获统计的作用,后续将minidump文件上传之后结合minidump_stackwalk以及addr2line工具可以还原出实际堆栈,示意图如下:

 在应用发生NE时,BreakPad会在手机本地生成一个dump文件,如图所示:

 得到了以上文件,我们只能知道应用发生了NE,但是这些文件其实是不可读的,需要解析这些文件。

下面着重讲一下如何分析上面产生的NE:

1、获取NE崩溃的dump文件,将刚才得到的minidump_stackwalk和dump文件放在同一个目录,也可以不放,填写路径的时候填写绝对路径即可。

然后在该目录下的终端窗口执行以下命令,该命令表示用minidump_stackwalk解析dump文件,解析后的信息输出到当前目录下的crashLog.txt文件。

./minidump_stackwalk xxxxxxxx.dmp >crashLog.txt

2、执行完之后,minidump_stackwalk会将NE的相关信息写到crashLog.txt里面,详细信息如图所示:

3、根据解析出的NE信息,关注图中红框,可以得知,这个崩溃发生的 libbreakpad-core.so 里面,0x161a0代表崩溃发生在相对根位置偏移161a0的位置。

获取崩溃堆栈

1、利用之前提到的addr2line工具,可以根据发生Crash的so文件以及偏移地址(0x161a0)可以得出产生crash的方法、行数和调用堆栈关系。

2、在其根目录对的终端窗口运行以下命令。

    arm-linux-androideabi-addr2line -C -f -e ${SOPATH} ${Address}
    -C -f           //打印错误行数所在的函数名称
    -e                //打印错误地址的对应路径及行数
    ${SOPATH}         //so库路径
    ${Address}        //需要转换的堆栈错误信息地址,可以添加多个,但是中间要用空格隔开,例如:0x161a0

3、如下图是真实运行的示例

    aarch64-linux-android-addr2line -f -C -e libbreakpad-core.so 0x161a0
    Crash()
    /Users/njvivo/Documents/project/Breakpad/breakpad-build/src/main/cpp/breakpad.cpp:111

由上图可以知道,该崩溃发生在breakpad.cpp文件的第111行,函数名是Crash(),与真实的文件一致,崩溃代码如下:

    void Crash() {
        volatile int *a = (int *) (NULL);
        *a = 1; //此处在代码里是111行
    }
     
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_online_breakpad_BreakpadInit_nUpdateLaunchInfo(JNIEnv *env, jobject instance,
                                                            jstring mLaunchInfoStr_) {
     
     
        DO_TRY
        {
            Crash();
            const char *mLaunchInfoStr = env->GetStringUTFChars(mLaunchInfoStr_, 0);
            launch_info = (char *) mLaunchInfoStr;
    //        env->ReleaseStringUTFChars(mLaunchInfoStr_, mLaunchInfoStr);
        }
        DO_CATCH("updateLaunchInfo");
     
    }

基于以上,便可以通过应用收集的dump文件解析的NE的详细堆栈信息。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值