Android中处理崩溃异常

Stella981
• 阅读 566

大家都知道,现在安装Android系统的手机版本和设备千差万别,在模拟器上运行良好的程序安装到某款手机上说不定就出现崩溃的现象,开发者个人 不可能购买所有设备逐个调试,所以在程序发布出去之后,如果出现了崩溃现象,开发者应该及时获取在该设备上导致崩溃的信息,这对于下一个版本的bug修复 帮助极大,所以今天就来介绍一下如何在程序崩溃的情况下收集相关的设备参数信息和具体的异常信息,并发送这些信息到服务器供开发者分析和调试程序。

我们先建立一个crash项目,项目结构如图:

Android中处理崩溃异常

在MainActivity.java代码中,代码是这样写的:

[java] view plain copy

  1. package com.scott.crash;

  2. import android.app.Activity;

  3. import android.os.Bundle;

  4. public class MainActivity extends Activity {

  5. private String s;

  6. @Override

  7. public void onCreate(Bundle savedInstanceState) {

  8. super.onCreate(savedInstanceState);

  9. System.out.println(s.equals("any string"));

  10. }

  11. }

 我们在这里故意制造了一个潜在的运行期异常,当我们运行程序时就会出现以下界面:

Android中处理崩溃异常

遇到软件没有捕获的异常之后,系统会弹出这个默认的强制关闭对话框。

我们当然不希望用户看到这种现象,简直是对用户心灵上的打击,而且对我们的bug的修复也是毫无帮助的。我们需要的是软件有一个全局的异常捕获器,当出现一个我们没有发现的异常时,捕获这个异常,并且将异常信息记录下来,上传到服务器公开发这分析出现异常的具体原因。

接下来我们就来实现这一机制,不过首先我们还是来了解以下两个类:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。

Application:用来管理应用程序的全局状态。在应用程序启动时Application会首先创建,然后才会根据情况(Intent)来启动相应的Activity和Service。本示例中将在自定义加强版的Application中注册未捕获异常处理器。

Thread.UncaughtExceptionHandler:线程未捕获异常处理器,用来处理未捕获异常。如果程序出现了未捕获异常,默认会 弹出系统中强制关闭对话框。我们需要实现此接口,并注册为程序中默认未捕获异常处理。这样当未捕获异常发生时,就可以做一些个性化的异常处理操作。

大家刚才在项目的结构图中看到的CrashHandler.java实现了Thread.UncaughtExceptionHandler,使我们用来处理未捕获异常的主要成员,代码如下:

