定制Android异常信息捕获类

吾生也有涯,而知也无涯。以有涯随无涯,殆已!

一、简介:UncaughtExceptionHandler

Android应用不可避免地会发生crash,也称之为崩溃,一个程序无论写的多么完美,都无法完全避免crash的发生。如果项目未上线,我们可以根据错误日志或debug调试来处理,但是如果项目已上线,且该异常现象无法复现,那么这个crash信息是很难获取到的,这非常不利于一个产品的课持续发展。幸运的是,Android提供了一个可以全局捕获crash信息的方法,即Thread类中的一个方法setDefaultUncaughtExceptionHandler:

 /**
     * Sets the default uncaught exception handler. This handler is invoked in
     * case any Thread dies due to an unhandled exception.
     *
     * @param handler
     *            The handler to set or null.
     */
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
        Thread.defaultUncaughtHandler = handler;
    }

注解的大致意思就是“设置默认未捕获的异常处理者,当程序发生异常崩溃时,该处理者可以在任意线程被调用”。当crash发生的时候,系统就会回调UncaughtExceptionHandler的uncaughtException方法,在该方法中我们便可以可以对异常信息进行处理。

二、使用:UncaughtExceptionHandler

1、写一个类实现UncaughtExceptionHandler接口,在它的uncaughtException方法中获取异常信息并将其存储在SD卡中或上传到服务器。
2、在Application中对这个类进行初始化。

三、具体实现

下面是一个典型的异常处理器的实现:

