1.引子
最近在做一个社交app的过程中,用户总是反映app在跳转到分享页面的时候App无故退出。
我在我的手机上实验了几下,都能成功,神奇的安卓啊,最后想到了一个办法,
记录用户app的崩溃日志来解决。
可是用户就是用户,提交错误日志,总不能给用户说,你到xx路径下面,把xx文件发给我吧。
所以想到了这种方法:
1.如果app退出,则将app的崩溃日志记录在某个文件下面;
2.当用户再次打开app的时候,提示,用户是否上传错误日志;
3.如果用户选择是,就将错误日志以附件的形式,添加到发送的邮件中;
4.选择否,就直接删除错误日志;
2.知识点讲解
1.如何记录App的崩溃日志
/** * UncaughtException处理类,当程序发生Uncaught异常的时候 * * @author user 注意修改文件的路径和文件名,在Manifest中添加文件读写权限; * */
public class CrashHandler implements UncaughtExceptionHandler {
// 错误日志文件夹的位置
private String mCrashLogDirPath = "";
public static final String TAG = CrashHandler.class.getSimpleName();
// 系统默认的UncaughtException处理类
private Thread.UncaughtExceptionHandler mDefaultHandler;
// CrashHandler实例
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-HH-mm-ss",
Locale.CHINA);
private CrashHandler() {
}
public static CrashHandler getInstance() {
return INSTANCE;
}
public void init(Context context) {
mContext = context;
mCrashLogDirPath = context.getExternalCacheDir() + File.separator
+ MainActivity.Error_DIR_NAME + File.separator;
// 获取系统默认的UncaughtException处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置该CrashHandler为程序的默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/** * 当UncaughtException发生时会转入该函数来处理 如果导入项目@Override报错,请修改project编译的jdk版本到1.5以上 */
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
// 退出程序
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;
}
// 收集设备参数信息
collectDeviceInfo(mContext);
saveCrashInfo2File(ex);
// 向配置文件中写入标识位
flagCrash();
return true;
}
public void flagCrash() {
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(mContext).edit();
editor.putBoolean(MainActivity.TAG_OCCURRED_ERROR, true);
editor.commit();
}
/** * 收集设备参数信息 * * @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 == null ? "null"
: 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());
Log.d(TAG, field.getName() + " : " + field.get(null));
} catch (Exception e) {
Log.e(TAG, "an error occured when collect crash info", e);
}
}
}
/** * 保存错误信息到文件中 * * @param ex * @return Boolean 判断文件保存到本地是否成功 */
private Boolean saveCrashInfo2File(Throwable ex) {
Boolean saveFlag = false;
StringBuffer sb = new StringBuffer();
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.close();
String result = writer.toString();
// TODO
System.err.println("*****下面打印错误信息*****");
System.err.println(result);
sb.append(result);
try {
long timestamp = System.currentTimeMillis();
String time = formatter.format(new Date());
String fileName = "crash-" + time + "-" + timestamp + ".log";
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
File dir = new File(mCrashLogDirPath);
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(mCrashLogDirPath
+ fileName);
fos.write(sb.toString().getBytes());
fos.close();
}
saveFlag = true;
} catch (Exception e) {
Log.e(TAG, "an error occured while writing file...", e);
}
return saveFlag;
}
和这片相似的代码,只要稍微搜索一下随处可见。注意相似不是雷同。在这里阐述一下我的小聪明:
1.在这里我使用的路径为this.getCacheDir(),在这个路径下面创建和删除文件是不需要任何权限的;
2.在将app崩溃信息保存到文件之前,我对错误信息进行了控制台打印。因为如果仅仅将错误信息保存到本地文件的话,对于程序员来说,每一次向在控制台打印错误日志,都必须在Application中将手机错误信息的日志功能关闭掉,但是下一次打包的时候,就很有可能把这件事情忘了(亲身经历啊);
3.app崩溃字段的保存。这里使用 PreferenceManager
.getDefaultSharedPreferences(mContext),获取App默认的sharedPreference文件,省略了自定义文件的麻烦,当然还需要自定义一个字段。当用户下次进入app后,对app之前是否发生过崩溃进行判断。
3.大家注意一下错误信息的差别:
1.在没有添加错误日志的时候,打印的错误信息如下:
打印信息为鲜红色,这个大家应该都比较熟悉。
2.看一下添加记录App崩溃日志后的效果:
这种红的颜色就淡了好多,就不是太惹眼了。
3.看一下报错的错误日志在手机上的效果吧:
下面贴上打开App检测是否发生过崩溃,上传崩溃(通过调用手机的邮件系统),清除崩溃字段和崩溃日志的完整代码
/** * * @author guchuanhang * */
public class MainActivity extends Activity implements android.view.View.OnClickListener {
public static final int TAG_ERROR_CODE = 10;
/** * 错误日志在cachedir下面的crash文件夹下面 */
public static final String Error_DIR_NAME = "crash";
/** * 通过该字段,进行判断,程序是否发生过crash ,使用getDefaultSharedPreferences */
public static final String TAG_OCCURRED_ERROR = "crashed";
private Dialog upErrorDialog;
private Button mbtnMakeError = null;
private String errorString;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mbtnMakeError = (Button) findViewById(R.id.btn_make_error);
mbtnMakeError.setOnClickListener(this);
// 判断上次是否发生过崩溃
SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(this);
if (preferences.getBoolean(TAG_OCCURRED_ERROR, false)) {
uploadErrorDialog();
}
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_make_error:
System.out.println(errorString.equals("xxx"));
break;
default:
break;
}
}
// 第一上传错误日志的对话框样式
protected void uploadErrorDialog() {
AlertDialog.Builder builder = new Builder(MainActivity.this);
builder.setMessage("上次程序异常退出,\n上传错误日志?");
builder.setTitle("提示");
builder.setPositiveButton("确定", new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setAttachment(MainActivity.this);
dialog.dismiss();
}
});
builder.setNegativeButton("取消", new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
deleteErrorLogAndRemoveTag();
dialog.dismiss();
}
});
upErrorDialog = builder.create();
upErrorDialog.show();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (upErrorDialog != null && upErrorDialog.isShowing()) {
upErrorDialog.dismiss();
}
if (requestCode == TAG_ERROR_CODE) {
deleteErrorLogAndRemoveTag();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
public void deleteErrorLogAndRemoveTag(){
File file = new File(getExternalCacheDir(), Error_DIR_NAME);
File[] errorFiles = file.listFiles();
for (int i = 0; i < errorFiles.length; i++) {
errorFiles[i].delete();
}
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(this).edit();
editor.remove(TAG_OCCURRED_ERROR);
editor.commit();
}
public void setAttachment(Context conext) {
Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
String[] tos = { "[email protected]" };
String[] ccs = { "[email protected]" };
intent.putExtra(Intent.EXTRA_EMAIL, tos);
intent.putExtra(Intent.EXTRA_CC, ccs);
intent.putExtra(Intent.EXTRA_TEXT, "我很生气,你要尽快解决。\n 否则后果不堪设想!");
intent.putExtra(Intent.EXTRA_SUBJECT, "叮咚FM崩溃日志");
ArrayList<Uri> imageUris = new ArrayList<Uri>();
File srcDir = new File(this.getExternalCacheDir(), "crash");
File[] logFiles = srcDir.listFiles();
for (int i = 0; i < logFiles.length; i++) {
imageUris.add(Uri.parse("file://" + logFiles[i].getAbsolutePath()));
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
intent.setType("image/*");
intent.setType("message/rfc882");
Intent.createChooser(intent, "Choose Email Client");
((Activity) conext).startActivityForResult(intent, TAG_ERROR_CODE);
}
}
下面贴上Application中打开错误日志的方法:
import pan.gch.demo.util.CrashHandler;
import android.app.Application;
/** * @author guchuanhang * */
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(getApplicationContext());
}
}
3.附上Demo
我啰嗦了折磨多,相信聪明的你,一定一看就会。Demo下载地址:
http://download.csdn.net/detail/guchuanhang/9148729