1.为什么需要反馈Crash报告?

       Crash最通俗直观的感受就是App软件出现崩溃导致的闪退等现象,在Android原生态下会出现一个App Force Close的Dialog,但是对于用户体验相当不好。Crash的产生是不可避免的,它产生的原因可能来自于Android底层的Bug,或是因为网络不畅,又或者是手机适配性问题,更严重的是代码质量不过关。Crash的产生是我们最不愿意看到的,即使我们花费大量时间进行测试,测试,再测试,bug还是会产生,为了能够获取到Crash信息,正式的软件中都会有Crash的反馈机制,开发人员会根据这些Crash信息对软件进行改进。

2.如何获取这些Crash信息?

      1)首先,先看这样一个方法

/**
     * Sets the default uncaught exception handler. This handler is invoked in
     * case any Thread dies due to an unhandled exception.
     *
     * @param handler
     *            The handler to set or null.
     */
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
        Thread.defaultUncaughtHandler = handler;
    }

       这个方法可以设置系统的默认异常处理器,我们可以利用这个方法解决应用中常见的crash问题。当crash发生的时候,我们可以捕获到异常信息,把异常信息存储到SD卡中,然后在合适的时机通过网络将crash信息上传到服务器上,这样开发人员就可以分析用户crash的场景从而在后面的版本中修复此类crash。我们还可以在crash发生时,弹出一个通知告诉用户程序crash了,然后再退出,这样做比闪退要温和一点。

       2)再看一个Java中的接口——UncaughtExceptionHandler

static interface

  Thread.UncaughtExceptionHandler            当 Thread

我们可以实现这个接口,然后在这个接口中的uncaughtException(Thread thread, Throwable ex)方法中实现对Crash信息的捕获。

3.思路描述

/*
		 * 1.捕获异常信息;
		 * 2.上报手机和App的相关信息
		 *   1).VersionCode
		 *   2).VersionName
		 *   3).PackageName
		 *   4).手机分辨率
		 *   5).手机系统版本号
		 *   6).手机品牌
		 *   7).手及型号
		 *   8).CPU架构
		 * 3.将以上信息写入指定的Crash日志文件中
		 *   1)假如写入SDCard中,则首先判断SDCard是否可用
		 *   2)假如可用,判断指定文件夹是否存在
		 *   3)如果不存在则创建一个文件夹,如果存在继续
		 *   4)常见一个以当前时间为文件名的日志文件并将Crash信息写入
		 * 4.适当的时机将Crash日志信息上传到服务器
		 * 	 1)Crash产生后,如果网络等条件允许则立即上传
		 *   2)如果网络等条件有限,则软件下次启动时检测有无Crash信息
		 *   3)如果有且网络正常,上传到服务器,否则重复(3)
		 */

4.具体实现

1)实现异常处理CrashHandler

package com.example.crash.overall;

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;

import com.example.crash.utils.AppUtil;

import android.content.Context;
import android.os.Environment;
import android.os.Process;
import android.util.Log;

/**
 * Description: Crash异常信息搜集类
 * 
 * @author danDingCongRong
 * @Version 1.0.0
 * @Created at 2014-7-28 14:06:49
 * @Modified by [作者] on [修改日期]
 */
public class CrashHandler implements UncaughtExceptionHandler {

	private static final String TAG = "CrashHandler";

	private Context context;

	private UncaughtExceptionHandler defaultExceptionHandler;

	private static CrashHandler crashHandler;

	private CrashHandler() {
		;
	}

	// 单例模式
	public static CrashHandler getInstance() {
		if (null == crashHandler) {
			crashHandler = new CrashHandler();
		}

		return crashHandler;
	}

	public void init(Context context) {
		// 系统默认的异常处理器
		defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
		// 将当前实例设为系统默认的异常处理器
		Thread.setDefaultUncaughtExceptionHandler(this);
		// 获取App信息时用户此参数
		context = context.getApplicationContext();
	}

	@Override
	public void uncaughtException(Thread thread, Throwable ex) {

		// 导出崩溃信息到日志文件
		dumpExceptionToSDCard(ex);

		// 打印崩溃信息到Log日志
		ex.printStackTrace();

		// 如果系统提供了默认的异常处理器,则交给系统去结束我们的程序,否则就由我们自己结束自己
		if (null != defaultExceptionHandler) {
			defaultExceptionHandler.uncaughtException(thread, ex);
		} else {
			Process.killProcess(Process.myPid());
		}
	}

