Android中处理崩溃异常,为什么叫它神器

2020-03-23 22:10 来源:未知

图片 1Crashlytics最近知乎上有同学邀请我回答:通过第三方统计所搜集的错误信息应该如何准确的修复?,其实是指收集上来的一些信息不充分难以定位的crash如何处理?刚好之前曾在这块做过一些工作,有一些体会,因此简单回答了一下,同时也是自己的一个整理总结,免得时间长了很多东西都忘记了。

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

Android APP Crash通常是由未捕获的Exceptionsignal引起app异常退出。

我在知乎上回答了这个问题: Android 开发:开始一个项目前,做好哪些准备可以事半功倍?,很多粉丝因此关注过来,也有同学想让我多分享实际工作中的一些经验,比如那篇回答最后提到的彩蛋-appsee。

crash大家肯定都遇到过,也应该遇到过一些没有头绪修不下去的crash, 有些在困扰你很久之后被你搞定,有些只能尘封在那里,弃之不管。

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

本文主要从以下6个方面分析介绍 Android App Crash
通过本篇文章,您将获取到以下内容

我们是在一款新闻类的产品上做的appsee尝试,今天我就给大家聊一聊这款神器以及我们实际使用后的体会。

修复crash最重要的是要找到root cause, 也就是产生这一问题的根本原因,然而很多时候大家经常头痛医头,脚痛医脚,因为这样看起来最轻松简单有效,但往往把隐患埋藏起来,下次如果再因为这个root cause导致一个很奇葩的问题,你也许压根就没法找到任何头绪,这就是典型的技术债务的一种。因此建议大家修crash 一定要尽量找到root cause, 实在找不到的话,如果问题不严重,宁愿不修;如果确定要修,也要在修复方案上注明TODO,写清楚这是一个workaround方案,也许今后有哪位高人能修掉它呢?

图片 2

  1. App Crash
  2. 检测 Crash 问题
  3. Android vitals
  4. 分析 App Crash
  5. 复现 Crash 小提示
  6. Logcat 抓取复现问题 Crash 的 Log

欢迎关注微信公众号:程序员Android
公众号ID:ProgramAndroid
获取更多信息

appsee创办于2012年,总部在以色列的特拉维夫,运营三年以来其实一直不愠不火,已经拉到过几笔风投,目前有包括英国天然气公司在内的一些公司正在使用他们的产品,最初只提供了iOS版本,最近已经扩展支持到Android平台。

回到知乎上的那个问题,通过crash收集系统所收集到的一些crash,仅凭crash堆栈信息难以定位问题,自己测试也难以必现,甚至根本无法重现,这种情况应该怎么办呢?

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

微信公众号:ProgramAndroid

本质上appsee是一款统计分析产品,类似友盟,但他们的产品有其他各家国内外统计分析平台所不具备的特点。

我这里抛砖引玉,给出一些自己的建议:

[java] view plaincopyprint?

