一个Android Log框架

一、概述

在工作过程中,发现一个比较有意思的现象:每个同事都有一个自己的代码库,里面基本都包含一个Log的工具。这充分说明了Log在Android 开发中的重要,不管如何实现,大家都不自觉地按照自己的习惯,完成了一个Log工具。每个开发者都有自己的习惯和特点,在Log工具上的实现大同小异,但如果在同一个项目中加入各自的Log工具,难免产生了重复代码,还可能会造成使用的困扰。一个团队,在基础工具上还是要保持一致,本文尝试实现一个大家都接受,并且能满足大家需求,在使用上还不会造成困扰的,高性能的,可扩展的 Log 框架,项目地址:Logger

二、Log 需求

  1. 参考Android自带的Log工具,log有,VERBOSE,DEBUG,INFO,WARN,ERROR,ASSERT六个等级,对应有六个方法(常用):v,d,I,w,e,wtf;
  2. 可以输出到logcat,也可以输出到文件或其它;
  3. 可以选择关闭或者打开某个或者所有log;
  4. 可以自定义增加新的log;
  5. 输出到logcat的log还可以显示代码行数,点击即可打开这个类文件,并且定位到对应的行;
  6. 可以上传到后台服务器;
  7. 其它…

三、定义Logger

从上面的需求,我们可以定义一个ILogger的接口:


public interface ILogger {

    int VERBOSE = 2;
    int DEBUG = 3;
    int INFO = 4;
    int WARN = 5;
    int ERROR = 6;
    int ASSERT = 7;
    void v(String tag, String msg);
    void d(String tag, String msg);
    void i(String tag, String msg);
    void w(String tag, String msg);
    void e(String tag, String msg);
    void enable(boolean enable); // 设置是否可用
    void close(); // 关闭这个Logger
    void save();  // 保存内存中的Log(文件或其它Log时使用)
    
}

为了统一log的格式,定义一个抽象Loger类:


public abstract class AbstractLogger implements ILogger {

    public static String LEFT_BRACKET = "(";
    public static String RIGHT_BRACKET = ")";
    public static String SHARP = "#";
    public static String COLON = ":";

    protected String formatLogMsg(String msg, String fileName, int lineNumber, String methodName) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(LEFT_BRACKET);
        buffer.append(fileName)
                .append(COLON).append(lineNumber).append(RIGHT_BRACKET)
                .append(SHARP).append(methodName)
                .append(COLON).append(msg);
        return buffer.toString();
    }

    protected StackTraceElement getStackTraceElement(int index) {
        StackTraceElement[] traceElements = Thread.currentThread().getStackTrace();
        StackTraceElement element = traceElements[index];
        return element;
    }
}

四、具体Logger实现

根据需求,首先我们需要一个可以输出到Logcat,并且可以显示代码行数的logger,可以命名为LogcatLogger,然后还要有一个输出到文件的logger,可以命名为FileLogger。

LogcatLogger

LogcatLogger很简单,就是调用一下系统的Log的对应方法即可,与系统的Log不一样的是,它可以显示代码行数,并且可以定位到具体的类文件,具体实现如下:


import android.util.Log;

import com.bottle.log.AbstractLogger;

public class LogcatLogger extends AbstractLogger {

    private boolean isDebuggable = true;

    public LogcatLogger() {

    }

    @Override
    public void enable(boolean enable) {
        this.isDebuggable = enable;
    }

    @Override
    public void close() {
        isDebuggable = false;
    }

    @Override
    public void save() {

    }

    @Override
    public void v(String tag, String msg) {
        if (isDebuggable == false) {
            return;
        }
        StackTraceElement element = getStackTraceElement(7);
        Log.v(tag, formatLogMsg(msg, element.getFileName(), element.getLineNumber(), element.getMethodName()));
    }

    @Override
    public void d(String tag, String msg) {
        if (isDebuggable == false) {
            return;
        }
        StackTraceElement element = getStackTraceElement(7);
        Log.d(tag, formatLogMsg(msg, element.getFileName(), element.getLineNumber(), element.getMethodName()));
    }

    @Override
    public void i(String tag, String msg) {
        if (isDebuggable == false) {
            return;
        }
        StackTraceElement element = getStackTraceElement(7);
        Log.i(tag, formatLogMsg(msg, element.getFileName(), element.getLineNumber(), element.getMethodName()));
    }

