你們都知道,如今安裝Android系統的手機版本和設備千差萬別,在模擬器上運行良好的程序安裝到某款手機上說不定就出現崩潰的現象,開發者我的不可能購買全部設備逐個調試,因此在程序發佈出去以後,若是出現了崩潰現象,開發者應該及時獲取在該設備上致使崩潰的信息,這對於下一個版本的bug修復幫助極大,因此今天就來介紹一下如何在程序崩潰的狀況下收集相關的設備參數信息和具體的異常信息,併發送這些信息到服務器供開發者分析和調試程序。html
咱們先創建一個crash項目,項目結構如圖:java

在MainActivity.java代碼中,代碼是這樣寫的:
[java] view plaincopyandroid
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"));
}
}
|
咱們在這裏故意製造了一個潛在的運行期異常,當咱們運行程序時就會出現如下界面:web

遇到軟件沒有捕獲的異常以後,系統會彈出這個默認的強制關閉對話框。服務器
咱們固然不但願用戶看到這種現象,簡直是對用戶心靈上的打擊,並且對咱們的bug的修復也是毫無幫助的。咱們須要的是軟件有一個全局的異常捕獲器,當出現一個咱們沒有發現的異常時,捕獲這個異常,而且將異常信息記錄下來,上傳到服務器公開發這分析出現異常的具體緣由。網絡
接下來咱們就來實現這一機制,不過首先咱們仍是來了解如下兩個類:android.app.Application和java.lang.Thread.UncaughtExceptionHandler。併發
Application:用來管理應用程序的全局狀態。在應用程序啓動時Application會首先建立,而後纔會根據狀況(Intent)來啓動相應的Activity和Service。本示例中將在自定義增強版的Application中註冊未捕獲異常處理器。app
Thread.UncaughtExceptionHandler:線程未捕獲異常處理器,用來處理未捕獲異常。若是程序出現了未捕獲異常,默認會彈出系統中強制關閉對話框。咱們須要實現此接口,並註冊爲程序中默認未捕獲異常處理。這樣當未捕獲異常發生時,就能夠作一些個性化的異常處理操做。編輯器
你們剛纔在項目的結構圖中看到的CrashHandler.java實現了Thread.UncaughtExceptionHandler,使咱們用來處理未捕獲異常的主要成員,代碼以下:
[java] view plaincopyide
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代碼以下:
[java] view plaincopy
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:
[html] view plaincopy
|
<application android:name=".CrashApplication" ...>
</application>
|
由於咱們上面的CrashHandler中,遇到異常後要保存設備參數和具體異常信息到SDCARD,因此咱們須要在AndroidManifest.xml中加入讀寫SDCARD權限:
[html] view plaincopy
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
搞定了上邊的步驟以後,咱們來運行一下這個項目:

看以看到,並不會有強制關閉的對話框出現了,取而代之的是咱們比較有好的提示信息。
而後看一下SDCARD生成的文件:

用文本編輯器打開日誌文件,看一段日誌信息:
[java] view plaincopy
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服務以前,須要肯定網絡暢通,咱們能夠使用下面的方式判斷網絡是否可用:
[java] view plaincopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
|
轉載自:http://my.eoe.cn/817027/archive/17997.html