Android自定义抓取异常日志上报服务器

前言

一个软件不可能没有BUG的,只是你没发现而儿。在软件上线后,总会出现各种各样的问题,然而这些问题怎么能够有效快速反馈到开发人员上呢?现在最有效的方法是通过日志。具体流程是:当软件出现错误,通过日志上传的方式上传到指定的地方(第三方服务器或者自己公司的服务器),通过查看错误日志,就可以快速定位错误并且解决。一般情况,都接入第三方的错误日志收集,第三方一般都做了管理,功能较全,一目了然。如:友盟错误统计:可以错误收集和查看渠道,TalkingData:支持预警,支持动态添加监控事件,bugly:有运营统计,页面美观。这些第三方都是可以的,接入都是相当简单的。那么,我们可不可以不接入第三方SDK,自己收集错误信息进行上报呢?答案是可以的。

JAVA中的错误类

异常类
从上图可得:Error和Exception是Throwable的子类,Error是不可控制的,经常用来用于表示系统错误或者低层资源的错误,如果可能的话,应该在系统级别(JVM)捕获,例如:虚拟机内存溢出,虚拟机错误,系统崩溃。Exception分可检查(checked)或者不可检查(unchecked),可检查就是必须在代码里显示进行捕获异常,是编译期检查的一部分,不可检查就是运行时异常,表示开发者开发导致的错误,应该在应用程序被处理,例如:ClassNotFoundException,NullPointerException。对于开发者来讲,应该尽可能的消灭exception,使程序更加健壮。如果在运行过程中满足某种条件导致线程中断,可以选择使用抛出运行级别异常来处理,Java中有解释:

All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.

那么当线程在运行过程中出现异常时,我们能不能统一捕获这些异常信息进行上报呢,结果是可以的。

异常捕获

我们现在的目的就是进行异常捕获,下面说说异常捕获的基本原则:

  1. try-catch代码段会产生额外的性能开销,往往会影响JVM对代码进行优化,因此建议仅仅捕获有必要的代码,尽量不要一个大的try块包整段代码。
  2. 尽量捕获特定的异常,避免捕获类似Exception这样的异常。
  3. 不要生吞异常,这样会导致非常难以诊断的情况。
  4. Java每实例化一个Exception,都会对当时的栈进行快照,如果频繁,开销比较大。

UncaughtExceptionHandler

线程在执行单元中是不允许抛出checked异常的,而且线程运行在自己的上下文中,派生它的线程是无法直接获得它运行中出现的异常信息。Runnable接口中的run方法原型是:public void run();具体的线程都是事先实现这个方法,所以线程代码不能抛出任何checked异常,所有的线程中checked异常只能被线程自己消耗掉,因为线程本来就是独立的执行片段,应该对自己负责,线程代码中是可以抛出(Error)和异常(RuntimeException)。Android应用不可避免发生crash,造成crash的方式有很多,版本功能适配,逻辑没写好等。对此,Java为我们提供一个UncaughtExceptionHanlder接口,当线程在运行过程中出现异常时,会回调UncaughtExceptionHandler接口,从而得知是哪个线程在运行时出错,以及出现什么样的错误。官方文档是这样介绍的:
当Thread因未捕获的异常而突然终止时,调用处理程序的接口,当某一线程因未捕获的异常而即将终止时,Java虚拟机将使用Thread.getUncaughtExceptionHandler()查询该线程以获得其UncaughtExceptionHandler的线程,并调用处理程序uncaughtException方法,将线程和异常作为参数传递。如果某一线程没有明确设置其UncaughtException,则将它的ThreadGroup对象作为其UncaughtExceptionHandler。如果ThreadGroup对象对处理没有什么特殊要求,那么它可以将调用转发给默认的未捕获异常处理程序。这个接口的源码是:

/**
     * Interface for handlers invoked when a <tt>Thread</tt> abruptly
     * terminates due to an uncaught exception.
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * <tt>UncaughtExceptionHandler</tt> using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * <tt>uncaughtException</tt> method, passing the thread and the
     * exception as arguments.
     * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
     * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
     * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *
     * @see #setDefaultUncaughtExceptionHandler
     * @see #setUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */
    @FunctionalInterface
    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);
    }

发现UncaughtExceptionHandler是一个FunctionalInterface的接口,FunctionalInterface是一个注解,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。

void uncaughtException(Thread t, Throwable e);

