CrashHandler--程序异常退出处理

  • 前言
  • UncaughtExceptionHandler
    • 实现自己的UncaughtExceptionHandler
    • 遇到的问题
  • 结尾

前言

       作为一个android开发,经常遇到crash情况。原因各种各样,即使是经过了测试的大量检测,但是到用户手上还是会遇到闪退。这和android设备的碎片化有关,也和使用时的环境有关,比如弱网,比如高铁频繁切换小区等等。然而我们不能让用户帮我们抓取log,那要怎么才能知道为什么闪退了呢?


UncaughtExceptionHandler

       幸运的是:安卓已经帮我们想好了解决问题的接口(UncaughtExceptionHandler)。从名称上就知道这是用来处理没有捕捉到的野生Exception的。平时我们try catch的Exception的那就叫捕捉到的。看一下UncaughtExceptionHandler的源码:

public interface UncaughtExceptionHandler {
   /**
    * Method invoked when the given thread terminates due to the
    * given uncaught exception.
    * 

Any exception thrown by this method will be ignored by the * Java Virtual Machine. * @param t the thread * @param e the exception */ void uncaughtException(Thread t, Throwable e); }

代码很简单,这是一个接口,并且只有一个方法。当出现未捕捉的exception时,系统会回调这个方法。具体实现在ThreadGroup.javauncaughtException:

public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler(); //获取当前默认的UncaughtExceptionHandler
        if (ueh != null) {
            ueh.uncaughtException(t, e); //此处回调uncaughtException()方法
        } else if (!(e instanceof ThreadDeath)) {
            System.err.print("Exception in thread \""
                             + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

实现自己的UncaughtExceptionHandler

       从上面的源码分析我们知道,只要我们重写一个类实现UncaughtExceptionHandler接口,替换当前线程的默认UncaughtExceptionHandler对象就行。系统为我们提供了方法Thread.getDefaultUncaughtExceptionHandler()Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler u)


public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String TAG = CrashHandler.class.getSimpleName();
    private static CrashHandler INSTANCE = new CrashHandler();
    private Context mContext;
    private Thread.UncaughtExceptionHandler mDefaultExceptionHandler;

    @Override
    public void uncaughtException(Thread t, Throwable e) {//当发生exception时候会回调该方法
        dumpToSDCard(t, e);//dump trace 信息到sd卡
        //todo 上传服务器
        e.printStackTrace();
        if (mDefaultExceptionHandler != null) { //交给系统的UncaughtExceptionHandler处理
            mDefaultExceptionHandler.uncaughtException(t, e);
        } else {
            Process.killProcess(Process.myPid()); //主动杀死进程
        }
    }

    private CrashHandler() {
    }

    public static CrashHandler getInstance() {
        return INSTANCE;
    }

    public void init(Context context) {
        this.mContext = context;
        mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();//获取当前默认ExceptionHandler,保存在全局对象
        Thread.setDefaultUncaughtExceptionHandler(this);//替换默认对象为当前对象
    }

    private void dumpToSDCard(final Thread t, final Throwable e) {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            Log.i(TAG, "no sdcard skip dump ");
            return;
        }

        String mLodPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/crashHandler/";
        File file = new File(mLodPath);
        if (!file.exists()) {
            file.mkdirs();
        }

        String time = new SimpleDateFormat("yyyy-mm-dd-HH:mm:ss", Locale.CHINA).format(new Date(System.currentTimeMillis()));
        Log.i(TAG, mLodPath + time + ".trace");
        File logFile = new File(mLodPath, time + ".trace");

        try {
            PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(logFile)));
            pw.println(time);
            dumpPhoneInfo(pw);
            pw.println();
            e.printStackTrace(pw);
            pw.close();

        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }

    private void dumpPhoneInfo(PrintWriter pw) {
        //应用的版本名称和版本号
        PackageManager pm = mContext.getPackageManager();
        PackageInfo pi = null;
        try {
            pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
            if (pi != null) {
                pw.print("App Version: ");
                pw.print(pi.versionName);
                pw.print('_');
                pw.println(pi.versionCode);

                //android版本号
                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);

                //cpu架构
                pw.print("CPU ABI: ");
                pw.println(Build.CPU_ABI);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }

    private void zip(String src, String dest) throws IOException { //压缩文件夹,为上传做准备。节省流量。
        ZipOutputStream out = null;
        File outFile = new File(dest);
        File fileOrDirectory = new File(src);
        out = new ZipOutputStream(new FileOutputStream(outFile));
        if (fileOrDirectory.isFile()) {
            zipFileOrDirectory(out, fileOrDirectory, "");
        }else {
            File[] entries = fileOrDirectory.listFiles();
            for (int i = 0; i < entries.length; i++) {
                zipFileOrDirectory(out, entries[i], "");
            }
        }
        if(null != out){
            out.close();
        }
    }

    private static void zipFileOrDirectory(ZipOutputStream out,File fileOrDirectory, String curPath) throws IOException {
        FileInputStream in = null;
        if (!fileOrDirectory.isDirectory()){
            byte[] buffer = new byte[4096];
            int bytes_read;
            in = new FileInputStream(fileOrDirectory);
            ZipEntry entry = new ZipEntry(curPath + fileOrDirectory.getName());
            out.putNextEntry(entry);
            while ((bytes_read = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytes_read);
            }
            out.closeEntry();
        }else{
            File[] entries = fileOrDirectory.listFiles();
            for (int i = 0; i < entries.length; i++) {
                zipFileOrDirectory(out, entries[i], curPath + fileOrDirectory.getName() + "/");
            }
        }
        if (null != in){
            in.close();
        }
    }
}

