最近在写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