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

Android程序崩溃异常收集框架

2014-05-27 10:50 531 查看
最近在写Android程序崩溃异常处理,完成之后,稍加封装与大家分享。

我的思路是这样的,在程序崩溃之后,将异常信息保存到一个日志文件中,然后对该文件进行处理,比如发送到邮箱,或发送到服务器。

所以,第一步是先定义一个接口,用于在保存好日志之后的回调。代码如下:

/*
* @(#)CrashListener.java		       Project: crash
* Date:2014-5-27
*
* Copyright (c) 2014 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.githang.android.crash;

import java.io.File;

/**
* @author Geek_Soledad <a target="_blank" href=
*         "http://mail.qq.com/cgi-bin/qm_share?t=qm_mailme&email=XTAuOSVzPDM5LzI0OR0sLHM_MjA"
*         style="text-decoration:none;"><img src=
*         "http://rescdn.qqmail.com/zh_CN/htmledition/images/function/qm_open/ico_mailme_01.png"
*         /></a>
*/
public interface CrashListener {
/**
* 保存异常的日志。
*
* @param file
*/
public void afterSaveCrash(File file);
}


接下来是用于处理崩溃异常的类,它要实现UncaughtExceptionHandler接口。实现它之后,将它设为默认的线程异常的处理者,这样程序崩溃之后,就会调用它了。但是在调用它之前,还需要先获取保存之前默认的handler,用于在我们收集了异常之后对程序进行处理,比如默认的弹出“程序已停止运行”的对话框(当然你也可以自己实现一个),终止程序,打印LOG。

我的实现如下:

/*
* @(#)CrashHandler.java		       Project: crash
* Date:2014-5-26
*
* Copyright (c) 2014 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.githang.android.crash;

import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
* @author Geek_Soledad <a target="_blank" href=
*         "http://mail.qq.com/cgi-bin/qm_share?t=qm_mailme&email=XTAuOSVzPDM5LzI0OR0sLHM_MjA"
*         style="text-decoration:none;"><img src=
*         "http://rescdn.qqmail.com/zh_CN/htmledition/images/function/qm_open/ico_mailme_01.png"
*         /></a>
*/
public class CrashHandler implements UncaughtExceptionHandler {
private static final CrashHandler sHandler = new CrashHandler();
private static final UncaughtExceptionHandler sDefaultHandler = Thread
.getDefaultUncaughtExceptionHandler();
private static final ExecutorService THREAD_POOL = Executors.newSingleThreadExecutor();
private Future<?> future;
private CrashListener mListener;
private File mLogFile;

public static CrashHandler getInstance() {
return sHandler;
}

@Override
public void uncaughtException(Thread thread, Throwable ex) {
CrashLogUtil.writeLog(mLogFile, "CrashHandler", ex.getMessage(), ex);
future = THREAD_POOL.submit(new Runnable() {
public void run() {
if (mListener != null) {
mListener.afterSaveCrash(mLogFile);
}
};
});
if (!future.isDone()) {
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
sDefaultHandler.uncaughtException(thread, ex);
}

public void init(File logFile, CrashListener listener) {
mLogFile = logFile;
mListener = listener;
}

}


这个类很简单,就是在发生未能捕获的异常之后,保存LOG到文件,然后 调用前面定义的接口,对日志文件进行处理。其中CrashLogUtil是我实现的保存LOG到文件的类。代码如下:
/*
* @(#)LogUtil.java		       Project: crash
* Date:2014-5-27
*
* Copyright (c) 2014 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.githang.android.crash;

import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;

/**
* @author Geek_Soledad <a target="_blank" href=
*         "http://mail.qq.com/cgi-bin/qm_share?t=qm_mailme&email=XTAuOSVzPDM5LzI0OR0sLHM_MjA"
*         style="text-decoration:none;"><img src=
*         "http://rescdn.qqmail.com/zh_CN/htmledition/images/function/qm_open/ico_mailme_01.png"
*         /></a>
*/
public class CrashLogUtil {
private static final SimpleDateFormat timeFormat = new SimpleDateFormat("MM-dd HH:mm:ss.SSS",
Locale.getDefault());

/**
* 将日志写入文件。
*
* @param tag
* @param message
* @param tr
*/
public static synchronized void writeLog(File logFile, String tag, String message, Throwable tr) {
logFile.getParentFile().mkdirs();
if (!logFile.exists()) {
try {
logFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
String time = timeFormat.format(Calendar.getInstance().getTime());
synchronized (logFile) {
FileWriter fileWriter = null;
BufferedWriter bufdWriter = null;
PrintWriter printWriter = null;
try {
fileWriter = new FileWriter(logFile, true);
bufdWriter = new BufferedWriter(fileWriter);
printWriter = new PrintWriter(fileWriter);
bufdWriter.append(time).append(" ").append("E").append('/').append(tag).append(" ")
.append(message).append('\n');
bufdWriter.flush();
tr.printStackTrace(printWriter);
printWriter.flush();
fileWriter.flush();
} catch (IOException e) {
closeQuietly(fileWriter);
closeQuietly(bufdWriter);
closeQuietly(printWriter);
}
}
}

public static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ioe) {
// ignore
}
}
}
}


