我是靠谱客的博主 谨慎热狗,最近开发中收集的这篇文章主要介绍Android 封装Log工具并上传Log文件到服务器(带类名、方法名、行数、Crash的捕捉),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

最近开发写一个Log的追踪日志。
将log信息写入到cache文件下的文件中,当遇到Crash的Log或者Log文件大小大于1mb,则上传至服务器。

那么首先,我们知道,上传服务器的操作一般要开子线程,而多个子线程同时写Log,那肯定就要考虑使用单例模式了:

public class LogTool {
    private static final String TAG = "LogTool";

    private static LogTool instance;
    private static String apiUrl = "http://xxxx";

    private LogTool() {
    }

    public static LogTool getInstance() {
        if (instance == null)
            instance = new LogTool();
        return instance;
    }
}    

接下来我们要考虑log日志的存储位置,我这里是放在cache文件下的(本身文件也不大,也要多写),我们在这里写init()方法来初始化路径,这个方法要在application类调用,在整个生命周期开始时就要执行:

    //初始化
    public void init(Context context) {
        //获得文件存储路径
        logPath = getFilePath(context);
    }
    
     private static String getFilePath(Context context) {
        if (Environment.MEDIA_MOUNTED.equals(Environment.MEDIA_MOUNTED) || !Environment.isExternalStorageRemovable()) {
            //如果外部储存可用 则存储在外部的缓存文件中
            return context.getExternalCacheDir().getPath();
        } else {
            //否则直接存在内部储存的缓存文件中
            return context.getCacheDir().getPath();
        }
    }

然后我们根据需求来划分Log等级,一般都为 debug<info<warning<error<crash
并提供对外的打印方法。
注:这里的等级划分、和打印方法所要的信息都是跟着需求来。但是都是大同小异。我这里多写什么信息,打印方法也只留最基本的

     /**
     * Log的分级为 Crash、Error、Warning、Info、Debug
     */
    public static final String CRASH_LEVEL = "CRASH";

    public static final String ERROR_LEVEL = "ERROR";

    public static final String WARNING_LEVEL = "WARNING";

    public static final String INFO_LEVEL = "INFO";

    public static final String DEBUG_LEVEL = "DEBUG";
    
    public void c(String logData) {
        writeToFile(CRASH_LEVEL, logData);
    }

    public void e(String logData) {
        writeToFile(ERROR_LEVEL, logData);
    }

    public void w(String logData) {
        writeToFile(WARNING_LEVEL, logData);
    }

    public void i(String logData) {
        writeToFile(INFO_LEVEL, type, logData);
    }

    public void d(String logData) {
        writeToFile(DEBUG_LEVEL, logData);
    }

witeTofile就是写入到文件的方法:

    private static StackTraceElement getCallerStackTraceElement() {
        return Thread.currentThread().getStackTrace()[5];
    }

    /**
     * 将Log信息写入文件
     * isDouble为是否连续两次写入,防止连续两次上传服务器。
     *
     * @param level
     * @param type
     * @param logData
     */
    public static void writeToFile(String level, String logData) {
        if (null == logPath) {
            LogUtil.e(TAG, "logPath == null ,未初始化LogToFile");
            return;
        }

        String fileName = logPath + "/AppLogs_Android.log";
        StackTraceElement caller = getCallerStackTraceElement();
         // 获取到类名
        String callerClazzName = caller.getClassName();
        callerClazzName = callerClazzName.substring(callerClazzName
                .lastIndexOf(".") + 1);

        //要写入的LOG内容
       String log = type + " - " + callerClazzName + " - " + caller.getMethodName() + " - line " + caller.getLineNumber() + " - " + logData + "n";

        LogUtil.d(TAG, log);
        //如果父路径不存在
        File file = new File(logPath);
        if (!file.exists()) {
            file.mkdirs();//创建父路径
        }

        FileOutputStream fos = null;
        BufferedWriter bw = null;
        try {
            fos = new FileOutputStream(fileName, true);
            bw = new BufferedWriter(new OutputStreamWriter(fos));
            bw.write(log);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();//关闭缓冲流
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (getLogsFileSize(fileName) >= 1f) {
                sendToServer(fileName);
        } else if (level == CRASH_LEVEL || level == ERROR_LEVEL || level == WARNING_LEVEL) {
                sendToServer(fileName);
        }
    }

上面有一个巨顶的方法:

Thread.currentThread().getStackTrace()[5]

这行函数能够得离当前执行代码的指令。(注:不一定是5)
因为在执行命令时,指令栈保存当前线程最近执行的代码。
它的原理是这样的:

1、MainActivity : LogTool.d(xxx,xxx)
跳转-2、LogTool:  d(xxx,xx){wirteToFile(xxx,xxx)}
跳转-3、LogTool:writeToFile(xxx){}

假如上面是一个指令栈,那么我就去获取这个栈的栈底元素 [1] MainActivity… 这一行,得到的是个StackTraceElement 对象。
得到该对象之后,我们可以将该指令反射,通过getClassName()获取类名,通过getMethodName()获取方法名、通过getLineNumber()获取当前行数,就省的我们一步一步去找具体哪行了。
所以上面代码中的第5行只是我这边的情况,别的代码就要自己去推理具体在哪一行咯。

接下来就是上传到服务器,这里使用Okhttp,上传的格式是包括文件+一些附带String信息,所以使用mutipart来提交表单,并且开启一个线程来提交。

     /**
     * 上传Log文件至服务器
     *
     * @return
     */
    public static void sendToServer(final String pathName) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                final File file = new File(pathName);
                MediaType MEDIA_TYPE_TXT = MediaType.parse("text/plain");
                RequestBody fileBody = MultipartBody.create(MEDIA_TYPE_TXT, file);
                MultipartBody multiBuilder = new MultipartBody.Builder()
                        .setType(MultipartBody.FORM)
                        .addFormDataPart("log_file", file.getName(), fileBody)
                        .addFormDataPart("system", "Android")
                        .addFormDataPart("phone_number", "110").build();
                        
                Request request = new Request.Builder().url(apiUrl).post(multiBuilder).build();
                OkHttpClient okHttpClient = new OkHttpClient();
                Call call = okHttpClient.newCall(request);
                call.enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {
                        //请求失败的处理
                        LogUtil.e(TAG, "上传Log文件失败 : " + e.toString());
                        if (getLogsFileSize(pathName) > 1f) {
                            file.delete();
                        }
                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        //请求成功的处理
                        LogUtil.d(TAG, "上传Log文件成功 : " + response.body().string());
                        //清空文件
                        file.delete();
                    }
                });
            }
        }).start();
    }