我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
我们不发送红包,我们只是红包的搬运工。

  • 一是点击热图(Touch Heatmaps)
  • 二是用户记录(User Recordings)
  1. 首先要根据实际情况,设置一些的crash相关指标,比如整体的crash rate不要超过0.2%, 某个crash影响的人数与数量,不要超过预期值。是的,我的意思是crash未必一定要修复,理论上大部分的都是可以修复的,但往往投入的精力与回报不成比例,也许我们可以把精力放在其它地方。另外就Android系统这碎片化的程度,各种兼容性问题太多太奇葩,我曾负责过一个crash顽疾整理工作,发现影响人数1~2个的一些crash, 居然有超过4000种,尼玛我看都看不完,谈何一个个分析处理?

  2. 根据crash的严重程度、影响范围,把值得修的问题都列出来,根据优先级逐个处理,新出现的crash理论上是都需要修复的。

  3. 善用Google, 耐心找找,大部分的问题别人也应该出现过,也头痛过,可能最终的现象不一样,但也许能在一些问题里找到蛛丝马迹。

  4. 实在找不到的,也许是收集到的信息有限,可以补充收集更多信息,一些第三方crash收集分析平台,比如crashlytics,其实也支持上传Log, 在关键地方多打些Log, 尽量保存更多crash现场附近的信息,异常要处理好,经常是一些异常被不合理的吃掉了,导致问题被掩盖。

  5. 还是不行就换个同事看看,别觉得丢脸,很多时候未必是水平问题,人很容易陷入到自己的思路里出不来。

  6. 同事大牛都请教过了,还是没辙?二分大法好,看这个crash是哪个版本开始出现的,一点一点定位到具体版本,再一点一点定位到具体的change,这就要求持续集成与代码管理要做到位。我们有不少超级难搞的问题,就是这种方式解决的,这种比较适合有一定重现概率的问题,如果完全没头绪重现,也只能根据提交的change, 根据经验联想猜测其可能性了。

  7. 如果还不行,只能用神器了,我的微信公众号里有文章介绍 《Appsee: 为什么叫它神器?》 Appsee有个功能,可以直接查看每次crash之前用户都干了些啥,如果这还不能定位问题,有点说不过去吧。

  1. package com.scott.crash;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5.   
  6. public class MainActivity extends Activity {  
  7.   
  8.     private String s;  
  9.       
  10.     @Override  
  11.     public void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         System.out.println(s.equals("any string"));  
  14.     }  
  15. }  

    package com.scott.crash;

    import android.app.Activity; import android.os.Bundle;

    public class MainActivity extends Activity {

    private String s;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.out.println(s.equals("any string"));
    }
    

    }

1. App Crash

如果app使用Java语言编写,那么,当Throwable抛出未处理的异常时,此时就会引起 app Crash

如果app 使用native-code编写,那么,当执行时遇到未处理的signal(例如SIGSEGV)时,app就会 Crash并退出。

当应用程序崩溃时,Android终止应用程序的进程并显示一个对话框,让用户知道应用程序已经停止。

app crash 举例

App Crash 不仅发生在前台进程,任何app组件,例如Broadcast Receivers,Content Providers,Service等在后台运行的组件,也可以引起App Crash。这些Crash很容易引起用户混淆,因为他们没有直接参与到你的app中。

先说点击热图,它在pc时代比较流行,有成熟的技术方案,主要应用在各种web页面上,类似下面这样,利用js实现或直接集成第三方服务。

很多时候即便知道了原因,也未必能修复,有时候真是系统或者环境的坑,有时候可以换个实现方式或设计避开它,总之crash要尽量先定位出原因,之后怎么处理大家就各想各招吧,尽量用一些优雅简洁的方式去处理它。

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

2. 检测Crash 问题

当你的app已经上线,过多的Crash 会给用户带来一个很糟糕的体验。Google也意思到这一点,Android vitals会帮我们意识到此问题的严重性。

图片 3PC Web热点图

另外可以使用一些静态分析工具,提前发现代码中的各种隐患,比如Lint, Find Bugs等等,平时养成良好的编码习惯,模块之间不要牵连太多,充分解藕,规范异常处理、仔细考虑各种分支异常场景,不要偷懒,多打些Log,单元测试能做好就更好了(我的公众号里也有介绍单元测试的文章,大家可以参考)。这样一方面可以提前发现问题,另一方面也可以在问题出现时轻松定位。

图片 4

3. Android vitals

当你的应用出现过多的Crash时候,Android vitals可以通过Play Console帮助你提高app的性能。
Android vitals 认为APP 过多Crash场景如下:

  • 使用一天APP,出现一次Crash的概率在1.09%之上。
  • 使用一天APP,出现两次或多次 Crash 的概率在0.18%之上

如需获取更多信息,请查看Play Console

但随着native app发展如火如荼,目前支持native app的点击热图分析服务却很少,国外有少量几款,国内几乎没有。

最后想提一下,这种难以重现的顽疾,对测试与开发都是挑战,很多时候需要大家通力合作才能有好的结果,切忌互相推诿,毕竟大家的最终目标是一致的。

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

4. 分析App Crash

解决崩溃可能很困难。 但是,如果您能够确定崩溃的根本原因,则很可能可以找到解决方案。

