3.4.2 dropbox日志文件的添加
要想理清一个Service,最好从它提供的服务开始进行分析。根据前面对DBMS的介绍可知,它提供了记录系统运行时日志信息的功能,所以这里先从dropbox日志文件的生成时说起。
当某个应用程序因为发生异常而崩溃(crash)时,ActivityManagerService(AMS)的handleApplicationCrash函数被调用,其代码如下:
[—>ActivityManagerService.java:handleApplicationCrash]
public void handleApplicationCrash(IBinder app,
ApplicationErrorReport.CrashInfo crashInfo){
ProcessRecord r=findAppProcess(app,"Crash");
……
//调用addErrorToDropBox函数,第一个参数是一个字符串,为“crash”
addErrorToDropBox("crash",r, null, null, null, null, null, crashInfo);
……
}
下面来看addErrorToDropBox函数,具体如下:
[—>ActivityManagerService.java:addErrorToDropBox]
public void addErrorToDropBox(String eventType,
ProcessRecord process, ActivityRecord activity,
ActivityRecord parent, String subject,
final String report, final File logFile,
final ApplicationErrorReport.CrashInfo crashInfo){
/*
dropbox日志文件的命名有一定的规则,其前缀都是一个特定的tag(标签),
tag由两部分组成,合起来是“进程类型_事件类型”。
下边代码中的processClass函数返回该进程的类型,包括“system_server”、“system_app”
和“data_app”3种。eventType用于指定事件类型,目前也有3种类型:“crash”、“wtf”
(what a terrible failure)和“anr”
*/
final String dropboxTag=processClass(process)+"_"+eventType;
//获取DBMS Bn端的对象DropBoxManager
final DropBoxManager dbox=(DropBoxManager)
mContext.getSystemService(Context.DROPBOX_SERVICE);
/*
对于DBMS,不仅通过tag标识文件名,还可以根据配置的情况,允许或禁止特定tag日志
文件的记录。isTagEnable将判断DBMS是否禁止该标签,如果该tag已被禁止,则不允许记
录日志文件
*/
if(dbox==null||!dbox.isTagEnabled(dropboxTag))return;
//创建一个StringBuilder,用于保存日志信息
final StringBuilder sb=new StringBuilder(1024);
appendDropBoxProcessHeaders(process, sb);
……//将信息保存到字符串sb中
//单独启动一个线程用于向DBMS添加信息
Thread worker=new Thread("Error dump:"+dropboxTag){
@Override
public void run(){
if(report!=null){
sb.append(report);
}
if(logFile!=null){
try{//如果有log文件,那么把log文件内容读到sb中
sb.append(FileUtils.readTextFile(logFile,
128*1024,"\n\n[[TRUNCATED]]"));
}……
}
//读取crashInfo信息,一般记录的是调用堆栈信息
if(crashInfo!=null&&crashInfo.stackTrace!=null){
sb.append(crashInfo.stackTrace);
}
String setting=Settings.Secure.ERROR_LOGCAT_PREFIX+dropboxTag;
//查询Settings数据库,判断该tag类型的日志是否对所记录的信息有行数限制,例如某
些tag的日志文件只准记录1000行的信息
int lines=Settings.Secure.getInt(mContext.getContentResolver(),
setting,0);
if(lines>0){
sb.append("\n");
InputStreamReader input=null;
try{
//创建一个新进程以运行logcat,后面的参数都是logcat常用的参数
java.lang.Process logcat=new
ProcessBuilder("/system/bin/logcat",
"-v","time","-b","events","-b","system","-b",
"main","-t",String.valueOf(lines))
.redirectErrorStream(true).start();
//由于新进程的输出已经重定向,因此这里可以获取最后lines行的信息,不熟悉
ProcessBuidler的读者可以查看SDK中关于它的用法说明
……
}
}
//调用DBMS的addText
dbox.addText(dropboxTag, sb.toString());
}
};
if(process==null||process.pid==MY_PID){
worker.run();//如果是SystemServer进程崩溃了,则不能在别的线程执行
}else{
worker.start();
}
}
由上面代码可知,addErrorToDropBox在生成日志的内容上花了不少工夫,因为这些是最重要的,最后仅调用addText函数便将内容传给DBMS的功能。
addText函数定义在DropBoxManager类中,代码如下:
[—>DropBoxManager.java:addText]
public void addText(String tag, String data){
/*
mService和DBMS交互。DBMS对外只提供一个add函数用于日志添加,而DBM提供了3个函数,
分别是addText、addData、addFile,以方便使用
*/
try{mService.add(new Entry(tag,0,data));}……
}
DBM向DBMS传递的数据被封装在一个Entry中。下面来看DBMS的add函数,其代码如下:
[—>DropBoxManagerService.java:add]
public void add(DropBoxManager.Entry entry){
File temp=null;
OutputStream output=null;
final String tag=entry.getTag();//先取出这个Entry的tag
try{
int flags=entry.getFlags();
……
//做一些初始化工作,包括生成dropbox目录、统计当前已有的dropbox文件信息等
init();
if(!isTagEnabled(tag))return;//如果该tag被禁止,则不能生成日志文件
long max=trimToFit();
long lastTrim=System.currentTimeMillis();
//BlockSize一般是4KB
byte[]buffer=new byte[mBlockSize];
//从Entry中得到一个输入流。与Java I/O相关的类比较多,且用法非常灵活,建议读者阅
读《Java编程思想》中“Java I/O系统”一章
InputStream input=entry.getInputStream();
……
int read=0;
while(read<buffer.length){
int n=input.read(buffer, read, buffer.length-read);
if(n<=0)break;
read+=n;
}
//先生成一个临时文件,命名方式为“drop线程id.tmp”
temp=new File(mDropBoxDir,"drop"+
Thread.currentThread().getId()+".tmp");
int bufferSize=mBlockSize;
if(bufferSize>4096)bufferSize=4096;
if(bufferSize<512)bufferSize=512;
FileOutputStream foutput=new FileOutputStream(temp);
output=new BufferedOutputStream(foutput, bufferSize);
//生成GZIP压缩文件
if(read==buffer.length&&
((flags&DropBoxManager.IS_GZIPPED)==0)){
output=new GZIPOutputStream(output);
flags=flags|DropBoxManager.IS_GZIPPED;
}
/*
DBMS很珍惜/data分区,若所生成文件的size大于一个BlockSize,
则一定要先压缩
*/
……//写文件,这段代码非常烦琐,其主要目的是尽量节省存储空间
/*
生成一个EntryFile对象,并保存到DBMS内部的一个数据容器中。
DBMS除了负责生成文件外,还提供查询的功能,这个功能由getNextEntry完成。
另外,刚才生成的临时文件在createEntry函数中会重命为带标签的名字,
读者可自行分析createEntry函数
*/
long time=createEntry(temp, tag, flags);
temp=null;
Intent dropboxIntent=new
Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
if(!mBooted){
dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
}
//发送DROPBOX_ENTRY_ADDED广播。系统中目前还没有程序接收该广播
mContext.sendBroadcast(dropboxIntent,
android.Manifest.permission.READ_LOGS);
}……
}
上面代码中略去了DBMS写文件的部分,我们从代码注释中可获悉,DBMS非常珍惜/data分区的空间,需要考虑每一个日志文件的压缩以节省存储空间。如果说细节体现功力,那么这正是一个极好的例证。
一个真实设备上/data/system/dropbox目录中的内容如图3-2所示。
图 3-2 真实设备中dropbox目录中的内容
图3-2中最后一项data_app_anr@1324836096560.txt.gz的大小是6.1KB,该文件解压后得到的文件大小是42KB。看来,压缩确实节省了不少存储空间。
另外,我们从图3-2中还发现了其他不同的tag,如SYSTEM_BOOT、SYSTEM_TOMBSTONE等,这些都是由BootReceiver在收到BOOT_COMPLETE广播后收集相关信息并传递给DBMS而生成的日志文件。