到这里就上传完啦。

但是我们还没有关注Crash的捕捉,因为我们自己无法判断Crash是出现在什么地方的,所以我们要写一个全局的Crash捕捉器,而Android提供了这个类,叫:Thread.UncaughtExceptionHandler,我们通过实现该接口,重写uncaughtException()方法,来处理遇到异常的情况。

该类如下(也需要在App初始化时init):

public class CrashHandlerManager implements Thread.UncaughtExceptionHandler {

    private static final String TAG = "CrashHandlerManager";

    private static CrashHandlerManager instance;
    private Context context;
    private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
    //收集信息集合
    private Map<String, String> infoMap = new HashMap<>();

    public synchronized static CrashHandlerManager getInstance() {
        if (instance == null) {
            instance = new CrashHandlerManager();
        }
        return instance;
    }

    private CrashHandlerManager() {
    }

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

    /**
     * 当UncaughtException发生会进入此方法
     *
     * @param t
     * @param e
     */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        if (!handleException(e) && uncaughtExceptionHandler != null) {
            //如果自定义没有处理就交给系统去处理
            uncaughtExceptionHandler.uncaughtException(t, e);
        }
    }

    private boolean handleException(Throwable e) {
        if (e == null) {
            return false;
        }
        collectionDeviceInfo(context, e.toString());
        return true;
    }


    /**
     * 收集错误处理信息
     *
     * @param context
     * @param e
     */
    private void collectionDeviceInfo(Context context, String e) {
        //获得包管理器
        try {
            PackageManager pm = context.getPackageManager();
            //获取该应用信息
            PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
             if (pi != null) {
                StringWriter stringWriter = new StringWriter();
                PrintWriter printWriter = new PrintWriter(stringWriter);
                e.printStackTrace(printWriter);
                StringBuffer stringBuffer = stringWriter.getBuffer();
                e.printStackTrace();
                String versionName = pi.versionName == null ? "null" : pi.versionName;
                String versionCode = pi.versionCode + "";
                infoMap.put("versionName", versionName);
                infoMap.put("versionCode", versionCode);
                infoMap.put("phone_brand", Build.BRAND);
                infoMap.put("phone_version", Build.VERSION.RELEASE);
                infoMap.put("error", stringBuffer.toString());
            }
        } catch (PackageManager.NameNotFoundException e1) {
            e1.printStackTrace();
            LogUtil.e(TAG, "获取信息失败");
        }

        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                infoMap.put(field.getName(), field.get("").toString());
            } catch (IllegalAccessException ex) {
                ex.printStackTrace();
            }
        }
        LogTool.getInstance().c(LogTool.CRASH_LOG, infoMap.toString());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.exit(0);
    }
}

在collectionDeviceInfo方法中,我们对遇到的错误的地方进行了收集和记录Log,并且在记录完时:

      try {
            Thread.sleep(3000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        System.exit(0);

这里让主线程 sleep3秒的原因是 Crash会造成主线程的停止,而停止会导致上传文件的线程也会停止,这就会导致文件上传不了,所以我们让主线程sleep的期间让子线程赶紧去上传,3s的速度即使是3G网络也能上传好1M以下的文件了(除非真的很垃圾的网)
然后在通过exit来回到主界面去。

ok,自己封装的一个Log工具就讲到这里了,这个Log工具还是比较简单的,所以可能会有一些问题。
比如在不断记录log时,频繁的打开和关闭输出流其实会耗费一定的性能,但我又不能上传时再将Log信息输入到文件中(因为这样不是实时的,不能知道输入完后是不是文件早早超出了1M)。
在输出的时候 使用了 BufferWriter来优化输出效率,(其实也快不了多少)
至于别的 关于 OkHttp的上传方式、Thread.UncaughtExceptionHandler使用的正确姿势,网上资料也很多,这里就不再赘述了。

最后

以上就是谨慎热狗为你收集整理的Android 封装Log工具并上传Log文件到服务器(带类名、方法名、行数、Crash的捕捉)的全部内容,希望文章能够帮你解决Android 封装Log工具并上传Log文件到服务器(带类名、方法名、行数、Crash的捕捉)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(41)

评论列表共有 0 条评论

立即
投稿
返回
顶部