有很多情况可能会导致应用程序崩溃。 一些原因是显而易见的,比如检查一个空值或空字符串,而另外一些更微妙,比如将无效参数传递给API甚至是复杂的多线程交互。

热图分析,一方面提供了可视化的数据呈现方式,直观清晰,另一方面,也可以通过热图分析出用户的操作习惯,以此对交互作出改进,比如用户可能会习惯地点击他认为可能可以点击的地方,结果却让用户失望,这种情况我们一般很难发现,用户当时可能心里嘀咕一声就算了,但其实有时候并不真的就没影响,一些交互细节问题的积累可能让用户的不满情绪累积,从而导致用户卸载或影响产品口碑,如果能尽早发现与改进此类问题,甚至可以极大程度提升某些交互的转化率。

本文首发在我的微信公众号 Android程序员,主要关注Android最佳实践、经验分享,大家可以通过搜索公众号ID:AndroidTrending 来关注,也可以扫码关注。

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

读取堆栈信息

解决App Crash,首先要找到在那些代码发生的。你可以通过logcat或者play Console等输出的堆栈信息进行分析查看。

Crash 堆栈信息

上述Crash 堆栈信息包含 以下信息

    1. Crash app 包名
    1. Crash app PID
    1. 引起Crash的异常信息(此异常时引起Crash的重要原因)
    1. 引起Crash 的代码位置,行号,哪个函数调用等等
  • 5 . 对于被调用的每个函数,另一行显示前面的调用站点(称为栈帧)。

通过走栈和检查代码,你可能会发现一个地方传递了一个不正确的值。 如果您的代码没有出现在堆栈跟踪中,则可能是在某处将异常操作传递给了一个无效的参数。 您可以经常通过检查堆栈跟踪的每一行,找到您使用的任何API类,并确认您传递的参数是正确的,并且从允许的地方调用该类来判断发生了什么。

而appsee帮我们解决了这个问题,以下是效果图:

图片 5扫码关注 Android程序员

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

5. 复现Crash 小提示

您可能无法通过启动模拟器或将设备连接到计算机来重现问题。 开发环境倾向于拥有更多资源,例如带宽,内存和存储。 使用异常类型来确定稀缺资源,或者在Android版本,设备类型或应用版本之间找到关联。

图片 6Appsee热点图

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

OutOfMemoryError 内存错误

如果你有一个OutOfMemoryError,那么你可以创建一个内存容量较低的模拟器来开始,下图显示了您可以控制设备上的内存量的AVD管理器设置。

创建低 RAM模拟器复现低内存问题

appsee用户操作记录的功能其实是最具特色的,目前全球只此一家,别无二店。在它的web后台才能感受到它的强大,先放张图大家看看。

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

Networking exceptions 网络异常

由于用户经常进出移动或WiFi网络覆盖范围,因此在应用程序网络中,例外情况通常不应被视为错误,而应视为意外发生的正常运行状况。

如果您需要重现网络异常(例如UnknownHostException),请尝试在应用程序尝试使用网络时打开飞行模式。

另一个选择是通过选择网络速度仿真和/或网络延迟来降低仿真器中网络的质量。 您可以使用AVD管理器上的速度和延迟设置,也可以使用-netdelay-netspeed标志启动模拟器,如以下命令行示例所示:

emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm

图片 7Appsee后台

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

6. Logcat 抓取复现问题Crash的Log

Logcat是一个命令行工具,用于转储系统消息日志,其中包括设备引发错误时的堆叠追踪以及从您的应用使用Log类编写的消息。

我分两个方面来介绍:

[java] view plaincopyprint?

命令行语法

[adb] logcat [<option>] ... [<filter-spec>] ...

Logcat 命令行选项

