文章大纲
一、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--
三、项目源码下载
链接:https://pan.baidu.com/s/1kEGfJ3PSoDnsyulCAoimjg
密码:xy0l