Android 封装Log工具并上传Log文件到服务器(带类名方法名行数Crash的捕捉)

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 adb命令

android中Log类的封装

Android Log文件查看工具

Android之Log封装

我的Android进阶之旅------&gt;Android关于Log的一个简单封装

Log4j_学习_03_自己动手封装log工具