一个session是指用户从进入app到退出到全过程。appsee 记录了整个seesion 操作录像,同时记录下每一步的操作,注意这是两个过程,它通过技术手段捕获到用户的每一个点击,滑动,按键,将其记录下来并与用户录像同步,在web后台查看屏幕录像,直接在录像上可视化这些操作,让你感觉仿佛正在观察用户的操作一样,想实际体验的可以去看下官网上的demo。

  1. package com.scott.crash;  
  2.   
  3. import java.io.File;  
  4. import java.io.FileOutputStream;  
  5. import java.io.PrintWriter;  
  6. import java.io.StringWriter;  
  7. import java.io.Writer;  
  8. import java.lang.Thread.UncaughtExceptionHandler;  
  9. import java.lang.reflect.Field;  
  10. import java.text.DateFormat;  
  11. import java.text.SimpleDateFormat;  
  12. import java.util.Date;  
  13. import java.util.HashMap;  
  14. import java.util.Map;  
  15.   
  16. import android.content.Context;  
  17. import android.content.pm.PackageInfo;  
  18. import android.content.pm.PackageManager;  
  19. import android.content.pm.PackageManager.NameNotFoundException;  
  20. import android.os.Build;  
  21. import android.os.Environment;  
  22. import android.os.Looper;  
  23. import android.util.Log;  
  24. import android.widget.Toast;  
  25.   
  26. /** 
  27.  * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告. 
  28.  *  
  29.  * @author user 
  30.  *  
  31.  */  
  32. public class CrashHandler implements UncaughtExceptionHandler {  
  33.       
  34.     public static final String TAG = "CrashHandler";  
  35.       
  36.     //系统默认的UncaughtException处理类   
  37.     private Thread.UncaughtExceptionHandler mDefaultHandler;  
  38.     //CrashHandler实例  
  39.     private static CrashHandler INSTANCE = new CrashHandler();  
  40.     //程序的Context对象  
  41.     private Context mContext;  
  42.     //用来存储设备信息和异常信息  
  43.     private Map<String, String> infos = new HashMap<String, String>();  
  44.   
  45.     //用于格式化日期,作为日志文件名的一部分  
  46.     private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");  
  47.   
  48.     /** 保证只有一个CrashHandler实例 */  
  49.     private CrashHandler() {  
  50.     }  
  51.   
  52.     /** 获取CrashHandler实例 ,单例模式 */  
  53.     public static CrashHandler getInstance() {  
  54.         return INSTANCE;  
  55.     }  
  56.   
  57.     /** 
  58.      * 初始化 
  59.      *  
  60.      * @param context 
  61.      */  
  62.     public void init(Context context) {  
  63.         mContext = context;  
  64.         //获取系统默认的UncaughtException处理器  
  65.         mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();  
  66.         //设置该CrashHandler为程序的默认处理器  
  67.         Thread.setDefaultUncaughtExceptionHandler(this);  
  68.     }  
  69.   
  70.     /** 
  71.      * 当UncaughtException发生时会转入该函数来处理 
  72.      */  
  73.     @Override  
  74.     public void uncaughtException(Thread thread, Throwable ex) {  
  75.         if (!handleException(ex) && mDefaultHandler != null) {  
  76.             //如果用户没有处理则让系统默认的异常处理器来处理  
  77.             mDefaultHandler.uncaughtException(thread, ex);  
  78.         } else {  
  79.             try {  
  80.                 Thread.sleep(3000);  
  81.             } catch (InterruptedException e) {  
  82.                 Log.e(TAG, "error : ", e);  
  83.             }  
  84.             //退出程序  
  85.             android.os.Process.killProcess(android.os.Process.myPid());  
  86.             System.exit(1);  
  87.         }  
  88.     }  
  89.   
  90.     /** 
  91.      * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. 
  92.      *  
  93.      * @param ex 
  94.      * @return true:如果处理了该异常信息;否则返回false. 
  95.      */  
  96.     private boolean handleException(Throwable ex) {  
  97.         if (ex == null) {  
  98.             return false;  
  99.         }  
  100.         //使用Toast来显示异常信息  
  101.         new Thread() {  
  102.             @Override  
  103.             public void run() {  
  104.                 Looper.prepare();  
  105.                 Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();  
  106.                 Looper.loop();  
  107.             }  
  108.         }.start();  
  109.         //收集设备参数信息   
  110.         collectDeviceInfo(mContext);  
  111.         //保存日志文件   
  112.         saveCrashInfo2File(ex);  
  113.         return true;  
  114.     }  
  115.       
  116.     /** 
  117.      * 收集设备参数信息 
  118.      * @param ctx 
  119.      */  
  120.     public void collectDeviceInfo(Context ctx) {  
  121.         try {  
  122.             PackageManager pm = ctx.getPackageManager();  
  123.             PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);  
  124.             if (pi != null) {  
  125.                 String versionName = pi.versionName == null ? "null" : pi.versionName;  
  126.                 String versionCode = pi.versionCode   "";  
  127.                 infos.put("versionName", versionName);  
  128.                 infos.put("versionCode", versionCode);  
  129.             }  
  130.         } catch (NameNotFoundException e) {  
  131.             Log.e(TAG, "an error occured when collect package info", e);  
  132.         }  
  133.         Field[] fields = Build.class.getDeclaredFields();  
  134.         for (Field field : fields) {  
  135.             try {  
  136.                 field.setAccessible(true);  
  137.                 infos.put(field.getName(), field.get(null).toString());  
  138.                 Log.d(TAG, field.getName()   " : "   field.get(null));  
  139.             } catch (Exception e) {  
  140.                 Log.e(TAG, "an error occured when collect crash info", e);  
  141.             }  
  142.         }  
  143.     }  
  144.   
  145.     /** 
  146.      * 保存错误信息到文件中 
  147.      *  
  148.      * @param ex 
  149.      * @return  返回文件名称,便于将文件传送到服务器 
  150.      */  
  151.     private String saveCrashInfo2File(Throwable ex) {  
  152.           
  153.         StringBuffer sb = new StringBuffer();  
  154.         for (Map.Entry<String, String> entry : infos.entrySet()) {  
  155.             String key = entry.getKey();  
  156.             String value = entry.getValue();  
  157.             sb.append(key   "="   value   "n");  
  158.         }  
  159.           
  160.         Writer writer = new StringWriter();  
  161.         PrintWriter printWriter = new PrintWriter(writer);  
  162.         ex.printStackTrace(printWriter);  
  163.         Throwable cause = ex.getCause();  
  164.         while (cause != null) {  
  165.             cause.printStackTrace(printWriter);  
  166.             cause = cause.getCause();  
  167.         }  
  168.         printWriter.close();  
  169.         String result = writer.toString();  
  170.         sb.append(result);  
  171.         try {  
  172.             long timestamp = System.currentTimeMillis();  
  173.             String time = formatter.format(new Date());  
  174.             String fileName = "crash-"   time   "-"   timestamp   ".log";  
  175.             if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {  
  176.                 String path = "/sdcard/crash/";  
  177.                 File dir = new File(path);  
  178.                 if (!dir.exists()) {  
  179.                     dir.mkdirs();  
  180.                 }  
  181.                 FileOutputStream fos = new FileOutputStream(path   fileName);  
  182.                 fos.write(sb.toString().getBytes());  
  183.                 fos.close();  
  184.             }  
  185.             return fileName;  
  186.         } catch (Exception e) {  
  187.             Log.e(TAG, "an error occured while writing file...", e);  
  188.         }  
  189.         return null;  
  190.     }  
  191. }  

    package com.scott.crash;

    import java.io.File; import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map;

    import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.Environment; import android.os.Looper; import android.util.Log; import android.widget.Toast;

    /**

    • UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告.
    • @author user
    • */ public class CrashHandler implements UncaughtExceptionHandler {

      public static final String TAG = "CrashHandler";

      //系统默认的UncaughtException处理类 private Thread.UncaughtExceptionHandler mDefaultHandler; //CrashHandler实例 private static CrashHandler INSTANCE = new CrashHandler(); //程序的Context对象 private Context mContext; //用来存储设备信息和异常信息 private Map infos = new HashMap();

      //用于格式化日期,作为日志文件名的一部分 private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");

      /* 保证只有一个CrashHandler实例 / private CrashHandler() { }

      /* 获取CrashHandler实例 ,单例模式 / public static CrashHandler getInstance() {

       return INSTANCE;
      

      }

      /**

      • 初始化
      • @param context */ public void init(Context context) { mContext = context; //获取系统默认的UncaughtException处理器 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); //设置该CrashHandler为程序的默认处理器 Thread.setDefaultUncaughtExceptionHandler(this); }

        /**

      • 当UncaughtException发生时会转入该函数来处理 */ @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) {

         //如果用户没有处理则让系统默认的异常处理器来处理
         mDefaultHandler.uncaughtException(thread, ex);
        

        } else {

         try {
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             Log.e(TAG, "error : ", e);
         }
         //退出程序
         android.os.Process.killProcess(android.os.Process.myPid());
         System.exit(1);
        

        } }

        /**

      • 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
      • @param ex
      • @return true:如果处理了该异常信息;否则返回false. */ private boolean handleException(Throwable ex) { if (ex == null) {

         return false;
        

        } //使用Toast来显示异常信息 new Thread() {

         @Override
         public void run() {
             Looper.prepare();
             Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();
             Looper.loop();
         }
        

        }.start(); //收集设备参数信息 collectDeviceInfo(mContext); //保存日志文件 saveCrashInfo2File(ex); return true; }

        /**

      • 收集设备参数信息
      • @param ctx */ public void collectDeviceInfo(Context ctx) { try {

         PackageManager pm = ctx.getPackageManager();
         PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
         if (pi != null) {
             String versionName = pi.versionName == null ? "null" : pi.versionName;
             String versionCode = pi.versionCode   "";
             infos.put("versionName", versionName);
             infos.put("versionCode", versionCode);
         }
        

        } catch (NameNotFoundException e) {

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

        } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) {

         try {
             field.setAccessible(true);
             infos.put(field.getName(), field.get(null).toString());
             Log.d(TAG, field.getName()   " : "   field.get(null));
         } catch (Exception e) {
             Log.e(TAG, "an error occured when collect crash info", e);
         }
        

        } }

        /**

      • 保存错误信息到文件中
      • @param ex
      • @return 返回文件名称,便于将文件传送到服务器 */ private String saveCrashInfo2File(Throwable ex) {

        StringBuffer sb = new StringBuffer(); for (Map.Entry entry : infos.entrySet()) {

         String key = entry.getKey();
         String value = entry.getValue();
         sb.append(key   "="   value   "n");
        

        }

        Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) {

         cause.printStackTrace(printWriter);
         cause = cause.getCause();
        

        } printWriter.close(); String result = writer.toString(); sb.append(result); try {

         long timestamp = System.currentTimeMillis();
         String time = formatter.format(new Date());
         String fileName = "crash-"   time   "-"   timestamp   ".log";
         if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
             String path = "/sdcard/crash/";
             File dir = new File(path);
             if (!dir.exists()) {
                 dir.mkdirs();
             }
             FileOutputStream fos = new FileOutputStream(path   fileName);
             fos.write(sb.toString().getBytes());
             fos.close();
         }
         return fileName;
        

        } catch (Exception e) {

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

        } return null; } }