[java] view plain copy

  1. package com.scott.crash;

  2. import java.io.File;

  3. import java.io.FileOutputStream;

  4. import java.io.PrintWriter;

  5. import java.io.StringWriter;

  6. import java.io.Writer;

  7. import java.lang.Thread.UncaughtExceptionHandler;

  8. import java.lang.reflect.Field;

  9. import java.text.DateFormat;

  10. import java.text.SimpleDateFormat;

  11. import java.util.Date;

  12. import java.util.HashMap;

  13. import java.util.Map;

  14. import android.content.Context;

  15. import android.content.pm.PackageInfo;

  16. import android.content.pm.PackageManager;

  17. import android.content.pm.PackageManager.NameNotFoundException;

  18. import android.os.Build;

  19. import android.os.Environment;

  20. import android.os.Looper;

  21. import android.util.Log;

  22. import android.widget.Toast;

  23. /**

  24. * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告.

  25. *

  26. * @author user

  27. *

  28. */

  29. public class CrashHandler implements UncaughtExceptionHandler {

  30. public static final String TAG = "CrashHandler";

  31. //系统默认的UncaughtException处理类

  32. private Thread.UncaughtExceptionHandler mDefaultHandler;

  33. //CrashHandler实例

  34. private static CrashHandler INSTANCE = new CrashHandler();

  35. //程序的Context对象

  36. private Context mContext;

  37. //用来存储设备信息和异常信息

  38. private Map<String, String> infos = new HashMap<String, String>();

  39. //用于格式化日期,作为日志文件名的一部分

  40. private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

  41. /** 保证只有一个CrashHandler实例 */

  42. private CrashHandler() {

  43. }

  44. /** 获取CrashHandler实例 ,单例模式 */

  45. public static CrashHandler getInstance() {

  46. return INSTANCE;

  47. }

  48. /**

  49. * 初始化

  50. *

  51. * @param context

  52. */

  53. public void init(Context context) {

  54. mContext = context;

  55. //获取系统默认的UncaughtException处理器

  56. mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();

  57. //设置该CrashHandler为程序的默认处理器

  58. Thread.setDefaultUncaughtExceptionHandler(this);

  59. }

  60. /**

  61. * 当UncaughtException发生时会转入该函数来处理

  62. */

  63. @Override

  64. public void uncaughtException(Thread thread, Throwable ex) {

  65. if (!handleException(ex) && mDefaultHandler != null) {

  66. //如果用户没有处理则让系统默认的异常处理器来处理

  67. mDefaultHandler.uncaughtException(thread, ex);

  68. } else {

  69. try {

  70. Thread.sleep(3000);

  71. } catch (InterruptedException e) {

  72. Log.e(TAG, "error : ", e);

  73. }

  74. //退出程序

  75. android.os.Process.killProcess(android.os.Process.myPid());

  76. System.exit(1);

  77. }

  78. }

  79. /**

  80. * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.

  81. *

  82. * @param ex

  83. * @return true:如果处理了该异常信息;否则返回false.

  84. */

  85. private boolean handleException(Throwable ex) {

  86. if (ex == null) {

  87. return false;

  88. }

  89. //使用Toast来显示异常信息

  90. new Thread() {

  91. @Override

  92. public void run() {

  93. Looper.prepare();

  94. Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();

  95. Looper.loop();

  96. }

  97. }.start();

  98. //收集设备参数信息

  99. collectDeviceInfo(mContext);

  100. //保存日志文件

  101. saveCrashInfo2File(ex);

  102. return true;

  103. }

  104. /**

  105. * 收集设备参数信息

  106. * @param ctx

  107. */

  108. public void collectDeviceInfo(Context ctx) {

  109. try {

  110. PackageManager pm = ctx.getPackageManager();

  111. PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);

  112. if (pi != null) {

  113. String versionName = pi.versionName == null ? "null" : pi.versionName;

  114. String versionCode = pi.versionCode + "";

  115. infos.put("versionName", versionName);

  116. infos.put("versionCode", versionCode);

  117. }

  118. } catch (NameNotFoundException e) {

  119. Log.e(TAG, "an error occured when collect package info", e);

  120. }

  121. Field[] fields = Build.class.getDeclaredFields();

  122. for (Field field : fields) {

  123. try {

  124. field.setAccessible(true);

  125. infos.put(field.getName(), field.get(null).toString());

  126. Log.d(TAG, field.getName() + " : " + field.get(null));

  127. } catch (Exception e) {

  128. Log.e(TAG, "an error occured when collect crash info", e);

  129. }

  130. }

  131. }

  132. /**

  133. * 保存错误信息到文件中

  134. *

  135. * @param ex

  136. * @return  返回文件名称,便于将文件传送到服务器

  137. */

  138. private String saveCrashInfo2File(Throwable ex) {

  139. StringBuffer sb = new StringBuffer();

  140. for (Map.Entry<String, String> entry : infos.entrySet()) {

  141. String key = entry.getKey();

  142. String value = entry.getValue();

  143. sb.append(key + "=" + value + "\n");

  144. }

  145. Writer writer = new StringWriter();

  146. PrintWriter printWriter = new PrintWriter(writer);

  147. ex.printStackTrace(printWriter);

  148. Throwable cause = ex.getCause();

  149. while (cause != null) {

  150. cause.printStackTrace(printWriter);

  151. cause = cause.getCause();

  152. }

  153. printWriter.close();

  154. String result = writer.toString();

  155. sb.append(result);

  156. try {

  157. long timestamp = System.currentTimeMillis();

  158. String time = formatter.format(new Date());

  159. String fileName = "crash-" + time + "-" + timestamp + ".log";

  160. if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {

  161. String path = "/sdcard/crash/";

  162. File dir = new File(path);

  163. if (!dir.exists()) {

  164. dir.mkdirs();

  165. }

  166. FileOutputStream fos = new FileOutputStream(path + fileName);

  167. fos.write(sb.toString().getBytes());

  168. fos.close();

  169. }

  170. return fileName;

  171. } catch (Exception e) {

  172. Log.e(TAG, "an error occured while writing file...", e);

  173. }

  174. return null;

  175. }

  176. }

