一、什么是Crash?
想必这个只要从事过编程工作的同学一定知道是什么?这里我们进行一些概念上的普及:Crash就是由于代码异常而导致App非正常退出现象,也就是我们常说的『崩溃』
二、Android中有哪些类型Crash
通常情况下会有以下两种类型Crash:
Java Crash
Native Crash
三、Java层捕获Crash
通过UncaughtExceptionHandler来记录dump异常日志
package com.devilwwj.androidcrashdemo;
/**
* com.devilwwj.androidcrashdemo
* Created by devilwwj on 16/5/27.
*/
import android.content.Context;
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.Process;
import android.util.Log;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.Thread.UncaughtExceptionHandler;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String PATH = Environment
.getExternalStorageDirectory() + "/CrashDemo/log/";
private static final String FILE_NAME = "crash";
private static final String FILE_NAME_SUFFIX = ".trace";
private static final String ABOLUTE_PATH = PATH + FILE_NAME + FILE_NAME_SUFFIX;
private String deviceToken;
private static CrashHandler sInstance = new CrashHandler();
private UncaughtExceptionHandler mDefaultCrashHandler;
private Context mContext;
private CrashHandler() {
}
public static CrashHandler getInstance() {
return sInstance;
}
public void init(Context context) {
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
}
/**
* 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用#uncaughtException方法
* thread为出现未捕获异常的线程,ex为未捕获的异常,有了这个ex,我们就可以得到异常信息。
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
try {
// 导出异常信息到SD卡中
dumpExceptionToSDCard(ex);
} catch (IOException e) {
e.printStackTrace();
}
ex.printStackTrace();
// 如果系统提供了默认的异常处理器,则交给系统去结束我们的程序,否则就由我们自己结束自己
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
} else {
Process.killProcess(Process.myPid());
}
}
private File dumpExceptionToSDCard(Throwable ex) throws IOException {
// 如果SD卡不存在或无法使用,则无法把异常信息写入SD卡
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.w(TAG, "sdcard unmounted,skip dump exception");
return null;
}
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
.format(new Date(current));
// File file = new File(PATH + FILE_NAME + time + "_"+ deviceToken +
// FILE_NAME_SUFFIX);
File file = new File(PATH + FILE_NAME + FILE_NAME_SUFFIX);
if (!file.exists()) {
file.createNewFile();
} else {
try {
// 追加内容
PrintWriter pw = new PrintWriter(new BufferedWriter(
new FileWriter(file, true)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
ex.printStackTrace(pw);
pw.println("---------------------------------分割线----------------------------------");
pw.println();
pw.close();
} catch (Exception e) {
Log.e(TAG, "dump crash info failed");
}
}
return file;
}
private void dumpPhoneInfo(PrintWriter pw) throws NameNotFoundException {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(),
PackageManager.GET_ACTIVITIES);
pw.print("App Version: ");
pw.print(pi.versionName);
pw.print('_');
pw.println(pi.versionCode);
// android版本号
pw.print("OS Version: ");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.println(Build.VERSION.SDK_INT);
// 手机制造商
pw.print("Vendor: ");
pw.println(Build.MANUFACTURER);
// 手机型号
pw.print("Model: ");
pw.println(Build.MODEL);
// cpu架构
pw.print("CPU ABI: ");
pw.println(Build.CPU_ABI);
}
/**
* 提供方法上传异常信息到服务器
* @param log
*/
private void uploadExceptionToServer(File log) {
// TODO Upload Exception Message To Your Web Server
}
}
四、Native层捕获Crash
1.Native Crash在Android上的特点
出错时界面不会弹出提示框提醒程序崩溃(Android 5.0以下)
出错时会弹出提示框提醒程序崩溃(Android 5.0以上)
程序会直接闪退到系统桌面
这类错误一般是由C++层代码错误引起的
绝大部分Crash工具不能够捕获
我们在实际Android开发的时候,可能会引入第三方的一些so库或者自己开发相应的so库供程序使用,然而so库一般是通过c或者c++开发的。Android开发者通过java层的JNI机制调用Native语言写的函数,然而Natice语言也可以调用java层的函数。 如果有同学不明白的话,建议先去了解下JNI的相应技术,总的来说通过JNI技术,就让我们让Java世界跟Native世界可以联系在一起,也因为这个特性,让Java具有跨平台的特性。
2.Native Crash是如何产生的?
上一节我们谈到so库是同通过Native语言开发的,自然在Android中使用so库的时候发生的Crash,就是我们所说的Native Crash。为了更好的让大家知道Native Crash是如何产生的,下面笔者举一个例子:
Java层定义Native方法
JNI层实现Native方法
这里我们制造一个Native Crash,空指针异常。
通过Java调用Native方法
3.Native Crash如何分析?
既然要分析就必须找到可以分析的东西,我们在分析Java层Crash的时候是通过logcat日志找到对应的出错代码,然而Native层Crash也是可以logcat日志来进行分析的。
这里我们截取上面制造的crash在logcat显示的日志:
这个是什么鬼,看不懂啊有木有。这个出错信息是我们调用native函数时打印出来的日志,只是简单的描述出错信号,出错地址还有进程号,看这个是完全摸不着调的。不过系统还是会提供相关有用的日志,我们在Android Studio查看logcat的时候需要做一下过滤。
在logcat添加完”DEBUG”的过滤项之后,我们就能得到以下log:
这下子可分析的内容就多起来了,我们逐个来看看:
- 进程信息:pid表示进程号,tid表示线程号,name表示进程名
- 错误信号:signal 11表示信号的数字,SIGSEGV表示信号的名字,code 1(SEGV_MAPERR)表示出错代码,fault addr 00000000 表示出错的地址。
- 寄存器快照:进程收到错误信号时保存下来的寄存器快照,一共有15个寄存器。
- 堆栈信息:##00表示栈顶,##01调用#00,以此往下都是嵌套的调用关系,直至到栈顶。
我们在栈顶就已经看到我们出错的地方了:
#00 pc 00000730 /data/app-lib/com.devilwwj.jnidemo-1/libJNIDemo.so (Java_com_devilwwj_jnidemo_TestJNI_createANativeCrash)
1
pc 00000730 表示出错的地址,后面可以看到我加载了libJNIDemo.so库,接着是我们前面声明的Native方法,通过这种方法我们就能准确的找到出错的地方。
从上面的分析我们可以看到,so库崩溃时会产生信号异常,如果我们能够捕获到信号异常,相当于我们也能够顾捕获到Android Native崩溃了。
闪退发生时在logcat中将日志过滤条件选为“No Filters”就可以看到完整的闪退日志,或者叫tombstone(墓碑)文件。
tombstone(墓碑)是当系统 crash 的时候,会保存一个 tombstone 文件到/data/tombstones目录下(Logcat中也会有相应的信息),文件就像墓碑一样记录了死亡了的进程的基本信息(例如进程的进程 号,线程号),死亡的地址(在哪个地址上发生了 Crash),死亡时的现场是什么样的(记录了一系列的堆栈调用信息)等等。
1)addr2line
addr2line 是 用来获得指定动态链接库文件或者可执行文件中指定地址对应的源代码信息的工具.可以根据崩溃地址获得对应的函数。
4.常见的crash信号量
与 Java 平台不同,C/C++ 没有一个通用的异常处理接口,在 C 层,CPU 通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式,linux 把这些中断处理,统一为信号量,每一种异常都有一个对应的信号,可以注册回调函数进行处理需要关注的信号量。
所有的信号量都定义在<signal.h>文件中,这里我将几乎全部的信号量以及所代表的含义都标注出来了:
#define SIGHUP 1 // 终端连接结束时发出(不管正常或非正常)
#define SIGINT 2 // 程序终止(例如Ctrl-C)
#define SIGQUIT 3 // 程序退出(Ctrl-\)
#define SIGILL 4 // 执行了非法指令,或者试图执行数据段,堆栈溢出
#define SIGTRAP 5 // 断点时产生,由debugger使用
#define SIGABRT 6 // 调用abort函数生成的信号,表示程序异常
#define SIGIOT 6 // 同上,更全,IO异常也会发出
#define SIGBUS 7 // 非法地址,包括内存地址对齐出错,比如访问一个4字节的整数, 但其地址不是4的倍数
#define SIGFPE 8 // 计算错误,比如除0、溢出
#define SIGKILL 9 // 强制结束程序,具有最高优先级,本信号不能被阻塞、处理和忽略
#define SIGUSR1 10 // 未使用,保留
#define SIGSEGV 11 // 非法内存操作,与SIGBUS不同,他是对合法地址的非法访问,比如访问没有读权限的内存,向没有写权限的地址写数据
#define SIGUSR2 12 // 未使用,保留
#define SIGPIPE 13 // 管道破裂,通常在进程间通信产生
#define SIGALRM 14 // 定时信号,
#define SIGTERM 15 // 结束程序,类似温和的SIGKILL,可被阻塞和处理。通常程序如果终止不了,才会尝试SIGKILL
#define SIGSTKFLT 16 // 协处理器堆栈错误
#define SIGCHLD 17 // 子进程结束时, 父进程会收到这个信号。
#define SIGCONT 18 // 让一个停止的进程继续执行
#define SIGSTOP 19 // 停止进程,本信号不能被阻塞,处理或忽略
#define SIGTSTP 20 // 停止进程,但该信号可以被处理和忽略
#define SIGTTIN 21 // 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号
#define SIGTTOU 22 // 类似于SIGTTIN, 但在写终端时收到
#define SIGURG 23 // 有紧急数据或out-of-band数据到达socket时产生
#define SIGXCPU 24 // 超过CPU时间资源限制时发出
#define SIGXFSZ 25 // 当进程企图扩大文件以至于超过文件大小资源限制
#define SIGVTALRM 26 // 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
#define SIGPROF 27 // 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间
#define SIGWINCH 28 // 窗口大小改变时发出
#define SIGIO 29 // 文件描述符准备就绪, 可以开始进行输入/输出操作
#define SIGPOLL SIGIO // 同上,别称
#define SIGPWR 30 // 电源异常
#define SIGSYS 31 // 非法的系统调用
五、遇到崩溃信息怎么办?
1.收集崩溃时的基本信息
进程(前台进程还是后台进程)
线程(是否是 UI 线程)
崩溃堆栈(具体崩溃在系统的代码,还是我们自己的代码里面)
崩溃堆栈类型(Java 崩溃、Native 崩溃 or ANR)
收集崩溃时的系统信息
机型、系统、厂商、CPU、ABI、Linux 版本等。(寻找共性)
Logcat。(包括应用、系统的运行日志,其中会记录 App 运行的一些基本情况)
收集崩溃时的内存信息(OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系)
系统剩余内存。(系统可用内存很小 – 低于 MemTotal 的 10%时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现)
虚拟内存(但是很多类似OOM、tgkill 等问题都是虚拟内存不足导致的)
应用使用内存(得出应用本身内存的占用大小和分布)
线程数()
2.收集崩溃时的应用信息
崩溃场景(崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中)
关键操作路径(记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助)
其他自定义信息(不同应用关心的重点不一样。例如运行时间、是否加载了补丁、是否是全新安装或升级等)
如何分析崩溃日志的总结
确认重点(内存 & 线程 需特别注意,很多崩溃都是由于它们使用不当造成的)
3.确认严重程度
崩溃基本信息
Java 崩溃(比如 NullPPointerException 是空指针,OutOfMemoryError 是资源不足)
Native 崩溃(比较常见的是有 SIGSEGV 和 SIGABRT)
ANR(先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题,或是 CPU 竞争问题,还是由于大量 GC 导致卡死)
Logcat。从 Logcat 中我们可以看到当时系统的一些行为跟手机的状态,当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志。
查找共性(机型、系统、ROM、厂商、ABI)
复现问题