Android全局异常捕获
PS:本文摘抄自《Android高级进阶》,仅供学习使用
Java API提供了一个全局异常捕获处理器,Android引用在Java层捕获Crash依赖的就是Thread.UncaughtExceptionHandler处理器接口,通常情况下,我们只需要实现这个接口,并重写其中的uncaughtException方法,在该方法中可以读取Crash的堆栈信息,语句如下:
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread thread, Throwable ex){
final Writer result = new StringWriter();
final PrintWriter printWriter = new PrintWriter(result);
//如果异常时在AsyncTask里面的后台线程抛出的
//那么实际的异常仍然可以通过getCause获得
Throwable cause = ex;
while(null!=cause){
cause.printStackTrach(printWriter);
cause = cause.getCause();
}
//stacktraceAsString就是获取的carsh堆栈信息
final String stacktraceAsString = result.toString();
printWriter.close();
}
}
为了使用自定义的UncaughtExceptionHandler,我们还需要对它进行注册,以替换应用默认的异常处理器,一般都是在Application类的onCreate方法中进行注册,语句如下:
public class MyApplication extends Application{
@Override
public void onCreate(){
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
}
}
通常情况下,收集发生Crash的堆栈信息就已经足够我们分析并定位出崩溃的原因,从而修复这个Crash。但复杂一点的Crash,可能仅有堆栈信息时不够的,我们还需要其他一些信息来辅助问题的定位和解决,这些信息包看如下内容:
1.线程信息
线程的基本信息包看ID、名字、优先级和所在的线程组,可以根据事件情况收集某些线程的信息,但通常收集发生Crash的线程信息即可,通用的线程信息收集代码如下:
public class ThreadCollector{
@NonNull
public static String collect(@Nullable Thread thread){
StringBuilder result = new StringBuilder();
if(thread!=null){
result.append("id=").append(thread.getId()).append("\n");
result.append("name=").append(thread.getName()).append("\n");
result.append("priority=").append(thread.getPriority()).append("\n");
if(t.getThreadGroup()!=null)){
result.append("groupName=").append(thread.getThreadGroup().getName()).append("\n");
}
}
return result.toString();
}
}
2.SharedPreference信息
某些类型的Crash依赖于应用的SharedPreference中的默写信息项。例如某个开关,当打开时,会导致APP运行发生Crash,关闭时不存在问题,这时为了准确复现这个Crash,如果有收集SharedPreference中的信息,将会极大的加速问题的定位,通用的收集代码如下:
final class SharedPreferencesCollector{
private final Context mContext;
private String[] mSharedPrefIds;
public SharedPreferencesCollector(Context context, String[] sharedPrefIds){
mContext = context;
mSharedPrefIds = sharedPrefIds;
}
@NonNull
public String collect(){
final StringBuilder result = new StringBuilder();
//收集默认的SharedPreferences信息
final Map<String, SharedPreferences> sharedPrefs = new TreeMap<String, SharedPreferences>();
sharedPrefs.put("default", PreferenceManager.getDefaultSharedPreferences(mContext));
//收集应用自定义的SharedPreferences信息
if(mSharedPrefIds != null){
for(final String sharedPrefId : mSharedPrefIds){
sharedPrefs.put("default", mContext.getSharedPreferences(sharedPrefId, Context.MODE_PRVATE));
}
}
//遍历所有的SharedPreferences文件
for(Map.Entry<String, SharedPreferences> entry : sharedPrefs.entrySet()){
final String sharedPrefId = entry.getKey();
final SharedPreferences prefs = entry.getValue();
final Map<String, ?> prefEntries = prefs.getAll();
//如果SharedPreferences文件内容为空
if(prefEntries.isEmpty()){
result.append(sharedPrefId).append("=").append("empty\n");
continue;
}
//遍历添加某个SharedPreferences文件中的内容
for(final Map.Entry<String, ?> predEntry : prefEntries.entrySet()){
final Object prefVaule = prefEntry.getValue();
result.append(sharedPrefId).append(".").append(prefEntry.getKey()).append("=");
result.append(prefVaule == null ? "null" : prefVaule.toString()).append("\n")
}
result.append("\n")
}
}
return result.toString();
}
3.系统设置
在Android中,许多的系统属性都是在系统设置中进行设置的,如果蓝牙、Wi-Fi的状态、当前的首选语言、屏幕亮度等。这些信息存放在数据库中,对应的URI为content://settings/system、content://setting/secure、content://settings/global等。对这些数据库的读写操作对应着Android SDK中的Settings类,我们对系统设置的读写本质上就是对这些数据库表的操作。
- System:以键值对的形式存放系统中各种类型的常规偏好设置,它是可读写的,获取这种类型设置的读写如下,使用反射的方式是为了兼容不容的APILevel
final class SettingsCollector{
private static final String LOG_TAG = "SetingsCollector"
private final Context mContext;
public SettingsCollector(Context context){
mContext = context;
}
@NonNull
public String collectSystemSettings(){
final StringBuilder result = new StringBuilder();
final Field[] keys = Settings.System.class.getFields();
for(final Field key : keys){
//Avoid retrieving deprecated fields... it is useless, has an
//impact on prefs, and the system weites many warnings in the
//logcat.
if(!key.isAnnotationPresent(Deprecated.class) && key.getType() == String.class){
try{
final Object value = Settings.System.getString(mContext.getContentResolver(), (String)key.get(null));
if(value != null){
result.append(key.getName()).append("=").append(value).append("\n);
}
}catch(@NunNull Exception e){
Log.w(LOG_TAG, "Error:", e);
}
}
}
}
return result.toString();
}
- Secure:以键值对的形式存放系统的安全设置,这个是只读的,获取这种类型设置的代码如下:
@NonNull
public String CoolectSecureSettings(){
final StringBuilder result = new StringBuilder();
final Field[] keys = Settings.Secure.class.getFields();
for(final Field key : keys){
if(!key.isAnnotationPresent(Deprecated.class) && key.getType() == String.class && isAuthorized(key)){
try{
final Object value = Settings.Secure.getString(mContext.getContentResolver(), (String)key.get(null));
if(value != null){
result.append(key.getName()).append("=").append(value).append("\n);
}
}catch(@NonNull Exception e){
Log.w(LOG_TAG, "Error", e);
}
}
}
return result.toString();
}
- Global:以键值对的形式存放系统中对所有用户公用的偏好设置,它是只读的,获取这种类型设置的代码如下:
@NonNull
public String collectGlobalSettins(){
if(Build.VERSION.SDK_INT < Builde.VERSION_CODES.JELLY_BEN_MR1){
return "";
}
final StringBuilder result = new StringBuilder();
try{
final Class<?> globalClass = Class.forName("android.provider.Settings$Global);
final Field[] keys = globalClass .getFields();
final Method getString = globalClass.getMethod("getString", ContentResolver.class, String.class);
for(final Field key : keys){
if(!key.isAnnotationPresent(Deprecated.class) && key.getType() == String.class && isAuthorized(key)){
final Object value = getString.invoke(null, mContext.getContentResolver(), key.get(null));
if(value!=null){
result.append(key.getName()).append("=").append(value).append("\n);
}
}
}
}catch(@NonNull Exception e){
Log.w(LOG_TAG, "Error", e);
}
return result.toString();
} private boolen isAuthorized(@Nullable Field key){
if(key == null && key.getName().startsWith("WIFI_AP")){
return false;
}
return true;
}
4.Logcat中的日志记录
捕获Logcat日志的好处是可以清楚地知道Crash发生前后的上下文,对于准确定位Crash来说提供了更完备的信息,实现代码如下:
class LogcatCollector{
private static final String LOG_TAG = "LogcatCollector";
private static final int DEFAULT_TAIL_COUNT = 100;//保留logcat输出中最后的行数
private static final int DEFAULT_BUFFER_SIZE_IN_BYTES = 8192;
public String collectLogcat(@Nullable String bufferName, boolean logcatFilterByPid, String[] logcatArguments]){
final int myPid = android.os.Process.myPid();
String myPidStr = null;
if(logcatFilterByPid && mPid >0){//只收集当前进程相关的logcat信息
myPidStr = Integer.toString(myPid) + ":";
}
fianl List<String> commandLine = new ArrayList<>();
commandLine.add("logcat");
if(bufferName!=null){
commandLine.add("-b");
commandLine.add(bufferName);
}
//logcat的"-t n"参数是API Level 8才引入的,对于之前的系统版本
//需要做特殊处理来模拟这种情况
final int tailCount;
final List<String> logcatArgumentsList = new ArrayList<>(Arrays.asList(logcatArguments));
final int tailIndex = logcatArgumentsList.index("-t");
if(tailIndex > -1 && tailIndex < logcatArgumentsList.size()){
tailCount = Integer.parseInt(logcatArgumentsList.get(tailIndex + 1));
if(Build.VERSION.SDK_INT < Build.VERSION_CODE.FROYO){
logcatArgumentsList.remove(tailIndex+1);
logcatArgumentsList.remove(tailIndex);
logcatArgumentsList.add("-d");
}
}else{
tailCount=-1;
}
}
final LinkedList<String> logcatBuf = new BoundedLinkedList<>(tailCount>0?tailCount:DEFAULT_TAIL_COUNT);
commandLine.addAll(logcatArgumentsList);
BufferedReader bufferedReader = null;
try{
final Process process = Runtime.getRuntime().exec(commandLine.toArray(new String[commandLine.size()]));
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream(), DEFAYLT_BUFFER_SIZE_IN_BYTES);
//Dump stderr to null
new Thread(new Runnable(){
public void run(){
try{
InputStream stderr = process.getErrorStream();
byte[] dummy = new byte[DEFAYLT_BUFFER_SIZE_IN_BYTES];
//noinspection StatementWithEmptyBody
while(stderr.read(dummy) >= 0);
}catch(Exception ignored){
}
}
}).start();
while(true){
final String line = bufferedReader.readLine();
if(line==null)break;
if(myPidStr==null||line.contains(myPidStr)){
logcatBuf.add(line+"\n");
}
}
}catch(Exception e){
Log.e(LOG_TAG, "LogcatCollector.collectLogcat could not retrieve data.", e);
}finally{
try{
if(bull!=bufferedReader){
bufferedReader.close
}
}catch(Exception ignored){
}
}
return logcatBuf.toString();
}
5.自定义Log文件中的内容
有时候,我们的APP会将一些重要的日志信息有选择的存放到内部存储或者外部存储的某个Log文件中,当发生Crash时,也可以收集这个Log文件中的内容并上传到服务器,帮助问题的分析和定位,实现代码如下。可以收集指定文件中指定行数的内容:
class LogFileCollector{
@NonNull
public String collectLogFile(@NonNull Context context, @NonNull String fileName, int numberOfLines) throws IOException{
final BoundedLinkedList<String> resultBuffer = new BoundedLinkedList<>(numberOfLines);
final BufferedReader reader = getReader(context, fileName);
try{
String line = reader.readLine();
while(line!=null){
resultBuffer.add(line+"\n");
line=reader.readLine();
}
}finally{
try{
reader.close();
}catch(Exception e){
}
}
return resultBuffer.toString();
}
}
@NonNull
private static BufferedReader getReader(@NonNull Context context, @NonNull String fileName){
try{
final FileInputStream inputStream;
if(fileName.startsWith("/")){
inputStream=new FileInputStream(fileName);//绝对路径
}else if(fileName.contains("/")){
inputStream=new FileInputStream(new File(context.getFilesDir(), fileName);//相对路径
}else{
inputStream=context.openFileInput(fileName);//用用内部存储中的某个文件
}
return new BufferedReader(new InputStreamReader(inputStream),1024)
}catch(Exception e){
return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(new Byte[0])));
}
}
6.MemInfo信息
Crash发生时的内存使用情况对某些类型的Crash定位也是有很大帮助的,通过执行dumpsys meminfo命令可以获取当前进程的内存使用信息,语句如下:
final class DumpsysCollector{
private static final String LOG_TAG = "DumpsysCollector";
private static final int DEFAULT_BUFFER_SIZE_IN_BYTES = 8192;
@NunNull
public static String collectMemInfo(){
final StringBuilder meminfo = new StringBuilder();
BufferedReader bufferedReader = null;
try{
final List<String commandLine = new ArrayList<>();
commandLine.add("dumpsys");
commandLine.add("meminfo");
commandLine.add(Integer.toString(android.os.Process.myPid()));
final Process process = Runtime.getRuntime().exec(commandLine.toArray(new String[commandLine.size()]));
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()), DEFAULT_BUFFER_SIZE_IN_BYTES);
while(true){
final String line = bufferedReader.readLine();
if(line==null)break;
meminfo.append(line);
meminfo.append("\n");
}
}catch(Exception e){
Log.e(LOG_TAG, "DumosysCollector.meminfo could not retrievedata", e);
}
try{
if(null!=bufferedReader){
bufferedReader.close();
}catch(Exception e){
}
}
return meminfo.toString();
}
}
7.Native层Crash捕获机制
Native层代码的错误可以分为两种。
C++异常:
在Native层,如果使用C++语言进行开发,而且使用了C++异常机制,那么函数执行可以抛出std::exception类型的异常;如果使用C/C++语言开发,使用的错误码机制,那么对于一些导致系统不可用的错误码,我们也可以进行捕获上报。总的来说,C++异常通常是可捕获的,一般不会引起APP Crash,当然如果处理不当,会引起逻辑错误。
Fatal Signal异常:
在Native层,由于C/C++野指针或者内存读取越界等原因,导致APP整个Crash的错误。这种Crash一般会在Logcat中打印出包含Fatal signal字样的日志。对于这种Crash,前面介绍的Java异常捕获类Thread.UncaughtExceptionHandler是检测不到的。那么如何捕获这种异常并上报呢?
熟悉Linux底层的应该很容易看出种种Crash是基于Linux的信号处理机制。信号(又称为软中断信号,signal)本质上是一种软件层面的中断机制,用来通知进程发生了异步事件。进程之间可以相互通过系统调用kill来发送软中断信号;Linux内核也可以应为内部事件而给进程发送信号,通知进程某个事件的发生。需要注意的是,信号并不携带任何数据,它只是用啦i通知某进程发生了什么事件。接受到信号后,通常有三种处理方式。
(1)自定义处理信号:进程为需要处理的信号提供信号处理函数。
(2)忽略信号:进程忽略不感兴趣的信号(SIGKILL和SIGSTOP忽略不了)/
(3)使用系统的默认处理:使用内核的默认信号处理函数,默认情况下,系统对大部分信号的缺省操作是终止进程。
了解信号的基本只是后,那么问题就变得恨简单了,由于Native层Crash大部分都是signal软中断类型错误,一次只要捕获signal并进行处理,得到中断的具体信息就很好帮助定位了。这一步可以通过sigaction注册信号处理函数来完成。
//要捕获的信号类型
const int handledSignals[] = {
SIGFPE, SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGIPE, SIGSTKFLT
};
//信号类型的个数
const int handledSignalsNum = sizeof(handledSignals)/sizeof(handledSignals[0]);
//保存老的信号
struct sigaction old_hanlders[handledSignalsNum];
void initCrashHandler(){
struct sigaction handler;
memset(&handler, 0, sizeof(sigaction));
handler.sa_sigaction=my_handler;
handler.sa_flags=SA_RESETHAND;
//注册信号处理函数的宏定义,减少冗余代码
#define CATCH_SIG(X) sigaction(handledSignals[X], &handler, &old_handlers[X])
//遍历所有关注的信号并注册信号处理器
for(int i=0;i<handledSignalsNum;++i){
CATCH_SIG(handledSignals[i]);
}
}
上面代码中的my_handler回调函数就是用来处理信号的,在这个函数中,我们设法获取Native Crash的相关堆栈信息,然后上报给服务器。但是Native层并没有提供像Java层那样的Throwable.printStackTrace函数来获取堆栈信息,目前来说有两种思路。
- 抓取Logcat日志:前面说过,Native层发生fatal signal导致APP崩溃,也会在Logcat中打印出相关的堆栈信息,因此,当在Native层检测到fatal signal,利用我们的信号处理函数my_handler可以向Java层发送信息,通知它去抓取Logcat的日志,抓取的方式上面已经介绍过,需要注意的一点是,这时候由于Crash应用原有的进程将会很快被结束掉,因此Logcat的抓取应该开启新的进程,例如启动一个新进程在Service中进行操作。
- Google Breakpad:这是一个跨平台的奔溃转储和分析工具,支持windows、linux、osx、android等,通过继承它提供的函数库,在应用发生奔溃时会将相关堆栈信息写入一个minidump格式文件中,通过将这个文件上传到服务器,开发人员可以通过addr2line等工具将dump文件中的函数地址转换成对应的代码行数,从而知道问题发生的具体位置。
Android全局异常捕获的更多相关文章
- MVC 好记星不如烂笔头之 ---> 全局异常捕获以及ACTION捕获
public class BaseController : Controller { /// <summary> /// Called after the action method is ...
- atitit.js浏览器环境下的全局异常捕获
atitit.js浏览器环境下的全局异常捕获 window.onerror = function(errorMessage, scriptURI, lineNumber) { var s= JSON. ...
- C#中的那些全局异常捕获
1.WPF全局捕获异常 public partial class App : Application { public App() { // 在异 ...
- Spring-MVC开发之全局异常捕获全面解读
异常,异常 我们一定要捕获一切该死的异常,宁可错杀一千也不能放过一个! 产品上线后的异常更要命,一定要屏蔽错误内容,以免暴露敏感信息! 在用Spring MVC开发WEB应用时捕获全局异常的方法基本有 ...
- Asp.Net MVC3(三)-MvcApp实现全局异常捕获
定义异常捕获类: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMu ...
- 使用spring利用HandlerExceptionResolver实现全局异常捕获
最近一直没有时间更新是因为一直在更新自己使用的框架. 之后会慢慢带来对之前使用的spring+mvc+mybatis的优化. 会使用一些新的特性,实现一些新的功能. 我会尽量分离业务,封装好再拿出来. ...
- .Net下的全局异常捕获问题
全局异常捕获主要目标并不是为了将异常处理掉防止程序崩溃.因为当错误被你的全局异常捕获器抓到的时候,已经证实了你程序中存在BUG. 一般而言,我们的全局异常捕获主要作用就是接收到异常之后进行异常的反馈. ...
- (转)C#中的那些全局异常捕获
C#中的那些全局异常捕获(原文链接:http://www.cnblogs.com/taomylife/p/4528179.html) 1.WPF全局捕获异常 public partia ...
- springboot(二 如何访问静态资源和使用模板引擎,以及 全局异常捕获)
在我们开发Web应用的时候,需要引用大量的js.css.图片等静态资源. 默认配置 Spring Boot默认提供静态资源目录位置需置于classpath下,目录名需符合如下规则: /static / ...
随机推荐
- CodeForces-652D:Nested Segments(树状数组+离散化)
You are given n segments on a line. There are no ends of some segments that coincide. For each segme ...
- Java-Runoob-高级教程-实例-数组:01. Java 实例 – 数组排序及元素查找
ylbtech-Java-Runoob-高级教程-实例-数组:01. Java 实例 – 数组排序及元素查找 1.返回顶部 1. Java 实例 - 数组排序及元素查找 Java 实例 以下实例演示 ...
- less 使用入门
LESSS是基于JavaScript,所以,是在客户端处理的. 使用less很简单: 1 下载less.js 2 新建less文件后缀名称是.less 3 在页面中引入less文件,跟引入普通的css ...
- hdu4975 A simple Gaussian elimination problem.(最大流+判环)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4975 题意:和hdu4888基本一样( http://www.cnblogs.com/a-clown/ ...
- window切换Java版本原因
查找Path环境变量的变量指向的目录,有一个Oracle目录存放着几个 java,javac等可执行文件,删除这个路径或文件就可以执行你指定的JavaHome目录拉 详情参考: https://blo ...
- IE浏览器下错误,不能执行已释放script的代码
错误提示: 错误原因: 我使用layui打开子页面,用到了父页面中的一个全局变量(我用的数组),子页面关闭后,使用该数组方法(如:arr.join(",")),便提示此错误 我的解 ...
- 在myeclipse中maven遇见的问题
An internal error occurred during: "Retrieving archetypes:". Java heap space 表示你的myeclipse ...
- [SHOI2002]取石子游戏之三
Wythoff's Game,详解请见浅谈算法--博弈论中的例6 /*program from Wolfycz*/ #include<cmath> #include<cstdio&g ...
- 自定义View(12)绘制.9图片
代码如下: // 绘制.9图片 void draw9Path(Canvas canvas){ //创建一个ninePatch的对象实例,第一个参数是bitmap.第二个参数是byte[],这里其实要求 ...
- poj2661Factstone Benchmark
链接 利用log函数来求解 n!<=2^k k会达到400+W 暴力就不要想了,不过可以利用log函数来做 log2(n!) = log2(1)+log2(2)+..log2(n)<=k ...