在日志保存之后,我们还需要生成一个报告,并发送给服务器。报告的方法,可以是发送到邮箱,或者http请求发送给服务器。所以这里写了一个抽象类,实现了生成标题和内容,设置日志路径等。代码如下:

/*
* @(#)AbstractReportHandler.java		       Project: crash
* Date:2014-5-27
*
* Copyright (c) 2014 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.githang.android.crash;

import java.io.File;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;

/**
* @author Geek_Soledad <a target="_blank" href=
*         "http://mail.qq.com/cgi-bin/qm_share?t=qm_mailme&email=XTAuOSVzPDM5LzI0OR0sLHM_MjA"
*         style="text-decoration:none;"><img src=
*         "http://rescdn.qqmail.com/zh_CN/htmledition/images/function/qm_open/ico_mailme_01.png"
*         /></a>
*/
public abstract class AbstractCrashReportHandler implements CrashListener {
private Context mContext;

public AbstractCrashReportHandler(Context context) {
mContext = context;
CrashHandler handler = CrashHandler.getInstance();
handler.init(getLogDir(context), this);
Thread.setDefaultUncaughtExceptionHandler(handler);
}

protected File getLogDir(Context context) {
return new File(context.getFilesDir(), "crash.log");
}

protected abstract void sendReport(String title, String body, File file);

@Override
public void afterSaveCrash(File file) {
sendReport(buildTitle(mContext), buildBody(mContext), file);
}

public String buildTitle(Context context) {
return "Crash Log: "
+ context.getPackageManager().getApplicationLabel(context.getApplicationInfo());
}

public String buildBody(Context context) {
StringBuilder sb = new StringBuilder();

sb.append("APPLICATION INFORMATION").append('\n');
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = context.getApplicationInfo();
sb.append("Application : ").append(pm.getApplicationLabel(ai)).append('\n');

try {
PackageInfo pi = pm.getPackageInfo(ai.packageName, 0);
sb.append("Version Code: ").append(pi.versionCode).append('\n');
sb.append("Version Name: ").append(pi.versionName).append('\n');
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}

sb.append('\n').append("DEVICE INFORMATION").append('\n');
sb.append("Board: ").append(Build.BOARD).append('\n');
sb.append("BOOTLOADER: ").append(Build.BOOTLOADER).append('\n');
sb.append("BRAND: ").append(Build.BRAND).append('\n');
sb.append("CPU_ABI: ").append(Build.CPU_ABI).append('\n');
sb.append("CPU_ABI2: ").append(Build.CPU_ABI2).append('\n');
sb.append("DEVICE: ").append(Build.DEVICE).append('\n');
sb.append("DISPLAY: ").append(Build.DISPLAY).append('\n');
sb.append("FINGERPRINT: ").append(Build.FINGERPRINT).append('\n');
sb.append("HARDWARE: ").append(Build.HARDWARE).append('\n');
sb.append("HOST: ").append(Build.HOST).append('\n');
sb.append("ID: ").append(Build.ID).append('\n');
sb.append("MANUFACTURER: ").append(Build.MANUFACTURER).append('\n');
sb.append("PRODUCT: ").append(Build.PRODUCT).append('\n');
sb.append("TAGS: ").append(Build.TAGS).append('\n');
sb.append("TYPE: ").append(Build.TYPE).append('\n');
sb.append("USER: ").append(Build.USER).append('\n');

return sb.toString();
}
}


这样一个框架就算基本完成了。

当然,下面我还给出了报告的一种实现,发送邮件。

如何发送邮箱,网上已有不少资料,这里不再简而言之。

首先需要用到三个jar包: activation.jar, additionnal.jar, mail.jar。

然后 写一个类,继承自Authenticator。代码如下:

