最近在写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
*/
public interface CrashListener {
/**
* 保存异常的日志。
*
* @param file
*/
public void afterSaveCrash(File file);
}
我的实现如下:
/*
* @(#)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
*/
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;
}
}
/*
* @(#)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
*/
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
}
}
}
}
/*
* @(#)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
*/
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 ([email protected]) 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
*/
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("[email protected]").setPass("xxxxxxxx")
.setFrom("[email protected]").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();
}
}
}
使用的时候,需要写一个继承自Application的类,在onCreate方法中加上如下代码,即设置接收邮箱。
new CrashEmailReport(this).setReceiver("[email protected]");
然后在AndroidManifest.xml中配置这个类。
项目已开源,见另一篇博客:http://blog.csdn.net/maosidiaoxian/article/details/27320815