启动 logcat

[adb] logcat [<option>] ... [<filter-spec>] ...

所有的收集动作在后台完成,对性能没有明显影响,收集到的用户数据,会在用户连接到wifi时上传。

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

过滤日志输出

Log 类允许您在logcat 工具中显示的代码中创建日志条目。常用的日志记录方法包括:

  • Log.v(String, String)(详细)
  • Log.d(String, String)(调试)
  • Log.i(String, String)(信息)
  • Log.w(String, String)(警告)
  • Log.e(String, String)(错误)

在web后台可以对这些数据进行筛选,以达到精细分析的目的,比如:

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

Logcat 个人建议

抓取Log之前请先清除缓存中的Log信息,防止干扰分析问题。

  • 清除缓存Log信息命令如下:
adb logcat -c
  • 复现问题,抓取log方法如下
adb logcat > 追加到指定文件中

Logcat抓取Log个人建议

感谢您的阅读,谢谢!

​欢迎关注微信公众号:程序员Android
公众号ID:ProgramAndroid
获取更多信息

微信公众号:ProgramAndroid

我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
我们不发送红包,我们只是红包的搬运工。

点击阅读原文,获取更多福利

  • Crash session:可以看到每一个crash的session, 以往单凭log无法重现没有头绪的crash, 现在有了重现步骤了。
  • 新用户session: 新用户用你的app, 他的使用是否符合你的期望?
  • 快速退出session: 为啥总有这么多人,进来没一会就走了?是哪里出问题了?
  • 忠诚用户session:忠诚用户到底喜欢我们什么?
  • 长session: 使用两个小时的用户,你到底在干嘛?

