我们在开发应用的过程中不可避免的会遇到各种Crash,今天分享一下如何姿势正确的处理这些Crash来提高我们的开发效率。

对于应用的Crash处理分为测试环境和生产环境。

1、测试环境
在开发过程中为了方便快速定位崩溃所发生的代码,要求我们能在崩溃的同时将日志打印出来,最好以直观的界面显示。这里推荐大家一个开源项目:CustomActivityOnCrash
github地址:https://github.com/Ereza/CustomActivityOnCrash

2、生产环境
当应用发布上线就不能给用户显示这样的界面了所以要用一种用户比较能接受的方式处理。
在Application类中配置如下:

 /**
     * 初始化程序崩溃捕捉处理
     */
    protected void initCrashHandler() {
        if (BuildConfig.isDebug) {
            CustomActivityOnCrash.install(this);
        } else {
            CrashHandler handler = CrashHandler.getInstance();
            handler.init(getApplicationContext());
            Thread.setDefaultUncaughtExceptionHandler(handler);
        }
    }

CrashHandler类文件:

 public class CrashHandler implements UncaughtExceptionHandler {
    /** Debug Log tag*/
    public static final String TAG = "CrashHandler";
    /** 是否开启日志输出,在Debug状态下开启,
     * 在Release状态下关闭以提示程序性能
     * */
    public static final boolean DEBUG = false;
    /** 系统默认的UncaughtException处理类 */
    private UncaughtExceptionHandler mDefaultHandler;
    /** CrashHandler实例 */
    private static CrashHandler INSTANCE;
    /** 程序的Context对象 */
    private Context mContext;
    /** 使用Properties来保存设备的信息和错误堆栈信息*/
    private Properties mDeviceCrashInfo = new Properties();
    private static final String VERSION_NAME = "versionName";
    private static final String VERSION_CODE = "versionCode";
    private static final String STACK_TRACE = "STACK_TRACE";
    /** 错误报告文件的扩展名 */
    private static final String CRASH_REPORTER_EXTENSION = ".cr";

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

    /** 获取CrashHandler实例 ,单例模式*/
    public static CrashHandler getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new CrashHandler();
        }
        return INSTANCE;
    }

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

    /**
     * 当UncaughtException发生时会转入该函数来处理
     */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            //如果用户没有处理则让系统默认的异常处理器来处理
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            //Sleep一会后结束程序
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Log.e(TAG, "Error : ", e);
            }
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(10);
        }
    }

    /**
     * 自定义错误处理,收集错误信息
     * 发送错误报告等操作均在此完成.
     * 开发者可以根据自己的情况来自定义异常处理逻辑
     * @param ex
     * @return true:如果处理了该异常信息;否则返回false
     */
    private boolean handleException(Throwable ex) {
        if (ex == null) {
            Log.w(TAG, "handleException --- ex==null");
            return true;
        }
        final String msg = ex.getLocalizedMessage();
        if(msg == null) {
            return false;
        }
        //使用Toast来显示异常信息
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                Toast toast = Toast.makeText(mContext, "程序出错,即将退出",
                        Toast.LENGTH_LONG);
                toast.setGravity(Gravity.CENTER, 0, 0);
                toast.show();
//              MsgPrompt.showMsg(mContext, "程序出错啦", msg+"\n点确认退出");
                Looper.loop();
            }
        }.start();
        //收集设备信息
        collectCrashDeviceInfo(mContext);
        //保存错误报告文件
        saveCrashInfoToFile(ex);
        //发送错误报告到服务器
        //sendCrashReportsToServer(mContext);
        return true;
    }

    /**
     * 在程序启动时候, 可以调用该函数来发送以前没有发送的报告
     */
    public void sendPreviousReportsToServer() {
        sendCrashReportsToServer(mContext);
    }
    /**
     * 把错误报告发送给服务器,包含新产生的和以前没发送的.
     * @param ctx
     */
    private void sendCrashReportsToServer(Context ctx) {
        String[] crFiles = getCrashReportFiles(ctx);
        if (crFiles != null && crFiles.length > 0) {
            TreeSet<String> sortedFiles = new TreeSet<String>();
            sortedFiles.addAll(Arrays.asList(crFiles));
            for (String fileName : sortedFiles) {
                File cr = new File(ctx.getFilesDir(), fileName);
                postReport(cr);
                cr.delete();// 删除已发送的报告
            }
        }
    }
    private void postReport(File file) {
        // TODO 发送错误报告到服务器
    }

    /**
     * 获取错误报告文件名
     * @param ctx
     * @return
     */
    private String[] getCrashReportFiles(Context ctx) {
        File filesDir = ctx.getFilesDir();
        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(CRASH_REPORTER_EXTENSION);
            }
        };
        return filesDir.list(filter);
    }

    /**
     * 保存错误信息到文件中
     * @param ex
     * @return
     */
    private String 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("EXEPTION", ex.getLocalizedMessage());
        mDeviceCrashInfo.put(STACK_TRACE, result);
        try {
            //long timestamp = System.currentTimeMillis();
            Time t = new Time("GMT+8");
            t.setToNow(); // 取得系统时间
            int date = t.year * 10000 + t.month * 100 + t.monthDay;
            int time = t.hour * 10000 + t.minute * 100 + t.second;
            String fileName = "crash-" + date + "-" + time + CRASH_REPORTER_EXTENSION;
            FileOutputStream trace = mContext.openFileOutput(fileName,
                    Context.MODE_PRIVATE);
            mDeviceCrashInfo.store(trace, "");
            trace.flush();
            trace.close();
            return fileName;
        } catch (Exception e) {
            Log.e(TAG, "an error occured while writing report file...", e);
        }
        return null;
    }

    /**
     * 收集程序崩溃的设备信息
     *
     * @param ctx
     */
    public void collectCrashDeviceInfo(Context ctx) {
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(),
                    PackageManager.GET_ACTIVITIES);
            if (pi != null) {
                mDeviceCrashInfo.put(VERSION_NAME,
                        pi.versionName == null ? "not set" : pi.versionName);
                mDeviceCrashInfo.put(VERSION_CODE, ""+pi.versionCode);
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Error while collect package info", e);
        }
        //使用反射来收集设备信息.在Build类中包含各种设备信息,
        //例如: 系统版本号,设备生产商 等帮助调试程序的有用信息
        //具体信息请参考后面的截图
        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                mDeviceCrashInfo.put(field.getName(), ""+field.get(null));
                if (DEBUG) {
                    Log.d(TAG, field.getName() + " : " + field.get(null));
                }
            } catch (Exception e) {
                Log.e(TAG, "Error while collect crash info", e);
            }
        }
    }
}


文/Logan62334(简书作者)
原文链接:http://www.jianshu.com/p/fc0f6e38e2f3
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。


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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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" )); 
  
}

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

遇到软件没有捕获的异常之后,系统会弹出这个默认的强制关闭对话框。
我们当然不希望用户看到这种现象,简直是对用户心灵上的打击,而且对我们的bug的修复也是毫无帮助的。我们需要的是软件有一个全局的异常捕获器,当出现一个我们没有发现的异常时,捕获这个异常,并且将异常信息记录下来,上传到服务器公开发这分析出现异常的具体原因。
接下来我们就来实现这一机制,不过首先我们还是来了解以下两个类:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。
Application:用来管理应用程序的全局状态。在应用程序启动时Application会首先创建,然后才会根据情况(Intent)来启动相应的Activity和Service。本示例中将在自定义加强版的Application中注册未捕获异常处理器。
Thread.UncaughtExceptionHandler:线程未捕获异常处理器,用来处理未捕获异常。如果程序出现了未捕获异常,默认会弹出系统中强制关闭对话框。我们需要实现此接口,并注册为程序中默认未捕获异常处理。这样当未捕获异常发生时,就可以做一些个性化的异常处理操作。
大家刚才在项目的结构图中看到的CrashHandler.java实现了Thread.UncaughtExceptionHandler,使我们用来处理未捕获异常的主要成员,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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<String, String> infos = new HashMap<String, String>(); 
   
   //用于格式化日期,作为日志文件名的一部分 
   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<String, String> 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
  
}

在收集异常信息时,朋友们也可以使用Properties,因为Properties有一个很便捷的方法properties.store(OutputStream out, String comments),用来将Properties实例中的键值对外输到输出流中,但是在使用的过程中发现生成的文件中异常信息打印在同一行,看起来极为费劲,所以换成Map来存放这些信息,然后生成文件时稍加了些操作。
完成这个CrashHandler后,我们需要在一个Application环境中让其运行,为此,我们继承android.app.Application,添加自己的代码,CrashApplication.java代码如下:

?
1
2
3
4
5
6
7
8
9
10
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()); 
  
}

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

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

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

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

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

看以看到,并不会有强制关闭的对话框出现了,取而代之的是我们比较有好的提示信息。
然后看一下SDCARD生成的文件:


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

?
1
2
3
4
5
6
7
8
9
10
11
12
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服务之前,需要确定网络畅通,我们可以使用下面的方式判断网络是否可用:   

?
1
2
3
4
5
6
7
8
9
10
11
12
13
/** <BR> * 网络是否可用 <BR>*/  
  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
   }

希望本文所述对大家学习Android软件编程有所帮助。



Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