文章大纲
一、Android崩溃日志管理简介
二、崩溃日志管理实战
三、项目源码下载
一、Android崩溃日志管理简介
1. 什么是android崩溃日志管理
开发中有些地方未注意可能造成异常抛出未能caught到,然后弹出系统对话框强制退出。这种交互不好,而且开发者也不能及时获取到底哪里出问题。因此我们可以使用android的UncaughtExceptionHandler来处理这种异常。
2. 操作逻辑
用户端(出现崩溃)
我们会封装一个通用的jar包,该jar包包括日志打印、捕获异常信息逻辑、网络传输、设置Debug和Release模式、获取本机的相关信息等,当出现异常时,将异常信息以文件方式保存在用户手机中,并且发送到后台,当后台接收成功时,自动删除用户手机的崩溃信息文件,若接收失败,在下次发生崩溃时,将历史发送失败的崩溃一同发送。
接收端(后台)
我们会编写一个地址,用于接收异常的具体信息,并储存在本地文件中,以此作为日志进行管理。
二、崩溃日志管理实战
1. 后台端
在该实战中,我以简单的servlet进行讲解,实际项目中,可以以ssm或spring boot等框架进行操作。
/**
* 接收崩溃信息,并进行打印(实际项目中,需要以文件形式归档)
* @author wxc
*
*/
public class Test extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//获取客户端传送过来的信息流
BufferedReader in=new BufferedReader(new InputStreamReader(request.getInputStream()));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = in.readLine()) != null) {
//将信息流进行打印
System.out.println(line);
}
}
}
2. 客户端通用项目
网络请求相关的配置管理类:HttpManager.java
/**
*
* 网络请求相关的配置管理
*
* @author 吴晓畅
*
*/
public class HttpManager {
private static final int SET_CONNECTION_TIMEOUT = 5 * 1000;
private static final int SET_SOCKET_TIMEOUT = 20 * 1000;
private static final String BOUNDARY = getBoundry();// UUID.randomUUID().toString();
private static final String MP_BOUNDARY = "--" + BOUNDARY;
private static final String END_MP_BOUNDARY = "--" + BOUNDARY + "--";
private static final String LINEND = "\r\n";
private static final String CHARSET = "UTF-8";
public static String uploadFile(String url, HttpParameters params,
File logFile) throws IOException{
HttpClient client = getHttpClient();
HttpPost post = new HttpPost(url);
ByteArrayOutputStream bos = null;
FileInputStream logFileInputStream = null;
String result = null;
try {
bos = new ByteArrayOutputStream();
if(params != null){
String key = "";
for (int i = 0; i < params.size(); i++) {
key = params.getKey(i);
StringBuilder temp = new StringBuilder(10);
temp.setLength(0);
temp.append(MP_BOUNDARY).append(LINEND);
temp.append("content-disposition: form-data; name=\"").append(key)
.append("\"").append(LINEND + LINEND);
temp.append(params.getValue(key)).append(LINEND);
bos.write(temp.toString().getBytes());
}
}
StringBuilder temp = new StringBuilder();
temp.append(MP_BOUNDARY).append(LINEND);
temp.append(
"content-disposition: form-data; name=\"logfile\"; filename=\"")
.append(logFile.getName()).append("\"").append(LINEND);
temp.append("Content-Type: application/octet-stream; charset=utf-8").append(LINEND + LINEND);
bos.write(temp.toString().getBytes());
logFileInputStream = new FileInputStream(logFile);
byte[] buffer = new byte[1024*8];//8k
while(true){
int count = logFileInputStream.read(buffer);
if(count == -1){
break;
}
bos.write(buffer, 0, count);
}
bos.write((LINEND+LINEND).getBytes());
bos.write((END_MP_BOUNDARY+LINEND).getBytes());
ByteArrayEntity formEntity = new ByteArrayEntity(bos.toByteArray());
post.setEntity(formEntity);
HttpResponse response = client.execute(post);
StatusLine status = response.getStatusLine();
int statusCode = status.getStatusCode();
Log.i("HttpManager", "返回结果为"+statusCode);
if(statusCode == HttpStatus.SC_OK){
result = readHttpResponse(response);
}
} catch (IOException e) {
throw e;
}finally{
if(bos != null){
try {
bos.close();
} catch (IOException e) {
throw e;
}
}
if(logFileInputStream != null){
try {
logFileInputStream.close();
} catch (IOException e) {
throw e;
}
}
}
return result;
}
private static String readHttpResponse(HttpResponse response){
String result = null;
HttpEntity entity = response.getEntity();
InputStream inputStream;
try {
inputStream = entity.getContent();
ByteArrayOutputStream content = new ByteArrayOutputStream();
int readBytes = 0;
byte[] sBuffer = new byte[512];
while ((readBytes = inputStream.read(sBuffer)) != -1) {
content.write(sBuffer, 0, readBytes);
}
result = new String(content.toByteArray(), CHARSET);
return result;
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return result;
}
private static HttpClient getHttpClient() {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore
.getDefaultType());
trustStore.load(null, null);
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 10000);
HttpConnectionParams.setSoTimeout(params, 10000);
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
registry.register(new Scheme("https", sf, 443));
ClientConnectionManager ccm = new ThreadSafeClientConnManager(
params, registry);
HttpConnectionParams.setConnectionTimeout(params,
SET_CONNECTION_TIMEOUT);
HttpConnectionParams.setSoTimeout(params, SET_SOCKET_TIMEOUT);
HttpClient client = new DefaultHttpClient(ccm, params);
return client;
} catch (Exception e) {
// e.printStackTrace();
return new DefaultHttpClient();
}
}
private static class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore)
throws NoSuchAlgorithmException, KeyManagementException,
KeyStoreException, UnrecoverableKeyException {
super(truststore);
TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
// TODO Auto-generated method stub
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// TODO Auto-generated method stub
}
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// TODO Auto-generated method stub
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
}
@Override
public Socket createSocket() throws IOException {
return sslContext.getSocketFactory().createSocket();
}
@Override
public Socket createSocket(Socket socket, String host, int port,
boolean autoClose) throws IOException, UnknownHostException {
return sslContext.getSocketFactory().createSocket(socket, host,
port, autoClose);
}
}
private static String getBoundry() {
StringBuffer _sb = new StringBuffer();
for (int t = 1; t < 12; t++) {
long time = System.currentTimeMillis() + t;
if (time % 3 == 0) {
_sb.append((char) time % 9);
} else if (time % 3 == 1) {
_sb.append((char) (65 + time % 26));
} else {
_sb.append((char) (97 + time % 26));
}
}
return _sb.toString();
}
}
文件上传相关类:UploadLogManager.java
package com.qihoo.linker.logcollector.upload;
import java.io.File;
import java.io.IOException;
import java.util.logging.Logger;
import com.qihoo.linker.logcollector.capture.LogFileStorage;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
/**
*
* @author 吴晓畅
*
*/
public class UploadLogManager {
private static final String TAG = UploadLogManager.class.getName();
private static UploadLogManager sInstance;
private Context mContext;
private HandlerThread mHandlerThread;
private static volatile MyHandler mHandler;
private volatile Looper mLooper;
private volatile boolean isRunning = false;
private String url;
private HttpParameters params;
private UploadLogManager(Context c){
mContext = c.getApplicationContext();
mHandlerThread = new HandlerThread(TAG + ":HandlerThread");
mHandlerThread.start();
}
//初始化UploadLogManager类
public static synchronized UploadLogManager getInstance(Context c){
if(sInstance == null){
sInstance = new UploadLogManager(c);
}
return sInstance;
}
/**
* 执行文件上传具体操作
*
* @param url
* @param params
*/
public void uploadLogFile(String url , HttpParameters params){
this.url = url;
this.params = params;
mLooper = mHandlerThread.getLooper();
mHandler = new MyHandler(mLooper);
if(mHandlerThread == null){
return;
}
if(isRunning){
return;
}
mHandler.sendMessage(mHandler.obtainMessage());
isRunning = true;
}
//用于uploadLogFile方法调用的线程
private final class MyHandler extends Handler{
public MyHandler(Looper looper) {
super(looper);
// TODO Auto-generated constructor stub
}
@Override
public void handleMessage(Message msg) {
File logFile = LogFileStorage.getInstance(mContext).getUploadLogFile();
if(logFile == null){
isRunning = false;
return;
}
try {
String result = HttpManager.uploadFile(url, params, logFile);
Log.i("UpLoad", "服务端返回数据为"+result);
if(result != null){
Boolean isSuccess = LogFileStorage.getInstance(mContext).deleteUploadLogFile();
Log.i("UpLoad", "删除文件结果为"+isSuccess);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
isRunning = false;
}
}
}
}
客户端崩溃日志文件的删除,保存等操作类:LogFileStorage.java
文件保存在Android/data/包名/Log/下
package com.qihoo.linker.logcollector.capture;
import java.io.File;
import java.io.FileOutputStream;
import com.qihoo.linker.logcollector.utils.LogCollectorUtility;
import com.qihoo.linker.logcollector.utils.LogHelper;
import android.content.Context;
import android.util.Log;
/**
*
* 客户端崩溃日志文件的删除,保存等操作
*
* @author 吴晓畅
*
*/
public class LogFileStorage {
private static final String TAG = LogFileStorage.class.getName();
public static final String LOG_SUFFIX = ".log";
private static final String CHARSET = "UTF-8";
private static LogFileStorage sInstance;
private Context mContext;
private LogFileStorage(Context ctx) {
mContext = ctx.getApplicationContext();
}
public static synchronized LogFileStorage getInstance(Context ctx) {
if (ctx == null) {
LogHelper.e(TAG, "Context is null");
return null;
}
if (sInstance == null) {
sInstance = new LogFileStorage(ctx);
}
return sInstance;
}
public File getUploadLogFile(){
File dir = mContext.getFilesDir();
File logFile = new File(dir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
if(logFile.exists()){
return logFile;
}else{
return null;
}
}
//删除客户端中崩溃日志文件
public boolean deleteUploadLogFile(){
File dir = mContext.getFilesDir();
File logFile = new File(dir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
Log.i("Log",
LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
return logFile.delete();
}
//保存文件
public boolean saveLogFile2Internal(String logString) {
try {
File dir = mContext.getFilesDir();
if (!dir.exists()) {
dir.mkdirs();
}
File logFile = new File(dir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
FileOutputStream fos = new FileOutputStream(logFile , true);
fos.write(logString.getBytes(CHARSET));
fos.close();
} catch (Exception e) {
e.printStackTrace();
LogHelper.e(TAG, "saveLogFile2Internal failed!");
return false;
}
return true;
}
public boolean saveLogFile2SDcard(String logString, boolean isAppend) {
if (!LogCollectorUtility.isSDcardExsit()) {
LogHelper.e(TAG, "sdcard not exist");
return false;
}
try {
File logDir = getExternalLogDir();
if (!logDir.exists()) {
logDir.mkdirs();
}
File logFile = new File(logDir, LogCollectorUtility.getMid(mContext)
+ LOG_SUFFIX);
/*if (!isAppend) {
if (logFile.exists() && !logFile.isFile())
logFile.delete();
}*/
LogHelper.d(TAG, logFile.getPath());
FileOutputStream fos = new FileOutputStream(logFile , isAppend);
fos.write(logString.getBytes(CHARSET));
fos.close();
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "saveLogFile2SDcard failed!");
return false;
}
return true;
}
private File getExternalLogDir() {
File logDir = LogCollectorUtility.getExternalDir(mContext, "Log");
LogHelper.d(TAG, logDir.getPath());
return logDir;
}
}
UncaughtExceptionHandler实现类:CrashHandler.java
当出现异常时,会进入public void uncaughtException(Thread thread, Throwable ex) 方法中。
/**
*
* 如果需要捕获系统的未捕获异常(如系统抛出了未知错误,这种异常没有捕获,这将导致系统莫名奇妙的关闭,使得用户体验差),
* 可以通过UncaughtExceptionHandler来处理这种异常。
*
* @author 吴晓畅
*
*/
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = CrashHandler.class.getName();
private static final String CHARSET = "UTF-8";
private static CrashHandler sInstance;
private Context mContext;
private Thread.UncaughtExceptionHandler mDefaultCrashHandler;
String appVerName;
String appVerCode;
String OsVer;
String vendor;
String model;
String mid;
//初始化该类
private CrashHandler(Context c) {
mContext = c.getApplicationContext();
// mContext = c;
appVerName = "appVerName:" + LogCollectorUtility.getVerName(mContext);
appVerCode = "appVerCode:" + LogCollectorUtility.getVerCode(mContext);
OsVer = "OsVer:" + Build.VERSION.RELEASE;
vendor = "vendor:" + Build.MANUFACTURER;
model = "model:" + Build.MODEL;
mid = "mid:" + LogCollectorUtility.getMid(mContext);
}
//初始化该类
public static CrashHandler getInstance(Context c) {
if (c == null) {
LogHelper.e(TAG, "Context is null");
return null;
}
if (sInstance == null) {
sInstance = new CrashHandler(c);
}
return sInstance;
}
public void init() {
if (mContext == null) {
return;
}
boolean b = LogCollectorUtility.hasPermission(mContext);
if (!b) {
return;
}
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 发生异常时候进来这里
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
//
handleException(ex);
//
ex.printStackTrace();
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
} else {
Process.killProcess(Process.myPid());
// System.exit(1);
}
}
//将异常信息保存成文件
private void handleException(Throwable ex) {
String s = fomatCrashInfo(ex);
// String bes = fomatCrashInfoEncode(ex);
LogHelper.d(TAG, s);
// LogHelper.d(TAG, bes);
//LogFileStorage.getInstance(mContext).saveLogFile2Internal(bes);
LogFileStorage.getInstance(mContext).saveLogFile2Internal(s);
if(Constants.DEBUG){
LogFileStorage.getInstance(mContext).saveLogFile2SDcard(s, true);
}
}
private String fomatCrashInfo(Throwable ex) {
/*
* String lineSeparator = System.getProperty("line.separator");
* if(TextUtils.isEmpty(lineSeparator)){ lineSeparator = "\n"; }
*/
String lineSeparator = "\r\n";
StringBuilder sb = new StringBuilder();
String logTime = "logTime:" + LogCollectorUtility.getCurrentTime();
String exception = "exception:" + ex.toString();
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
String dump = info.toString();
String crashMD5 = "crashMD5:"
+ LogCollectorUtility.getMD5Str(dump);
String crashDump = "crashDump:" + "{" + dump + "}";
printWriter.close();
sb.append("&start---").append(lineSeparator);
sb.append(logTime).append(lineSeparator);
sb.append(appVerName).append(lineSeparator);
sb.append(appVerCode).append(lineSeparator);
sb.append(OsVer).append(lineSeparator);
sb.append(vendor).append(lineSeparator);
sb.append(model).append(lineSeparator);
sb.append(mid).append(lineSeparator);
sb.append(exception).append(lineSeparator);
sb.append(crashMD5).append(lineSeparator);
sb.append(crashDump).append(lineSeparator);
sb.append("&end---").append(lineSeparator).append(lineSeparator)
.append(lineSeparator);
return sb.toString();
}
private String fomatCrashInfoEncode(Throwable ex) {
/*
* String lineSeparator = System.getProperty("line.separator");
* if(TextUtils.isEmpty(lineSeparator)){ lineSeparator = "\n"; }
*/
String lineSeparator = "\r\n";
StringBuilder sb = new StringBuilder();
String logTime = "logTime:" + LogCollectorUtility.getCurrentTime();
String exception = "exception:" + ex.toString();
Writer info = new StringWriter();
PrintWriter printWriter = new PrintWriter(info);
ex.printStackTrace(printWriter);
String dump = info.toString();
String crashMD5 = "crashMD5:"
+ LogCollectorUtility.getMD5Str(dump);
try {
dump = URLEncoder.encode(dump, CHARSET);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String crashDump = "crashDump:" + "{" + dump + "}";
printWriter.close();
sb.append("&start---").append(lineSeparator);
sb.append(logTime).append(lineSeparator);
sb.append(appVerName).append(lineSeparator);
sb.append(appVerCode).append(lineSeparator);
sb.append(OsVer).append(lineSeparator);
sb.append(vendor).append(lineSeparator);
sb.append(model).append(lineSeparator);
sb.append(mid).append(lineSeparator);
sb.append(exception).append(lineSeparator);
sb.append(crashMD5).append(lineSeparator);
sb.append(crashDump).append(lineSeparator);
sb.append("&end---").append(lineSeparator).append(lineSeparator)
.append(lineSeparator);
String bes = Base64.encodeToString(sb.toString().getBytes(),
Base64.NO_WRAP);
return bes;
}
}
项目调用封装类:LogCollector.java
/**
*
* 执行文件上传相关的类
*
*
* @author 吴晓畅
*
*/
public class LogCollector {
private static final String TAG = LogCollector.class.getName();
private static String Upload_Url;
private static Context mContext;
private static boolean isInit = false;
private static HttpParameters mParams;
//初始化文件上传的url,数据等内容
public static void init(Context c , String upload_url , HttpParameters params){
if(c == null){
return;
}
if(isInit){
return;
}
Upload_Url = upload_url;
mContext = c;
mParams = params;
//初始化自己定义的异常处理
CrashHandler crashHandler = CrashHandler.getInstance(c);
crashHandler.init();
isInit = true;
}
/**
* 执行文件上传的网路请求
*
* if(isWifiOnly && !isWifiMode){
return;
}表示只在wifi状态下执行文件上传
*
* @param isWifiOnly
*/
public static void upload(boolean isWifiOnly){
if(mContext == null || Upload_Url == null){
Log.d(TAG, "please check if init() or not");
return;
}
if(!LogCollectorUtility.isNetworkConnected(mContext)){
return;
}
boolean isWifiMode = LogCollectorUtility.isWifiConnected(mContext);
if(isWifiOnly && !isWifiMode){
return;
}
UploadLogManager.getInstance(mContext).uploadLogFile(Upload_Url, mParams);
}
/**
* 用于设置是否为测试状态
*
* @param isDebug true为是,false为否 如果是,能看到LOG日志,同时能够在将文件夹看到崩溃日志
*/
public static void setDebugMode(boolean isDebug){
Constants.DEBUG = isDebug;
LogHelper.enableDefaultLog = isDebug;
}
}
3. 客户端接入使用
为通用项目设置is Library模式
实际android项目使用
添加Library
在Application子类中进行初始化
public class MyApplication extends Application {
//后台地址地址
private static final String UPLOAD_URL = "http://192.168.3.153:8080/bengkuitest/servlet/Test";
@Override
public void onCreate() {
super.onCreate();
boolean isDebug = true;
//设置是否为测试模式,如果是,同时能够在将文件夹看到崩溃日志
LogCollector.setDebugMode(isDebug);
//params的数据可以为空 初始化LogCollector的相关数据,用于文件上传到服务器
LogCollector.init(getApplicationContext(), UPLOAD_URL, null);
}
}
编写异常并上传异常
public class MainActivity extends Activity implements OnClickListener {
private Button btn_crash;
private Button btn_upload;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_crash = (Button) findViewById(R.id.button1);
btn_upload = (Button) findViewById(R.id.button2);
btn_crash.setOnClickListener(this);
btn_upload.setOnClickListener(this);
}
//产生异常
private void causeCrash(){
String s = null;
s.split("1");
}
//上传文件
private void uploadLogFile(){
//设置为只在wifi下上传文件
boolean isWifiOnly = true;//only wifi mode can upload
//执行文件上传服务器
LogCollector.upload(isWifiOnly);//upload at the right time
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
causeCrash();
break;
case R.id.button2:
//上传文件
uploadLogFile();
break;
default:
break;
}
}
}
运行结果如下图所示
--No1Qr4Tu7Wx
content-disposition: form-data; name="logfile"; filename="c5c63fec3651fdebdd411582793fa40c.log"
Content-Type: application/octet-stream; charset=utf-8
&start---
logTime:2019-04-07 10:54:47
appVerName:1.0
appVerCode:1
OsVer:5.1.1
vendor:samsung
model:SM-G955F
mid:c5c63fec3651fdebdd411582793fa40c
exception:java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String[] java.lang.String.split(java.lang.String)' on a null object reference
crashMD5:74861b8fb97ef57b82a87a826ab6b08f
crashDump:{java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String[] java.lang.String.split(java.lang.String)' on a null object reference
at com.jiabin.logcollectorexample.MainActivity.causeCrash(MainActivity.java:32)
at com.jiabin.logcollectorexample.MainActivity.onClick(MainActivity.java:45)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5293)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
}
&end---
--No1Qr4Tu7Wx--