我们写程序的时候都希望能写出一个没有任何Bug的程序,期望在任何情况下都不会发生程序崩溃。但没有一个程序员能保证自己写的程序绝对不会出现异常崩溃。特别是当你用户数达到一定数量级后,你也更容易发现应用不同情况下的崩溃。
对于还没发布的应用程序,我们可以通过测试、分析Log的方法来收集崩溃信息。但对已经发布的程序,我们不可能让用户去查看崩溃信息然后再反馈给开发者。所以,设计一个对于小白用户都可以轻松实现反馈的应用就显得很重要了。我这里结合我自己写的一个Demo,来分析从崩溃开始到崩溃信息反馈到我们服务器,我们程序都需要做什么。
当我们的程序因未捕获的异常而突然终止时,系统会调用处理程序的接口 UncaughtExceptionHandler 。如果我们想处理未被程序正常捕获的异常,只需实现这个接口里的uncaughtException方法,uncaughtException方法回传了Thread 和 Throwable两个参数。通过这两个参数,我们来对异常进行我们需要的处理。
综上,我对异常处理方式的思路是这样的:
1.我们需要首先收集产生崩溃的手机信息,因为Android的样机种类繁多,很可能某些特定机型下会产生莫名的bug。2.将手机的信息和崩溃信息写入文件系统中。这样方便后续处理。
3.崩溃的应用需要可以自动重启。重启的页面设置成反馈页面,询问 用户是否需要上传崩溃报告。
4.用户同意后,即将2中写入的崩溃信息文件发送到自己的服务器。
通过上面的步骤,我们就可以写出大概的伪代码:
[Java] 纯文本查看 复制代码
|
1
2
3
4
5
|
handleException() {
collectDeviceInfo(context);
//手机手机信息
writeCrashInfoToFile(ex);
//写入崩溃文件
restart();
//应用重启
}
|
最后,在重启页面通过AsyncTask将崩溃信息上传服务器。
有了以上思路,我们一步一步的写出每个伪函数的具体代码。
1.收集手机的信息:[Java] 纯文本查看 复制代码
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
/**
*
* @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);
infos.put(
"crashTime"
, formatter.format(
new
Date()));
}
}
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);
}
}
}
|
[Java] 纯文本查看 复制代码
|
01
02
03
04
05
06
07
08
09
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
|
/**
*
* @param ex
* 将崩溃写入文件系统
*/
private
void
writeCrashInfoToFile(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);
//这里把刚才异常堆栈信息写入SD卡的Log日志里面
if
(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
{
String sdcardPath = Environment.getExternalStorageDirectory().getPath();
String filePath = sdcardPath +
"/cym/crash/"
;
localFileUrl = writeLog(sb.toString(), filePath);
}
}
/**
*
* @param log
* @param name
* [url=home.php?mod=space&uid=309376]@return[/url] 返回写入的文件路径
* 写入Log信息的方法,写入到SD卡里面
*/
private
String writeLog(String log, String name)
{
CharSequence timestamp =
new
Date().toString().replace(
" "
,
""
);
timestamp =
"crash"
;
String filename = name + timestamp +
".log"
;
File file =
new
File(filename);
if
(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try
{
Log.d(
"TAG"
,
"写入到SD卡里面"
);
// FileOutputStream stream = new FileOutputStream(new File(filename));
// OutputStreamWriter output = new OutputStreamWriter(stream);
file.createNewFile();
FileWriter fw=
new
FileWriter(file,
true
);
BufferedWriter bw =
new
BufferedWriter(fw);
//写入相关Log到文件
bw.write(log);
bw.newLine();
bw.close();
fw.close();
return
filename;
}
catch
(IOException e)
{
Log.e(TAG,
"an error occured while writing file..."
, e);
e.printStackTrace();
return
null
;
}
}
|
[Java] 纯文本查看 复制代码
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
注:我尝试过好多种应用重启的方法,最终选择采用PendingIntent的方式。
private
void
restart(){
try
{
Thread.sleep(
2000
);
}
catch
(InterruptedException e){
Log.e(TAG,
"error : "
, e);
}
Intent intent =
new
Intent(context.getApplicationContext(), SendCrashActivity.
class
);
PendingIntent restartIntent = PendingIntent.getActivity(
context.getApplicationContext(),
0
, intent,
Intent.FLAG_ACTIVITY_NEW_TASK);
//退出程序
AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() +
1000
,
restartIntent);
// 1秒钟后重启应用
}
|
应用重启后来到的是SendCrashActivity界面,在这里我设置了一个简单的按钮,点击后即可上传崩溃信息。代码比较多,这里列一个比较有用的上传方法吧:
[Java] 纯文本查看 复制代码
|
01
02
03
04
05
06
07
08
09
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
|
public
static
String uploadFile(File file,String requestUrl){
String result =
null
;
String BOUNDARY = UUID.randomUUID().toString();
//边界标识 随机生成
String PREFIX =
"--"
;
String LINE_END =
"\r\n"
;
String CONTENT_TYPE =
"multipart/form-data"
;
//内容类型
try
{
URL url =
new
URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(TIME_OUT);
conn.setConnectTimeout(TIME_OUT);
conn.setDoInput(
true
);
//允许输入流
conn.setDoOutput(
true
);
//允许输出流
conn.setUseCaches(
false
);
//不允许使用缓存
conn.setRequestMethod(
"POST"
);
//请求方式
conn.setRequestProperty(
"Charset"
, CHARSET);
//设置编码
conn.setRequestProperty(
"connection"
,
"keep-alive"
);
conn.setRequestProperty(
"Content-Type"
, CONTENT_TYPE +
";boundary="
+ BOUNDARY);
if
(file!=
null
)
{
/**
* 当文件不为空,把文件包装并且上传
*/
DataOutputStream dos =
new
DataOutputStream(conn.getOutputStream());
StringBuffer sb =
new
StringBuffer();
sb.append(PREFIX);
sb.append(BOUNDARY);
sb.append(LINE_END);
/**
* 这里重点注意:
* name里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/
sb.append(
"Content-Disposition: form-data; name=\"uploadcrash\"; filename=\""
+file.getName()+
"\""
+LINE_END);
sb.append(
"Content-Type: application/octet-stream; charset="
+CHARSET+LINE_END);
sb.append(LINE_END);
dos.write(sb.toString().getBytes());
InputStream is =
new
FileInputStream(file);
byte
[] bytes =
new
byte
[
1024
];
int
len =
0
;
while
((len=is.read(bytes))!=-
1
)
{
dos.write(bytes,
0
, len);
}
is.close();
dos.write(LINE_END.getBytes());
byte
[] end_data = (PREFIX+BOUNDARY+PREFIX+LINE_END).getBytes();
dos.write(end_data);
dos.flush();
/**
* 获取响应码 200=成功
* 当响应成功,获取响应的流
*/
int
res = conn.getResponseCode();
Log.e(TAG,
"response code:"
+res);
// if(res==200)
// {
Log.e(TAG,
"request success"
);
InputStream input = conn.getInputStream();
StringBuffer sb1=
new
StringBuffer();
int
ss ;
while
((ss=input.read())!=-
1
)
{
sb1.append((
char
)ss);
}
result = sb1.toString();
Log.e(TAG,
"result : "
+ result);
// }
// else{
// Log.e(TAG, "request error");
// }
}
}
catch
(MalformedURLException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
return
result;
}
|
整个流程基本走完,我们来看一下最终效果。(MainActivity点击按钮后执行了一个2/0的操作,所以崩溃)
我将崩溃上传到了我的sae服务器的storage里。下图中红色圈起来的文件即是我们上传的崩溃文件。
我把这个文件下载下来,内容如下:
[AppleScript] 纯文本查看 复制代码
|
01
02
03
04
05
06
07
08
09
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
|
TIME
=
1383016889000
FINGERPRINT
=
generic
/
sdk
/
generic
:
4.4
/
KRT
16
L
/
892118
:
eng
/
test
-
keys
HARDWARE
=
goldfish
UNKNOWN
=
unknown
RADIO
=
unknown
BOARD
=
unknown
versionCode
=
1
PRODUCT
=
sdk
versionName
=
1.0
DISPLAY
=
sdk
-
eng
4.4
KRT
16
L
892118
test
-
keys
USER
=
android
-
build
HOST
=
vpak
27.
mtv.corp.google.com
DEVICE
=
generic
TAGS
=
test
-
keys
MODEL
=
sdk
BOOTLOADER
=
unknown
crashTime
=
2014
-09
-24
05
:
39
:
21
CPU_ABI
=
armeabi
-
v
7
a
CPU_ABI
2
=
armeabi
IS_DEBUGGABLE
=
true
ID
=
KRT
16
L
SERIAL
=
unknown
MANUFACTURER
=
unknown
BRAND
=
generic
TYPE
=
eng
java.lang.IllegalStateException
:
Could
not
execute method
of
the
activity
at
android.
view
.View$
1.
onClick
(
View.java
:
3814
)
at
android.
view
.View.performClick
(
View.java
:
4424
)
at
android.
view
.View$PerformClick.
run
(
View.java
:
18383
)
at
android.os.Handler.handleCallback
(
Handler.java
:
733
)
at
android.os.Handler.dispatchMessage
(
Handler.java
:
95
)
at
android.os.Looper.loop
(
Looper.java
:
137
)
at
android.app.ActivityThread.
main
(
ActivityThread.java
:
4998
)
at
java.lang.reflect.Method.invokeNative
(
Native Method
)
at
java.lang.reflect.Method.invoke
(
Method.java
:
515
)
at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.
run
(
ZygoteInit.java
:
777
)
at
com.android.internal.os.ZygoteInit.
main
(
ZygoteInit.java
:
593
)
at
dalvik.system.NativeStart.
main
(
Native Method
)
Caused
by
:
java.lang.reflect.InvocationTargetException
at
java.lang.reflect.Method.invokeNative
(
Native Method
)
at
java.lang.reflect.Method.invoke
(
Method.java
:
515
)
at
android.
view
.View$
1.
onClick
(
View.java
:
3809
)
...
11
more
Caused
by
:
java.lang.ArithmeticException
:
divide
by
zero
at
so.cym.crashhandlerdemo.MainActivity.generateAnr
(
MainActivity.java
:
20
)
...
14
more
java.lang.reflect.InvocationTargetException
at
java.lang.reflect.Method.invokeNative
(
Native Method
)
at
java.lang.reflect.Method.invoke
(
Method.java
:
515
)
at
android.
view
.View$
1.
onClick
(
View.java
:
3809
)
at
android.
view
.View.performClick
(
View.java
:
4424
)
at
android.
view
.View$PerformClick.
run
(
View.java
:
18383
)
at
android.os.Handler.handleCallback
(
Handler.java
:
733
)
at
android.os.Handler.dispatchMessage
(
Handler.java
:
95
)
at
android.os.Looper.loop
(
Looper.java
:
137
)
at
android.app.ActivityThread.
main
(
ActivityThread.java
:
4998
)
at
java.lang.reflect.Method.invokeNative
(
Native Method
)
at
java.lang.reflect.Method.invoke
(
Method.java
:
515
)
at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.
run
(
ZygoteInit.java
:
777
)
at
com.android.internal.os.ZygoteInit.
main
(
ZygoteInit.java
:
593
)
at
dalvik.system.NativeStart.
main
(
Native Method
)
Caused
by
:
java.lang.ArithmeticException
:
divide
by
zero
at
so.cym.crashhandlerdemo.MainActivity.generateAnr
(
MainActivity.java
:
20
)
...
14
more
java.lang.ArithmeticException
:
divide
by
zero
at
so.cym.crashhandlerdemo.MainActivity.generateAnr
(
MainActivity.java
:
20
)
at
java.lang.reflect.Method.invokeNative
(
Native Method
)
at
java.lang.reflect.Method.invoke
(
Method.java
:
515
)
at
android.
view
.View$
1.
onClick
(
View.java
:
3809
)
at
android.
view
.View.performClick
(
View.java
:
4424
)
at
android.
view
.View$PerformClick.
run
(
View.java
:
18383
)
at
android.os.Handler.handleCallback
(
Handler.java
:
733
)
at
android.os.Handler.dispatchMessage
(
Handler.java
:
95
)
at
android.os.Looper.loop
(
Looper.java
:
137
)
at
android.app.ActivityThread.
main
(
ActivityThread.java
:
4998
)
at
java.lang.reflect.Method.invokeNative
(
Native Method
)
at
java.lang.reflect.Method.invoke
(
Method.java
:
515
)
at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.
run
(
ZygoteInit.java
:
777
)
at
com.android.internal.os.ZygoteInit.
main
(
ZygoteInit.java
:
593
)
at
dalvik.system.NativeStart.
main
(
Native Method
)
|
通过上面的文件,我们就可以分析什么时候产生崩溃,什么机型下会产生崩溃。
Android里有一种崩溃(严格意义将不叫崩溃)是捕获不到的,那就是ANR,关于ANR的相关知识可以阅读我的另一篇博文 http://blog.saymagic.cn/2014/09/25/ANR%E5%AE%8C%E5%85%A8%E8%A7%A3%E6%9E%90.html
如果你对源码感兴趣,欢迎到此处进行star或者fork: https://gitcafe.com/saymagic/AndroidCrashHandler 。
原地址:http://blog.saymagic.cn/2014/09/25/Android%E5%B4%A9%E6%BA%83%E5%AE%8C%E5%85%A8%E8%A7%A3%E6%9E%90.html?utm_source=tuicool
转载于:https://my.oschina.net/Gxhpro/blog/424121