Android将程序崩溃信息保存本地文件
大家都知道,现在安装Android系统的手机版本和设备千差万别,在模拟器上运行良好的程序安装到某款手机上说不定就出现崩溃的现象,开发者个人不可能购买所有设备逐个调试,所以在程序发布出去之后,如果出现了崩溃现象,开发者应该及时获取在该设备上导致崩溃的信息,这对于下一个版本的bug修复帮助极大,所以今天就来介绍一下如何在程序崩溃的情况下收集相关的设备参数信息和具体的异常信息,并发送这些信息到服务器供开发者分析和调试程序。
源码下载地址:http://download.csdn.net/detail/weidi1989/4588310
我们先建立一个crash项目,项目结构如图:
在MainActivity.java代码中,故意制作一个错误的例子,以便于我们实验:
package com.scott.crash; import android.app.Activity;
import android.os.Bundle; public class MainActivity extends Activity { private String s; @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.out.println(s.equals("any string"));
}
}
我们在这里故意制造了一个潜在的运行期异常,当我们运行程序时就会出现以下界面:
遇到软件没有捕获的异常之后,系统会弹出这个默认的强制关闭对话框。
我们当然不希望用户看到这种现象,简直是对用户心灵上的打击,而且对我们的bug的修复也是毫无帮助的。
我们需要的是软件有一个全局的异常捕获器,当出现一个我们没有发现的异常时,捕获这个异常,并且将异常信息记录下来,上传到服务器公开发这分析出现异常的具体原因。
接下来我们就来实现这一机制,不过首先我们还是来了解以下两个类:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。
Application:用来管理应用程序的全局状态。在应用程序启动时Application会首先创建,然后才会根据情况(Intent)来启动相应的Activity和Service。本示例中将在自定义加强版的Application中注册未捕获异常处理器。
Thread.UncaughtExceptionHandler:线程未捕获异常处理器,用来处理未捕获异常。如果程序出现了未捕获异常,默认会弹出系统中强制关闭对话框。我们需要实现此接口,并注册为程序中默认未捕获异常处理。这样当未捕获异常发生时,就可以做一些个性化的异常处理操作。
大家刚才在项目的结构图中看到的CrashHandler.java实现了Thread.UncaughtExceptionHandler,使我们用来处理未捕获异常的主要成员,代码如下:
package com.way.crash; import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
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.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map; 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.Looper;
import android.util.Log;
import android.widget.Toast; /**
* UncaughtException处理类,当程序发生Uncaught异常的时候,由该类来接管程序,并记录发送错误报告.
*
* @author way
*
*/
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private Thread.UncaughtExceptionHandler mDefaultHandler;// 系统默认的UncaughtException处理类
private static CrashHandler INSTANCE = new CrashHandler();// CrashHandler实例
private Context mContext;// 程序的Context对象
private Map<String, String> info = new HashMap<String, String>();// 用来存储设备信息和异常信息
private SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd-HH-mm-ss");// 用于格式化日期,作为日志文件名的一部分 /** 保证只有一个CrashHandler实例 */
private CrashHandler() { } /** 获取CrashHandler实例 ,单例模式 */
public static CrashHandler getInstance() {
return INSTANCE;
} /**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();// 获取系统默认的UncaughtException处理器
Thread.setDefaultUncaughtExceptionHandler(this);// 设置该CrashHandler为程序的默认处理器
} /**
* 当UncaughtException发生时会转入该重写的方法来处理
*/
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果自定义的没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(3000);// 如果处理了,让程序继续运行3秒再退出,保证文件保存并上传到服务器
} catch (InterruptedException e) {
e.printStackTrace();
}
// 退出程序
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
} /**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* 异常信息
* @return true 如果处理了该异常信息;否则返回false.
*/
public boolean handleException(Throwable ex) {
if (ex == null)
return false;
new Thread() {
public void run() {
Looper.prepare();
Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 0).show();
Looper.loop();
}
}.start();
// 收集设备参数信息
collectDeviceInfo(mContext);
// 保存日志文件
saveCrashInfo2File(ex);
return true;
} /**
* 收集设备参数信息
*
* @param context
*/
public void collectDeviceInfo(Context context) {
try {
PackageManager pm = context.getPackageManager();// 获得包管理器
PackageInfo pi = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_ACTIVITIES);// 得到该应用的信息,即主Activity
if (pi != null) {
String versionName = pi.versionName == null ? "null"
: pi.versionName;
String versionCode = pi.versionCode + "";
info.put("versionName", versionName);
info.put("versionCode", versionCode);
}
} catch (NameNotFoundException e) {
e.printStackTrace();
} Field[] fields = Build.class.getDeclaredFields();// 反射机制
for (Field field : fields) {
try {
field.setAccessible(true);
info.put(field.getName(), field.get("").toString());
Log.d(TAG, field.getName() + ":" + field.get(""));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
} private String saveCrashInfo2File(Throwable ex) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : info.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\r\n");
}
Writer writer = new StringWriter();
PrintWriter pw = new PrintWriter(writer);
ex.printStackTrace(pw);
Throwable cause = ex.getCause();
// 循环着把所有的异常信息写入writer中
while (cause != null) {
cause.printStackTrace(pw);
cause = cause.getCause();
}
pw.close();// 记得关闭
String result = writer.toString();
sb.append(result);
// 保存文件
long timetamp = System.currentTimeMillis();
String time = format.format(new Date());
String fileName = "crash-" + time + "-" + timetamp + ".log";
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
try {
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "crash");
Log.i("CrashHandler", dir.toString());
if (!dir.exists())
dir.mkdir();
FileOutputStream fos = new FileOutputStream(new File(dir,
fileName));
fos.write(sb.toString().getBytes());
fos.close();
return fileName;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
然后,我们需要在应用启动的时候在Application中注册一下:
package com.way.crash; import android.app.Application; public class CrashApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
}
}
最后,为了让我们的CrashApplication取代android.app.Application的地位,在我们的代码中生效,我们需要修改AndroidManifest.xml:
<application android:name=".CrashApplication"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
</application>
因为我们上面的CrashHandler中,遇到异常后要保存设备参数和具体异常信息到SDCARD,所以我们需要在AndroidManifest.xml中加入读写SDCARD权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
搞定了上边的步骤之后,我们来运行一下这个项目:
可以看到,并不会有强制关闭的对话框出现了,取而代之的是我们比较有好的提示信息。
然后看一下SDCARD生成的文件:
用文本编辑器打开日志文件,看一段日志信息:
CPU_ABI=armeabi
CPU_ABI2=unknown
ID=FRF91
MANUFACTURER=unknown
BRAND=generic
TYPE=eng
......
Caused by: java.lang.NullPointerException
at com.scott.crash.MainActivity.onCreate(MainActivity.java:13)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
... 11 more
这些信息对于开发者来说帮助极大,所以我们需要将此日志文件上传到服务器。
下面是一个以邮件形式提交错误报告的方法
由于context为非Activity的context,所以,我把弹出的对话框用了系统windows属性,记得加上以下权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
/**
* UncaughtException处理类,当程序发生Uncaught异常的时候,由该类来接管程序,并记录发送错误报告.
*
* @author way
*
*/
public class CrashHandler implements UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefaultHandler;// 系统默认的UncaughtException处理类
private static CrashHandler INSTANCE;// CrashHandler实例
private Context mContext;// 程序的Context对象 /** 保证只有一个CrashHandler实例 */
private CrashHandler() { } /** 获取CrashHandler实例 ,单例模式 */
public static CrashHandler getInstance() {
if (INSTANCE == null)
INSTANCE = new CrashHandler();
return INSTANCE;
} /**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context; mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();// 获取系统默认的UncaughtException处理器
Thread.setDefaultUncaughtExceptionHandler(this);// 设置该CrashHandler为程序的默认处理器
} /**
* 当UncaughtException发生时会转入该重写的方法来处理
*/
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果自定义的没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
}
} /**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* 异常信息
* @return true 如果处理了该异常信息;否则返回false.
*/
public boolean handleException(Throwable ex) {
if (ex == null || mContext == null)
return false;
final String crashReport = getCrashReport(mContext, ex);
Log.i("error", crashReport);
new Thread() {
public void run() {
Looper.prepare();
File file = save2File(crashReport);
sendAppCrashReport(mContext, crashReport, file);
Looper.loop();
} }.start();
return true;
} private File save2File(String crashReport) {
// TODO Auto-generated method stub
String fileName = "crash-" + System.currentTimeMillis() + ".txt";
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
try {
File dir = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath() + File.separator + "crash");
if (!dir.exists())
dir.mkdir();
File file = new File(dir, fileName);
FileOutputStream fos = new FileOutputStream(file);
fos.write(crashReport.toString().getBytes());
fos.close();
return file;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
} private void sendAppCrashReport(final Context context,
final String crashReport, final File file) {
// TODO Auto-generated method stub
AlertDialog mDialog = null;
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setTitle("程序出错啦");
builder.setMessage("请把错误报告以邮件的形式提交给我们,谢谢!");
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { // 发送异常报告
try {
//注释部分是已文字内容形式发送错误信息
// Intent intent = new Intent(Intent.ACTION_SENDTO);
// intent.setType("text/plain");
// intent.putExtra(Intent.EXTRA_SUBJECT,
// "推聊Android客户端 - 错误报告");
// intent.putExtra(Intent.EXTRA_TEXT, crashReport);
// intent.setData(Uri
// .parse("mailto:way.ping.li@gmail.com"));
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// context.startActivity(intent); //下面是以附件形式发送邮件
Intent intent = new Intent(Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
String[] tos = { "way.ping.li@gmail.com" };
intent.putExtra(Intent.EXTRA_EMAIL, tos); intent.putExtra(Intent.EXTRA_SUBJECT,
"推聊Android客户端 - 错误报告");
if (file != null) {
intent.putExtra(Intent.EXTRA_STREAM,
Uri.fromFile(file));
intent.putExtra(Intent.EXTRA_TEXT,
"请将此错误报告发送给我,以便我尽快修复此问题,谢谢合作!\n");
} else {
intent.putExtra(Intent.EXTRA_TEXT,
"请将此错误报告发送给我,以便我尽快修复此问题,谢谢合作!\n"
+ crashReport);
}
intent.setType("text/plain");
intent.setType("message/rfc882");
Intent.createChooser(intent, "Choose Email Client");
context.startActivity(intent);
} catch (Exception e) {
Toast.makeText(context,
"There are no email clients installed.",
Toast.LENGTH_SHORT).show();
} finally {
dialog.dismiss();
// 退出
android.os.Process.killProcess(android.os.Process
.myPid());
System.exit(1);
}
}
});
builder.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
// 退出
android.os.Process.killProcess(android.os.Process
.myPid());
System.exit(1);
}
});
mDialog = builder.create();
mDialog.getWindow().setType(
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
mDialog.show();
} /**
* 获取APP崩溃异常报告
*
* @param ex
* @return
*/
private String getCrashReport(Context context, Throwable ex) {
PackageInfo pinfo = getPackageInfo(context);
StringBuffer exceptionStr = new StringBuffer();
exceptionStr.append("Version: " + pinfo.versionName + "("
+ pinfo.versionCode + ")\n");
exceptionStr.append("Android: " + android.os.Build.VERSION.RELEASE
+ "(" + android.os.Build.MODEL + ")\n");
exceptionStr.append("Exception: " + ex.getMessage() + "\n");
StackTraceElement[] elements = ex.getStackTrace();
for (int i = 0; i < elements.length; i++) {
exceptionStr.append(elements[i].toString() + "\n");
}
return exceptionStr.toString();
} /**
* 获取App安装包信息
*
* @return
*/
private PackageInfo getPackageInfo(Context context) {
PackageInfo info = null;
try {
info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
} catch (NameNotFoundException e) {
// e.printStackTrace(System.err);
// L.i("getPackageInfo err = " + e.getMessage());
}
if (info == null)
info = new PackageInfo();
return info;
} }
Android将程序崩溃信息保存本地文件的更多相关文章
- Android 应用程序崩溃日志捕捉
程序崩溃是应用迭代中不可避免的问题,即使有着5年或者10年经验的程序猿也无法完全保证自己的代码没有任何的bug导致崩溃,现在有一些第三方平台可以帮助我们搜集应用程序的崩溃,比如友盟,详情如下图 虽然能 ...
- Android平台程序崩溃的类型及原因列举
Android平台程序崩溃大家都应该遇到过,force close和ANR应该是大家遇到较多的. 这里把Android平台程序崩溃的各种类型做一个简述和原因列举. 1.ANR(可见ANR): 发生场景 ...
- Android将应用log信息保存文件
相信大家在做应用调试的时候,不可能时时通过USB线连着电脑去查看log信息,所以,将应用的log信息保存到手机本地就很有必要了,有助我们从这些log信息中提取有用的部分,以解决一些bug,下面我把网上 ...
- Android记录程序崩溃Log写入文件
将导致程序崩溃的堆栈调用Log写入文件,便于收集bug.在调试安卓程序,由于某些原因调试时手机不能连接PC端,无法通过IDE查看程序崩溃的Log,希望log能够写入文件中,对于已经发布的App可以通过 ...
- APP全局异常捕获,并保存本地文件
public class CrashHandler implements Thread.UncaughtExceptionHandler { public static final String TA ...
- Android应用程序的组成部分和Manifest文件(转)
Android应用程序由松散耦合的组件组成,并使用应用程序Manifest绑定到一起:应用程序Manifest描述了每一组件和它们之间的交互方式,还用于指定应用程序元数据.其硬件和平台要求.外部库以及 ...
- 结合程序崩溃后的core文件分析bug
引言 在<I/O的效率比较>中,我们在修改图1程序的BUF_SIZE为8388608时,运行程序出现崩溃,如下图1: 图1. 段错误 一般而言,导致程序段 ...
- Android在程序崩溃或者捕获异常之后重新启动app
在Android应用开发中,偶尔会因为测试的不充分导致一些异常没有被捕获,这时应用会出现异常并强制关闭,这样会导致很不好的用户体验,为了解决这个问题,我们需要捕获相关的异常并做处理. 首先捕获程序崩溃 ...
- Eclipse将控制台输出信息保存为文件
当你在Eclipse中 running/debugging一个应用程序的时候,有关该应用程序的运行调试信息及日志信息都会输出到控制台(console )显示,但是Eclipse只会显示最后一部分的日志 ...
随机推荐
- Unity3D研究院之Assetbundle的原理(六十一)
www.xuanyusong.com/archives/2373 Assetbundle 是Unity Pro提供提供的功能,它可以把多个游戏对象或者资源二进制文件封装到Assetbundle中,提供 ...
- !heap 和 _HEAP_ENTRY
WinDBG提供了!heap命令帮助我们查找heap,同时我们也可以通过dt和MS SYMBOL来了解memory layout. 假设我们有下面一个小程序. int _tmain(int argc, ...
- Oracle查询数据表结构(字段,类型,大小,备注)
作用:想要生成整个Oracle数据库所有表结构WORD文档(数据库设计说明书) Oracle数据库字典介绍 Oracle数据字典是有表和视图组成的,存储有关数据库结构信息的一些数据库对象.数据库 ...
- php之laravel学习
http://laravel-china.github.io/php-the-right-way/#composer_and_packagist laravel 添加 dingoapi路由插件 并运用 ...
- Linux网络工具lsof和netstat
lsof全名为list opened files,即列举系统中已经被打开的文件,基本使用如下: (1) 查看/etc/passwd使用情况 lsof /etc/password (2) 查看监听的so ...
- [置顶]
linux getline()函数
getline()函数是什么?百度百科这样解释: getline不是C库函数,而是C++库函数.它会生成一个包含一串从输入流读入的字符的字符串,直到以下情况发生会导致生成的此字符串结束.1) ...
- python web框架 Django基本操作
django 操作总结! django框架安装: cmd安装: pip3 install django pycharm安装: 在python变量下 搜索 django 安装 创建django项目: c ...
- 利用DNS进行传输数据
曾经有这样一道题目,困了我数个小时,最后我尝试利用此方法时我知道真相的时候,眼泪已掉下来. 遇到的是一个流量分析题,分析DNS数据,拿到flag 流量如图所示: 进入linux,提取: [root@s ...
- SpringMVC中使用forward和redirect进行转发和重定向以及重定向时如何传参详解
转自:http://blog.51cto.com/983836259/1877188 2016-11-28 09:45:59 如题所示,在SpringMVC中可以使用forward和redirec ...
- ks8基础(1) etcd安装
下载安装 https://github.com/coreos/etcd/releases 在这网页,可以看到有多个版本共选择. 下载3.25 解压后, cd etcd-v3.2.5-linux-amd ...