[java] view plaincopyprint?

是指对每个app界面进行单独统计分析,包括:

  1. package com.scott.crash;  
  2.   
  3. import android.app.Application;  
  4.   
  5. public class CrashApplication extends Application {  
  6.     @Override  
  7.     public void onCreate() {  
  8.         super.onCreate();  
  9.         CrashHandler crashHandler = CrashHandler.getInstance();  
  10.         crashHandler.init(getApplicationContext());  
  11.     }  
  12. }  

    package com.scott.crash;

    import android.app.Application;

    public class CrashApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        CrashHandler crashHandler = CrashHandler.getInstance();
        crashHandler.init(getApplicationContext());
    }
    

    }

  • 点击热图:到底喜欢点哪里呢?
  • 平均停留时长: 用户是有多喜欢在这呆?
  • 交互占比: 用户在这个界面的操作是有多频繁?
  • 从哪里来: 用户都是从哪些界面到这里来的,分别占比多少?
  • 到哪里去: 用户从这个界面去了哪里,分别占比多少?
  • 退出率:用户从这个界面退出app的占比是多少?还能改进吗?

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

A: 这得看你的节操,用户可能没法接受你这种做法,即便你的本意只是为了更好的改进产品。因此建议明确告知用户你们会采样收集数据,并且确保不会定位到具体个人,很多用户还是愿意帮你的。