    @Override
    public void w(String tag, String msg) {
        if (isDebuggable == false) {
            return;
        }
        StackTraceElement element = getStackTraceElement(7);
        Log.w(tag, formatLogMsg(msg, element.getFileName(), element.getLineNumber(), element.getMethodName()));
    }

    @Override
    public void e(String tag, String msg) {
        if (isDebuggable == false) {
            return;
        }
        StackTraceElement element = getStackTraceElement(7);
        Log.e(tag, formatLogMsg(msg, element.getFileName(), element.getLineNumber(), element.getMethodName()));
    }

}

FileLogger

FileLogger是输出到文件,而不是Logcat,所以FileLogger必定会有文件写的操作(使用时注意申请外部存储卡的读写权限)。因为文件读写是一个耗时操作,所以,FileLogger的实现放在一个独立的线程,这里使用了HandlerThread这个方便类,当需要记录一个log时,就往这个线程发送一个消息,并且带上相应的参数,然后在线程中拼接log,然后先写入到一个内存缓存中,等达到一定量,再写入文件(所以,需要在应用切换到后台的时候保存一次,如自定义的Application中的onTrimMemory方法中调用一次save)。因为大大减少了文件的读写操作,可以大大地提高FileLogger的性能 ,不必担心因为文件耗时操作而导致性能问题,具体实现如下:


import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;

