最近在寫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);}
接下來是用於處理崩潰異常的類,它要實現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 */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 */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 */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 */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中配置這個類。