一、全局异常捕捉
a) 首先创建全局异常捕捉类CrashHander
/** * UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告. * @author user */ public class CrashHander implements UncaughtExceptionHandler { public static final String TAG = "CrashHandler"; //系统默认的UncaughtException处理类 private Thread.UncaughtExceptionHandler mDefaultHandler; //CrashHandler实例 private static CrashHander INSTANCE = new CrashHander(); //程序的Context对象 private Context mContext; //用来存储设备信息和异常信息 private Map<String, String> infos = new HashMap<String, String>(); //用于格式化日期,作为日志文件名的一部分 private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); /** 保证只有一个CrashHandler实例 */ private CrashHander() { } /** 获取CrashHandler实例 ,单例模式 */ public static CrashHander getInstance() { return INSTANCE; } /** * 初始化 * @param context */ public void init(Context context) { mContext = context; //获取系统默认的UncaughtException处理器 mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler(); //设置该CrashHandler为程序的默认处理器 Thread.setDefaultUncaughtExceptionHandler(this); } /** * 当UncaughtException发生时会转入该函数来处理 */ @Override public void uncaughtException(Thread thread, Throwable ex) { if (!handleException(ex) && mDefaultHandler != null) { //如果用户没有处理则让系统默认的异常处理器来处理 mDefaultHandler.uncaughtException(thread, ex); } else { try { Thread.sleep(3000); } catch (InterruptedException e) { Log.e(TAG, "error : ", e); } //退出程序 android.os.Process.killProcess(android.os.Process.myPid()); System.exit(1); } } /** * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. * @param ex * @return true:如果处理了该异常信息;否则返回false. */ private boolean handleException(Throwable ex) { if (ex == null) { return false; } //使用Toast来显示异常信息 new Thread() { @Override public void run() { Looper.prepare(); Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show(); Looper.loop(); } }.start(); //收集设备参数信息 collectDeviceInfo(mContext); //保存日志文件 saveCrashInfo2File(ex); return true; } }
其中使用到的收集设备参数的方法collectDeviceInfo
/** * 收集设备参数信息 * @param ctx */ public void collectDeviceInfo(Context ctx) { try { PackageManager pm = ctx.getPackageManager(); PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName == null ? "null" : pi.versionName; String versionCode = pi.versionCode + ""; infos.put("versionName", versionName); infos.put("versionCode", versionCode); } } catch (NameNotFoundException e) { Log.e(TAG, "an error occured when collect package info", e); } Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); infos.put(field.getName(), field.get(null).toString()); Log.d(TAG, field.getName() + " : " + field.get(null)); } catch (Exception e) { Log.e(TAG, "an error occured when collect crash info", e); } } }
保存错误信息到文件中的方法
/** * 保存错误信息到文件中 * @param ex * @return 返回文件名称,便于将文件传送到服务器 */ private String saveCrashInfo2File(Throwable ex) { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, String> entry : infos.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); sb.append(key + "=" + value + "\n"); } Writer writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); ex.printStackTrace(printWriter); Throwable cause = ex.getCause(); while (cause != null) { cause.printStackTrace(printWriter); cause = cause.getCause(); } printWriter.close(); String result = writer.toString(); sb.append(result); try { long timestamp = System.currentTimeMillis(); String time = formatter.format(new Date()); String fileName = "crash-" + time + "-" + timestamp + ".log"; if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { String path = "/sdcard/crash/"; File dir = new File(path); if (!dir.exists()) { dir.mkdirs(); } FileOutputStream fos = new FileOutputStream(path + fileName); fos.write(sb.toString().getBytes()); fos.close(); } return fileName; } catch (Exception e) { Log.e(TAG, "an error occured while writing file...", e); } return null; }
b) 然后创建CrashApplication类继承自Application,这里是程序的入口,在此处设置异常捕捉。
public class CrashApplication extends Application { private static CrashApplication mApplication; public static CrashApplication getInstance(){ return mApplication; } @Override public void onCreate() { super.onCreate(); CrashHandler catchHandler = CrashHandler.getInstance(); catchHandler.init(getApplicationContext()); mApplication = this; } }
设置全局异常捕捉后,出现异常提示的是自定义的信息,无法看到堆栈信息,可以在此处注释掉异常捕捉类来进行调试。
c) 最后在清单文件中配置name属性指向自定义的捕捉类,让程序使用该类来捕获全局异常。
<application android:name="com.hll.phoneserver.CrashApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity" android:label="@string/app_name"> <meta-data android:name="android.support.UI_OPTIONS" android:value="splitActionBarWhenNarrow" /> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
二、日志框架
a) 首先我们来创建日志过滤工具类,通过TAG判断是哪个应用的日志信息,且方便项目中打印日志使用
public class LogUtils { public static final String TAG = "PhoneServer"; public LogUtils() { } /** * Send a VERBOSE log message. * * @param msg * The message you would like logged. */ public static void v(String msg) { android.util.Log.v(TAG, buildMessage(msg)); } /** * Send a VERBOSE log message and log the exception. * @param msg * The message you would like logged. * @param thr * An exception to log */ public static void v(String msg, Throwable thr) { android.util.Log.v(TAG, buildMessage(msg), thr); } /** * Send a DEBUG log message. * @param msg */ public static void d(String msg) { android.util.Log.d(TAG, buildMessage(msg)); } /** * Send a DEBUG log message and log the exception. * @param msg * The message you would like logged. * @param tr * An exception to log */ public static void d(String msg, Throwable thr) { android.util.Log.d(TAG, buildMessage(msg), thr); } /** * Send an INFO log message. * @param msg * The message you would like logged. */ public static void i(String msg) { android.util.Log.i(TAG, buildMessage(msg)); } /** * Send a INFO log message and log the exception. * @param msg * The message you would like logged. * @param thr * An exception to log */ public static void i(String msg, Throwable thr) { android.util.Log.i(TAG, buildMessage(msg), thr); } /** * Send an ERROR log message. * * @param msg * The message you would like logged. */ public static void e(String msg) { android.util.Log.e(TAG, buildMessage(msg)); } /** * Send a WARN log message * @param msg * The message you would like logged. */ public static void w(String msg) { android.util.Log.w(TAG, buildMessage(msg)); } /** * Send a WARN log message and log the exception. * @param msg * The message you would like logged. * @param thr * An exception to log */ public static void w(String msg, Throwable thr) { android.util.Log.w(TAG, buildMessage(msg), thr); } /** * Send an empty WARN log message and log the exception. * @param thr * An exception to log */ public static void w(Throwable thr) { android.util.Log.w(TAG, buildMessage(""), thr); } /** * Send an ERROR log message and log the exception. * * @param msg * The message you would like logged. * @param thr * An exception to log */ public static void e(String msg, Throwable thr) { android.util.Log.e(TAG, buildMessage(msg), thr); } /** * Building Message * @param msg * The message you would like logged. * @return Message String */ protected static String buildMessage(String msg) { StackTraceElement caller = new Throwable().fillInStackTrace() .getStackTrace()[2]; return new StringBuilder().append(caller.getClassName()).append(".") .append(caller.getMethodName()).append("(): ").append(msg) .toString(); } }
b) 我们创建LogcatHelper类来过滤掉一些无用的日志,只收集当前程序的日志(目前无法多次上传,有时间进行改造)
public class LogcatHelper { private static final String LOG_FILE_NAME = "phoneserver.log"; private static LogcatHelper INSTANCE = null; private static String mPathLogcat; private LogFilter mLogFilter = null; private Context mContext; private int mPId; private static String mStartTime = ""; // 用来存储设备信息和异常信息 // private Map<String, String> infos = new HashMap<String, String>(); /** * 拼接本地日志存放路径,创建日志文件,得到当前时间 * @param context 上下文 */ public static void init(Context context) { // 拼接日志存放路径 StringBuffer LogPath = new StringBuffer(); LogPath.append(Environment.getExternalStorageDirectory()); LogPath.append("/Android/data/"); LogPath.append(context.getPackageName()).append("/"); LogPath.append("logs").append("/"); mPathLogcat = LogPath.toString(); // 文件不存在则创建 File file = new File(mPathLogcat); if (!file.exists()) { file.mkdirs(); } long currentTime = System.currentTimeMillis() / 1000; mStartTime = String.valueOf(currentTime); } /** * 创建LogcatHelper实例 * @param context 上下文 * @return LogcatHelper实例 */ public static LogcatHelper getInstance(Context context) { if (INSTANCE == null) { INSTANCE = new LogcatHelper(context); } return INSTANCE; } /** * 得到当前进程的唯一标识符 * @param context 上下文 */ private LogcatHelper(Context context) { mContext = context; mPId = android.os.Process.myPid(); } public void start() { if (mLogFilter == null) { mLogFilter = new LogFilter(String.valueOf(mPId), mPathLogcat); mLogFilter.start(); } } public void stop() { if (mLogFilter != null) { mLogFilter.stopLogs(); mLogFilter = null; } } /**日志文件大小超过1.5M自动上传*/ public void sendMaxLogMessage() { if (mLogFilter != null) { mLogFilter.setLogFileLock(true); String file = mLogFilter.getLogFileName(); File sendFile = new File(file); if (sendFile.exists() && sendFile.length() > 2000) { uploadLog(); File newFile = new File(file); try { newFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } mLogFilter.setLogFileLock(false); } } /**随时向后台上传日志*/ public void sendLogMessage() { if (mLogFilter == null) { mLogFilter = new LogFilter(String.valueOf(mPId), mPathLogcat); // 开启线程过滤收集 mLogFilter.start(); } if (mLogFilter != null) { mLogFilter.setLogFileLock(true); String file = mLogFilter.getLogFileName(); File sendFile = new File(file); if (sendFile.exists() && sendFile.length() > 2000) { uploadLog(); File newFile = new File(file); try { newFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } mLogFilter.setLogFileLock(false); } } /** * 收集设备参数信息 * @param ctx */ public void collectDeviceInfo() { try { PackageManager pm = mContext.getPackageManager(); PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(),PackageManager.GET_ACTIVITIES); if (pi != null) { String versionName = pi.versionName == null ? "null": pi.versionName; String versionCode = pi.versionCode + ""; Log.e(LogUtils.TAG, "versionName : " + versionName); Log.e(LogUtils.TAG, "versionCode : " + versionCode); } } catch (NameNotFoundException e) { Log.e(LogUtils.TAG, "an error occured when collect package info", e); } // 暴力反射,获取所有成员 Field[] fields = Build.class.getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); Log.e(LogUtils.TAG, field.getName() + " : " + field.get(null)); } catch (Exception e) { Log.e(LogUtils.TAG,"an error occured when collect crash info: ", e); } } } public String getLogInfo() { StringBuilder sb = new StringBuilder(""); String logContent = ""; if (mLogFilter != null) { try { String file = mLogFilter.getLogFileName(); File f = new File(file); if (!f.exists()) { return ""; } FileInputStream fin = new FileInputStream(file); int length = fin.available(); byte[] buffer = new byte[length]; fin.read(buffer); logContent = EncodingUtils.getString(buffer, "UTF-8"); } catch (IOException e) { e.printStackTrace(); } } return logContent; } /** * 回收宝工程师、用户app向后台上传崩溃日志 */ public void uploadLog() { LogUtils.w("upload log"); String logInfo = getLogInfo(); if ("".equals(logInfo)) { return; } String uid = (String) SharedPreferencesUtils.getParam(mContext, Constant.UID, "-1"); String userkey = (String) SharedPreferencesUtils.getParam(mContext, Constant.UKEY, "-1"); String utype = (String) SharedPreferencesUtils.getParam(mContext, Constant.UTYPE, "-1"); String username = (String) SharedPreferencesUtils.getParam(mContext, Constant.USERNAME, "-1"); HttpUtils httpUtils = new HttpUtils(); httpUtils.configCurrentHttpCacheExpiry(0); httpUtils.configDefaultHttpCacheExpiry(0); JSONObject data = new JSONObject(); try { data.put("uid", uid); data.put("ukey", userkey); data.put("time", mStartTime); data.put("type", "0"); data.put("systype", "1"); data.put("uname", username); data.put("logdata", logInfo); data.put("utype", utype); } catch (JSONException e) { e.printStackTrace(); } RequestParams params = new RequestParams(); params.addBodyParameter("data", data.toString()); httpUtils.send(HttpMethod.POST, Constant.UPLOADE_LOG, params, new RequestCallBack<String>() { @Override public void onFailure(HttpException arg0, String arg1) { LogUtils.w("员工版向后台上传日志失败"); } @Override public void onSuccess(ResponseInfo<String> arg0) { try { JSONObject json = new JSONObject(arg0.result); if ("0".equals(json.getString("ret"))) { LogUtils.w("员工版向后台上传日志成功"); } else { LogUtils.w(Constant.UPLOADE_LOG + json.toString()); } } catch (JSONException e) { e.printStackTrace(); } } }); stop(); } private class LogFilter extends Thread { private String mLogFileDir; private Process logcatProc; private BufferedReader mReader = null; private boolean mRunning = false; private String cmds = null; private final String mPID; private FileOutputStream out = null; private List<String> logsMessage = new ArrayList<String>(); private boolean mLogFileLock = false; private String mLogFileName; public void setLogFileLock(boolean lock) { mLogFileLock = lock; } public boolean isLogFileLock() { return mLogFileLock; } public LogFilter(String pid, String file) { mPID = String.valueOf(pid); mLogFileDir = file; initLogFile(); // 日志等级:*:v , *:d , *:w , *:e , *:f , *:s 显示当前mPID程序的 E和W等级的日志. cmds = "logcat -v time *:e *:w | grep \"(" + mPID + ")\""; } private void initLogFile() { try { Runtime.getRuntime().exec("logcat -c"); } catch (IOException e1) { e1.printStackTrace(); } File logFile = new File(mLogFileDir, LOG_FILE_NAME); if (logFile.exists()) { logFile.delete(); } try { logFile.createNewFile(); } catch (IOException e) { e.printStackTrace(); } try { mLogFileName = logFile.toString(); out = new FileOutputStream(logFile, true); } catch (FileNotFoundException e) { e.printStackTrace(); } } public String getLogFileName() { return mLogFileName; } public void stopLogs() { mRunning = false; } private boolean checkFileMaxSize(String file) { File sizefile = new File(file); if (sizefile.exists()) { // 1.5MB if (sizefile.length() > 1572864) { return true; } else { return false; } } else { return false; } } @Override public void run() { System.out.println("LogCatHelper'"); mRunning = true; try { // 收集设备信息 collectDeviceInfo(); logcatProc = Runtime.getRuntime().exec(cmds); mReader = new BufferedReader(new InputStreamReader( logcatProc.getInputStream()), 1024); String line = null; while (mRunning && (line = mReader.readLine()) != null) { if (!mRunning) { break; } if (line.length() == 0) { continue; } synchronized (out) { if (out != null) { boolean maxSize = checkFileMaxSize(getLogFileName()); if (maxSize) { // 文件大小超过1.5mb sendMaxLogMessage(); } if (isLogFileLock()) { if (line.contains(mPID)) { logsMessage.add(line.getBytes() + "\n"); } } else { if (logsMessage.size() > 0) { for (String _log : logsMessage) { LogUtils.w("write: " + logsMessage); out.write(_log.getBytes()); } logsMessage.clear(); } // 再次过滤日志,筛选当前日志中有 mPID 则是当前程序的日志. if (line.contains(mPID)) { out.write(line.getBytes()); out.write("\n".getBytes()); } } } } } } catch (IOException e) { e.printStackTrace(); return; } finally { if (logcatProc != null) { logcatProc.destroy(); logcatProc = null; } if (mReader != null) { try { mReader.close(); mReader = null; } catch (IOException e) { e.printStackTrace(); } } if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } out = null; } } } } }
c) 当然不要忘记添加权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
使用方法:
LogcatHelper helper = LogcatHelper.getInstance(this); helper.init(this); helper.start();
需要停止收集日志则直接调用stop即可。
三、内存管理
/**内存管理*/ @Override public void onTrimMemory(int level) { switch (level) { case TRIM_MEMORY_COMPLETE:// 即将清理后台进程列表最后一个 LogUtils.w("onTrimMemory:即将清理后台进程列表最后一个" + level); break; case TRIM_MEMORY_MODERATE:// 即将清理后台进程列表中部 LogUtils.w("onTrimMemory:即将清理后台进程列表中部" + level); break; case TRIM_MEMORY_BACKGROUND:// 内存不足,切该进程是后台进程 LogUtils.w("onTrimMemory:内存不足,切该进程是后台进程" + level); break; case TRIM_MEMORY_UI_HIDDEN:// 内存不足,并且该进程ui已不可见 LogUtils.w("onTrimMemory:内存不足,并且该进程ui已不可见" + level); break; } }
################################################