前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CrashHandler--程序异常退出处理

CrashHandler--程序异常退出处理

作者头像
雁字回时
发布2022-12-13 18:55:49
8160
发布2022-12-13 18:55:49
举报
文章被收录于专栏:安卓随笔

前言

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


UncaughtExceptionHandler

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

代码语言:javascript
复制
public interface UncaughtExceptionHandler {
   /**
    * Method invoked when the given thread terminates due to the
    * given uncaught exception.
    * <p>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:

代码语言:javascript
复制
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)

代码语言:javascript
复制
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() 中进行初始化

代码语言:javascript
复制
    public void onCreate() {
        super.onCreate();
        CrashHandler.getInstance().init(this);
    }

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

遇到的问题

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

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

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

代码语言:javascript
复制
    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命令。

代码语言:javascript
复制
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

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

代码语言:javascript
复制
  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 ");
      }
  });

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

代码语言:javascript
复制
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开发艺术探索》一书的作者,普及很多基础知识。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-03-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • UncaughtExceptionHandler
    • 实现自己的UncaughtExceptionHandler
      • 遇到的问题
      • 结尾
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档