一、概述
在工作过程中,发现一个比较有意思的现象:每个同事都有一个自己的代码库,里面基本都包含一个Log的工具。这充分说明了Log在Android 开发中的重要,不管如何实现,大家都不自觉地按照自己的习惯,完成了一个Log工具。每个开发者都有自己的习惯和特点,在Log工具上的实现大同小异,但如果在同一个项目中加入各自的Log工具,难免产生了重复代码,还可能会造成使用的困扰。一个团队,在基础工具上还是要保持一致,本文尝试实现一个大家都接受,并且能满足大家需求,在使用上还不会造成困扰的,高性能的,可扩展的 Log 框架,项目地址:Logger。
二、Log 需求
- 参考Android自带的Log工具,log有,VERBOSE,DEBUG,INFO,WARN,ERROR,ASSERT六个等级,对应有六个方法(常用):v,d,I,w,e,wtf;
- 可以输出到logcat,也可以输出到文件或其它;
- 可以选择关闭或者打开某个或者所有log;
- 可以自定义增加新的log;
- 输出到logcat的log还可以显示代码行数,点击即可打开这个类文件,并且定位到对应的行;
- 可以上传到后台服务器;
- 其它…
三、定义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;
}
}
}
六、如何使用
初始化:
- 在自定义的Application中初始化Log:
@Override
public void onCreate() {
super.onCreate();
Log.addLogger(new LogcatLogger());
Log.addLogger(new FileLogger(this));
}
- 在自定义的Application的onTrimMemory中保存Log
@Override
public void onTrimMemory(int level) {
Log.save();
super.onTrimMemory(level);
}
- 在代码中使用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中。
总结
- 保留了系统Log的API风格,与系统的Log相比,只有常用的v(),d(),i(),w(),e();
- 支持扩展,可以自定义Logger,并且一行代码就能添加到框架中;
- 由于文件Logger并不是实时写入文件,而且是在线程中运行,只占用很少的资源,不会阻塞线程;
- LogcatLogger支持定位到具体的代码,在调试代码时非常方便;
- 不支持json,xml,对象等输出
- 未参考其它日记框架的实现,可以先学习一下其它框架