/*
* @(#)Snippet.java		       Project: CrashHandler
* Date: 2014-5-27
*
* Copyright (c) 2014 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.githang.android.crash;

import android.util.Log;

import java.util.Date;
import java.util.Properties;

import javax.activation.CommandMap;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.activation.MailcapCommandMap;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

/**
* Author: msdx (645079761@qq.com) Time: 14-5-27 上午9:07
*/
public class LogMail extends Authenticator {
private String host;
private String port;
private String user;
private String pass;
private String from;
private String to;
private String subject;
private String body;

private Multipart multipart;
private Properties props;

public LogMail() {
}

public LogMail(String user, String pass, String from, String to, String host, String port,
String subject, String body) {
this.host = host;
this.port = port;
this.user = user;
this.pass = pass;
this.from = from;
this.to = to;
this.subject = subject;
this.body = body;
}

public LogMail setHost(String host) {
this.host = host;
return this;
}

public LogMail setPort(String port) {
this.port = port;
return this;
}

public LogMail setUser(String user) {
this.user = user;
return this;
}

public LogMail setPass(String pass) {
this.pass = pass;
return this;
}

public LogMail setFrom(String from) {
this.from = from;
return this;
}

public LogMail setTo(String to) {
this.to = to;
return this;
}

public LogMail setSubject(String subject) {
this.subject = subject;
return this;
}

public LogMail setBody(String body) {
this.body = body;
return this;
}

public void init() {
multipart = new MimeMultipart();
// There is something wrong with MailCap, javamail can not find a
// handler for the multipart/mixed part, so this bit needs to be added.
MailcapCommandMap mc = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
mc.addMailcap("text/html;; x-java-content-handler=com.sun.mail.handlers.text_html");
mc.addMailcap("text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml");
mc.addMailcap("text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain");
mc.addMailcap("multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed");
mc.addMailcap("message/rfc822;; x-java-content-handler=com.sun.mail.handlers.message_rfc822");
CommandMap.setDefaultCommandMap(mc);

props = new Properties();

props.put("mail.smtp.host", host);
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.port", port);
props.put("mail.smtp.socketFactory.port", port);
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.socketFactory.fallback", "false");
}

public boolean send() throws MessagingException {
if (!user.equals("") && !pass.equals("") && !to.equals("") && !from.equals("")) {
Session session = Session.getDefaultInstance(props, this);
Log.d("SendUtil", host + "..." + port + ".." + user + "..." + pass);

MimeMessage msg = new MimeMessage(session);

msg.setFrom(new InternetAddress(from));

InternetAddress addressTo = new InternetAddress(to);
msg.setRecipient(MimeMessage.RecipientType.TO, addressTo);

msg.setSubject(subject);
msg.setSentDate(new Date());

// setup message body
BodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setText(body);
multipart.addBodyPart(messageBodyPart);

// Put parts in message
msg.setContent(multipart);

// send email
Transport.send(msg);

return true;
} else {
return false;
}
}

public void addAttachment(String filePath, String fileName) throws Exception {
BodyPart messageBodyPart = new MimeBodyPart();
DataSource source = new FileDataSource(filePath);
messageBodyPart.setDataHandler(new DataHandler(source));
messageBodyPart.setFileName(fileName);
multipart.addBodyPart(messageBodyPart);
}

@Override
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, pass);
}
}
然后是发送报告邮件的类了,它继承自前面所写的AbstractCrashReportHandler,实现如下:

/*
* @(#)CrashEmailReport.java		       Project: CrashHandler
* Date: 2014-5-27
*
* Copyright (c) 2014 CFuture09, Institute of Software,
* Guangdong Ocean University, Zhanjiang, GuangDong, China.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
*  you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0 *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.githang.android.crash;

import java.io.File;

import android.content.Context;

/**
* @author Geek_Soledad <a target="_blank" href=
*         "http://mail.qq.com/cgi-bin/qm_share?t=qm_mailme&email=XTAuOSVzPDM5LzI0OR0sLHM_MjA"
*         style="text-decoration:none;"><img src=
*         "http://rescdn.qqmail.com/zh_CN/htmledition/images/function/qm_open/ico_mailme_01.png"
*         /></a>
*/
public class CrashEmailReport extends AbstractCrashReportHandler {
private String mReceiveEmail;

public CrashEmailReport(Context context) {
super(context);
}

public void setReceiver(String receiveEmail) {
mReceiveEmail = receiveEmail;
}

@Override
protected void sendReport(String title, String body, File file) {
LogMail sender = new LogMail().setUser("irain_log@163.com").setPass("xxxxxxxx")
.setFrom("irain_log@163.com").setTo(mReceiveEmail).setHost("smtp.163.com")
.setPort("465").setSubject(title).setBody(body);
sender.init();
try {
sender.addAttachment(file.getPath(), file.getName());
sender.send();
file.delete();
} catch (Exception e) {
e.printStackTrace();
}
}
}


这样,一个完整的程序崩溃异常框架就完成了。对于日志报告,可自己继承AbstractCrashReportHandler来扩展实现。

使用的时候,需要写一个继承自Application的类,在onCreate方法中加上如下代码,即设置接收邮箱。

new CrashEmailReport(this).setReceiver("log@msdx.pw");

然后在AndroidManifest.xml中配置这个类。

项目已开源,见另一篇博客:http://blog.csdn.net/maosidiaoxian/article/details/27320815
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: