您的位置:首页 > 移动开发 > Android开发

Android崩溃处理

2015-06-03 00:00 399 查看
我们写程序的时候都希望能写出一个没有任何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
/**


*


*@paramctx


*手机设备相关信息


*/


public
void
collectDeviceInfo(Contextctx){


try
{


PackageManagerpm=ctx.getPackageManager();


PackageInfopi=pm.getPackageInfo(ctx.getPackageName(),


PackageManager.GET_ACTIVITIES);


if
(pi!=
null
){


StringversionName=pi.versionName==
null
?
"null"


:pi.versionName;


StringversionCode=pi.versionCode+
""
;


infos.put(
"versionName"
,versionName);


infos.put(
"versionCode"
,versionCode);


infos.put(
"crashTime"
,formatter.format(
new
Date()));


}


}
catch
(NameNotFoundExceptione){


Log.e(TAG,
"anerroroccuredwhencollectpackageinfo"
,e);


}


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


for
(Fieldfield:fields){


try
{


field.setAccessible(
true
);


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


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


}
catch
(Exceptione){


Log.e(TAG,
"anerroroccuredwhencollectcrashinfo"
,e);


}


}


}
2.崩溃和手机信息写入文件:
[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
/**


*


*@paramex


*将崩溃写入文件系统


*/


private
void
writeCrashInfoToFile(Throwableex){


StringBuffersb=
new
StringBuffer();


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


Stringkey=entry.getKey();


Stringvalue=entry.getValue();


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


}


Writerwriter=
new
StringWriter();


PrintWriterprintWriter=
new
PrintWriter(writer);


ex.printStackTrace(printWriter);


Throwablecause=ex.getCause();


while
(cause!=
null
){


cause.printStackTrace(printWriter);


cause=cause.getCause();


}


printWriter.close();


Stringresult=writer.toString();


sb.append(result);


//这里把刚才异常堆栈信息写入SD卡的Log日志里面


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


{


StringsdcardPath=Environment.getExternalStorageDirectory().getPath();


StringfilePath=sdcardPath+
"/cym/crash/"
;


localFileUrl=writeLog(sb.toString(),filePath);


}


}


/**


*


*@paramlog


*@paramname


*@return返回写入的文件路径


*写入Log信息的方法,写入到SD卡里面


*/


private
StringwriteLog(Stringlog,Stringname)


{


CharSequencetimestamp=
new
Date().toString().replace(
""
,
""
);


timestamp=
"crash"
;


Stringfilename=name+timestamp+
".log"
;


Filefile=
new
File(filename);


if
(!file.getParentFile().exists()){


file.getParentFile().mkdirs();


}


try


{


Log.d(
"TAG"
,
"写入到SD卡里面"
);


//FileOutputStreamstream=newFileOutputStream(newFile(filename));


//OutputStreamWriteroutput=newOutputStreamWriter(stream);


file.createNewFile();


FileWriterfw=
new
FileWriter(file,
true
);


BufferedWriterbw=
new
BufferedWriter(fw);


//写入相关Log到文件


bw.write(log);


bw.newLine();


bw.close();


fw.close();


return
filename;


}


catch
(IOExceptione)


{


Log.e(TAG,
"anerroroccuredwhilewritingfile..."
,e);


e.printStackTrace();


return
null
;


}


}
3.重启应用:
[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
(InterruptedExceptione){


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


}


Intentintent=
new
Intent(context.getApplicationContext(),SendCrashActivity.
class
);


PendingIntentrestartIntent=PendingIntent.getActivity(


context.getApplicationContext(),
0
,intent,


Intent.FLAG_ACTIVITY_NEW_TASK);


//退出程序


AlarmManagermgr=(AlarmManager)context.getSystemService(Context.ALARM_SERVICE);


mgr.set(AlarmManager.RTC,System.currentTimeMillis()+
1000
,


restartIntent);
//1秒钟后重启应用


}
4.上传崩溃
应用重启后来到的是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
StringuploadFile(Filefile,StringrequestUrl){


Stringresult=
null
;


StringBOUNDARY=UUID.randomUUID().toString();
//边界标识随机生成


StringPREFIX=
"--"
;


StringLINE_END=
"\r\n"
;


StringCONTENT_TYPE=
"multipart/form-data"
;
//内容类型


try
{


URLurl=
new
URL(requestUrl);


HttpURLConnectionconn=(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
)


{


/**


*当文件不为空,把文件包装并且上传


*/


DataOutputStreamdos=
new
DataOutputStream(conn.getOutputStream());


StringBuffersb=
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());


InputStreamis=
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,
"responsecode:"
+res);


//if(res==200)


//{


Log.e(TAG,
"requestsuccess"
);


InputStreaminput=conn.getInputStream();


StringBuffersb1=
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,"requesterror");


//}


}


}
catch
(MalformedURLExceptione){


e.printStackTrace();


}
catch
(IOExceptione){


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
executemethod
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
(
NativeMethod
)


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
(
NativeMethod
)

Caused
by
:
java.lang.reflect.InvocationTargetException


at
java.lang.reflect.Method.invokeNative
(
NativeMethod
)


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
(
NativeMethod
)


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
(
NativeMethod
)


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
(
NativeMethod
)

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
(
NativeMethod
)


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
(
NativeMethod
)


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
(
NativeMethod
)
总结
通过上面的文件,我们就可以分析什么时候产生崩溃,什么机型下会产生崩溃。
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

举报编辑


(1)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: