1. 程式人生 > >Android全域性捕獲崩潰異常記錄日誌儲存至本地並定時刪除

Android全域性捕獲崩潰異常記錄日誌儲存至本地並定時刪除



MainActivity內的程式碼如下:
package com.example.mycrashtest;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.widget.TextView;

public class MainActivity extends Activity {
	
	private TextView tv;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		tv.setText("我沒初始化");
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// Inflate the menu; this adds items to the action bar if it is present.
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

}

在這裡,我沒對TextView進行初始化,直接給其進行設定文字,會造成執行時異常,沒有對此異常進行處理,系統會彈出停止執行並強制關閉。



三.Appcation與Thread.UncaughtExceptionHandler

Appcation:Application和Activity,Service一樣,是android框架的一個系統元件,當android程式啟動時系統會建立一個 application物件,用來儲存系統的一些資訊。通常我們是不需要指定一個Application的,這時系統會自動幫我們建立,如果需要建立自己 的Application,也很簡單建立一個類繼承 Application並在manifest的application標籤中進行註冊(只需要給Application標籤增加個name屬性把自己的 Application的名字定入即可)。
    android系統會為每個程式執行時建立一個Application類的物件且僅建立一個,所以Application可以說是單例 (singleton)模式的一個類.且application物件的生命週期是整個程式中最長的,它的生命週期就等於這個程式的生命週期。因為它是全域性 的單例的,所以在不同的Activity,Service中獲得的物件都是同一個物件。所以通過Application來進行一些,資料傳遞,資料共享 等,資料快取等操作。

Thread.UncaughtExceptionHandler:當某一執行緒因未捕獲的異常而即將終止時,Java 虛擬機器將使用
Thread.getUncaughtExceptionHandler()
查詢該執行緒以獲得其 UncaughtExceptionHandler 的執行緒,並呼叫處理程式的uncaughtException 方法,將執行緒和異常作為引數傳遞。如果某一執行緒沒有明確設定其UncaughtExceptionHandler,則將它的ThreadGroup 物件作為其UncaughtExceptionHandler。如果ThreadGroup 物件對處理異常沒有什麼特殊要求,那麼它可以將呼叫轉發給預設的未捕獲異常處理程式
我們需要實現此介面,並註冊為程式中預設未捕獲異常處理。這樣當未捕獲異常發生時,就可以做一些個性化的異常處理操作。

四.程式碼實現

<pre name="code" class="java">package com.example.mycrashtest;

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.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.widget.Toast;

/**
 * <h3>全域性捕獲異常</h3>
 * <br>
 * 當程式發生Uncaught異常的時候,有該類來接管程式,並記錄錯誤日誌
 * 
 */
@SuppressLint("SimpleDateFormat")
public class CrashHandler implements UncaughtExceptionHandler {

    public static String TAG = "MyCrash";
    // 系統預設的UncaughtException處理類
    private Thread.UncaughtExceptionHandler mDefaultHandler;

    private static CrashHandler instance = new CrashHandler();
    private Context mContext;

    // 用來儲存裝置資訊和異常資訊
    private Map<String, String> infos = new HashMap<String, String>();

    // 用於格式化日期,作為日誌檔名的一部分
    private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

    /** 保證只有一個CrashHandler例項 */
    private CrashHandler() {
    }

    /** 獲取CrashHandler例項 ,單例模式 */
    public static CrashHandler getInstance() {
        return instance;
    }

    /**
     * 初始化
     * 
     * @param context
     */
    public void init(Context context) {
        mContext = context;
        // 獲取系統預設的UncaughtException處理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 設定該CrashHandler為程式的預設處理器
        Thread.setDefaultUncaughtExceptionHandler(this);
        autoClear(5);
    }

    /**
     * 當UncaughtException發生時會轉入該函式來處理
     */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            // 如果使用者沒有處理則讓系統預設的異常處理器來處理
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            SystemClock.sleep(3000);
            // 退出程式
            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;

        try {
            // 使用Toast來顯示異常資訊
            new Thread() {

                @Override
                public void run() {
                    Looper.prepare();
                    Toast.makeText(mContext, "很抱歉,程式出現異常,即將重啟.",
                            Toast.LENGTH_LONG).show();
                    Looper.loop();
                }
            }.start();
            // 收集裝置引數資訊
            collectDeviceInfo(mContext);
            // 儲存日誌檔案
            saveCrashInfoFile(ex);
            SystemClock.sleep(3000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return true;
    }

    /**
     * 收集裝置引數資訊
     * 
     * @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 + "";
                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());
            } catch (Exception e) {
                Log.e(TAG, "an error occured when collect crash info", e);
            }
        }
    }

    /**
     * 儲存錯誤資訊到檔案中
     * @param ex
     * @return 返回檔名稱,便於將檔案傳送到伺服器
     * @throws Exception
     */
    private String saveCrashInfoFile(Throwable ex) throws Exception {
        StringBuffer sb = new StringBuffer();
        try {
            SimpleDateFormat sDateFormat = new SimpleDateFormat(
                    "yyyy-MM-dd HH:mm:ss");
            String date = sDateFormat.format(new java.util.Date());
            sb.append("\r\n" + date + "\n");
            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.flush();
            printWriter.close();
            String result = writer.toString();
            sb.append(result);

            String fileName = writeFile(sb.toString());
            return fileName;
        } catch (Exception e) {
            Log.e(TAG, "an error occured while writing file...", e);
            sb.append("an error occured while writing file...\r\n");
            writeFile(sb.toString());
        }
        return null;
    }

    private String writeFile(String sb) throws Exception {
        String time = formatter.format(new Date());
        String fileName = "crash-" + time + ".log";
        if (FileUtil.hasSdcard()) {
            String path = getGlobalpath();
            File dir = new File(path);
            if (!dir.exists())
                dir.mkdirs();

            FileOutputStream fos = new FileOutputStream(path + fileName, true);
            fos.write(sb.getBytes());
            fos.flush();
            fos.close();
        }
        return fileName;
    }

    public static String getGlobalpath() {
        return Environment.getExternalStorageDirectory().getAbsolutePath()
                + File.separator + "crash" + File.separator;
    }
    
    public static void setTag(String tag) {
        TAG = tag;
    }
    
    /**
     * 檔案刪除
     * @param day 檔案儲存天數
     */
    public void autoClear(final int autoClearDay) {
    	FileUtil.delete(getGlobalpath(), new FilenameFilter() {

            @Override
            public boolean accept(File file, String filename) {
                String s = FileUtil.getFileNameWithoutExtension(filename);
                int day = autoClearDay < 0 ? autoClearDay : -1 * autoClearDay;
                String date = "crash-" + DateUtil.getOtherDay(day);
                return date.compareTo(s) >= 0;
            }
        });
    	
    }

}

需要在自己的Appcation類中初始化這個CrashHandler:
package com.example.mycrashtest;

import android.app.Application;

public class MyAppcation extends Application{
	
	@Override
	public void onCreate() {
		super.onCreate();
		CrashHandler.getInstance().init(this);
	}

}
在AndroidManifest.xml檔案中需要修改為自己的Appcation,並且加入操作SDCARD許可權(寫日誌):




四.執行結果

系統會自動在SDCARD下建立crash目錄:


開啟該檔案:


至此,整個專案梳理完畢,關於將日誌檔案上傳FTP,後面會另外講述,部分知識本人也處於學習階段,不足之處望大家諒解並提出,共同學習,謝謝!

專案下載:點選開啟連結