import com.bottle.log.AbstractLogger;
import com.bottle.log.ILogger;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileLogger extends AbstractLogger {

    public static final String SPLIT = "|";
    public static final String V = "[V]";
    public static final String D = "[D]";
    public static final String I = "[I]";
    public static final String W = "[W]";
    public static final String E = "[E]";
    public static final String N = "\n";

    private final int MSG_WHAT = 10;
    private final String MSG_TAG = "tag";
    private final String MSG_PRIORITY = "proi";
    private final String MSG_LOG = "msg";
    private final int CACHE_SIZE = 10240; // 10 * 1024B = 10K


	private final SimpleDateFormat TIMESTAMP_FMT;
    private StringBuffer cacheLog;
    private String packageName;
    private HandlerThread logThread;
    private Handler logHandler;
    private String logPath;
    private boolean isDebuggable = true;

	public FileLogger(Context context) {
        TIMESTAMP_FMT = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss] ");
        cacheLog = new StringBuffer();
        packageName = context.getPackageName();
        logPath = Environment.getExternalStorageDirectory().getAbsolutePath()
                + "/Android/data/" + packageName + "/cache/log";
        logThread = new HandlerThread("file-log-thread");
        logThread.start();
        logHandler = new Handler(logThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if(msg == null) {
                    return;
                }
                int what = msg.what;
                if(what == MSG_WHAT) {
                    Bundle bundle = msg.getData();
                    if(bundle == null) {
                        return;
                    }
                    int priority = bundle.getInt(MSG_PRIORITY);
                    String tag = bundle.getString(MSG_TAG);
                    String message = bundle.getString(MSG_LOG);
                    cacheLog(priority, tag, message);
                }
            }
        };
	}

    /**
     * 日志保存在这个目录下,每天一个文件,文件名的格式:yyyyyMMdd.txt,如果需要上报日志,
     * 可以把这个目录下的文件上传到服务器。
     * @return
     */
	public String getLogPath() {
	    return logPath;
    }

    @Override
    public void close() {
        isDebuggable = false;
        if(logThread != null) {
            logThread.quit();
            logThread = null;
            logHandler = null;
        }
    }

    @Override
    public void save() {
        File logDir = new File(logPath);
        if (!logDir.exists()){
            logDir.mkdirs();
        }
        String filePath = logDir.getAbsolutePath() + "/"+ getCurrentTimeString() + ".txt";
        FileWriter fileWriter;
        Writer mWriter = null;
        try{
            File file = new File(filePath);
            if (!file.exists()){
                file.createNewFile();
            }
            String logs = cacheLog.toString();
            fileWriter = new FileWriter(file, true);
            mWriter = new PrintWriter(new BufferedWriter(fileWriter,2028));
            mWriter.write(logs);
            mWriter.flush();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            try {
                if(cacheLog != null) {
                    cacheLog.setLength(0);
                }
                if(mWriter != null) {
                    mWriter.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void enable(boolean enable) {
	    this.isDebuggable = enable;
    }

    @Override
    public void v(String tag, String message) {
        println(VERBOSE, tag, message);
    }

	@Override
	public void d(String tag, String message) {
		// TODO Auto-generated method stub
		println(DEBUG, tag, message);
	}

    @Override
	public void i(String tag, String message) {
		println(INFO, tag, message);
	}

	@Override
	public void w(String tag, String message) {
		println(WARN, tag, message);
	}

    @Override
    public void e(String tag, String message) {
        println(ERROR, tag, message);
    }

    private String getCurrentTimeString() {
        Date now = new Date();
        // 每天保存一个文件
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        return simpleDateFormat.format(now);
    }

    /**
     * 文件消息的格式:[yyyy-MM-dd HH:mm:ss] [D] com.bottle.alive|tag|message
     * @param priority
     * @param tag
     * @param message
     */
	private void println(int priority, String tag, String message) {
	    if( this.isDebuggable == false) {
	        return; // 不打印日志
        }
	    if(logHandler == null) {
	        return;
        }
        Message msg = logHandler.obtainMessage(MSG_WHAT);
        Bundle bundle = new Bundle();
        bundle.putInt(MSG_PRIORITY, priority);
        bundle.putString(MSG_TAG, tag);
        StackTraceElement element = getStackTraceElement(8);
        bundle.putString(MSG_LOG, formatLogMsg(message, element.getFileName(), element.getLineNumber(), element.getMethodName()));
        msg.setData(bundle);
        logHandler.sendMessage(msg);
	}

    private void cacheLog(int priority, String tag, String message) {
        cacheLog.append(TIMESTAMP_FMT.format(new Date()));
        switch (priority){
            case ILogger.VERBOSE:
                cacheLog.append(V);
                break;
            case ILogger.DEBUG:
                cacheLog.append(D);
                break;
            case ILogger.INFO:
                cacheLog.append(I);
                break;
            case ILogger.WARN:
                cacheLog.append(W);
                break;
            case ILogger.ERROR:
                cacheLog.append(E);
                break;
            default:
                break;
        }
        cacheLog.append(SPLIT).append(packageName)
                .append(SPLIT).append(tag)
                .append(SPLIT).append(message)
                .append(N); // 换行
        if(cacheLog.length() > CACHE_SIZE) {
            // 当日志达到一定数量时保存一次
            save();
        }
    }

}

五、对外提供接口

ILogger接口有了,具体的实现LoggerCatLogger和FileLogger也有了,提到的需求基本能满足了。接下来就是如何对外提供方便的接口。这里还是参考系统的Log的调用方法,提供我们的方法。为了方便,我们也定义一个Log类(方便替换现在系统log,或者以后被系统的log替换,只需要更换包名即可),也有静态的v(),d(),i(),w(),e()等方法。除此以外,还需要提供管理Logger的方法add,remove,已实现扩展,具体实现如下:


import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

public class Log{

	private static boolean logAble = true;

	private static HashMap<String, ILogger> loggerHashMap = new HashMap<String, ILogger>();

	public static boolean addLogger(ILogger logger) {
		String loggerName = logger.getClass().getName();
		boolean isSuccess = false;
		if (!loggerHashMap.containsKey(loggerName)){
			loggerHashMap.put(loggerName, logger);
			isSuccess = true;
		}
		return isSuccess;
	}

	public static void removeLogger(ILogger logger){
		String loggerName = logger.getClass().getName();
		if (loggerHashMap.containsKey(loggerName)){
		    try {
                logger.close();
                logger.save(); // 退出前先保存日记(需要保存的Logger)
                loggerHashMap.remove(loggerName);
            } catch (Exception e) {
		        e.printStackTrace();
            }
		}
	}

    public static void setLogAble(boolean logAble) {
        Log.logAble = logAble;
    }

    /**
     * @param logger 关闭某一个logger
     */
	public static void save(ILogger logger) {
	    if(logger != null) {
	        try {
                logger.save();
            } catch (Exception e) {
	            e.printStackTrace();
            }
        }
    }

    /**
     * 关闭所有的logger
     */
    public static void save() {
	    if(loggerHashMap == null || loggerHashMap.size() == 0) {
	        return;
        }
        for(String key : loggerHashMap.keySet()) {
			save(loggerHashMap.get(key));
        }
    }

    public static void v(Object object, String message){
        printLogger(ILogger.VERBOSE, object, message);
    }

	public static void d(Object object, String message){
		printLogger(ILogger.DEBUG, object, message);
	}

	public static void i(Object object, String message){
		printLogger(ILogger.INFO, object, message);
	}

	public static void w(Object object, String message){
		printLogger(ILogger.WARN, object, message);
	}

    public static void w(Object tag, Throwable ex){
        printLogger(ILogger.WARN, tag, dumpThrowable(ex));
    }

    public static void e(Object object, String message){
        printLogger(ILogger.ERROR, object, message);
    }

	public static void e(Object object, Throwable ex){
		printLogger(ILogger.ERROR, object, dumpThrowable(ex));
	}

    public static void v(String tag, String message){
        printLogger(ILogger.VERBOSE, tag, message);
    }

	public static void d(String tag, String message){
		printLogger(ILogger.DEBUG, tag, message);
	}

	public static void i(String tag, String message){
		printLogger(ILogger.INFO, tag, message);
	}

	public static void w(String tag, String message){
		printLogger(ILogger.WARN, tag, message);
	}

    public static void w(String tag, Throwable ex){
        printLogger(ILogger.WARN, tag, dumpThrowable(ex));
    }

    public static void e(String tag, String message){
        printLogger(ILogger.ERROR, tag, message);
    }

	public static void e(String tag, Throwable ex){
		printLogger(ILogger.ERROR, tag, dumpThrowable(ex));
	}

	private static String dumpThrowable(Throwable ex){
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		ex.printStackTrace(pw);
		return sw.toString();
	}

	public static void println(int priority, String tag, String message){
		printLogger(priority, tag, message);
	}

	private static void printLogger(int priority, Object object , String message){
		Class<?> cls = object.getClass();
		String tag = cls.getSimpleName();
		printLogger(priority, tag, message);
	}

	private static void printLogger(int priority, String tag, String message){
		if (logAble == false){
			return;
		}
        if (message == null) {
            message = "";
        }
        Iterator<Entry<String, ILogger>> iterator = loggerHashMap.entrySet().iterator();
        while (iterator.hasNext()){
            Entry<String, ILogger> entry = iterator.next();
            ILogger logger = entry.getValue();
            if (logger != null){
                printLogger(logger, priority, tag, message);
            }
        }
	}

	private static void printLogger(ILogger logger, int priority, String tag, String message){
		switch (priority){
		case ILogger.VERBOSE:
			logger.v(tag, message);
			break;
		case ILogger.DEBUG:
			logger.d(tag, message);
			break;
		case ILogger.INFO:
			logger.i(tag, message);
			break;
		case ILogger.WARN:
			logger.w(tag, message);
			break;
		case ILogger.ERROR:
			logger.e(tag, message);
			break;
		default:
			break;
		}
	}

}

六、如何使用

初始化:

  1. 在自定义的Application中初始化Log:
 @Override
 public void onCreate() {
     super.onCreate();
     Log.addLogger(new LogcatLogger());
     Log.addLogger(new FileLogger(this));
 }
  1. 在自定义的Application的onTrimMemory中保存Log
    @Override
    public void onTrimMemory(int level) {
        Log.save();
        super.onTrimMemory(level);
    }
  1. 在代码中使用Log

和系统的Log一样的使用方法,特意命名了和系统一样的名字:Log。

Log.v(TAG, "message");
Log.d(TAG, "message");
Log.i(TAG, "message");
Log.w(TAG, "message");
Log.e(TAG, "message");

就是如此简单,然后就可以使用自定义的Log框架,并且以后需要增加新的Logger也可以通过实现ILogger,实现自己的Logger,然后在添加到Log中。

总结

  1. 保留了系统Log的API风格,与系统的Log相比,只有常用的v(),d(),i(),w(),e();
  2. 支持扩展,可以自定义Logger,并且一行代码就能添加到框架中;
  3. 由于文件Logger并不是实时写入文件,而且是在线程中运行,只占用很少的资源,不会阻塞线程;
  4. LogcatLogger支持定位到具体的代码,在调试代码时非常方便;
  5. 不支持json,xml,对象等输出
  6. 未参考其它日记框架的实现,可以先学习一下其它框架
原创文章 25 获赞 12 访问量 1万+

猜你喜欢

转载自blog.csdn.net/half_bottle/article/details/85242341