前段时间公司提个需求要做异常捕获,本来为了方便是想集成第三方的sdk,但是因为我们的产品比较特殊,对比了友盟、testin、腾讯、阿里都不太符合公司需求,没办法只好自己做了。上网找了一些案例开始造轮子了~
1、首先要实现Thread.UncaughtExceptionHandler接口,当出现crash报错时程序会通过回调uncaughtException方法进行日志的抓取
2、将抓取的日志进行存储到本地
3、根据自己的需求,将一些需要的信息同日志信息同时存储都本地
4、为了避免缓存日志过多占用不必要的内存,我们可以做一个限制缓存大小,比如本地只缓存十条信息,超出十条就将最旧的数据删除
1、初始化集合用来存储需要的字段
/**
* 保存异常日志信息集合
*/
private LinkedHashMap crashAppLog = new LinkedHashMap();
2、正常在收集日志的时候肯定是要知道异常发生的时间,所以存储时间是必不可少的
private void readTimer(){
//获取写入当前时间
curtTimer = ""+System.currentTimeMillis();
if (formate == null) {
formate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
}
timer = formate.format(new Date());
crashAppLog.put("time", timer);
}
3、根据自己的需求存储一些设备信息或应用信息
/**
* 获取包相关信息
* @param mContext
* @throws PackageManager.NameNotFoundException
*/
private void readPACKAGE_INFO(Context mContext) throws PackageManager.NameNotFoundException {
PackageManager packageManager = mContext.getPackageManager();
if (packageManager != null) {
PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
if (packageInfo != null) {
String versionName = packageInfo.versionName;
String versionCode = ""+packageInfo.versionCode;
String packName = packageInfo.packageName;
crashAppLog.put("version_name",versionName);
}
}
}
4、关键部分,写入捕获的日志
/**
* 写入文件中
* @param ex
*/
private void writerCrashLogToFile(Throwable ex) {
try {
StringBuffer buffer = new StringBuffer();
if (crashAppLog != null && crashAppLog.size() >0) {
for (Map.Entry entry:crashAppLog.entrySet()) {
buffer.append(entry.getKey()+":"+entry.getValue()+"\n");
}
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
//e.printStackTrace()是打印异常的堆栈信息,由内向外层层捕获,所以下面需要while依次遍历
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while(cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.flush();
printWriter.close();
String result = writer.toString();
buffer.append("Exception_log:\n");
buffer.append(result);
writerToFile(buffer.toString(), timer, curtTimer);
}catch (Exception e) {
Log.e(TAG, "writerCrashLogToFile - "+e.getMessage());
}
}
5、为了防止写入和读取上传同时操作一份文件,这里做了一个重命名处理,上传的地方加一下过滤
private void writerToFile(String s, String timer, String curtTimer) {
try {
/**
* 创建日志文件名称
*
* 此处为了防止读写同时发生,写完以后重命名
*/
String fileName = "write-"+timer+"-"+curtTimer+".log";
String refileName = "crash-"+timer+"-"+curtTimer+".log";
/**
* 创建文件夹
*/
File folder = new File(CAHCE_CRASH_LOG);
if (!folder.exists())
folder.mkdirs();
/**
* 创建日志文件
*/
File file = new File(folder.getAbsolutePath()+File.separator+fileName);
if (!file.exists())
file.createNewFile();
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(s);
bufferedWriter.flush();
file.renameTo(new File(folder.getAbsolutePath()+File.separator+refileName));
bufferedWriter.close();
//sendCrashLogToServer(folder, file);//此处另开一个app负责上传文件
}catch (Exception e) {
Log.e(TAG, "writerToFile - "+e.getMessage());
}
}
5、限制存储文件数量与过滤排序
/**
* 最大文件数量与排序
* @param limitLogCount
*/
private void limitAppLogCount(int limitLogCount) {
try {
File file = new File(CAHCE_CRASH_LOG);
if (file != null && file.isDirectory()) {
File[] files = file.listFiles(new CrashLogFliter());
if(files != null && files.length >0) {
Arrays.sort(files, comparator);
if (files.length > LIMIT_LOG_COUNT) {
for (int i = 0 ; i < files.length - LIMIT_LOG_COUNT ;i++) {
files[i].delete();
}
}
}
}
}catch (Exception e) {
Log.e(TAG, "limitAppLogCount - "+e.getMessage());
}
}
此类贴出时临时删除了自己公司采集的设备信息,如有误删可以查看下上下逻辑即可,应该是可以直接使用的
/**
* 实现bug收集存储、上传的抽象类
*/
public abstract class CrashUtil implements Thread.UncaughtExceptionHandler{
private static final String TAG = "CrashUtil";
/**
* 允许最大日志文件的数量
*/
private int LIMIT_LOG_COUNT = 10;
/**
* 简单日期格式
*/
private SimpleDateFormat formate = null;
/**
* 保存异常日志信息集合
*/
private LinkedHashMap crashAppLog = new LinkedHashMap();
/**
* 默认放在内存卡的root路径
*/
private String CAHCE_CRASH_LOG = Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator;
/**
* 系统默认的异常类
*/
private Thread.UncaughtExceptionHandler mUncaughtExceptionHandler;
/**
* 日志写入时间
*/
private String curtTimer;
private String timer;
/**
* 抽象方法,
* 在该类初始化的时候使用
*/
public abstract void initParams(CrashUtil crashUtil);
/**
* 发送一场日志文件到服务器
* @param folder 文件路径
* @param file 文件
*/
public abstract void sendCrashLogToServer(File folder, File file);
/**
* 上下文参数
*/
private Context mContext;
public void init(Context context) {
try {
if (context == null)
throw new NullPointerException("Application 的Context不能为空");
this.mContext = context;
/**
* 获取系统默认的异常处理类
*/
mUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
/**
* 在获取系统异常类之前子类可以先做一些初始化的操作
*/
initParams(this);
/**
* 使用当前的类为异常处理类
*/
Thread.setDefaultUncaughtExceptionHandler(this);
}catch (Exception e){
Log.e(TAG, "init - "+e.getMessage());
}
}
/**
* 此类是当应用出现异常的时候执行该方法
* @param thread
* @param throwable
*/
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
if (!hanlderException(throwable) && mUncaughtExceptionHandler != null) {
/**
* 如果此异常不处理则由系统自己处理
*/
this.mUncaughtExceptionHandler.uncaughtException(thread, throwable);
}else{
/**
* 可以延迟一秒钟在退出
*/
// Thread.sleep(1000);
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
}catch (Exception e) {
Log.e(TAG, "uncaughtException - "+e.getMessage());
}
}
/**
* 用户处理异常日志
* @param throwable
* @return
*/
private boolean hanlderException(Throwable throwable) {
try {
if (throwable == null)
return false;
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "程序崩溃", Toast.LENGTH_SHORT).show();
Looper.loop();
}
}).start();
/**
* 收集应用信息
*/
collectCrashLogInfo(mContext);
/**
* 将日志写入文件
*/
writerCrashLogToFile(throwable);
/**
* 限制日子志文件的数量
*/
limitAppLogCount(LIMIT_LOG_COUNT);
} catch (Exception e) {
Log.e(TAG, "hanlderException - " + e.getMessage());
}
return false;
}
/**
* 最大文件数量与排序
* @param limitLogCount
*/
private void limitAppLogCount(int limitLogCount) {
try {
File file = new File(CAHCE_CRASH_LOG);
if (file != null && file.isDirectory()) {
File[] files = file.listFiles(new CrashLogFliter());
if(files != null && files.length >0) {
Arrays.sort(files, comparator);
if (files.length > LIMIT_LOG_COUNT) {
for (int i = 0 ; i < files.length - LIMIT_LOG_COUNT ;i++) {
files[i].delete();
}
}
}
}
}catch (Exception e) {
Log.e(TAG, "limitAppLogCount - "+e.getMessage());
}
}
/**
* 日志文件按修改时间排序
*/
private Comparator comparator = new Comparator() {
@Override
public int compare(File l, File r) {
if (l.lastModified() > r.lastModified())
return 1;
if (l.lastModified() < r.lastModified())
return -1;
return 0;
}
};
/**
* 过滤.log的文件
*/
public class CrashLogFliter implements FileFilter {
@Override
public boolean accept(File file) {
if (file.getName().endsWith(".log"))
return true;
return false;
}
}
/**
* 写入文件中
* @param ex
*/
private void writerCrashLogToFile(Throwable ex) {
try {
StringBuffer buffer = new StringBuffer();
if (crashAppLog != null && crashAppLog.size() >0) {
for (Map.Entry entry:crashAppLog.entrySet()) {
buffer.append(entry.getKey()+":"+entry.getValue()+"\n");
}
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
//e.printStackTrace()是打印异常的堆栈信息,由内向外层层捕获,所以下面需要while依次遍历
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while(cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.flush();
printWriter.close();
String result = writer.toString();
buffer.append("Exception_log:\n");
buffer.append(result);
writerToFile(buffer.toString(), timer, curtTimer);
}catch (Exception e) {
Log.e(TAG, "writerCrashLogToFile - "+e.getMessage());
}
}
private void writerToFile(String s, String timer, String curtTimer) {
try {
/**
* 创建日志文件名称
*
* 此处为了防止读写同时发生,写完以后重命名
*/
String fileName = "write-"+timer+"-"+curtTimer+".log";
String refileName = "crash-"+timer+"-"+curtTimer+".log";
/**
* 创建文件夹
*/
File folder = new File(CAHCE_CRASH_LOG);
if (!folder.exists())
folder.mkdirs();
/**
* 创建日志文件
*/
File file = new File(folder.getAbsolutePath()+File.separator+fileName);
if (!file.exists())
file.createNewFile();
FileWriter fileWriter = new FileWriter(file);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(s);
bufferedWriter.flush();
file.renameTo(new File(folder.getAbsolutePath()+File.separator+refileName));
bufferedWriter.close();
//sendCrashLogToServer(folder, file);//此处另开一个app负责上传文件
}catch (Exception e) {
Log.e(TAG, "writerToFile - "+e.getMessage());
}
}
/**
* 获取应用信息
* @param mContext
*/
private void collectCrashLogInfo(Context mContext) {
try {
if (mContext == null)
return ;
readTimer();
readPACKAGE_INFO(mContext);
}catch (Exception e) {
Log.e(TAG, "collectDeviceInfo - "+e.getMessage());
}
}
private void readTimer(){
//获取写入当前时间
curtTimer = ""+System.currentTimeMillis();
if (formate == null) {
formate = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
}
timer = formate.format(new Date());
crashAppLog.put("time", timer);
}
/**
* 获取包相关信息
* @param mContext
* @throws PackageManager.NameNotFoundException
*/
private void readPACKAGE_INFO(Context mContext) throws PackageManager.NameNotFoundException {
PackageManager packageManager = mContext.getPackageManager();
if (packageManager != null) {
PackageInfo packageInfo = packageManager.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
if (packageInfo != null) {
String versionName = packageInfo.versionName;
String versionCode = ""+packageInfo.versionCode;
String packName = packageInfo.packageName;
crashAppLog.put("version_name",versionName);
}
}
}
/**
* 去除设备号文件中的空格和回车
* @param str
* @return
*/
public String stringUtils(String str){
String dest = "";
if (str != null) {
Pattern p = Pattern.compile("\\s*|\n");
Matcher m = p.matcher(str);
dest = m.replaceAll("");
return dest;
}
return null;
}
public void getDEVICE_INFO(){
//手机型号:android.os.Build.MODEL;
//系统版本:android.os.Build.VERSION.SDK;
//Android版本(SDK):android.os.Build.VERSION.RELEASE;
try {
Field[] fields = Build.class.getFields();
if (fields != null && fields.length > 0) {
for (Field field:fields) {
if (field != null) {
field.setAccessible(true);
//crashAppLog.put(field.getName(), field.get(null).toString());
}
}
}
} catch (Exception e) {
Log.e(TAG, "getDEVICE_INFO - "+e.getMessage());
}
}
public void setLIMIT_LOG_COUNT(int LIMIT_LOG_COUNT) {
this.LIMIT_LOG_COUNT = LIMIT_LOG_COUNT;
}
public void setCAHCE_CRASH_LOG(String CAHCE_CRASH_LOG) {
this.CAHCE_CRASH_LOG = CAHCE_CRASH_LOG;
}
}
提供了一个对外暴露接口的类,方便初始化也可以动态设置日志存储路径和存储大小以及上传接口
/**
* 动态设置存储路径、限制文件大小
*/
public class CrashUtilhandler extends CrashUtil {
private String TAG = "CrashUtilhandler";
public static CrashUtilhandler mCrashUtilhandler = null;
private CrashUtilhandler(){};
public static CrashUtilhandler getInstance() {
if (mCrashUtilhandler == null)
mCrashUtilhandler = new CrashUtilhandler();
return mCrashUtilhandler;
}
@Override
public void initParams(CrashUtil crashUtil) {
if (crashUtil != null){
crashUtil.setCAHCE_CRASH_LOG(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"crashlogs");
crashUtil.setLIMIT_LOG_COUNT(10);
}
}
@Override
public void sendCrashLogToServer(File folder, File file) {
Log.e(TAG, "文件夹:"+folder.getAbsolutePath()+" - "+file.getAbsolutePath()+"");
}
}
OK!有了这两个类,还差最后一个初始化调用就可以将日志信息存储到本地了
//最好是放在工程的Application里初始化
CrashApphandler.getInstance().init(this);
还有写入权限千万不要忘记加了~
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
看一下存储效果
time:2017-07-14-11-39-03
version_name:5.6.10
Exception_log:
java.lang.IndexOutOfBoundsException: Invalid index 1, size is 1
at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:251)
at java.util.ArrayList.get(ArrayList.java:304)
at com.garea.launcher.login.LauncherLogin.onClick(LauncherLogin.java:447)
at android.view.View.performClick(View.java:3511)
at android.view.View$PerformClick.run(View.java:14105)
at android.os.Handler.handleCallback(Handler.java:605)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4424)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)
代码里有上传接口,直接使用也可以,不过公司采用了另起一个apk专门用来上传本地存储crash日志的方式,下一篇再讲解采用retrofit上传本地crash日志,今天就到这里吧~