在收集异常信息时,朋友们也可以使用Properties,因为Properties有一个很便捷的方法 properties.store(OutputStream out, String comments),用来将Properties实例中的键值对外输到输出流中,但是在使用的过程中发现生成的文件中异常信息打印在同一行,看起来极为费 劲,所以换成Map来存放这些信息,然后生成文件时稍加了些操作。

完成这个CrashHandler后,我们需要在一个Application环境中让其运行,为此,我们继承android.app.Application,添加自己的代码,CrashApplication.java代码如下:

[java] view plain copy

  1. package com.scott.crash;

  2. import android.app.Application;

  3. public class CrashApplication extends Application {

  4. @Override

  5. public void onCreate() {

  6. super.onCreate();

  7. CrashHandler crashHandler = CrashHandler.getInstance();

  8. crashHandler.init(getApplicationContext());

  9. }

  10. }

最后,为了让我们的CrashApplication取代android.app.Application的地位,在我们的代码中生效,我们需要修改AndroidManifest.xml:

[html] view plain copy

  1. <application android:name=".CrashApplication" ...>
  2. </application>

因为我们上面的CrashHandler中,遇到异常后要保存设备参数和具体异常信息到SDCARD,所以我们需要在AndroidManifest.xml中加入读写SDCARD权限:

[html] view plain copy

  1. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

搞定了上边的步骤之后,我们来运行一下这个项目:

Android中处理崩溃异常

看以看到,并不会有强制关闭的对话框出现了,取而代之的是我们比较有好的提示信息。

然后看一下SDCARD生成的文件:

Android中处理崩溃异常

用文本编辑器打开日志文件,看一段日志信息:

[java] view plain copy

  1. CPU_ABI=armeabi
  2. CPU_ABI2=unknown
  3. ID=FRF91
  4. MANUFACTURER=unknown
  5. BRAND=generic
  6. TYPE=eng
  7. ......
  8. Caused by: java.lang.NullPointerException
  9. at com.scott.crash.MainActivity.onCreate(MainActivity.java:13)
  10. at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
  11. at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
  12. ... 11 more

这些信息对于开发者来说帮助极大,所以我们需要将此日志文件上传到服务器,有关文件上传的技术,请参照Android中使用HTTP服务相关介绍。

不过在使用HTTP服务之前,需要确定网络畅通,我们可以使用下面的方式判断网络是否可用:

[java] view plain copy

  1. /**
  2. * 网络是否可用
  3. *
  4. * @param context
  5. @return
  6. */
  7. public static boolean isNetworkAvailable(Context context) {
  8. ConnectivityManager mgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  9. NetworkInfo[] info = mgr.getAllNetworkInfo();
  10. if (info != null) {
  11. for (int i = 0; i < info.length; i++) {
  12. if (info[i].getState() == NetworkInfo.State.CONNECTED) {
  13. return true;
  14. }
  15. }
  16. }
  17. return false;
  18. }
