Android之崩溃日志管理
文章大纲
一、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--