3.9 使用本地运行时应用程序日志分析现场错误情况
Atul Nene
3.9.1 问题
当用户报告了你认为不应该发生的情况时,往往应用程序的发布版本已经投放市场,你无法了解用户的环境中到底发生了什么,而缺陷报告提供的是一个“无法复制”的情景。
3.9.2 解决方案
为你的应用程序设计一个内建的机制,在这种情况下能够提供更多的细节。你知道重要的事件或者状态变化以及应用程序的资源需求,如果在应用程序的运行时日志中记录这些情况,日志就可以成为调查所报告的错误核心情况的又一个必需的资源。这种简单的预防手段和机制有助于减少因为不可预见的情况造成用户的抱怨,并且改进整体用户体验。
解决方案之一是使用标准的java.util.logging包。本攻略提供了一个RuntimeLog示例,使用java.util.logging写入设备上的一个日志文件,并且让开发人员能够全面控制记录的详细程度。
3.9.3 讨论
你已经设计、开发并且测试了应用程序,并将其发布到Android Market,因此你认为可以给自己放个假了。别着急!除了最简单的应用程序以外,开发人员无法在开发应用程序期间顾及所有可能的情境,开发时间也没有如此宽松,用户一定会报告某些预料之外的应用行为。这些行为不一定是缺陷;可能只是在测试中没有碰到的运行时情境。在应用程序中设计一个运行时应用程序日志机制,就能应对意外情况。
将应用程序中最重要的事件记录到日志中,例如,状态变化、资源超时(网络访问,线程等待)或者最高重试次数。甚至,可以防御性地记录异常情况下的代码路径执行,或者发送给用户的最重要通知。
警告: 只创建能够洞察应用程序工作情况的语句。否则,日志的尺寸本身就成为问题,虽然开发完毕的应用程序在运行时会忽略Log.d()调用,但是太多的日志语句仍然会使应用程序的速度降低。
注意: 你可能疑惑为什么不使用LogCat或者BugSense/ACRA来应对这一任务。这些解决方案无法满足需求的原因如下:
因为用户不太可能为其设备附加一个调试程序。代码中过多的Log.d和Log.i语句对应用程序性能有负面的影响。实际上,你在发布的应用程序中不应该编译Log.*语句。
ACRA/BugSense在设备连接到互联网时工作得很好,但是情况并非总是如此,除了ACRA之外,应用程序的一些类完全不需要互联网。而且,ACRA栈跟踪只提供抛出异常时的详情,而本攻略提供了应用程序运行中较长期的视图。
例3-5展示了RuntimeLog类。
例3-5:RuntimeLog类
//使用这些内建的机制
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
public class RuntimeLog {
public static final int MODE_DEBUG = 1;
public static final int MODE_RELEASE = 2;
public static final int ERROR = 3;
public static final int WARNING = 4;
public static final int INFO = 5;
public static final int DEBUG = 6;
public static final int VERBOSE = 7;
//将模式修改为MODE_DEBUG,用于内部调试
static boolean Mode = MODE_RELEASE;
static logfileName = "/sdcard/YourAppName.log"
static Logger logger;
static LogRecord record;
//在第一次使用类的时候实例化日志并创建自定义的格式化器
static {
try {
FileHandler fh = new FileHandler(logfileName, true);
fh.setFormatter(new Formatter() {
public String format(LogRecord rec) {
StringBuffer buf = new StringBuffer(1000);
buf.append(new java.util.Date().getDate());
buf.append('/');
buf.append(new java.util.Date().getMonth());
buf.append('/');
buf.append((new java.util.Date().getYear())%100);
buf.append(' ');
buf.append(new java.util.Date().getHours());
buf.append(':');
buf.append(new java.util.Date().getMinutes());
buf.append(':');
buf.append(new java.util.Date().getSeconds());
buf.append('\n');
return buf.toString();
}
});
logger = Logger.getLogger(logfileName);
logger.addHandler(fh);
}
catch (IOException e) {
e.printStackTrace();
}
}
// 日志方法
public static void log(int logLevel,String msg) {
//在发行模式下不记录DEBUG和VERBOSE语句
if (Mode == MODE_RELEASE) && (logLevel >= DEBUG))
return;
record=new LogRecord(Level.ALL, msg);
record.setLoggerName(logfileName);
try {
switch(logLevel) {
case ERROR:
record.setLevel(Level.SEVERE);
logger.log(record);
break;
case WARNING:
record.setLevel(Level.WARNING);
logger.log(record);
break;
case INFO:
record.setLevel(Level.INFO);
logger.log(record);
break;
//在某些API版本上FINE和FINEST级别可能无法正常工作,用INFO代替
case DEBUG:
record.setLevel(Level.INFO);
logger.log(record);
break;
case VERBOSE:
record.setLevel(Level.INFO);
logger.log(record);
break;
}
}
catch(Exception exception) {
exception.printStackTrace();
}
}
}
当然,有几个替代方案可供使用:
你可以使用相同的机制在开发应用程序时发现复杂的运行时问题。为此,将Mode变量设置为MODE_DEBUG。
对于具备许多模块的复杂应用,在日志调用中添加模块名为附加参数可能有用。
你还可以从LogRecord中提取ClassName和MethodName添加到日志语句中;但是不建议在运行时日志中这么做。
例3-6说明,这种机制的基本用法和常规的Log.d调用同样简单。
例3-6:使用RuntimeLog类
RuntimeLog.log (RuntimeLog.ERROR, "Network resource access request failed");
RuntimeLog.log (RuntimeLog.WARNING, "App changed state to STRANGE_STATE");
...
如果有必要,你可以要求用户从其SD卡中读取日志文件,并发送给你的支持团队。更好的办法是,编写代码,在按下某个按钮时完成这一任务!
下面是一些附加的注意事项:
这种机制不一定要处于“始终开启”的状态。你可以根据用户设置的配置选项记录日志,仅在最终用户试图重现某种情景的时候启用它。
如果日志始终开启,用当前日期(在应用程序启动时确定)命名日志文件,删除某个日期之前确实不再使用的旧日志,这能够有效地控制日志文件的大小。
3.9.4 参阅
ACRA网站;攻略3.7;攻略3.8