Android中Crash收集

问题
Android应用不可避免会发生Crash,不管你的代码写得有多风骚,在这个复杂的网络环境中,Crash还是时常的会发生。也就是常说的应用程序发生崩溃。常见表现就是闪屏然后退出。

原因
有些Crash是只在特定网络环境中才会出现,比如说网络环境为2g的时候。而Crash是发生在用户手机端,如果我们不对这些异常信息进行收集,那我们就没办法分析出Crash的原因,也就无法进行修复。下面就介绍一下如何收集这些Crash信息。

问题分析:
首先,Crash是发生在客户端,并且带有不确定性,即有的用户可能发生而又的不发生。所以,我们可以在应用程序发生Crash后,对Crash信息进行收集,保存在本地客户端(如写入的sd卡),然后在合适的时候(如网络环境好)将Crash文件上传到服务器进行统计分析。

技术实现:
Thread中提供了一个方法叫做setDefaultUncaughtUncaughtExceptionHandler(UncaughtExceptionHandler handler)的方法,用来设置一个UncaughtExceptionHandler对象。

这个对象有什么用呢?

作用:当Crash发生的时候,系统就会回调UncaughtExceptionHandler 中的uncaughtException方法。所以我们就可以在这个方法中去保存crash日志。然后在时机好的时候发送至服务器。

具体逻辑
下面代码中,主要是创建了一个实现Thread.UncaughtExceptionHandler接口的类CrashHandler。
然后以单例的形式向外提供支持。

主要的方法有:

  • init (Context) 进行初始化
  • uncaughtException(Thread , Throwable ) 复写的方法,每次Crash之后被调用
  • handleException(Throwable)处理Crash的逻辑
  • uploadExceptionToServer()上传到服务器的实现了
  • dumpExceptionToSDCard(Throwable )保存到sd卡的实现类
  • dumpPhoneInfo(PrintWriter)获取收集信息

实现的逻辑
在Application中调用init()方法初始化CrashHandler,当发生Crash的时候CrashHandler的uncaughtException方法被调用。然后在这个方法中调用handleException方法,handleException方法中再去组织具体逻辑(调用具体实现类)。uploadExceptionToServer、dumpExceptionToSDCard、dumpPhoneInfo这三个方法则是具体逻辑实现。

代码实现:

  • CrashHandler 类:
public class CrashHandler implements Thread.UncaughtExceptionHandler {

    private static final String TAG = "CrashHandler";
    private static final boolean DEBUG = true;

    private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";

    private static final String FILE_NAME = "crash";
    private static final String FILE_NAME_SUFFIX = ".txt";

    private static CrashHandler sInstance = new CrashHandler();

    private Context mContext;

    //私有构造器
    private CrashHandler() {

    }

    //单例模式
    public static CrashHandler getInstance() {

        return sInstance;
    }

    //初始化
    public void init(Context context) {

        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context;

    }

    /**
     * 重写uncaughtException
     * @param t 发生Crash的线程
     * @param ex Throwale对象
     */
    @Override
    public void uncaughtException(Thread t, Throwable ex) {
        //处理逻辑需要开启一个子线程,用于文件的写入操作
        handleException(ex);
        //在程序关闭之前休眠2秒,以避免在文件写入的操作完
        //成。之前进程被杀死。
        //也可以考虑弹出对话框友好提示用户
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.exit(0);
//        Process.killProcess(Process.myPid());
    }

    //处理异常
    private void handleException(final Throwable ex) {
        try {
            Executors.newSingleThreadExecutor().submit(new Runnable() {
                @Override
                public void run() {
                    dumpExceptionToSDCard(ex);
                    uploadExceptionToServer();
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 将异常信息上传至服务器
     */
    private void uploadExceptionToServer() {


    }

    /**
     * 将异常信息写入sd卡
     * @param ex
     */
    private void dumpExceptionToSDCard(Throwable ex) {
        //判断是否支持SD卡
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            if (DEBUG) {
                Log.i(TAG, "sdcard unfind ,skip dump exception");
                return;
            }
        }

        File dir = new File(PATH);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        long current = System.currentTimeMillis();
        String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date(current));

        File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);

        try {
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));

            pw.println(time);
            dumpPhoneInfo(pw);
            pw.println();
            //将抛出的异常信息写入到文件
            ex.printStackTrace(pw);
            pw.close();
        } catch (Exception e) {
            Log.d(TAG, "dump Exception Exception" + e.getMessage());
            e.printStackTrace();
        }

    }


    /**
     *
     * 获取手机信息
     * @param pw 写入流
     * @throws PackageManager.NameNotFoundException 异常
     */
    private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {

        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);

        pw.print("App Version : ");
        pw.print(pi.versionName);
        Log.d(TAG,"name : "+pi.versionName);
        pw.print('_');
        pw.println(pi.versionCode);

        pw.print("OS Version : ");
        pw.print(Build.VERSION.RELEASE);
        pw.print("_");
        pw.println(Build.VERSION.SDK_INT );

        pw.print("Vendor : ");
        pw.println(Build.MANUFACTURER);

        pw.print("Model : ");
        pw.println(Build.MODEL);
        pw.print("Cpu ABI : ");
        pw.println(Build.CPU_ABI);
    }

}
  • Application中初始化:
public class App extends Application {

    private  Context mContext;

    private static App sInstance;

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;

        CrashHandler crashHandler = CrashHandler.getInstance();
        crashHandler.init(this);

        mContext = getApplicationContext();

    }


    public  Context getContext() {
        return mContext;
    }

    public static App getsInstance() {
        return sInstance;
    }

}

大致实现就是这样,主要是学习 Thread.UncaughtExceptionHandler和了解uncaughtException方法在进程Crash后会被调用。然后在这个基础上去处理逻辑(重写uncaughtException方法)。

后记
大家可以自己抛出一个异常,去测试一下是否成功。比如在Activity中使用以下代码:

  throw new RuntimeException("自定义异常");

成功生成的文件内容:
文件内容

遇到的坑

  • 文件创建失败,可能是文件的路径不规范
  • 文件打开权限不足(Read Only),6.0以上需要动态权限申请
  • 开启一个子线程去处理文件的读写,并且将当前线程sleep一定的时间。才能保证在文件写完后,程序才被杀死。
  • 文件找不到,代码中显示已经生成,而使用DDMS查看或手机上自带的文件管理程序查看的时候却没有。可以考虑直接使用adb shell查看或者使用RE文件管理器。

参考书籍

  • 《Android开发与艺术探索》
发布了150 篇原创文章 · 获赞 138 · 访问量 26万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章