代码比较简单,就是替换当前线程默认的UncaughtExceptionHandler为我们自己实现的handler,当出现exception时候保存信息到sd卡,在上传服务器(还未实现 >_<||| )。为了节省流量,可以选择打包文件。
实现了之后怎么使用?
只需要在 application 的 onCreate() 中进行初始化

    public void onCreate() {
        super.onCreate();
        CrashHandler.getInstance().init(this);
    }

到这里基本就ok了。其实这些代码网上很多人都写过,我重写一遍加深记忆。但是很多人写到这就完成了,我想可能他们没有具体测试过,我自己实现了发现了不少问题。

遇到的问题

上面初始化之后,我们手贱的自己去抛个exception。
随便找个地儿来一句 throw new RuntimeException("测试Crash");
然后坐等生效。然而事实总是爱打脸。接下来说一下遇到的问题。

  1. 创建log 文件总是报错:No such file or directory
    一脸蒙蔽 ing.jpg ,什么鬼 。我不是做了判断:
if (!file.exists()) {
    file.mkdirs();
}

于是我打印了mkdirs()返回值,是false 创建失败。我想到了权限,看了 mainfast 有声明:WRITE_EXTERNAL_STORAGE。并且思丢丢也没有打印permission denied。而是报错No such file or directory。这是我打开了百度,一番搜索之后还是权限问题:6.0之后的动态申请权限。

    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = {
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE" };
    public static void verifyStoragePermissions(Activity activity) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
            try {
                //检测是否有写的权限
                int permission = ActivityCompat.checkSelfPermission(activity,
                        "android.permission.WRITE_EXTERNAL_STORAGE");
                if (permission != PackageManager.PERMISSION_GRANTED) {
                    // 没有写的权限,去申请写的权限,会弹出对话框
                    ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

申请之后,完美!文件可以生成了。
2. adb找不到文件,无法pull出来
文件生成之后,我们打开文件管理,看到crashHandler文件夹和里面的trace文件。把手机插到电脑上,用电脑的文件管理器访问sd卡,找不到crashHandler文件夹。。。。刷新插拔都找不到。不怕,我还有其他技能,adb命令。

adb shell
//ok 文件存在
bullhead:/ $ cd sdcard/cr
crashHandler/  crash_cache/
bullhead:/ $ cd sdcard/crashHandler/
bullhead:/sdcard/crashHandler $ ls
2018-43-14-09:43:43.trace
bullhead:/sdcard/crashHandler $ exit
//pull出来就行,但是找不到,刚刚不是还在,为啥?
adb pull /sdcard/crashHandler/2018-43-14-09:43:43.trace
adb: error: cannot create '.\2018-43-14-09:43:43.trace': No such file or directory
adb pull /sdcard/crashHandler
adb: error: cannot create '.\crashHandler\2018-43-14-09:43:43.trace': No such file or directory

上面这个问题折腾了好久,终于在一篇文章上找到了问题。安卓只在开机时候会扫描文件结构,之后不会主动去扫描,只有通知它扫描某个文件,它才会扫描新的文件加入到文件结构中。所以我就需要主动去驱动扫描新文件。(估计做图库的同学比较懂)

  String mLodPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/crashHandler/";
  MediaScannerConnection.scanFile(this.getApplicationContext(), new String[]{ mLodPath }, null, new MediaScannerConnection.OnScanCompletedListener() {
      @Override
      public void onScanCompleted(final String path, final Uri uri) {
          Log.i(TAG, "MediaScannerConnection ionScanCompleted ");
      }
  });

到此没毛病。其实还有简单方法就是重启手机,这样也可以解决。。这个问题如果不是要电脑查看是不会遇到的,因为代码是可以访问这个文件的,所以加不加无所谓。
贴一下文件内容:

2018-21-14-10:21:41
App Version: 1.0_1
OS Version: 7.1.1_25
Vendor: LGE
Model: Nexus 5X
CPU ABI: arm64-v8a

java.lang.RuntimeException: 测试Crash
    at com.lw.wanandroid.MainActivity.onNavigationItemSelected(MainActivity.java:56)
    at android.support.design.widget.BottomNavigationView$1.onMenuItemSelected(BottomNavigationView.java:184)
    at android.support.v7.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:822)
    at android.support.v7.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:156)
    at android.support.v7.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:969)
    at android.support.design.internal.BottomNavigationMenuView$1.onClick(BottomNavigationMenuView.java:90)
    at android.view.View.performClick(View.java:5637)
    at android.view.View$PerformClick.run(View.java:22429)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6119)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

结尾

以上就是crash收集的简单实现,复杂的话,可以看不少公司都有相关的运营统计的sdk方案,比腾讯的bugly。
同样在此感谢《Android开发艺术探索》一书的作者,普及很多基础知识。

你可能感兴趣的:(Android)