public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
    /** Debug Log Tag */
    public static final String TAG = "MyCrashHandler ";
    /** 是否开启日志输出, 在Debug状态下开启, 在Release状态下关闭以提升程序性能 */
    public static final boolean DEBUG = true;
    /** CrashHandler实例 */
    private static CrashHandler INSTANCE;
    /** 程序的Context对象 */
    private Context mContext;
    /** 系统默认的UncaughtException处理类 */
    private Thread.UncaughtExceptionHandler mDefaultHandler;

    /** 使用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 String msg;

    private MyCrashHandler () {

    }

    /** 获取CrashHandler实例 ,单例模式 */
    public static CrashHandler getInstance() {
        if (INSTANCE == null)
            INSTANCE = new CrashHandler();
        return INSTANCE;
    }

    /**
     * 初始化,注册Context对象, 获取系统默认的UncaughtException处理器, 设置该CrashHandler为程序的默认处理器
     * 
     * @param ctx
     *            Context
     */
    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 {
            // 让线程停止一会是为了显示Toast信息给用户,然后Kill程序
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                Log.e(TAG, "Error : ", e);
            }
            Process.killProcess(android.os.Process.myPid());
        }
    }

    /**
     * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成. 开发者可以根据自己的情况来自定义异常处理逻辑
     * 
     * @param ex
     *            异常
     * @return true:如果处理了该异常信息;否则返回false
     */
    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return true;
        }
        msg = getExceptTrace(ex);
        if (DEBUG) {
            Log.e(TAG, msg);
        }
        // 使用Toast来显示异常信息
        new Thread() {
            @Override
            public void run() {
                // Toast 显示需要出现在一个线程的消息队列中
                Looper.prepare();
                View view = LayoutInflater.from(mContext).inflate(
                        R.layout.error_dialog_layout, null);
                TextView tvTitle = (TextView) view
                        .findViewById(R.id.dialog_title);
                TextView tvContent = (TextView) view
                        .findViewById(R.id.dialog_content);
                tvTitle.setText("程序异常!");
                tvContent.setText(msg);
                Toast toast = new Toast(mContext);
                toast.setGravity(Gravity.CENTER, 0, 0);
                toast.setDuration(Toast.LENGTH_LONG);
                toast.setView(view);
                toast.show();
                Looper.loop();
            }
        }.start();
        // 收集设备信息
        collectCrashDeviceInfo(mContext);
        // 保存错误报告文件
        String crashFileName = saveCrashInfoToFile(ex);
        sendCrashReportsToServer(mContext);
        return true;
    }

    /**
     * 收集程序崩溃的设备信息
     * 
     * @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 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段
        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                // setAccessible(boolean flag)
                // 将此对象的 accessible 标志设置为指示的布尔值。
                // 通过设置Accessible属性为true,才能对私有变量进行访问,不然会得到一个IllegalAccessException的异常
                field.setAccessible(true);
                mDeviceCrashInfo.put(field.getName(), field.get(null)
                        .toString());
            } catch (Exception e) {
                Log.e(TAG, "Error while collect crash info", e);
            }
        }
    }

    /**
     * 保存错误信息到文件中
     * 
     * @param ex
     *            异常
     * @return 保存的文件名
     */
    private String saveCrashInfoToFile(Throwable ex) {
        if (msg == null || msg.equals("")) {
            msg = getExceptTrace(ex);
        }
        mDeviceCrashInfo.put(STACK_TRACE, msg);

        try {
            long timestamp = System.currentTimeMillis();
            String fileName = "Android_crash-" + timestamp
                    + 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
     *            Context
     */
    private void sendCrashReportsToServer(Context ctx) {
        String[] crFiles = getCrashReportFiles(ctx);
        if (crFiles != null && crFiles.length > 0) {
            TreeSet sortedFiles = new TreeSet();
            sortedFiles.addAll(Arrays.asList(crFiles));

            for (String fileName : sortedFiles) {
                File cr = new File(ctx.getFilesDir(), fileName);
                postReport(cr);
            }
        }
    }

    /**
     * 获取错误报告文件名
     * 
     * @param ctx
     *            Context
     * @return
     */
    private String[] getCrashReportFiles(Context ctx) {
        File filesDir = ctx.getFilesDir();
        // 实现FilenameFilter接口的类实例可用于过滤器文件名
        FilenameFilter filter = new FilenameFilter() {
            // accept(File dir, String name)
            // 测试指定文件是否应该包含在某一文件列表中。
            public boolean accept(File dir, String name) {
                return name.endsWith(CRASH_REPORTER_EXTENSION);
            }
        };
        // list(FilenameFilter filter)
        // 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中满足指定过滤器的文件和目录
        return filesDir.list(filter);
    }

    // 使用HTTP Post 发送错误报告到服务器
    private void postReport(final File file) {

    }

    /**
     * 在程序启动时候, 可以调用该函数来发送以前没有发送的报告
     */
    public void sendPreviousReportsToServer() {
        sendCrashReportsToServer(mContext);
    }

    /**
     * 获得异常追踪信息
     * 
     * @param ex
     *            Throwable
     */
    private String getExceptTrace(Throwable ex) {
        Writer info = new StringWriter();
        PrintWriter printWriter = new PrintWriter(info);
        // printStackTrace(PrintWriter s)
        // 将此 throwable 及其追踪输出到指定的 PrintWriter
        ex.printStackTrace(printWriter);

        // getCause() 返回此 throwable 的 cause;如果 cause 不存在或未知,则返回 null。
        Throwable cause = ex.getCause();
        while (cause != null) {
            cause.printStackTrace(printWriter);
            cause = cause.getCause();
        }

        // toString() 以字符串的形式返回该缓冲区的当前值。
        String result = info.toString();
        printWriter.close();
        return result;
    }
}

在Application中注册未捕获异常处理器:

public class MyApp extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
        //在这里为应用设置异常处理,然后程序才能获取未处理的异常
        CrashHandler crashHandler = CrashHandler.getInstance();
        crashHandler.init(this);
    }
}

最后别忘了在AndroidManifest中声明MyApp:

如果要将异常信息存储到SD卡上,还需在AndroidManifest中进行相关权限的声明:



至此,我们便完成了所有crash信息捕获的工作,当手机APP发生异常崩溃时,我们便可以在手机SD卡或者服务器上查看相关异常信息了,如下图所示:


MyCrashHandler 捕获的异常信息文件

你可能感兴趣的:(定制Android异常信息捕获类)