	// 导出崩溃信息到日志文件
	private void dumpExceptionToSDCard(Throwable ex) {
		// 检测SDCard是否可用--如果不可以在log中给予提醒
		if (!Environment.getExternalStorageState().equals(
				Environment.MEDIA_MOUNTED)) {
			Log.w(TAG, "SDCard unmounted!");
			return;
		}

		// 判断Crash文件夹是否存在,如果不存在则创建
		File crashDir = new File(Constants.CRASH_FILE_PATH);
		if (!crashDir.exists()) {
			crashDir.mkdir();
		}

		// 创建当前崩溃日志文件
		String currentTime = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
				.format(new Date(System.currentTimeMillis()));
		String logFileName = currentTime + ".txt";
		File logFile = new File(crashDir, logFileName);

		BufferedWriter bufferedWriter = null;
		PrintWriter printWriter = null;
		try {
			bufferedWriter = new BufferedWriter(new FileWriter(logFile));
			printWriter = new PrintWriter(bufferedWriter);

			// 崩溃发生时间
			printWriter.write(currentTime);
			// 崩溃手机的系统信息及其用户所用的软件信息
			String phoneInfo = new AppUtil(context).getPhoneInfoToCrash();
			printWriter.write(phoneInfo);
			// 崩溃信息
			ex.printStackTrace(printWriter);

			printWriter.flush();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (null != bufferedWriter) {
				try {
					bufferedWriter.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

			if (null != printWriter) {
				printWriter.close();
			}
		}
	}

}

2)获取手机及App软件等相关信息

package com.example.crash.utils;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;

/**
 * Description: 用户获取软件信息和系统信息的工具
 * 
 * @author danDingCongRong
 * @Version 1.0.0
 * @Created at 2014-7-28 15:05:41
 * @Modified by [作者] on [修改日期]
 */
public class AppUtil {

	private Context context;

	public AppUtil(Context context) {
		this.context = context;
	}

	// 获取App的VersionCode
	public int getAppVersionCode() {
		PackageManager packageManager = context.getPackageManager();

		PackageInfo packageInfo = null;
		try {
			packageInfo = packageManager.getPackageInfo(
					context.getPackageName(), PackageManager.GET_ACTIVITIES);
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}

		int versionCode = 0;
		if (null != packageInfo) {
			versionCode = packageInfo.versionCode;
		}

		return versionCode;
	}

	// 获取App的VersionName
	public String getAppVersionName() {
		PackageManager packageManager = context.getPackageManager();

		PackageInfo packageInfo = null;
		try {
			packageInfo = packageManager.getPackageInfo(
					context.getPackageName(), PackageManager.GET_ACTIVITIES);
		} catch (NameNotFoundException e) {
			e.printStackTrace();
		}

		return packageInfo.versionName;
	}

	public String getPhoneInfoToCrash() {
		StringBuilder stringBuilder = new StringBuilder();

		stringBuilder.append("PackageName:").append(context.getPackageName())
				.append('\n');
		stringBuilder.append("VesionCode:").append(getAppVersionCode())
				.append('\n');
		stringBuilder.append("VersionName:").append(getAppVersionName())
				.append('\n');
		stringBuilder.append("OS Version:")
				.append(android.os.Build.VERSION.RELEASE).append('_')
				.append(android.os.Build.VERSION.SDK_INT).append('\n');
		stringBuilder.append("Model:").append(android.os.Build.MODEL)
				.append('\n');
		stringBuilder.append("Manufacturer:")
				.append(android.os.Build.MANUFACTURER).append('\n');
		stringBuilder.append("CPU:").append(android.os.Build.CPU_ABI)
				.append('\n');

		return stringBuilder.toString();
	}
}

3)为UI线程添加默认异常事件Handler

//Thread类中标识默认异常事件Handler的成员

private static UncaughtExceptionHandler defaultUncaughtHandler;

这里涉及到在哪里添加的问题,从源码中注意到,这个defaultUncaughtHandler是Thread类中一个静态的成员,所以,按道理,我们为任意一个线程设置异常处理,所有的线程都应该能共用这个异常处理器,这个是我的猜测,没有经过验证,不过没关系,有一个观点是大家都认可的:就是为主线程也就是ui线程添加异常程序器。为了在ui线程中添加异常处理Handler,我们推荐大家在Application中添加而不是在Activity中添加。Application标识着整个应用,在Android声明周期中是第一个启动的,早于任何的Activity、Service等。

/**
 * 
 */
package com.example.crash.overall;

import android.app.Application;

/**
 * Description: Application
 * 
 * @author danDingCongRong
 * @Version 1.0.0
 * @Created at 2014-7-28 14:04:06
 * @Modified by [作者] on [修改日期]
 */
public class App extends Application {

	@Override
	public void onCreate() {
		super.onCreate();

		// 开启崩溃日志,这样我们的程序就能捕获未处理的异常
		CrashHandler crashHandler = CrashHandler.getInstance();
		crashHandler.init(getApplicationContext());
	}

}

4.测试代码

package com.example.crash.activity;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;

import com.example.crash.R;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	public void testCrash(View view) {
		if (1 > 0) {
			throw new RuntimeException("Test Exception!!!");
		}
	}

}

5.备注

     日志的上传属于网络相关的内容,暂且不做专门介绍