点赞
收藏
评论区
推荐文章
Souleigh ✨ Souleigh ✨
3年前
前端 - 常见的异常捕获方法
前端异常捕获在ES3之前js代码执行的过程中,一旦出现错误,整个js代码都会停止执行,这样就显的代码非常的不健壮。从ES3开始,js也提供了类似的异常处理机制,从而让js代码变的更健壮,程序执行的过程中出现了异常,也可以让程序具有了一部分的异常恢复能力。js异常的特点是,出现不会导致JS引擎崩溃,最多只会终止当前执行的任务。回归正题,我们该如何在程序异常发生
WinanDH WinanDH
2年前
Frida下so注入(动态加载)
WinanDH目的:解决fridanew主动调用so函数,调用次数过多出现程序崩溃问题。解决函数外部调用耗时过长问题环境:fridaversion12.8.0、某汽车故障诊断设备
红橙Darren 红橙Darren
3年前
Android热修复之 - 收集崩溃信息上传服务器
1.概述开始想收集崩溃信息是因为测试的哥们老是说崩了,但是一过来就开始拍脑袋说我\怎么好了?所以后来上网查了很多信息,找到了一种方法。大致的流程就是在用户崩溃的时候,我们获取崩溃信息、应用当前的信息和手机信息,然后把它保存到手机内存卡,再找我就直接找出来看看。后来衍生到上线后某些奇葩机型会有部分问题,所以不得不上传到服务器,后来发现居然可以配合热修复一
Wesley13 Wesley13
3年前
ulua,slua,tolua,xlua 等跨语言C#Lua 接口崩溃原因
使用ulua等unitylua脚本接口工具时,经常会出现各种崩溃这些崩溃本质上有一个共同模式:C调用Lua,Lua调用c,接着C出现异常也就是跨语言异常处理上存在严重问题;ulua,slua等库都抄袭了一个叫做LuaInterface的库,而这个异常传递导致崩溃的问题,就是LuaInterface自身设计的问题
Stella981 Stella981
3年前
Android Native crash 处理案例分享
1\.背景目前mPaas\1\Android使用CrashSDK对闪退进行的处理,CrashSDK是Android平台上一款功能强大的崩溃日志收集SDK,有着极高的崩溃收集率和完整、全面的崩溃日志信息,生成的日志内容非常利于问题的跟进和解决。在日常运维中,经常遇到一些闪退,无法直接从闪退堆栈找到原因,尤其是一些非Java
流浪剑客 流浪剑客
1年前
iOS设备数据恢复软件:PhoneRescue for iOS Mac中文版
是一款专业的iOS设备数据恢复软件,它可以帮助Mac用户从iPhone、iPad、iPodtouch等设备中恢复丢失的数据。无论是因为误删除、系统崩溃、设备损坏还是其他原因导致的数据丢失,PhoneRescueMac都可以帮助您找回这些重要的数据。Phon
燕青 燕青
1年前
iOS数据恢复工具:PhoneRescue for Mac中文版
是一款终极的iOS数据恢复程序,设计用于在iPhone、iPad和iPodtouch上检索丢失的数据,包括照片、消息、联系人、便笺等,同时也可以从任何iOS崩溃错误修复iDevice。这款软件可以恢复设备上丢失的数据,包括从设备、iTunes备份和iClo
燕青 燕青
1年前
HTTP协议抓包工具:Charles for Mac
是一款终极的iOS数据恢复程序,设计用于在iPhone、iPad和iPodtouch上检索丢失的数据,包括照片、消息、联系人、便笺等,同时也可以从任何iOS崩溃错误修复iDevice。这款软件可以恢复设备上丢失的数据,包括从设备、iTunes备份和iClo
燕青 燕青
1年前
邮件管理工具:Outlook LTSC 2021 for Mac中文版
是一款终极的iOS数据恢复程序,设计用于在iPhone、iPad和iPodtouch上检索丢失的数据,包括照片、消息、联系人、便笺等,同时也可以从任何iOS崩溃错误修复iDevice。这款软件可以恢复设备上丢失的数据,包括从设备、iTunes备份和iClo
燕青 燕青
1年前
AI智能视频清晰处理工具:Perfectly Clear Video for Mac
是一款终极的iOS数据恢复程序,设计用于在iPhone、iPad和iPodtouch上检索丢失的数据,包括照片、消息、联系人、便笺等,同时也可以从任何iOS崩溃错误修复iDevice。这款软件可以恢复设备上丢失的数据,包括从设备、iTunes备份和iClo