1.概述
大致的流程就是在用户崩溃的时候,我们获取崩溃信息、应用当前的信息和手机信息,然后把它保存到手机内存卡,再找我就直接找出来看看。后来衍生到上线后某些奇葩机型会有部分问题,所以不得不上传到服务器,后来发现居然可以配合热修复一步一步如此神奇,接下来我们来玩一玩,如何才能把用户的崩溃信息上传到服务器。大家也可以去找腾讯他有现成的:https://bugly.qq.com/v2/index 友盟也有现成的:http://www.umeng.com/ 实现的原理都类似。
2.实现
2.1 拦截闪退信息
如何去收集我们的闪退信息?我们需要认识一下这个类Thread.UncaughtExceptionHandler,一言不和就看源码,这个可以不看,且看我是如何写的。
拦截应用的闪退信息
1 public class ExceptionCrashHandler implements Thread.UncaughtExceptionHandler { 2 3 private static final String TAG = "ExceptionCrashHandler"; 4 // 单例设计模式 5 private static ExceptionCrashHandler mInstance; 6 // 留下原来的,便于开发的时候调试 7 private Thread.UncaughtExceptionHandler mDefaultHandler; 8 // 上下文 获取版本信息和手机信息 9 private Context mContext; 10 11 public static ExceptionCrashHandler getInstance() { 12 if (mInstance == null) { 13 synchronized (ExceptionCrashHandler.class) { 14 if (mInstance == null) { 15 mInstance = new ExceptionCrashHandler(); 16 } 17 } 18 } 19 return mInstance; 20 } 21 22 private ExceptionCrashHandler() { 23 24 } 25 26 public void init(Context context) { 27 /** 28 * 官方解释 29 * Set the handler invoked when this thread abruptly terminates 30 * due to an uncaught exception. 31 **/ 32 Thread.currentThread().setUncaughtExceptionHandler(this); 33 // 获取系统默认的UncaughtException处理器 34 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); 35 this.mContext = context; 36 } 37 38 @Override 39 public void uncaughtException(Thread t, Throwable ex) { 40 Log.e(TAG, "到拦截闪退信息"); 41 } 42 43 }
在Application的onCreate()中配置一下,然后在任何一个地方写一个异常试一试:
public class BaseApplication extends Application { @Override public void onCreate() { super.onCreate(); ExceptionCrashHandler.getInstance().init(this); } }
2.2 收集闪退信息
这样每次崩溃的时候都会进入uncaughtException(),这个时候我们只需要收集信息写入本地文件就好了,收集的信息肯定需要包含好几个部分:当前崩溃信息,当前应用的版本信息,当前手机的信息,有的时候我们还需要其他部分,这里大概就只收集这三部分。为什么收集收集手机信息呢?因为有的时候是由于某些特定手机引起的Bug,若怪罪下来的话我们要甩锅给他。
1 @Override 2 public void uncaughtException(Thread t, Throwable ex) { 3 Log.e(TAG, "捕捉到了异常"); 4 // 1. 获取信息 5 // 1.1 崩溃信息 6 // 1.2 手机信息 7 // 1.3 版本信息 8 // 2.写入文件 9 String crashFileName = saveInfoToSD(ex); 10 11 Log.e(TAG, "fileName --> " + crashFileName); 12 13 // 3. 缓存崩溃日志文件 14 cacheCrashFile(crashFileName); 15 // 系统默认处理 16 mDefaultHandler.uncaughtException(t, ex); 17 } 18 19 /** 20 * 缓存崩溃日志文件 21 * 22 * @param fileName 23 */ 24 private void cacheCrashFile(String fileName) { 25 SharedPreferences sp = mContext.getSharedPreferences("crash", Context.MODE_PRIVATE); 26 sp.edit().putString("CRASH_FILE_NAME", fileName).commit(); 27 } 28 29 30 /** 31 * 获取崩溃文件名称 32 * 33 * @return 34 */ 35 public File getCrashFile() { 36 String crashFileName = mContext.getSharedPreferences("crash", 37 Context.MODE_PRIVATE).getString("CRASH_FILE_NAME", ""); 38 return new File(crashFileName); 39 } 40 41 /** 42 * 保存获取的 软件信息,设备信息和出错信息保存在SDcard中 43 * 44 * @param ex 45 * @return 46 */ 47 private String saveInfoToSD(Throwable ex) { 48 String fileName = null; 49 StringBuffer sb = new StringBuffer(); 50 51 for (Map.Entry<String, String> entry : obtainSimpleInfo(mContext) 52 .entrySet()) { 53 String key = entry.getKey(); 54 String value = entry.getValue(); 55 sb.append(key).append(" = ").append(value).append(" "); 56 } 57 58 sb.append(obtainExceptionInfo(ex)); 59 60 if (Environment.getExternalStorageState().equals( 61 Environment.MEDIA_MOUNTED)) { 62 File dir = new File(mContext.getFilesDir() + File.separator + "crash" 63 + File.separator); 64 65 // 先删除之前的异常信息 66 if (dir.exists()) { 67 deleteDir(dir); 68 } 69 70 // 再从新创建文件夹 71 if (!dir.exists()) { 72 dir.mkdir(); 73 } 74 try { 75 fileName = dir.toString() 76 + File.separator 77 + getAssignTime("yyyy_MM_dd_HH_mm") + ".txt"; 78 FileOutputStream fos = new FileOutputStream(fileName); 79 fos.write(sb.toString().getBytes()); 80 fos.flush(); 81 fos.close(); 82 } catch (Exception e) { 83 e.printStackTrace(); 84 } 85 } 86 return fileName; 87 } 88 89 /** 90 * 返回当前日期根据格式 91 **/ 92 private String getAssignTime(String dateFormatStr) { 93 DateFormat dataFormat = new SimpleDateFormat(dateFormatStr); 94 long currentTime = System.currentTimeMillis(); 95 return dataFormat.format(currentTime); 96 } 97 98 99 /** 100 * 获取一些简单的信息,软件版本,手机版本,型号等信息存放在HashMap中 101 * 102 * @return 103 */ 104 private HashMap<String, String> obtainSimpleInfo(Context context) { 105 HashMap<String, String> map = new HashMap<>(); 106 PackageManager mPackageManager = context.getPackageManager(); 107 PackageInfo mPackageInfo = null; 108 try { 109 mPackageInfo = mPackageManager.getPackageInfo( 110 context.getPackageName(), PackageManager.GET_ACTIVITIES); 111 } catch (PackageManager.NameNotFoundException e) { 112 e.printStackTrace(); 113 } 114 map.put("versionName", mPackageInfo.versionName); 115 map.put("versionCode", "" + mPackageInfo.versionCode); 116 map.put("MODEL", "" + Build.MODEL); 117 map.put("SDK_INT", "" + Build.VERSION.SDK_INT); 118 map.put("PRODUCT", "" + Build.PRODUCT); 119 map.put("MOBLE_INFO", getMobileInfo()); 120 return map; 121 } 122 123 124 /** 125 * Cell phone information 126 * 127 * @return 128 */ 129 public static String getMobileInfo() { 130 StringBuffer sb = new StringBuffer(); 131 try { 132 Field[] fields = Build.class.getDeclaredFields(); 133 for (Field field : fields) { 134 field.setAccessible(true); 135 String name = field.getName(); 136 String value = field.get(null).toString(); 137 sb.append(name + "=" + value); 138 sb.append(" "); 139 } 140 } catch (Exception e) { 141 e.printStackTrace(); 142 } 143 return sb.toString(); 144 } 145 146 147 /** 148 * 获取系统未捕捉的错误信息 149 * 150 * @param throwable 151 * @return 152 */ 153 private String obtainExceptionInfo(Throwable throwable) { 154 StringWriter stringWriter = new StringWriter(); 155 PrintWriter printWriter = new PrintWriter(stringWriter); 156 throwable.printStackTrace(printWriter); 157 printWriter.close(); 158 return stringWriter.toString(); 159 } 160 161 162 /** 163 * 递归删除目录下的所有文件及子目录下所有文件 164 * 165 * @param dir 将要删除的文件目录 166 * @return boolean Returns "true" if all deletions were successful. If a 167 * deletion fails, the method stops attempting to delete and returns 168 * "false". 169 */ 170 private boolean deleteDir(File dir) { 171 if (dir.isDirectory()) { 172 String[] children = dir.list(); 173 // 递归删除目录中的子目录下 174 for (int i = 0; i < children.length; i++) { 175 boolean success = deleteDir(new File(dir, children[i])); 176 if (!success) { 177 return false; 178 } 179 } 180 } 181 // 目录此时为空,可以删除 182 return true; 183 }
保存的路径最好不要在用户的外部存储卡中,因为6.0的时候如果访问外部存储卡需要动态的申请权限,那这个时候信息是获取到了但是GG。都蹦了还拖着我不放,还需要申请权限,纳尼???
2.2 上传闪退信息
每次启动应用的时候就获取上次闪退的信息日志,然后上传到服务器。
public class MainActivity extends BaseActivity { @Override protected void initData() { // 获取上次的崩溃信息 File crashFile = ExceptionCrashHandler.getInstance().getCrashFile(); // 上传到服务器,后面再说....... } @Override protected void initView() { } @Override protected void setContentView() { setContentView(R.layout.activity_main); } @Override protected void initTitle() { } }