[html] view plaincopyprint?

A: 好在appsee已经帮我们考虑了这些,默认情况下,所有带有键盘操作的部分都会被抹去。

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

A: 根据我们的实际使用测试,发现appsee确实如它自己声称的那样,对性能几乎没有影响。但一旦开启此功能,还是会有些耗电的,因为要录像,当然你也可以选择只收集操作记录,不录像。

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

A: 默认采集10%的用户数据,可以调到100%,当然你也可以选择只收集crash用户的、只收集三星手机的、只收集新用户的、或其他任意定制的。

[html] view plaincopyprint?

A: 哈哈,免费试用15天,15天之后要想再用,得交钱,官网上没写价钱,好在我已经帮各位问到了。

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

图片 8Appsee价格

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

觉得贵?试用一下又不要钱,赶紧来试试吧!提醒各位它会认你的包名的,别指望换个新账号又能接着用,时间一到不交钱所有数据都没法访问的,别问我为什么知道这么多- -!

图片 9

值得一提的是AppSee最近被Twitter引入了著名的Fabric工具集,有大厂给它背书,各位应该可以放心使用了吧。

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

图片 10扫码关注我

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

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

[java] view plaincopyprint?

  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  

    CPU_ABI=armeabi CPU_ABI2=unknown ID=FRF91 MANUFACTURER=unknown BRAND=generic TYPE=eng ...... Caused by: java.lang.NullPointerException

    at com.scott.crash.MainActivity.onCreate(MainActivity.java:13)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2627)
    ... 11 more
    

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

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

[java] view plaincopyprint?

  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.     }  

    /**

     * 网络是否可用
     * 
     * @param context
     * @return
     */
    public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager mgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo[] info = mgr.getAllNetworkInfo();
        if (info != null) {
            for (int i = 0; i < info.length; i  ) {
                if (info[i].getState() == NetworkInfo.State.CONNECTED) {
                    return true;
                }
            }
        }
        return false;
    }
    

 

TAG标签:
版权声明:本文由美高梅网投平台发布于新闻中心,转载请注明出处:Android中处理崩溃异常,为什么叫它神器