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发生时会转入该函数来处理*/@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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{@Overridepublic 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 00000000000161a0Crash()
/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的详细堆栈信息。
Android NativeCrash 捕获与解析
Android 平台的Crash崩溃捕获-全相关推荐
- Android平台实现系统内录(捕获播放的音频)并推送RTMP服务技术方案探究
几年来,我们在做无纸化同屏或在线教育相关场景的时候,总是被一件事情困扰:如何实现Android平台的系统内录,并推送到其他播放端,常用的场景比如做无纸化会议或教育的时候,主讲人或老师需要放一个视频,该 ...
- Android 打造异常崩溃捕获工具
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/112476811 本文出自[赵彦军的博客] 文章目录 前言 Thread.dumpS ...
- android+程序崩溃,Android平台程序崩溃的类型及原因列举
Android平台程序崩溃大家都应该遇到过,force close和ANR应该是大家遇到较多的. 这里把Android平台程序崩溃的各种类型做一个简述和原因列举. 1.ANR(可见ANR): 发生场景 ...
- Android平台程序崩溃的类型及原因列举
Android平台程序崩溃大家都应该遇到过,force close和ANR应该是大家遇到较多的. 这里把Android平台程序崩溃的各种类型做一个简述和原因列举. 1.ANR(可见ANR): 发生场景 ...
- android 崩溃捕获_Android使用FileProvider捕获图像相机图库
android 崩溃捕获 In this tutorial, we'll be developing an application which displays images captured fro ...
- Android进阶——Crash异常捕获并发送到服务器
在项目中,我们常常会遇到Crash的现象,也就是程序崩溃的时候,这个时候最常看到的就是这个界面 如果你的项目已经发布到市场上了,这样的崩溃对于开发人员是看不到的,所以我们得想方法将崩溃信息发送到服务器 ...
- android 崩溃捕获_Android从相机和图库捕获图像
android 崩溃捕获 In this tutorial we'll develop an application that picks an image from camera or galler ...
- 【流媒体开发】VLC Media Player - Android 平台源码编译 与 二次开发详解 (提供详细800M下载好的编译源码及eclipse可调试播放器源码下载)
作者 : 韩曙亮 博客地址 : http://blog.csdn.net/shulianghan/article/details/42707293 转载请注明出处 : http://blog.csd ...
- Android开发高性能之 (崩溃优化)
文章目录 1. Android 的两种崩溃 1.1 Native 崩溃的捕获流程 2. 如何客观地衡量崩溃 3.如何客观地衡量稳定性 4.应用崩溃了,你应该如何去分析? 4.1 崩溃信息 4.2 系统 ...
最新文章
- 【PHPExcel】设置打印格式
- swift:简单使用翻页控制器UIPageViewController
- python执行gradle脚本
- Android 快速实现微信支付(真的!很快!)
- Linux嵌入式开发_修改机器码
- 如何在服务器上上传手游源码,【仙变3手游】跨服服务端游戏源码+架设教程+双端+后台...
- 1 Arduino开发软件和下载程序
- css背景图铺满后图片变模糊的解决办法
- 作为技术面试官,我在面试时考虑什么?
- 1分钟学会PS去水印
- [C++]cpp小笔记3 --- C++ String and char
- 电单车中的N车模长得啥样呢?
- Xmanager或者Xshell用户连接信息迁移
- 什么是拼多多上货助手?拼多多上货助手有哪些功能?详细介绍
- nbu备份本机oracle,使用NetBackup进行oracle备份和恢复
- 【信息收集】第六章 社会工程学
- 小人物解决四大数学问题:记传奇华人数学家李天岩
- Windows 10 启用telnet client功能
- 查看oracle是否open,Oracle查看已被使用的open_cursorssession_cached_cursors
- oracle 自定义数据类型