Android全域性異常捕獲機制
文章背景
程式猿或是程式媛們在開發Android專案的時候,經常出現各種奇葩的Crash,有可能是服務端返回資料的原因所造成的、也可能是客戶端自己的原因。我個人認為出現Bug並不是那麼的重要,快速定位問題才是解決問題的開始、如果我們有一個能夠幫助我們快速定位異常的機制,那麼不僅在開發的效率上提高,而且在維護成本也會在一定程度上降低成本。
那麼目前主流的第三方Bug分析框架有騰訊的Bugly和友盟都能夠很好的統計分析各種Crash等Bug,但是在我們做專案中,難免面對的使用者是政府或銀行等客戶,這類客戶有一些硬性的的要求,就是不允許嵌入第三方的框架,有可能是安全方面的擔心,怕被監控或是盜取資料等。
另一方面來講,現在的第三方庫提供商,總說自己的sdk特牛逼、然而一下載下來一看就有幾十兆、甚至有幾百兆、如果嵌入太多的第三方sdk會增大apk大小、若是網際網路產品,包越大越難在使用者手機上存活。
文章目標
為我們的專案提供一個異常捕獲跟蹤處理機制,我認為應包含捕獲異常、寫入異常資料到SD卡中、定時上傳異常資料給服務端、服務端統計分析異常、最終目標為解決異常從而提高程式碼的健壯性。
下面提供一個客戶端這邊的異常處理類,先上個演示效果圖:
異常資訊列印效果:
日誌寫入效果圖:
異常處理類:
package com.example.yangdechengapplication.tools;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Looper;
import android.text.format.Time;
import android.util.Log;
import android.view.Gravity;
import android.widget.Toast;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Properties;
import java.util.TreeSet;
/**
* Created by Administrator on 2017/6/15.
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
/** Debug Log 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();
/** 保證只有一個CrashHandler例項 */
private CrashHandler() {}
/** 獲取CrashHandler例項 ,單例模式*/
public static CrashHandler getInstance() {
/* if (INSTANCE == null) {
INSTANCE = new CrashHandler();
}
return INSTANCE;*/
// 防止多執行緒訪問安全,這裡使用了雙重鎖
if (INSTANCE == null)
{
synchronized (syncRoot)
{
if (INSTANCE == null)
{
INSTANCE = new CrashHandler();
}
}
}
return INSTANCE;
}
/**
* 初始化,註冊Context物件,
* 獲取系統預設的UncaughtException處理器,
* 設定該CrashHandler為程式的預設處理器
* @param ctx
*/
public void init(Context ctx) {
mContext = ctx;
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 當UncaughtException發生時會轉入該函式來處理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
//如果使用者沒有處理則讓系統預設的異常處理器來處理
mDefaultHandler.uncaughtException(thread, ex);
} else {
//Sleep一會後結束程式
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(TAG, "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) {
Log.w(TAG, "handleException --- ex==null");
return true;
}
final String msg = ex.getLocalizedMessage();
if(msg == null) {
return false;
}
//使用Toast來顯示異常資訊
new Thread() {
@Override
public void run() {
Looper.prepare();
if(DEBUG){
Log.d(TAG, "異常資訊->"+msg);
Toast toast = Toast.makeText(mContext, "程式出錯,即將退出:\r\n" + msg,
Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
//儲存錯誤報告檔案
LogToFile.w("my",msg);**//這句話可以先註釋掉,這是我單獨寫的一個log寫入類,下面已提供了該類**
}
// MsgPrompt.showMsg(mContext, "程式出錯啦", msg+"\n點確認退出");
Looper.loop();
}
}.start();
//收集裝置資訊
collectCrashDeviceInfo(mContext);
//儲存錯誤報告檔案
//saveCrashInfoToFile(ex);
//傳送錯誤報告到伺服器
//sendCrashReportsToServer(mContext);
return true;
}
/**
* 在程式啟動時候, 可以呼叫該函式來發送以前沒有傳送的報告
*/
public void sendPreviousReportsToServer() {
sendCrashReportsToServer(mContext);
}
/**
* 把錯誤報告發送給伺服器,包含新產生的和以前沒傳送的.
* @param ctx
*/
private void sendCrashReportsToServer(Context ctx) {
String[] crFiles = getCrashReportFiles(ctx);
if (crFiles != null && crFiles.length > 0) {
TreeSet<String> sortedFiles = new TreeSet<String>();
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);
}
/**
* 儲存錯誤資訊到檔案中
* @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 {
//long timestamp = System.currentTimeMillis();
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) {
Log.e(TAG, "an error occured while writing report file...", e);
}
return null;
}
/**
* 收集程式崩潰的裝置資訊
*
* @param ctx
*/
public void 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) {
Log.e(TAG, "Error while collect package info", e);
}
//使用反射來收集裝置資訊.在Build類中包含各種裝置資訊,
//例如: 系統版本號,裝置生產商 等幫助除錯程式的有用資訊
//具體資訊請參考後面的截圖
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
mDeviceCrashInfo.put(field.getName(), ""+field.get(null));
if (DEBUG) {
Log.d(TAG, field.getName() + " : " + field.get(null));
}
} catch (Exception e) {
Log.e(TAG, "Error while collect crash info", e);
}
}
}
}
使用方法:
package com.example.yangdechengapplication;
import android.app.Application;
import com.example.yangdechengapplication.tools.CrashHandler;
import com.example.yangdechengapplication.tools.LogToFile;
/**
* Created by Administrator on 2017/6/15.
*/
public class MyApplication extends Application {
private final static float HEAP_UTILIZATION = 0.75f;
private final static int MIN_HEAP_SIZE = 6* 1024* 1024 ;
@Override
public void onCreate() {
super.onCreate();
// 異常處理,不需要處理時註釋掉這兩句即可!
CrashHandler crashHandler = CrashHandler.getInstance();
// 註冊crashHandler
crashHandler.init(getApplicationContext());
}
}
開始測試:
package com.example.yangdechengapplication;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import com.example.yangdechengapplication.tools.LogToFile;
public class MainActivity extends AppCompatActivity {
Button btn_crashHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_crashHandler=(Button)findViewById(R.id.btn_crashHandler);
btn_crashHandler.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int a= Integer.parseInt("q");//當用戶點選按鈕的時候特意寫了一個異常
//LogToFile.w("my","我在測試檔案讀寫66666666666啊");
}
});
}
}
LogToFile(異常日誌寫入類,已單獨抽取出來了,也算職責分明吧)
注:寫入日誌需要在配置檔案中加上檔案讀寫許可權
<!--往sdcard中寫入資料的許可權 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<!--在sdcard中建立/刪除檔案的許可權 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission>
package com.example.yangdechengapplication.tools;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* Created by Administrator on 2017/6/15.
*/
public class LogToFile {
//日誌是否需要讀寫開關
public static final boolean DEBUG_FLAG = false;
private static String TAG = "LogToFile";
private static String logPath = null;//log日誌存放路徑
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US);//日期格式;
private static Date date = new Date();//因為log日誌是使用日期命名的,使用靜態成員變數主要是為了在整個程式執行期間只存在一個.log檔案中;
/**
* 初始化,須在使用之前設定,最好在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.MEDIA_MOUNTED) || !Environment.isExternalStorageRemovable()) {//如果外部儲存可用
return context.getExternalFilesDir(null).getPath();//獲得外部儲存路徑,預設路徑為 /storage/emulated/0/Android/data/com.waka.workspace.logtofile/files/Logs/log_2016-03-14_16-15-09.log
} 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;
}
String fileName = logPath + "/log_" + dateFormat.format(new Date()) + ".log";//log日誌名,使用時間命名,保證不重複
String log = dateFormat.format(date) + " " + type + " " + tag + " " + msg + "\n";//log日誌內容,可以自行定製
//如果父路徑不存在
File file = new File(logPath);
if (!file.exists()) {
file.mkdirs();//建立父路徑
}
FileOutputStream fos = null;//FileOutputStream會自動呼叫底層的close()方法,不用關閉
BufferedWriter bw = null;
try {
fos = new FileOutputStream(fileName, true);//這裡的第二個引數代表追加還是覆蓋,true為追加,flase為覆蓋
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();
}
}
}
}
其實正確的做法應該是在開發過程中就養成良好的編碼習慣和敏銳的嗅覺才對,能夠使用各種設計模式編寫出可維護、可擴充套件、可複用、靈活性好的程式、避免出現不必要的Bug,作為團隊中的一員我們應該駕馭好自己的一畝三分地。
注:在我的前期部落格中已經提供了幾種常用的設計模式文章,能夠對大家有所幫助。