看上面的英文介绍:当给定线程因给定的未捕获异常而终止时,调用该方法,java虚拟机将忽略该方法抛出的任何异常。这个空抽象方法,该回调接口会被Thead中的dispatchUncaughtExce方法调用,如下所示:

    /**
     * Dispatch an uncaught exception to the handler. This method is
     * intended to be called only by the runtime and by tests.
     *
     * @hide
     */
    // @VisibleForTesting (would be private if not for tests)
    public final void dispatchUncaughtException(Throwable e) {
        Thread.UncaughtExceptionHandler initialUeh =
                Thread.getUncaughtExceptionPreHandler();
        if (initialUeh != null) {
            try {
                initialUeh.uncaughtException(this, e);
            } catch (RuntimeException | Error ignored) {
                // Throwables thrown by the initial handler are ignored
            }
        }
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

就是向处理异常程序发送未捕获的异常,也就是程序运行过程中出现异常,JVM会调用dispatchUncaughtException方法,该方法会将对应的线程实例以及异常信息传递给回调接口。

例子

那么现在通过一个例子实现全局异常捕获,步骤如下:
1.定义一个类实现UncaughtExceptionHandler接口
2.在Application初始化自定义异常处理器
3.收集异常信息
4.以文件形式或者以get参数形式上传

实现UncaughtExceptionHandler接口

public class CrashHandlerManage implements Thread.UncaughtExceptionHandler{
    /**
     * 创建唯一实例
     *
     */
    private CrashHandlerManage(){

    }
    public synchronized static CrashHandlerManage getInstance(){
        if(INSTANCE == null){
            INSTANCE = new CrashHandlerManage();
        }
        return INSTANCE;
    }

    /**
     * 当UncaughtException发生会进入此方法
     * @param thread
     * @param throwable
     */
    @Override
    public void uncaughtException(Thread thread, Throwable throwable)
    {

    }

初始化自定义异常处理器

 /**
     * 初始化程序异常处理器
     * @param context
     */
    public void initCrash(Context context)
    {
        this.context = context;
        //获取系统默认的UncaughtException处理器
        uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        //设置该CrashHandler为程序得默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);

    }

在Application中初始化

public class BaseApplication extends Application{

    //全局唯一的context
    private static BaseApplication baseApplication;
    //Activity管理器
    private ActivityManage activityManager;

    @Override
    public void onCreate(){
        super.onCreate();
        baseApplication = this;
        initCrashManage();
    }
        /**
     * 初始化异常管理器
     *
     */
    private void initCrashManage(){
        if(!BuildConfig.DEBUG)
        {
            CrashHandlerManage.getInstance().initCrash(getApplicationContext());
        }
    }
    
  }

收集异常信息

/**
     * 收集错误信息
     * @param context
     */
    private void collectDeviceInfo(Context context,String ex)
    {
        //获得包管理器
        try {
            PackageManager pm = context.getPackageManager();
            //得到该应用的信息
            PackageInfo pi = pm.getPackageInfo(context.getPackageName(),PackageManager.GET_ACTIVITIES);
            if(pi != null)
            {
                String versionName = pi.versionName == null ? "null" : pi.versionName;
                String versionCode = pi.versionCode + "";
                infoMap.put("versionName",versionName);
                infoMap.put("versionCode",versionCode);
                infoMap.put("phone_brand",android.os.Build.BRAND);
                infoMap.put("phone_version",android.os.Build.VERSION.RELEASE);
                infoMap.put("error",ex);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            Logger.d("获取信息失败");
        }
        //反射机制
        Field[] fields = Build.class.getDeclaredFields();
        for(Field field : fields)
        {
            try {
                field.setAccessible(true);
                infoMap.put(field.getName(),field.get("").toString());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

上传服务器非文件形式

 private void upLoadCrash(){
        RequestBody formBody = new FormEncodingBuilder()
                .add("uid", F.getUser().getUid()+"")
                .add("error",infoMap.toString()) //sb+""
                .build();
        //构建请求
        Request request = new Request.Builder()
                .url(BaseService.ZHENG_CRASH_FILE_UPLOAD)//地址
                .post(formBody)//添加请求体
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                Log.d("asdds", "上传失败:e.getLocalizedMessage() = " + e.getLocalizedMessage());
            }

            @Override
            public void onResponse(com.squareup.okhttp.Response response) throws IOException {
                if(response.isSuccessful()){
                    Log.d("asdds", "上传日志成功:response = " + response.body().string());
                }else{
                    Log.d("asdds", "上传日志失败:response = " + response.body().string());
                }
            }
        });
    }

以文件形式

    //文件名
    public final static String LOGPATH = Environment.getExternalStorageDirectory() + "/crash";
 /**
     * 文件上传方式
     * @param throwable
     */
    private void upLoadFile(Throwable throwable) {
        //SD卡不可用
        if (!Environment.getExternalStorageDirectory().equals(Environment.MEDIA_MOUNTED)) {
          return;
        }
        File dir = new File(LOGPATH);
        //不存在就创建文件
        if(!dir.exists()){
            boolean isSuccess = dir.mkdir();
        }
        long current = System.currentTimeMillis();
        File file = new File(LOGPATH + "/" + current +"crash.txt");
        try {
            PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            //将错误打印到文件中
            throwable.printStackTrace(printWriter);
            //需要收集什么就用如收集手机品牌
            //printWriter.println("phone_brand"+android.os.Build.BRAND);
            printWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

至于怎么把文件上传到服务器就自行自查了。
源码如下:

public class CrashHandlerManage implements Thread.UncaughtExceptionHandler{

    private static final String TAG = "CrashHandlerManage";
    private static CrashHandlerManage INSTANCE;
    private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
    private Context context;
    //收集信息集合
    private Map<String,String> infoMap = new HashMap<>();
    private static final OkHttpClient client = new OkHttpClient();
        //文件名
    public final static String LOGPATH = Environment.getExternalStorageDirectory() + "/crash";


    /**
     * 创建唯一实例
     *
     */
    private CrashHandlerManage(){

    }
    public synchronized static CrashHandlerManage getInstance(){
        if(INSTANCE == null){
            INSTANCE = new CrashHandlerManage();
        }
        return INSTANCE;
    }

    /**
     * 当UncaughtException发生会进入此方法
     * @param thread
     * @param throwable
     */
    @Override
    public void uncaughtException(Thread thread, Throwable throwable)
    {
        if(!handleException(throwable) && uncaughtExceptionHandler != null)
        {
            //如果自定义没有处理就交给系统去处理
            uncaughtExceptionHandler.uncaughtException(thread,throwable);
        } else
        {
            try {
                //上传到服务器
                //处理睡眠3秒再退出就是为了能够上传异常信息
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    /**
     * 初始化程序异常处理器
     * @param context
     */
    public void initCrash(Context context)
    {
        this.context = context;
        //获取系统默认的UncaughtException处理器
        uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        //设置该CrashHandler为程序得默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);

    }

    /**
     * 自定义错误处理,收集错误信息 发送错误报告
     * @param ex 异常信息
     * @return 处理异常返回true,否则返回false
     */
    private boolean handleException(Throwable ex)
    {
        if(ex == null)
        {
            return false;
        }
        //收集设备参数信息
        collectDeviceInfo(context,ex.toString());
        //非文件形式
        //upLoadCrash();
        //文件形式
        //upLoadFile(ex);
        return true;
    }

    /**
     * 收集错误信息
     * @param context
     */
    private void collectDeviceInfo(Context context,String ex)
    {
        //获得包管理器
        try {
            PackageManager pm = context.getPackageManager();
            //得到该应用的信息
            PackageInfo pi = pm.getPackageInfo(context.getPackageName(),PackageManager.GET_ACTIVITIES);
            if(pi != null)
            {
                String versionName = pi.versionName == null ? "null" : pi.versionName;
                String versionCode = pi.versionCode + "";
                infoMap.put("versionName",versionName);
                infoMap.put("versionCode",versionCode);
                infoMap.put("phone_brand",android.os.Build.BRAND);
                infoMap.put("phone_version",android.os.Build.VERSION.RELEASE);
                infoMap.put("error",ex);
            }
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            Logger.d("获取信息失败");
        }
        //反射机制
        Field[] fields = Build.class.getDeclaredFields();
        for(Field field : fields)
        {
            try {
                field.setAccessible(true);
                infoMap.put(field.getName(),field.get("").toString());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
     private void upLoadCrash() {
        RequestBody formBody = new FormEncodingBuilder()
                .add("uid", F.getUser().getUid() + "")
                .add("error", infoMap.toString()) //sb+""
                .build();
        //构建请求
        Request request = new Request.Builder()
                .url(BaseService.ZHENG_CRASH_FILE_UPLOAD)//地址
                .post(formBody)//添加请求体
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                Log.d("asdds", "上传失败:e.getLocalizedMessage() = " + e.getLocalizedMessage());
            }

            @Override
            public void onResponse(com.squareup.okhttp.Response response) throws IOException {
                if (response.isSuccessful()) {
                    Log.d("asdds", "上传日志成功:response = " + response.body().string());
                } else {
                    Log.d("asdds", "上传日志失败:response = " + response.body().string());
                }
            }
        });
    }
/**
     * 文件上传方式
     * @param throwable
     */
    private void upLoadFile(Throwable throwable) {
        //SD卡不可用
        if (!Environment.getExternalStorageDirectory().equals(Environment.MEDIA_MOUNTED)) {
          return;
        }
        File dir = new File(LOGPATH);
        //不存在就创建文件
        if(!dir.exists()){
            boolean isSuccess = dir.mkdir();
        }
        long current = System.currentTimeMillis();
        File file = new File(LOGPATH + "/" + current +"crash.txt");
        try {
            PrintWriter printWriter = new PrintWriter(new BufferedWriter(new FileWriter(file)));
            //将错误打印到文件中
            throwable.printStackTrace(printWriter);
            //需要收集什么就用如收集手机品牌
            //printWriter.println("phone_brand"+android.os.Build.BRAND);
            printWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

注意

                //上传到服务器
                //处理睡眠3秒再退出就是为了能够上传异常信息
                Thread.sleep(3000);

这行代码很重要,为了能够让错误上传到服务器,让主线程睡眠三秒钟,不让程序那么快退出,这样整个例子完成。

猜你喜欢

转载自blog.csdn.net/qq_33453910/article/details/84966328
今日推荐