先看效果:
当用户使用你的app时,难免会发生奔溃,而这个时候我们就需要在程序中实现一个需求:将奔溃的相关信息和设备信息保存下来,并且在适当的时候发送给我们的服务器,便于分析bug,解决bug。相信不少同学应该用过腾讯Bugly吧,所以今天我们要做的就是实现一个它的简易版Demo。
实现步骤:
1.自定义一个实现了Thread.UncaughtExceptionHandle接口的类,并重写它的uncaughtException方法。为什么要这样做呢,简单地说平常我们不去自定义这个类的话,捕获异常这些事儿都是系统帮我们做的,就是你平常在Logcat里看到的那些不想看见的满江红。而如果我们想自己去捕获异常信息的话,就必须这么做。
2.在uncaughtException这个方法中做自己想做的事儿,在这里就是保存奔溃信息和我们关心的设备信息到本地。
3.确定上报给服务器的时机,在本demo中我选在Application创建时上报
4.将错误日志文件发送到服务器,并完成一些后续操作(比如说删除)
本Demo的主要架构及说明:
CrashHandler:完成上述实现步骤中的1和2。
CrashReportedUtils:包含一些对保存的日志文件的操作,如获取最新修改的日志文件,获得已经保存的全部日志文件等
MyApplication:自定义的Application,别忘了在AndroidManifest文件中将它声明在name属性中
MainActivity:手动抛出一个异常,用于试验
好了,该做的铺垫已经做完了,接下来就赶紧撸代码吧!o(* ̄▽ ̄*)ブ
CrashHandler:
首先是声明的一些变量:
/**
* CrashHandler实例
*/
private static CrashHandler sCrashHandler;
/**
* 系统默认的UncaughtExceptionHandler处理类
*/
private Thread.UncaughtExceptionHandler mDefaultHandler;
/**
* 用LinkedHashMap来保存设备信息和错误堆栈信息
*/
private Map mDeviceCrashInfo = new LinkedHashMap<>();
private static final String VERSION_NAME = "versionName";
private static final String VERSION_CODE = "versionCode";
private static final String STACK_TRACE = "stackTrace";
public static final String CRASH_REPORTER_EXTENSION = ".log";
private Context mContext;
然后私有化构造方法,并提供单例:
private CrashHandler() {
}
public static CrashHandler getInstance() {
if (sCrashHandler == null) {
sCrashHandler = new CrashHandler();
}
return sCrashHandler;
}
紧接着是一个init方法,看名字就知道是用来初始化CrashHandler的, mDefaultHandler是获得的系统用来处理异常的类,通过Thread.setDefaultUncaughtExceptionHandler(this)将我们自定义的这个CrashHandler设置为默认的捕获异常的类。
uncaughtException这个就是必须要重写的方法,在这个方法中我做了一个判断,假如handleException没有返回true并且mDefaultHandler不为空的时候,发生奔溃时就交给系统来处理,妥妥的备胎,被我安排的明明白白。
/**
* 初始化,设置CrashHandler为程序的默认处理类
*
* @param context
*/
public void init(Context context) {
mContext = context;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 必须实现的方法,用来处理uncaughtException
*
* @param t
* @param e
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
//如果没有自己处理则交给系统来处理
if (!handleException(e) && mDefaultHandler != null) {
mDefaultHandler.uncaughtException(t, e);
}
}
handleException这个方法里写了我自己处理这些异常的逻辑,算是这个类的核心代码吧。
/**
* 错误处理,收集和存储
*
* @param ex
* @return 处理成功返回true
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
Log.i("TAG", "ex is null (○´・д・)ノ");
return true;
}
if (BuildConfig.DEBUG) {
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast toast = Toast.makeText(mContext, "程序发生异常...即将退出\r\n", Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
Looper.loop();
}
}.start();
}
//收集设备信息
collectCrashedDeviceInfo(mContext);
//保存错误报告到文件
saveCrashInfoToFile(ex);
return true;
}
如果ex为空,就是没有异常,那就直接return true结束这个方法,后面的代码就不用执行了。紧接着如果实在debug模式,则弹一个土司提示开发者,当然你也可以选择忽略这段代码。最后先通过collectCrashedDeviceInfo来收集一些我们关心的设备信息,然后通过saveCrashInfoToFile将生成的日志文件保存到本地。
collectCrashedDeviceInfo(收集设备信息):
/**
* 收集设备信息
*
* @param context
*/
private void collectCrashedDeviceInfo(Context context) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (packageInfo != null) {
mDeviceCrashInfo.put(VERSION_NAME, packageInfo.versionName); //版本名
mDeviceCrashInfo.put(VERSION_CODE, packageInfo.versionCode + ""); //版本号
mDeviceCrashInfo.put("manufacturer", Build.MANUFACTURER); //厂商名
mDeviceCrashInfo.put("product", Build.PRODUCT); //产品名
mDeviceCrashInfo.put("brand", Build.BRAND); //手机品牌
mDeviceCrashInfo.put("model", Build.MODEL); //手机型号
mDeviceCrashInfo.put("device", Build.DEVICE); //设备名
mDeviceCrashInfo.put("sdkInt", Build.VERSION.SDK_INT + ""); //SDK版本
mDeviceCrashInfo.put("release", Build.VERSION.RELEASE); //Android版本
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
版本名,版本号这些信息都可以自己选择,在这里我选了这些,拿到信息后记得要保存到mDeviceCrashInfo中。
saveCrashInfoToFile(保存错误信息到文件):
/**
* 保存奔溃的错误信息到文件
*
* @param ex
*/
private void saveCrashInfoToFile(Throwable ex) {
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
String result = info.toString();
printWriter.close();
mDeviceCrashInfo.put("EXCEPTION", ex.getLocalizedMessage());
mDeviceCrashInfo.put(STACK_TRACE, result);
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : mDeviceCrashInfo.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key);
sb.append("=");
sb.append(value);
sb.append("\n");
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA);
Date date = new Date(System.currentTimeMillis());
String fileName = "crash" + simpleDateFormat.format(date) + CRASH_REPORTER_EXTENSION;
File file = null;
File directory = new File(CrashReportedUtils.sCrashDirectory);
if (CrashReportedUtils.sCrashDirectory != null && directory.isDirectory()) {
file = new File(CrashReportedUtils.sCrashDirectory, fileName);
} else {
CrashReportedUtils.sCrashDirectory = mContext.getFilesDir().getAbsolutePath();
file = new File(CrashReportedUtils.sCrashDirectory, fileName);//默认放在files目录下
}
try {
FileOutputStream trace = new FileOutputStream(file);
trace.write(sb.toString().getBytes());
trace.close();
} catch (IOException e) {
Log.e("TAG", "an error occurred while write report file..", e);
e.printStackTrace();
}
}
用字符串拼接的方式拿到mDeviceCrashInfo中的键值对,也就是我们之前保存的那些设备信息,然后拼接,换行。保存的日志文件格式我指定为log,文件名称就用发生奔溃时的时间和日期来命名,简洁明了。
代码中有一个CrashReportedUtils,这是我自定义的一个辅助类,下面会详述,在这里你只要知道它的作用是用于判断使用者指定的保存日志文件的路径是否存在,存在则应用,不存在则使用默认的路径。
到了这里CrashHandler就已经讲完了,应该挺好理解的吧哈哈,下面我们就来说说CrashReportedUtils里都做了哪些事儿!
CrashReportedUtils:
/**
* 日志文件的工具类
* Created by Administrator on 2018/8/23.
*/
public class CrashReportedUtils {
/**
* 日志文件的缓存目录
*/
public static String sCrashDirectory;
/**
* 上传的服务器地址
*/
private static final String SERVER_UPLOAD = "https://www.baidu.com/";
/**
* 初始化
*/
public static void init(Context context, String directoryPath) {
setStoredDirectory(directoryPath, context);
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(context);
}
/**
* 设置日志文件保存的目录
*
* @param directoryPath
*/
private static void setStoredDirectory(String directoryPath, Context context) {
if (directoryPath == null) {
return;
}
File dir = new File(directoryPath);
if (!dir.exists()) {
dir.mkdirs();
}
if (dir.isDirectory()) {
sCrashDirectory = dir.getAbsolutePath();
} else {
sCrashDirectory = context.getFilesDir().getAbsolutePath();//传入的路径无效就用这个默认的路径
try {
throw new Exception("自定义的保存路径格式无效,已使用默认路径");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 获得已经保存的全部日志文件
*/
public static File[] getCrashReportedFiles() {
File filesDir = new File(sCrashDirectory);
FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(CrashHandler.CRASH_REPORTER_EXTENSION);
}
};
return filesDir.listFiles(filter);
}
/**
* 删除日志文件或这个目录下的所有日志文件
*
* @param file
*/
public static void deleteFile(File file) {
if (file == null) {
throw new NullPointerException("file is null");
}
if (!file.exists()) {
return;
}
if (file.isFile()) {
file.delete();
}
if (!file.isDirectory()) {
return;
}
File[] files = file.listFiles();
for (File crashFile : files
) {
crashFile.delete();
}
}
/**
* 获取最新修改的日志文件
*
* @return
*/
public static File getLatestCrashReportFile() {
if (CrashReportedUtils.getCrashReportedFiles().length == 0) {
return null;
}
File directory = new File(sCrashDirectory);
File[] files = directory.listFiles();
Arrays.sort(files, new Comparator() {
@Override
public int compare(File file1, File file2) {
return (int) (file2.lastModified() - file1.lastModified());
}
});
return files[0];
}
/**
* 发送最新的错误报告到服务器
*/
public static void sendCrashedReportsToServer() {
final File file = CrashReportedUtils.getLatestCrashReportFile();
if (file == null) {
Log.i("TAG", "奔溃日志文件并不存在,发送失败(;′⌒`)");
return;
}
OkHttpClient client = new OkHttpClient();
MediaType type = MediaType.parse("File/*");
RequestBody requestBody = RequestBody.create(type, file);
final Request request = new Request.Builder()
.post(requestBody)
.url(SERVER_UPLOAD)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
}
}
代码一下子有点多,下面挨个解释一下:
sCrashDirectory:日志文件的缓存目录,通过setStoredDirectory这个方法来设置
SERVER_UPLOAD:需要上传的服务器地址,这里我就随便用一个地址来意思一下,具体到每个人身上这个地址肯定不一样
init(...):做了一些初始化工作,包括设置缓存目录,拿到CrashHandler实例
setStoredDirectory(...):设置日志文件保存的目录,这里做了一些简单的判断,根据传入的路径是否有效来确定最终缓存的目录
getCrashReportedFiles(...):获得已经保存的全部日志文件,拿到缓存目录下所有.log格式的文件
deleteFile(...):删除日志文件或这个目录下的所有日志文件,也是做了一些简单的判断
getLatestCrashReportFile(...):获取最新修改的日志文件,File的lastModified方法就是获得一个文件最新修改的时间
sendCrashedReportsToServer():发送最新的错误报告到服务器,这里只是为了演示,简单地使用了okHttp里关于上传文件的一 些操作,网络框架那么多,要选哪一个就看你自己了,用法网上也一大堆,这里就不谈咯
该说的就说完了,下面就是使用的步骤,很简单,只要两步:
MyApplication:
public class MyApplication extends Application {
@Override
public void onCreate() {
CrashReportedUtils.init(getApplicationContext(), getFilesDir() + "/crash");
CrashReportedUtils.sendCrashedReportsToServer();
super.onCreate();
}
}
自定义一个MyApplication,两行代码分别完成初始化和发送,init(...)的第二个参数就是使用者指定的缓存目录的路径。
最后别忘了在AndroidManifest中设置
年轻人的第一个关于奔溃信息收集的小工具到这里就大功告成啦,赶紧去试试看吧!
Demo地址:
https://github.com/limanma/CrashDemo