一、记录局的成立:DBMS 的启动流程
在 Android 城市的市政厅(SystemServer)里,有一个专门负责记录城市重大事件的部门 ------ 事件记录局(DropBoxManagerService,简称 DBMS)。当城市开始新一天的运转时,市政厅秘书长会首先宣布记录局成立:
java
csharp
// 市政厅启动代码(SystemServer.java)
private void startOtherServices() {
// 成立事件记录局,地址设在/data/system/dropbox
ServiceManager.addService("dropbox",
new DropBoxManagerService(城市, new File("/data/system/dropbox")));
}
记录局的筹备工作
记录局局长上任后,首先要做的是筹备办公室:
java
scss
// 记录局局长的初始化工作(DropBoxManagerService.java)
public DropBoxManagerService(城市, 地址) {
mDropBoxDir = 地址; // 办公室地址
mContext = 城市;
// 安装城市广播接收器,监听重要事件
IntentFilter filter = new IntentFilter();
filter.addAction("存储警报"); // 存储空间不足时的警报
filter.addAction("城市启动完成"); // 城市启动完毕的通知
城市.registerReceiver(记录员, filter);
// 监听城市规划变更(Settings变化)
mContentResolver.registerContentObserver(
城市规划URI, true, new ContentObserver(新Handler()) {
public void onChange(是否变更) {
记录员.onReceive(城市, null);
}
});
// 准备消息处理员
mHandler = new Handler() {
public void handleMessage(消息 msg) {
if (msg.what == 发送广播) {
城市.sendBroadcastAsUser((Intent)msg.obj, 市长, 读取日志权限);
}
}
};
}
记录员的日常工作
当记录局收到广播通知(如城市启动完成、存储警报等),记录员会立即开始工作:
java
typescript
// 记录员的工作流程(DropBoxManagerService.java)
private final 广播记录员 mReceiver = new 广播记录员() {
public void onReceive(城市, 广播 intent) {
if (广播是启动完成) {
mBooted = true;
return;
}
// 重置存储检查时间
mCachedQuotaUptimeMillis = 0;
// 开启后台工作线程
new Thread() {
public void run() {
try {
筹备办公室(); // 初始化目录和文件
整理文件(); // 清理过期文件
} catch (IOException e) {
// 处理异常
}
}
}.start();
}
};
筹备办公室:初始化目录和文件
java
ini
private synchronized void 筹备办公室() throws IOException {
if (mStatFs == null) {
// 创建办公室目录,如果不存在
if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) {
// 处理创建失败
}
mStatFs = new StatFs(mDropBoxDir.getPath());
mBlockSize = mStatFs.getBlockSize(); // 文件块大小4096字节
}
if (mAllFiles == null) {
// 列出所有文件
File[] files = mDropBoxDir.listFiles();
mAllFiles = new FileList();
mFilesByTag = new HashMap<String, FileList>();
for (File file : files) {
if (文件是临时文件) {
file.delete(); // 删除临时文件
continue;
}
// 创建文件记录对象
EntryFile entry = new EntryFile(file, mBlockSize);
if (entry.tag == null) {
continue; // 忽略无标签文件
} else if (entry.timestampMillis == 0) {
file.delete(); // 删除时间戳为0的文件
continue;
}
// 登记文件记录
enrollEntry(entry);
}
}
}
整理文件:清理过期记录
java
java
private synchronized long 整理文件() {
// 获取配置参数
int 保存天数 = Settings.Global.getInt(城市规划,
Settings.Global.DROPBOX_AGE_SECONDS, 3天);
int 最大文件数 = Settings.Global.getInt(城市规划,
Settings.Global.DROPBOX_MAX_FILES, 1000);
long 过期时间 = 当前时间 - 保存天数 * 1000;
// 删除过期或超出数量的文件
while (!mAllFiles.contents.isEmpty()) {
EntryFile entry = mAllFiles.contents.first();
if (entry.timestampMillis > 过期时间 && mAllFiles.contents.size() < 最大文件数) break;
// 移除文件记录并删除物理文件
// ...
}
// 检查存储空间使用情况
long 系统运行时间 = SystemClock.uptimeMillis();
if (系统运行时间 > mCachedQuotaUptimeMillis + 5秒) {
// 重新计算存储配额
int 配额百分比 = Settings.Global.getInt(城市规划,
Settings.Global.DROPBOX_QUOTA_PERCENT, 10);
int 保留百分比 = Settings.Global.getInt(城市规划,
Settings.Global.DROPBOX_RESERVE_PERCENT, 10);
int 配额大小 = Settings.Global.getInt(城市规划,
Settings.Global.DROPBOX_QUOTA_KB, 5*1024);
mStatFs.restat(mDropBoxDir.getPath());
int 可用空间 = mStatFs.getAvailableBlocks();
int 非保留空间 = 可用空间 - mStatFs.getBlockCount() * 保留百分比 / 100;
int 最大配额块数 = 配额大小 * 1024 / mBlockSize;
mCachedQuotaBlocks = Math.min(最大配额块数, Math.max(0, 非保留空间 * 配额百分比 / 100));
mCachedQuotaUptimeMillis = 系统运行时间;
}
// 当文件占用超过配额时,按标签公平清理
if (mAllFiles.blocks > mCachedQuotaBlocks) {
// 公平分配配额并清理旧文件
// ...
}
return mCachedQuotaBlocks * mBlockSize;
}
二、事件记录:DBMS 处理系统异常
当 Android 城市中发生重大事件(如应用崩溃、ANR 等),城市警察局(AMS)会通知记录局来收集证据:
java
arduino
// 警察局的事件报告方法(ActivityManagerService.java)
public void 报告事件(String 事件类型,
进程记录 process, String 进程名, ActivityRecord activity,
ActivityRecord parent, String 事件主题,
final String 报告内容, final File 日志文件,
final 崩溃信息 crashInfo) {
// 生成事件标签(如"system_server_crash")
final String 标签 = 进程类别(process) + "_" + 事件类型;
// 获取记录局代理
final DropBoxManager 记录局 = (DropBoxManager)
mContext.getSystemService(记录局服务);
if (记录局 == null || !记录局.isTagEnabled(标签)) return;
final StringBuilder 报告 = new StringBuilder(1024);
// 添加进程头部信息
添加进程信息(process, 进程名, 报告);
// 添加事件主题和构建信息
if (事件主题 != null) {
报告.append("Subject: ").append(事件主题).append("\n");
}
报告.append("Build: ").append(Build.FINGERPRINT).append("\n");
报告.append("\n");
// 开启后台线程处理,避免阻塞
Thread 工作者 = new Thread("Error dump: " + 标签) {
@Override
public void run() {
if (报告内容 != null) {
报告.append(报告内容); // 添加ANR时的CPU信息或内存信息
}
if (日志文件 != null) {
// 添加traces文件内容,最大256KB
报告.append(FileUtils.readTextFile(日志文件, 最大大小, "\n\n[[TRUNCATED]]"));
}
if (crashInfo != null && crashInfo.stackTrace != null) {
报告.append(crashInfo.stackTrace); // 添加崩溃堆栈
}
// 添加相关logcat日志
int 日志行数 = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ERROR_LOGCAT_PREFIX + 标签, 0);
if (日志行数 > 0) {
// 执行logcat命令获取日志
java.lang.Process logcat = new ProcessBuilder("/system/bin/logcat",
"-v", "time", "-b", "events", "-b", "system", "-b", "main",
"-b", "crash", "-t", String.valueOf(日志行数))
.redirectErrorStream(true).start();
// 读取logcat输出并添加到报告
// ...
}
// 将报告提交给记录局
记录局.addText(标签, 报告.toString());
}
};
if (process == null) {
// 系统进程崩溃时直接处理
工作者.run();
} else {
工作者.start(); // 普通情况开启后台线程
}
}
事件分类:标签的含义
记录局对不同事件使用不同标签,例如:
java
arduino
// 进程分类方法(ActivityManagerService.java)
private static String 进程类别(进程记录 process) {
if (process == null || process.pid == 市政厅进程ID) {
return "system_server"; // 市政厅进程
} else if ((process.info.flags & 系统应用标志) != 0) {
return "system_app"; // 系统应用
} else {
return "data_app"; // 普通应用
}
}
常见标签及其含义:
system_server_anr
:市政厅进程无响应system_server_crash
:市政厅进程崩溃data_app_crash
:普通应用崩溃system_app_anr
:系统应用无响应
记录局的文件存储
记录局收到事件报告后,会将信息存储为文件:
java
scss
// 记录局的文件存储流程(DropBoxManagerService.java)
public void add(DropBoxManager.Entry 事件) {
File 临时文件 = null;
OutputStream 输出 = null;
final String 标签 = 事件.getTag();
try {
int 标志 = 事件.getFlags();
筹备办公室(); // 初始化
long 最大空间 = 整理文件(); // 清理空间
long 上次整理时间 = System.currentTimeMillis();
byte[] 缓冲区 = new byte[mBlockSize];
InputStream 输入 = 事件.getInputStream();
// 读取事件数据
int 读取大小 = 0;
while (读取大小 < 缓冲区.length) {
int n = 输入.read(缓冲区, 读取大小, 缓冲区.length - 读取大小);
if (n <= 0) break;
读取大小 += n;
}
// 创建临时文件
临时文件 = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
int 缓冲区大小 = mBlockSize;
if (缓冲区大小 > 4096) 缓冲区大小 = 4096;
if (缓冲区大小 < 512) 缓冲区大小 = 512;
FileOutputStream 文件输出 = new FileOutputStream(临时文件);
输出 = new BufferedOutputStream(文件输出, 缓冲区大小);
// 压缩文件(如果需要)
if (读取大小 == 缓冲区.length && (标志 & 是否压缩) == 0) {
输出 = new GZIPOutputStream(输出);
标志 = 标志 | 是否压缩;
}
// 写入数据并持续清理空间
do {
输出.write(缓冲区, 0, 读取大小);
long 当前时间 = System.currentTimeMillis();
if (当前时间 - 上次整理时间 > 30秒) {
最大空间 = 整理文件(); // 超过30秒则清理空间
上次整理时间 = 当前时间;
}
读取大小 = 输入.read(缓冲区);
if (读取大小 <= 0) {
FileUtils.sync(文件输出);
输出.close();
输出 = null;
} else {
输出.flush();
}
long 文件大小 = 临时文件.length();
if (文件大小 > 最大空间) {
临时文件.delete();
临时文件 = null;
break;
}
} while (读取大小 > 0);
// 创建正式记录文件
long 时间戳 = 创建记录(临时文件, 标签, 标志);
临时文件 = null;
// 发送新记录广播
final Intent 新记录广播 = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
新记录广播.putExtra(标签, 标签);
新记录广播.putExtra(时间, 时间戳);
mHandler.sendMessage(mHandler.obtainMessage(发送广播, 新记录广播));
} catch (IOException e) {
// 处理异常
} finally {
// 清理资源
}
}
三、记录局的工作总结
DBMS 的核心职责
-
存储目录:所有记录保存在
/data/system/dropbox
目录 -
记录事件:当系统发生以下事件时记录信息:
- 应用崩溃(crash)
- 应用无响应(anr)
- 严重错误(wtf)
- 内存不足(lowmem)
- Watchdog 事件(watchdog)
-
记录内容:
- 进程信息(进程名、flags、包名及版本)
- 事件相关日志(logcat)
- 崩溃堆栈(crash 时)
- CPU 信息(ANR 时)
文件管理策略
-
过期时间:默认 3 天
-
最大文件数:默认 1000 个
-
最大存储空间:默认 5MB
-
当空间不足时,按标签公平清理旧文件
通过这套完整的事件记录系统,Android 城市的开发人员可以在系统出现问题时,从记录局获取详细的事件报告,快速定位和解决问题,确保城市的正常运转。