美文网首页AndroidUtilsAndroid开发技术分享
手把手撸一个实用必备CrashHandler

手把手撸一个实用必备CrashHandler

作者: Rayhaha | 来源:发表于2017-06-05 00:01 被阅读395次

    撸一个项目必备的CrashHandler

    上周工作中新来的小伙伴问了一下项目中CrashHandler,当时只是简单讲了一下
    周末到了,心血来潮,手把手撸一个好用全面的CrashHandler吧,对于以后项目开发和当前项目的完善也有一定的帮助。


    目录

    • 认识与作用
    • Crash的捕获
    • Crash信息的获取
    • Crash日志写入上传
    • 使用方式
    • 一些注意
    • 最后

    认识与作用

    CrashHandler: 崩溃处理器,捕获Crash信息并作出相应的处理

    1. 测试使用:应用在日常的开发中,我们经常需要去Logcat测试我们的App,但由于很多原因,Android Monitor会闪屏或者Crash信息丢失。 这个时候就需要一个CrashHandler来将Crash写入到本地方便我们随时随地查看。
    2. 上线使用:应用的崩溃率是用户衡量筛选应用的重要标准,那么应用上线以后 我们无法向用户借手机来分析崩溃原因。为了减低崩溃率,这个时候需要CrashHandler 来帮我们将崩溃信息返回给后台,以便及时修复。

    下面我们就手把手写一个实用本地化轻量级的CrashHandler吧。

    Crash的捕获

    1. 实现Thread.UncaughtExceptionHandler接口,并重写uncaughtException方法,此时你的CrashHandler就具备了接收处理异常的能力了。
    2. 调用Thread.setDefaultUncaughtExceptionHandler(CrashHandler) ,来使用我们自定义的CrashHandler来取代系统默认的CrashHandler
    3. 结合单例模式
    4. 总体三步: 捕获异常、信息数据获取、数据写入和上传
      总体的初始化代码如下:
    
        private RCrashHandler(String dirPath) {
            mDirPath = dirPath;
            File mDirectory = new File(mDirPath);
            if (!mDirectory.exists()) {
                mDirectory.mkdirs();
            }
        }
    
        public static RCrashHandler getInstance(String dirPath) {
            if (INSTANCE == null) {
                synchronized (RCrashHandler.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new RCrashHandler(dirPath);
                    }
                }
            }
            return INSTANCE;
        }
    
        /**
         * 初始化
         *
         * @param context       上下文
         * @param crashUploader 崩溃信息上传接口回调
         */
        public void init(Context context, CrashUploader crashUploader) {
            mCrashUploader = crashUploader;
            mContext = context;
            //保存一份系统默认的CrashHandler
            mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
            //使用我们自定义的异常处理器替换程序默认的
            Thread.setDefaultUncaughtExceptionHandler(this);
        }
    
        /**
         * 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用uncaughtException方法
         *
         * @param t 出现未捕获异常的线程
         * @param e 未捕获的异常,有了这个ex,我们就可以得到异常信息
         */
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            if (!catchCrashException(e) && mDefaultHandler != null) {
                //没有自定义的CrashHandler的时候就调用系统默认的异常处理方式
                mDefaultHandler.uncaughtException(t, e);
            } else {
                //退出应用
                killProcess();
            }
        }
    
    /**
         * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
         *
         * @param ex
         * @return true:如果处理了该异常信息;否则返回false.
         */
        private boolean catchCrashException(Throwable ex) {
            if (ex == null) {
                return false;
            }
    
            new Thread() {
                public void run() {
    //              Looper.prepare();
    //              Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 0).show();
    //              Looper.loop();
                    Intent intent = new Intent();
                    intent.setClass(mContext, CrashActivity.class);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    ActivityCollector.finishAll();
                    mContext.startActivity(intent);
                }
            }.start();
            //收集设备参数信息
            collectInfos(mContext, ex);
            //保存日志文件
            saveCrashInfo2File();
            //上传崩溃信息
            uploadCrashMessage(infos);
    
            return true;
        }
    
    
      /**
         * 退出应用
         */
        public static void killProcess() {
            //结束应用
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Looper.prepare();
                    ToastUtils.showLong("哎呀,程序发生异常啦...");
                    Looper.loop();
                }
            }).start();
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {
                RLog.e("CrashHandler.InterruptedException--->" + ex.toString());
            }
            //退出程序
            Process.killProcess(Process.myPid());
            System.exit(1);
        }
    
    

    Crash信息的获取

    • 获取异常信息
     /**
         * 获取捕获异常的信息
         *
         * @param ex
         */
        private String collectExceptionInfos(Throwable ex) {
            Writer mWriter = new StringWriter();
            PrintWriter mPrintWriter = new PrintWriter(mWriter);
            ex.printStackTrace(mPrintWriter);
            ex.printStackTrace();
            Throwable mThrowable = ex.getCause();
            // 迭代栈队列把所有的异常信息写入writer中
            while (mThrowable != null) {
                mThrowable.printStackTrace(mPrintWriter);
                // 换行 每个个异常栈之间换行
                mPrintWriter.append("\r\n");
                mThrowable = mThrowable.getCause();
            }
            // 记得关闭
            mPrintWriter.close();
            return mWriter.toString();
        }
    
    • 获取应用信息
     /**
         * 获取应用包参数信息
         */
        private void collectPackageInfos(Context context) {
            try {
                // 获得包管理器
                PackageManager mPackageManager = context.getPackageManager();
                // 得到该应用的信息,即主Activity
                PackageInfo mPackageInfo = mPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
                if (mPackageInfo != null) {
                    String versionName = mPackageInfo.versionName == null ? "null" : mPackageInfo.versionName;
                    String versionCode = mPackageInfo.versionCode + "";
                    mPackageInfos.put(VERSION_NAME, versionName);
                    mPackageInfos.put(VERSION_CODE, versionCode);
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
    
    • 获取设备硬件信息(针对不同机型的用户更有效地定位Bug)
    /**
         * 从系统属性中提取设备硬件和版本信息
         */
        private void collectBuildInfos() {
            // 反射机制
            Field[] mFields = Build.class.getDeclaredFields();
            // 迭代Build的字段key-value 此处的信息主要是为了在服务器端手机各种版本手机报错的原因
            for (Field field : mFields) {
                try {
                    field.setAccessible(true);
                    mDeviceInfos.put(field.getName(), field.get("").toString());
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    
    • 获取系统常规信息(针对不同设定的用户更有效定位Bug)
     /**
         * 获取系统常规设定属性
         */
        private void collectSystemInfos() {
            Field[] fields = Settings.System.class.getFields();
            for (Field field : fields) {
                if (!field.isAnnotationPresent(Deprecated.class)
                        && field.getType() == String.class) {
                    try {
                        String value = Settings.System.getString(mContext.getContentResolver(), (String) field.get(null));
                        if (value != null) {
                            mSystemInfos.put(field.getName(), value);
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    • 获取安全设置信息
     /**
         * 获取系统安全设置信息
         */
        private void collectSecureInfos() {
            Field[] fields = Settings.Secure.class.getFields();
            for (Field field : fields) {
                if (!field.isAnnotationPresent(Deprecated.class)
                        && field.getType() == String.class
                        && field.getName().startsWith("WIFI_AP")) {
                    try {
                        String value = Settings.Secure.getString(mContext.getContentResolver(), (String) field.get(null));
                        if (value != null) {
                            mSecureInfos.put(field.getName(), value);
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    • 获取应用内存信息(需要权限)
    /**
         * 获取内存信息
         */
        private String collectMemInfos() {
            BufferedReader br = null;
            StringBuffer sb = new StringBuffer();
    
            ArrayList<String> commandLine = new ArrayList<>();
            commandLine.add("dumpsys");
            commandLine.add("meminfo");
            commandLine.add(Integer.toString(Process.myPid()));
            try {
                java.lang.Process process = Runtime.getRuntime()
                        .exec(commandLine.toArray(new String[commandLine.size()]));
                br = new BufferedReader(new InputStreamReader(process.getInputStream()), 8192);
    
                while (true) {
                    String line = br.readLine();
                    if (line == null) {
                        break;
                    }
                    sb.append(line);
                    sb.append("\n");
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (br != null) {
                    try {
                        br.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return sb.toString();
        }
    
    • 最后将这些信息储存到infos中,以便之后我们回传给上传具体功能时候更加方便,有了这些数据,我们应该能够快速定位崩溃的原因了
     /**
         * 获取设备参数信息
         *
         * @param context
         */
        private void collectInfos(Context context, Throwable ex) {
            mExceptionInfos = collectExceptionInfos(ex);
            collectPackageInfos(context);
            collectBuildInfos();
            collectSystemInfos();
            collectSecureInfos();
            mMemInfos = collectMemInfos();
    
            //将信息储存到一个总的Map中提供给上传动作回调
            infos.put(EXCEPETION_INFOS_STRING, mExceptionInfos);
            infos.put(PACKAGE_INFOS_MAP, mPackageInfos);
            infos.put(BUILD_INFOS_MAP, mDeviceInfos);
            infos.put(SYSTEM_INFOS_MAP, mSystemInfos);
            infos.put(SECURE_INFOS_MAP, mSecureInfos);
            infos.put(MEMORY_INFOS_STRING, mMemInfos);
        }
    

    Crash日志写入

    1. 将崩溃数据写入到本地文件中(这里我只收集了异常信息应用信息,具体情况可以根据自己需求来拼接其他数据)
     /**
         * 将崩溃日志信息写入本地文件
         */
        private String saveCrashInfo2File() {
            StringBuffer mStringBuffer = getInfosStr(mPackageInfos);
            mStringBuffer.append(mExceptionInfos);
            // 保存文件,设置文件名
            String mTime = formatter.format(new Date());
            String mFileName = "CrashLog-" + mTime + ".log";
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                try {
                    File mDirectory = new File(mDirPath);
                    Log.v(TAG, mDirectory.toString());
                    if (!mDirectory.exists())
                        mDirectory.mkdirs();
                    FileOutputStream mFileOutputStream = new FileOutputStream(mDirectory + File.separator + mFileName);
                    mFileOutputStream.write(mStringBuffer.toString().getBytes());
                    mFileOutputStream.close();
                    return mFileName;
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    
     /**
         * 将HashMap遍历转换成StringBuffer
         */
        @NonNull
        public static StringBuffer getInfosStr(ConcurrentHashMap<String, String> infos) {
            StringBuffer mStringBuffer = new StringBuffer();
            for (Map.Entry<String, String> entry : infos.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                mStringBuffer.append(key + "=" + value + "\r\n");
            }
            return mStringBuffer;
        }
    
    1. 由于每一个应用上传的服务器或者数据类型都不同,所以为了更好的延展性,我们使用接口回调,将获取的全部数据抛给应用具体去实现(我这里为了演示,使用了Bmob后端云)
    
    
    
    
     /**
         * 上传崩溃信息到服务器
         */
        public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
            mCrashUploader.uploadCrashMessage(infos);
        }
    
     /**
         * 崩溃信息上传接口回调
         */
        public interface CrashUploader {
            void uploadCrashMessage(ConcurrentHashMap<String, Object> infos);
        }
    
    

    使用方式

     /**
         * 初始化崩溃处理器
         */
        private void initCrashHandler() {
    
            mCrashUploader = new RCrashHandler.CrashUploader() {
                @Override
                public void uploadCrashMessage(ConcurrentHashMap<String, Object> infos) {
                    CrashMessage cm = new CrashMessage();
                    ConcurrentHashMap<String, String> packageInfos = (ConcurrentHashMap<String, String>) infos.get(RCrashHandler.PACKAGE_INFOS_MAP);
                    cm.setDate(DateTimeUitl.getCurrentWithFormate(DateTimeUitl.sysDateFormate));
                    cm.setVersionName(packageInfos.get(RCrashHandler.VERSION_NAME));
                    cm.setVersionCode(packageInfos.get(RCrashHandler.VERSION_CODE));
                    cm.setExceptionInfos(((String) infos.get(RCrashHandler.EXCEPETION_INFOS_STRING)));
                    cm.setMemoryInfos((String) infos.get(RCrashHandler.MEMORY_INFOS_STRING));
                    cm.setDeviceInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
                            .get(RCrashHandler.BUILD_INFOS_MAP)).toString());
                    cm.setSystemInfoss(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
                            .get(RCrashHandler.SYSTEM_INFOS_MAP)).toString());
                    cm.setSecureInfos(RCrashHandler.getInfosStr((ConcurrentHashMap<String, String>) infos
                            .get(RCrashHandler.SECURE_INFOS_MAP)).toString());
                    cm.save(new SaveListener<String>() {
                        @Override
                        public void done(String s, BmobException e) {
                            if (e == null) {
                                RLog.e("上传成功!");
    
                            } else {
                                RLog.e("上传Bmob失败 错误码:" + e.getErrorCode());
                            }
                        }
                    });
                }
            };
            RCrashHandler.getInstance(FileUtils.getRootFilePath() + "EasySport/crashLog")
                    .init(mAppContext, mCrashUploader);
        }
    

    一些注意

    使用过程中发现在Activity中 Process.killProcess(Process.myPid());System.exit(1); 会导致应用自动重启三次,会影响一点用户体验

    所以我们使用了一个土方法,就是让它去打开一个我们自己设定的CrashActivity来提高我们应用的用户体验

      /**
         * 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
         *
         * @param ex
         * @return true:如果处理了该异常信息;否则返回false.
         */
        private boolean catchCrashException(Throwable ex) {
            if (ex == null) {
                return false;
            }
    
    //启动我们自定义的页面
            new Thread() {
                public void run() {
    //              Looper.prepare();
    //              Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", 0).show();
    //              Looper.loop();
                    Intent intent = new Intent();
                    intent.setClass(mContext, CrashActivity.class);
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    ActivityCollector.finishAll();
                    mContext.startActivity(intent);
                }
            }.start();
    
            //收集设备参数信息
            collectInfos(mContext, ex);
            //保存日志文件
            saveCrashInfo2File();
            //上传崩溃信息
            uploadCrashMessage(infos);
    
            return true;
        }
    

    CrashActivity的话就看个人需求了,可以使一段Sorry的文字或者一些交互的反馈操作都是可以的。

    最后

    CrashHandler整个写下来思路是三步 :
    1、异常捕获
    2、信息数据采集
    3、 数据写入本地和上传服务器
    项目地址:Github地址
    这是我一个随便写写的项目
    CrashHandler主要在rbase的util,还有app的MyApplication 中应用到

    相关文章

      网友评论

      • 某L_GreeceBeast:撸主可以贴一下源码吗 (⊙﹏⊙)b
        Rayhaha:@某L_cc91 github上有~
      • Bernardo_Silva: if (ex == null) {
        return false;
        }
        楼主这里是return true;吧
        Bernardo_Silva:@Rayhaha ex都为空了退出应用不就好了,还要用系统默认的异常捕获什么:joy:
        Rayhaha:是return false~~~~他是表示ex为空就调用系统默认的异常捕获~

      本文标题:手把手撸一个实用必备CrashHandler

      本文链接:https://www.haomeiwen.com/subject/rpgbfxtx.html