在开发中经常遇到 APP 在某些场景下莫名的奔溃或者闪退等异常,为了提升用户体验,今天的文章就是捕获 APP 全局异常,统一处理(非使用第三方),并在此基础上提供了以下功能:
UncaughtExceptionHandler
是 Android 系统在 Thread 类中定义的一个接口,并且在 Thread 类中提供了获取该实例的方法:
/**
* Returns the default handler invoked when a thread abruptly terminates
* due to an uncaught exception. If the returned value is null,
* there is no default.
* @since 1.5
* @see #setDefaultUncaughtExceptionHandler
* @return the default uncaught exception handler for all threads
*/
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
return defaultUncaughtExceptionHandler;
}
我们看看该接口提供的方法:
/**
* Interface for handlers invoked when a Thread abruptly
* terminates due to an uncaught exception.
* When a thread is about to terminate due to an uncaught exception
* the Java Virtual Machine will query the thread for its
* UncaughtExceptionHandler using
* {@link #getUncaughtExceptionHandler} and will invoke the handler's
* uncaughtException method, passing the thread and the
* exception as arguments.
* If a thread has not had its UncaughtExceptionHandler
* explicitly set, then its ThreadGroup object acts as its
* UncaughtExceptionHandler. If the ThreadGroup object
* has no
* special requirements for dealing with the exception, it can forward
* the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
* default uncaught exception handler}.
*
* @see #setDefaultUncaughtExceptionHandler
* @see #setUncaughtExceptionHandler
* @see ThreadGroup#uncaughtException
* @since 1.5
*/
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
看见,程序的异常和线程都会在这里这个方法返回,所以我们的异常信息最后都可以从这里捕捉,然后进行相应的处理。下面就是我们的具体处理代码。
public class CrashHandler implements Thread.UncaughtExceptionHandler {
/**
* Debug L tag
*/
public static final String TAG = "CrashHandler";
/**
* 是否开启日志输出,在Debug状态下开启,
* 在Release状态下关闭以提示程序性能
*/
public static final boolean DEBUG = true;
/**
* 系统默认的UncaughtException处理类
*/
private Thread.UncaughtExceptionHandler mDefaultHandler;
/**
* CrashHandler实例
*/
private static CrashHandler INSTANCE;
/**
* 程序的Context对象
*/
private Context mContext;
/**
* 使用Properties来保存设备的信息和错误堆栈信息
*/
private Properties mDeviceCrashInfo = new Properties();
private static final String VERSION_NAME = "versionName";
private static final String VERSION_CODE = "versionCode";
private static final String STACK_TRACE = "STACK_TRACE";
/**
* 错误报告文件的扩展名
*/
private static final String CRASH_REPORTER_EXTENSION = ".cr";
private static Object syncRoot = new Object();
private String deviceInfo = "";
private CrashHandler() {
}
public static CrashHandler getInstance() {
// 防止多线程访问安全,这里使用了双重锁
if (INSTANCE == null) {
synchronized (syncRoot) {
if (INSTANCE == null) {
INSTANCE = new CrashHandler();
}
}
}
return INSTANCE;
}
/**
* 初始化
* @param ctx
*/
public void init(Context ctx) {
mContext = ctx;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
deviceInfo = collectCrashDeviceInfo(mContext);
}
/**
* 当UncaughtException发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
//如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
L.e("Error : " + e);
}
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
}
/**
* 自定义错误处理,收集错误信息
* 发送错误报告等操作均在此完成.
* 开发者可以根据自己的情况来自定义异常处理逻辑
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
return true;
}
StackTraceElement[] stackTraceElements = ex.getStackTrace();
StringBuffer stringBuffer = new StringBuffer();
for (StackTraceElement s : stackTraceElements) {
stringBuffer.append(s + "\n");
}
final String msg = ex.getLocalizedMessage() + "\n" + stringBuffer.toString();
new Thread() {
@Override
public void run() {
Looper.prepare();
if (DEBUG) {
L.e("异常信息->" + msg);
Toast toast = Toast.makeText(mContext, "程序异常,即将退出",
Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
// sendMessage("出错信息:\n" + msg + "\r\n\n设备信息:" + deviceInfo);
//保存错误报告文件
// LogToFile.w("xxxEL", msg+"\n设备信息:" + deviceInfo);
}
Looper.loop();
}
}.start();
return true;
}
/**
* 保存错误信息到文件中
*
* @param ex
* @return
*/
private String saveCrashInfoToFile(Throwable ex) {
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
String result = info.toString();
printWriter.close();
mDeviceCrashInfo.put("EXEPTION", ex.getLocalizedMessage());
mDeviceCrashInfo.put(STACK_TRACE, result);
try {
Time t = new Time("GMT+8");
t.setToNow(); // 取得系统时间
int date = t.year * 10000 + t.month * 100 + t.monthDay;
int time = t.hour * 10000 + t.minute * 100 + t.second;
String fileName = "crash-" + date + "-" + time + CRASH_REPORTER_EXTENSION;
FileOutputStream trace = mContext.openFileOutput(fileName,
Context.MODE_PRIVATE);
mDeviceCrashInfo.store(trace, "");
trace.flush();
trace.close();
return fileName;
} catch (Exception e) {
L.e("an error occured while writing report file..." + e);
}
return null;
}
/**
* 收集程序崩溃的设备信息
*
* @param ctx
*/
private String collectCrashDeviceInfo(Context ctx) {
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(),
PackageManager.GET_ACTIVITIES);
if (pi != null) {
mDeviceCrashInfo.put(VERSION_NAME,
pi.versionName == null ? "not set" : pi.versionName);
mDeviceCrashInfo.put(VERSION_CODE, "" + pi.versionCode);
}
} catch (PackageManager.NameNotFoundException e) {
L.e("Error while collect package info" + e);
}
//使用反射来收集设备信息.在Build类中包含各种设备信息
Field[] fields = Build.class.getDeclaredFields();
StringBuffer sb = new StringBuffer();
for (Field field : fields) {
try {
field.setAccessible(true);
mDeviceCrashInfo.put(field.getName(), "" + field.get(null));
if (DEBUG) {
L.e(field.getName() + " : " + field.get(null));
sb.append(field.getName() + " : " + field.get(null) + "\r\n");
}
} catch (Exception e) {
L.e("Error while collect crash info" + e);
}
}
return sb.toString();
}
}
说明:
定义了异常接口对象的单例;提供了初始化对象,在初始化的时候通过反射来提取设备信息;接着便是在接口回调方法uncaughtException()
中处理异常逻辑,首先判断异常是否通过开发者处理,如果没有处理,则会交给系统默认的异常处理机制去处理。
如果我们有处理,那么会让主线程 sleep() 几秒钟后结束当前进程,而在主线程 sleep() 这几秒钟的时间里就是我们处理异常信息的时候,比如上面的代码,实现了将异常信息(包括设备信息)保存在本地,其中 LogToFile
是一个文件存储的工具类,下面是代码。
LogToFile.java
public class LogToFile {
//日志是否需要读写开关
private static final boolean DEBUG_FLAG = false;
private static String TAG = "LogToFile";
//log日志存放路径
private static String logPath = null;
//日期格式;
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US);
//因为log日志是使用日期命名的,使用静态成员变量主要是为了在整个程序运行期间只存在一个.log文件中;
private static Date date = new Date();
/**
* 初始化,须在使用之前设置,最好在Application创建时调用
*
* @param context
*/
public static void init(Context context) {
logPath = getFilePath(context) + "/Logs";//获得文件储存路径,在后面加"/Logs"建立子文件夹
}
/**
* 获得文件存储路径
*
* @return
*/
private static String getFilePath(Context context) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {
/**
* 如果外部储存可用
*
* 获得外部存储路径,默认路径为
* /storage/emulated/0/Android/data/com.waka.workspace.logtofile/files/Logs/log_2018-09-06_16-15-09.log
*/
return context.getExternalFilesDir(null).getPath();
} else {
return context.getFilesDir().getPath();//直接存在/data/data里,非root手机是看不到的
}
}
private static final char VERBOSE = 'v';
private static final char DEBUG = 'd';
private static final char INFO = 'i';
private static final char WARN = 'w';
private static final char ERROR = 'e';
public static void v(String tag, String msg) {
if (DEBUG_FLAG) {
writeToFile(VERBOSE, tag, msg);
}
}
public static void d(String tag, String msg) {
if (DEBUG_FLAG) {
writeToFile(DEBUG, tag, msg);
}
}
public static void i(String tag, String msg) {
if (DEBUG_FLAG) {
writeToFile(INFO, tag, msg);
}
}
public static void w(String tag, String msg) {
if (DEBUG_FLAG) {
writeToFile(WARN, tag, msg);
}
}
public static void e(String tag, String msg) {
if (DEBUG_FLAG) {
writeToFile(ERROR, tag, msg);
}
}
/**
* 将log信息写入文件中
*
* @param type
* @param tag
* @param msg
*/
private static void writeToFile(char type, String tag, String msg) {
if (null == logPath) {
Log.e(TAG, "logPath == null ,未初始化LogToFile");
return;
}
//log日志名,使用时间命名,保证不重复
String fileName = logPath + "/log_" + dateFormat.format(new Date()) + ".log";
//log日志内容,可以自行定制
String log = dateFormat.format(date) + " " + type + " " + tag + " " + msg + "\n";
//如果父路径不存在
File file = new File(logPath);
if (!file.exists()) {
file.mkdirs();//创建父路径
}
FileOutputStream fos = null;
BufferedWriter bw = null;
try {
//这里的第二个参数代表追加还是覆盖,true为追加,flase为覆盖
fos = new FileOutputStream(fileName, true);
bw = new BufferedWriter(new OutputStreamWriter(fos));
bw.write(log);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) {
bw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
该类的功能很简单,不依赖其他类。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 异常处理,不需要处理时注释掉这两句即可!
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
}
}
一定要记得在清单文件中使用这个 MyApplication
在上面代码的基础上,只需要将收集到的异常信息通过接口调用的方式给服务器上传文件即可。
/**
* 把错误报告发送给服务器,包含新产生的和以前没发送的.
*
* @param ctx
*/
private void sendCrashReportsToServer(Context ctx) {
String[] crFiles = getCrashReportFiles(ctx);
if (crFiles != null && crFiles.length > 0) {
TreeSet sortedFiles = new TreeSet();
sortedFiles.addAll(Arrays.asList(crFiles));
for (String fileName : sortedFiles) {
File cr = new File(ctx.getFilesDir(), fileName);
postReport(cr);
cr.delete();// 删除已发送的报告
}
}
}
private void postReport(File file) {
// TODO 发送错误报告到服务器
}
/**
* 获取错误报告文件名
*
* @param ctx
* @return
*/
private String[] getCrashReportFiles(Context ctx) {
File filesDir = ctx.getFilesDir();
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(CRASH_REPORTER_EXTENSION);
}
};
return filesDir.list(filter);
}
有可能会遇到在未发送成功的时候,程序已经退出,所以这里的 sendCrashReportsToServer(Context ctx)
方法也可以在程序启动的时候调用一次,以保证异常信息发送到服务器。
这里以网易邮箱发送,QQ邮箱接收为例。邮件内容包括异常出错的信息和设备信息。需要特别注意发送邮件的邮箱需要有特殊设置才行。发送的邮件这里提供两种形式:普通文本和HTML形式
引入
implementation 'com.sun.mail:android-mail:1.6.0'
implementation 'com.sun.mail:android-activation:1.6.0'
还需要一个 jar 包additionnal.jar
,请自行搜索下载。
发送代码流程
需要定义一个邮件实体
public class MailSenderInfo {
// 发送邮件的服务器的IP和端口
private String mailServerHost;
private String mailServerPort = "25";
// 邮件发送者的地址
private String fromAddress;
// 邮件接收者的地址
private String toAddress;
// 登陆邮件发送服务器的用户名和密码
private String userName;
private String password;
// 是否需要身份验证
private boolean validate = false;
// 邮件主题
private String subject;
// 邮件的文本内容
private String content;
// 邮件附件的文件名
private String[] attachFileNames;
/**
* 获得邮件会话属性
*/
public Properties getProperties() {
Properties p = new Properties();
p.put("mail.smtp.host", this.mailServerHost);
p.put("mail.smtp.port", this.mailServerPort);
p.put("mail.smtp.auth", validate ? "true" : "false");
return p;
}
public String getMailServerHost() {
return mailServerHost;
}
public void setMailServerHost(String mailServerHost) {
this.mailServerHost = mailServerHost;
}
public String getMailServerPort() {
return mailServerPort;
}
public void setMailServerPort(String mailServerPort) {
this.mailServerPort = mailServerPort;
}
public boolean isValidate() {
return validate;
}
public void setValidate(boolean validate) {
this.validate = validate;
}
public String[] getAttachFileNames() {
return attachFileNames;
}
public void setAttachFileNames(String[] fileNames) {
this.attachFileNames = fileNames;
}
public String getFromAddress() {
return fromAddress;
}
public void setFromAddress(String fromAddress) {
this.fromAddress = fromAddress;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getToAddress() {
return toAddress;
}
public void setToAddress(String toAddress) {
this.toAddress = toAddress;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getContent() {
return content;
}
public void setContent(String textContent) {
this.content = textContent;
}
}
定义发送邮件的工具类
public class SimpleMailSender {
/**
* 以文本格式发送邮件
*
* @param mailInfo 待发送的邮件的信息
*/
public static boolean sendTextMail(MailSenderInfo mailInfo) {
// 判断是否需要身份认证
MyAuthenticator authenticator = null;
Properties pro = mailInfo.getProperties();
if (mailInfo.isValidate()) {
// 如果需要身份认证,则创建一个密码验证器
authenticator = new MyAuthenticator(mailInfo.getUserName(), mailInfo.getPassword());
}
// 根据邮件会话属性和密码验证器构造一个发送邮件的session
Session sendMailSession = Session.getDefaultInstance(pro, authenticator);
try {
// 根据session创建一个邮件消息
Message mailMessage = new MimeMessage(sendMailSession);
// 创建邮件发送者地址
Address from = new InternetAddress(mailInfo.getFromAddress());
// 设置邮件消息的发送者
mailMessage.setFrom(from);
// 创建邮件的接收者地址,并设置到邮件消息中
Address to = new InternetAddress(mailInfo.getToAddress());
mailMessage.setRecipient(Message.RecipientType.TO, to);
// 设置邮件消息的主题
mailMessage.setSubject(mailInfo.getSubject());
// 设置邮件消息发送的时间
mailMessage.setSentDate(new Date());
// 设置邮件消息的主要内容
String mailContent = mailInfo.getContent();
mailMessage.setText(mailContent);
// 发送邮件
Transport.send(mailMessage);
return true;
} catch (MessagingException ex) {
ex.printStackTrace();
}
return false;
}
/**
* 以HTML格式发送邮件
*
* @param mailInfo 待发送的邮件信息
*/
public static boolean sendHtmlMail(MailSenderInfo mailInfo) {
// 判断是否需要身份认证
MyAuthenticator authenticator = null;
Properties pro = mailInfo.getProperties();
//如果需要身份认证,则创建一个密码验证器
if (mailInfo.isValidate()) {
authenticator = new MyAuthenticator(mailInfo.getUserName(), mailInfo.getPassword());
}
// 根据邮件会话属性和密码验证器构造一个发送邮件的session
Session sendMailSession = Session.getDefaultInstance(pro, authenticator);
try {
// 根据session创建一个邮件消息
Message mailMessage = new MimeMessage(sendMailSession);
// 创建邮件发送者地址
Address from = new InternetAddress(mailInfo.getFromAddress());
// 设置邮件消息的发送者
mailMessage.setFrom(from);
// 创建邮件的接收者地址,并设置到邮件消息中
Address to = new InternetAddress(mailInfo.getToAddress());
// Message.RecipientType.TO属性表示接收者的类型为TO
mailMessage.setRecipient(Message.RecipientType.TO, to);
// 设置邮件消息的主题
mailMessage.setSubject(mailInfo.getSubject());
// 设置邮件消息发送的时间
mailMessage.setSentDate(new Date());
// MiniMultipart类是一个容器类,包含MimeBodyPart类型的对象
Multipart mainPart = new MimeMultipart();
// 创建一个包含HTML内容的MimeBodyPart
BodyPart html = new MimeBodyPart();
// 设置HTML内容
html.setContent(mailInfo.getContent(), "text/html; charset=utf-8");
mainPart.addBodyPart(html);
// 将MiniMultipart对象设置为邮件内容
mailMessage.setContent(mainPart);
// 发送邮件
Transport.send(mailMessage);
return true;
} catch (MessagingException ex) {
ex.printStackTrace();
}
return false;
}
}
这里提供了两种发送形式,使用只需要调用对应方法即可,其中身份验证代码如下:
public class MyAuthenticator extends Authenticator {
String userName = null;
String password = null;
public MyAuthenticator() {
}
public MyAuthenticator(String username, String password) {
this.userName = username;
this.password = password;
}
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(userName, password);
}
}
设置发送参数
private void sendMessage(String msg) {
MailSenderInfo mailInfo = new MailSenderInfo();
// 邮箱(发送邮件的邮箱)服务主机地址 不同邮箱地址不同
mailInfo.setMailServerHost("smtp.163.com");
// 协议端口号 25 具体参考后面的图
mailInfo.setMailServerPort("25");
mailInfo.setValidate(true);
// 发送邮箱地址
mailInfo.setUserName("[email protected]");
// 发送邮箱的授权码 需要在对应邮箱的网页版设置里去设置
mailInfo.setPassword("xxxxxxxxxx");
// 发送邮箱地址
mailInfo.setFromAddress("[email protected]");
// 接受邮箱地址
mailInfo.setToAddress("[email protected]");
// 邮件主题
mailInfo.setSubject("程序异常日志");
// 邮件内容
mailInfo.setContent(msg);
boolean isSuccess = SimpleMailSender.sendTextMail(mailInfo);
if (isSuccess) {
Log.e(TAG, "发送成功");
} else {
Log.e(TAG, "发送失败");
}
}
只要以上参数设置没有问题,那么就可以正常发送邮件和收到邮件。这里需要格外注意邮箱的设置,通过设置才可以获取授权码,下面是网易邮箱的截图。
这里的授权码就是上面参数password
所需要的值。
到这,关于邮箱的设置就完成了,我们来看看发送的邮件是什么效果。
关于设备信息,这里有说明:
1、BOARD 主板:The name of the underlying board, like goldfish.
2、BOOTLOADER 系统启动程序版本号:The system bootloader version number.
3、BRAND 系统定制商:The consumer-visible brand with which the product/hardware will be associated, if any.
4、CPU_ABI cpu指令集:The name of the instruction set (CPU type + ABI convention) of native code.
5、CPU_ABI2 cpu指令集2:The name of the second instruction set (CPU type + ABI convention) of native code.
6、DEVICE 设备参数:The name of the industrial design.
7、DISPLAY 显示屏参数:A build ID string meant for displaying to the user
8、FINGERPRINT 唯一识别码:A string that uniquely identifies this build. Do not attempt to parse this value.
9、HARDWARE 硬件名称:The name of the hardware (from the kernel command line or /proc).
10、HOST
11、ID 修订版本列表:Either a changelist number, or a label like M4-rc20.
12、MANUFACTURER 硬件制造商:The manufacturer of the product/hardware.
13、MODEL 版本即最终用户可见的名称:The end-user-visible name for the end product.
14、PRODUCT 整个产品的名称:The name of the overall product.
15、RADIO 无线电固件版本:The radio firmware version number. 在API14后已过时。使用getRadioVersion()代替。
16、SERIAL 硬件序列号:A hardware serial number, if available. Alphanumeric only, case-insensitive.
17、TAGS 描述build的标签,如未签名,debug等等。:Comma-separated tags describing the build, like unsigned,debug.
18、TIME
19、TYPE build的类型:The type of build, like user or eng.
20、USER