异常日志机制与进程冻结:问题排查的黑匣子
日志是问题排查的"黑匣子",也是系统健康状态的"体检报告"。
引言:凌晨3点的日志抢救行动
记得那是一个周五的凌晨3点,我被一阵急促的电话铃声惊醒。电话那头是值班同事焦急的声音:"线上用户反馈系统频繁重启,但是现场已经来不及抓日志了,怎么办?"
我迅速清醒过来:"别慌,看看DropBox里有没有system_server_watchdog的日志!" 几分钟后,同事兴奋地回复:"找到了!有5条Watchdog日志!" 就是靠着这些自动保存的异常日志,我们在一个小时内定位到了问题:一个第三方系统服务死锁导致Watchdog触发系统重启。
这次经历让我深刻体会到:异常日志就是问题排查的"黑匣子",它在系统出问题的那一刻自动记录下关键信息,为我们后续的分析提供了宝贵的线索。
但问题来了:
- 这些日志是如何自动生成的?
- 它们保存在哪里?
- 如何才能有效地获取和利用这些日志?
- 新的进程冻结机制又是怎么回事?
本文将带你深入Android异常日志系统的底层机制,从日志打印到存储,从获取到分析,建立完整的异常日志知识体系。读完本文,你将能够:
- 理解Android异常日志的完整生命周期
- 掌握DropBox日志系统的工作原理和使用方法
- 了解进程冻结(Freeze)机制及其对稳定性的影响
- 学会高效获取各类异常日志的方法和技巧
- 具备基于日志快速定位问题的能力
一、异常日志系统概览
在深入具体机制之前,让我们先建立一个全局视角,了解Android日志系统的整体架构。
1.1 日志分类:各司其职的记录者
Android的日志系统就像一个大型档案馆,不同类型的日志存放在不同的"档案室"里:
Android日志体系
├── Logcat日志 (运行时日志)
│ ├── System Log - 系统运行日志
│ ├── Events Log - 系统事件日志
│ ├── Radio Log - 无线通信日志
│ └── Crash Log - 崩溃日志buffer
│
├── DropBox日志 (异常日志)
│ ├── ANR日志 - 应用无响应
│ ├── Java Crash日志 - Java异常崩溃
│ ├── Native Crash日志 - Native崩溃
│ ├── Watchdog日志 - 系统看门狗
│ ├── StrictMode日志 - 严格模式违规
│ └── Lowmem日志 - 低内存事件
│
└── Tombstone日志 (Native崩溃详情)
└── /data/tombstones/ - Native Crash堆栈
让我用一个比喻来解释这些日志的区别:
- Logcat日志就像是一个实时的"监控摄像头",记录着系统运行的每一刻
- DropBox日志就像是"事故报告书",只在出问题时才会生成
- Tombstone日志就像是"尸检报告",专门记录Native崩溃的详细现场
1.2 日志系统架构:从产生到存储
让我们看看一条异常日志是如何从产生到最终落盘的:

核心组件说明:
- Logd守护进程: 负责接收和管理Logcat日志,维护多个日志缓冲区
- DropBoxManagerService: 系统服务,专门负责异常日志的接收、存储和查询
- Debuggerd守护进程: 负责处理Native崩溃,生成Tombstone文件
二、异常日志打印机制
异常发生的瞬间,系统如何捕获并记录关键信息?让我们深入每一种异常的打印流程。
2.1 Java层异常日志
当Java代码抛出未捕获的异常时,Android的异常处理机制会自动介入。
异常捕获机制
每个线程都有一个未捕获异常处理器(UncaughtExceptionHandler):
java
// Thread.java
public void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}
// RuntimeInit.java - 默认的异常处理器
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
// 1. 打印异常堆栈到Logcat
Clog_e(TAG, "FATAL EXCEPTION: " + t.getName(), e);
// 2. 写入DropBox
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} finally {
// 3. 杀死进程
Process.killProcess(Process.myPid());
System.exit(10);
}
}
}
日志内容示例
AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.myapp, PID: 12345
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
at com.example.myapp.MainActivity.onCreate(MainActivity.java:25)
at android.app.Activity.performCreate(Activity.java:7224)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3270)
...
流程总结:
Java异常发生
↓
Thread.dispatchUncaughtException()
↓
RuntimeInit.KillApplicationHandler
↓
1. 打印到Logcat (AndroidRuntime buffer)
2. 写入DropBox (data_app_crash)
3. 杀死进程 (Process.killProcess)
2.2 Native层异常日志
Native代码崩溃时,Linux内核会发送信号给进程,Android通过信号处理机制捕获这些崩溃。
信号处理机制
cpp
// system/core/debuggerd/handler/debuggerd_handler.cpp
// 注册信号处理器
void debuggerd_init(debuggerd_callbacks_t* callbacks) {
// 注册需要处理的信号
struct sigaction action;
sigaction(SIGABRT, &action, nullptr); // abort()调用
sigaction(SIGBUS, &action, nullptr); // 总线错误
sigaction(SIGFPE, &action, nullptr); // 浮点异常
sigaction(SIGILL, &action, nullptr); // 非法指令
sigaction(SIGSEGV, &action, nullptr); // 段错误
sigaction(SIGTRAP, &action, nullptr); // 断点/跟踪陷阱
// ...
}
// 信号处理函数
static void debuggerd_signal_handler(int signal_number,
siginfo_t* info,
void* context) {
// 1. 停止其他线程
// 2. 连接debuggerd服务
// 3. 发送崩溃信息
// 4. 等待debuggerd生成Tombstone
// 5. 退出进程
}
Tombstone生成流程

Tombstone文件示例
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Android/aosp_car_arm64/generic_arm64:13/TQ1A.221205.011/eng:userdebug/test-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2024-12-29 03:15:42.123456789+0800
Process uptime: 5s
Cmdline: com.example.nativeapp
pid: 12345, tid: 12345, name: example.nati >>> com.example.nativeapp <<<
uid: 10123
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000010
Cause: null pointer dereference
x0 0000000000000000 x1 0000007ff5e8a2a0 x2 0000000000000000 x3 0000000000000001
x4 0000000000000000 x5 0000000000000000 x6 0000000000000000 x7 0000000000000000
...
backtrace:
#00 pc 0000000000001234 /system/lib64/libnative.so (crash_function+20)
#01 pc 0000000000005678 /system/lib64/libnative.so (main_function+100)
#02 pc 00000000000089ab /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+80)
2.3 ANR日志生成
ANR日志的生成过程在前文《ANR机制深度解析》中已有详细介绍,这里简要回顾关键步骤:
java
// ActivityManagerService.java
void appNotResponding(ProcessRecord app, String activityShortComponentName,
ApplicationInfo aInfo, String parentShortComponentName,
WindowProcessController parentProcess, boolean aboveSystem,
String annotation) {
// 1. 更新CPU使用统计
updateCpuStatsNow();
// 2. dump所有Java线程堆栈
// 发送SIGQUIT信号,ART会dump堆栈到traces文件
Process.sendSignal(app.pid, Process.SIGNAL_QUIT);
// 3. 收集系统信息
// - CPU使用率
// - 内存使用情况
// - Binder调用状态
// 4. 写入DropBox
addErrorToDropBox("anr", app, process, activity, parent,
annotation, cpuInfo, tracesFile, null);
// 5. 显示ANR Dialog (如果前台应用)
if (showAnrDialog) {
Message msg = Message.obtain();
msg.what = SHOW_NOT_RESPONDING_UI_MSG;
mUiHandler.sendMessage(msg);
}
}
ANR日志包含的关键信息:
- 发生时间和ANR类型(Input/Broadcast/Service)
- 进程信息(PID/UID/包名)
- 所有线程的堆栈(traces.txt)
- CPU使用情况(最近1分钟、5分钟、15分钟)
- 内存使用情况
三、DropBox日志系统深度解析
DropBox是Android的"异常事件记录仪",专门用于保存各类异常日志。让我们深入了解它的工作机制。
3.1 DropBox架构设计
DropBoxManagerService
├─ 日志接收接口 (addText/addFile/addData)
├─ 文件存储管理 (createEntry/writeEntry)
├─ 空间配额控制 (trimToFit)
├─ 日志查询接口 (getNextEntry)
└─ 日志订阅通知 (Intent广播)
核心实现代码
java
// frameworks/base/services/core/java/com/android/server/DropBoxManagerService.java
public final class DropBoxManagerService extends SystemService {
// 默认配置
private static final int DEFAULT_AGE_SECONDS = 3 * 86400; // 3天
private static final int DEFAULT_MAX_FILES = 1000; // 最多1000个文件
private static final int DEFAULT_QUOTA_KB = 5 * 1024; // 5MB配额
private static final int DEFAULT_QUOTA_PERCENT = 10; // 磁盘空间10%
// 存储目录
private final File mDropBoxDir; // /data/system/dropbox
// 接收日志
@Override
public void add(DropBoxManager.Entry entry) {
// 1. 检查权限
// 2. 创建文件
// 3. 写入内容
// 4. 发送广播通知
// 5. 检查并清理旧文件
}
}
3.2 DropBox存储机制
文件命名规则
DropBox使用特定的文件命名格式来组织日志:
<tag>@<timestamp>.txt[.gz]
例如:
anr@1703318400123.txt # ANR日志
data_app_crash@1703318401456.txt # Java Crash
system_server_wtf@1703318402789.txt # WTF日志
system_server_watchdog@1703318403012.txt.gz # Watchdog日志(压缩)
命名规则说明:
tag: 日志类型标识timestamp: 毫秒级时间戳.gz: 大文件会自动gzip压缩
常见的Tag类型
| Tag | 含义 | 触发条件 |
|---|---|---|
anr |
ANR日志 | 应用无响应超时 |
data_app_crash |
应用崩溃 | Java异常未捕获 |
data_app_native_crash |
Native崩溃 | Native代码信号异常 |
system_app_crash |
系统应用崩溃 | 系统应用Java崩溃 |
system_server_crash |
SystemServer崩溃 | 核心系统服务崩溃 |
system_server_watchdog |
Watchdog触发 | 系统服务死锁或超时 |
system_server_wtf |
WTF日志 | 不应该发生的错误 |
strict_mode |
严格模式违规 | StrictMode检测到问题 |
system_tombstone |
Tombstone | Native崩溃墓碑文件 |
3.3 写入DropBox
API使用示例
java
// 1. 写入文本日志
DropBoxManager dropbox = getSystemService(DropBoxManager.class);
if (dropbox != null) {
dropbox.addText("custom_tag", "Log content here");
}
// 2. 写入文件
File logFile = new File("/path/to/log.txt");
dropbox.addFile("custom_tag", logFile, DropBoxManager.IS_TEXT);
// 3. 写入二进制数据
byte[] data = "Binary data".getBytes();
dropbox.addData("custom_tag", data, 0);
// 4. 写入输入流
InputStream inputStream = new FileInputStream(file);
dropbox.addData("custom_tag", inputStream, DropBoxManager.IS_TEXT);
系统内部写入示例
java
// ActivityManagerService写入ANR日志
private void addErrorToDropBox(String eventType, ProcessRecord process,
String processName, ActivityRecord activity, ActivityRecord parent,
String subject, final String report, final File dataFile,
final ApplicationErrorReport.CrashInfo crashInfo) {
// 构建完整的ANR报告
final String dropboxTag = processClass(process) + "_" + eventType;
final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
// 写入DropBox
if (dbox != null) {
dbox.addText(dropboxTag, report);
}
}
3.4 读取DropBox
查询最近的ANR日志
java
DropBoxManager dropbox = getSystemService(DropBoxManager.class);
if (dropbox == null) return;
// 查询最近24小时的ANR日志
long timestamp = System.currentTimeMillis() - 24 * 60 * 60 * 1000;
DropBoxManager.Entry entry;
while ((entry = dropbox.getNextEntry("anr", timestamp)) != null) {
try {
// 更新时间戳,获取下一条
timestamp = entry.getTimeMillis();
// 读取日志内容(限制最大1MB)
String text = entry.getText(1024 * 1024);
// 处理日志内容
Log.d(TAG, "ANR at " + new Date(timestamp) + ": " + text);
} finally {
entry.close(); // 记得关闭
}
}
监听新日志产生
java
// 注册广播接收器,监听新日志
IntentFilter filter = new IntentFilter();
filter.addAction(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String tag = intent.getStringExtra(DropBoxManager.EXTRA_TAG);
long time = intent.getLongExtra(DropBoxManager.EXTRA_TIME, 0);
if ("anr".equals(tag)) {
Log.d(TAG, "New ANR log detected at " + time);
// 读取并分析新产生的ANR日志
}
}
}, filter);
3.5 空间管理与配额控制
DropBox会自动管理存储空间,防止日志占用过多磁盘:
java
// DropBoxManagerService.java
private void trimToFit() throws IOException {
// 1. 获取配额设置
int ageSeconds = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS);
int maxFiles = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_MAX_FILES, DEFAULT_MAX_FILES);
long quotaBytes = calculateQuotaBytes();
// 2. 统计当前使用情况
long totalBytes = 0;
List<EntryFile> allFiles = new ArrayList<>();
for (EntryFile entry : mAllFiles.contents) {
totalBytes += entry.file.length();
allFiles.add(entry);
}
// 3. 超出配额时删除最旧的文件
Collections.sort(allFiles, (a, b) -> Long.compare(a.timestampMillis, b.timestampMillis));
while (totalBytes > quotaBytes || allFiles.size() > maxFiles) {
EntryFile oldest = allFiles.remove(0);
oldest.file.delete();
totalBytes -= oldest.file.length();
}
// 4. 删除过期文件(默认超过3天)
long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000L;
for (EntryFile entry : allFiles) {
if (entry.timestampMillis < cutoffMillis) {
entry.file.delete();
}
}
}
配额控制策略:
- 时间限制: 默认保留3天内的日志
- 文件数量: 最多保留1000个文件
- 空间限制: 不超过5MB或磁盘空间的10%
- 清理策略: 优先删除最旧的日志
四、进程冻结(Freeze)机制
Android 11引入了进程冻结机制,作为LMK(Low Memory Killer)的补充,优化后台进程管理。
4.1 什么是进程冻结
**进程冻结(Freezer)**是一种暂停后台进程的技术,让进程"冬眠"而不是"杀死"。
与LMK的对比
| 特性 | Freeze | LMK (Low Memory Killer) |
|---|---|---|
| 进程状态 | 暂停(冻结) | 杀死 |
| 内存占用 | 保留在内存中 | 释放内存 |
| 恢复速度 | 快(只需解冻) | 慢(需要冷启动) |
| CPU消耗 | 0%(完全停止) | N/A(进程已不存在) |
| 适用场景 | 内存充足时 | 内存紧张时 |
| 用户体验 | 快速切回 | 需要重新加载 |
打个比喻:
- Freeze就像把电脑休眠: 所有数据还在内存里,唤醒后立即可用
- LMK就像关机: 下次开机要重新加载所有东西
4.2 冻结触发条件
系统不会随意冻结进程,需要满足一系列条件:
java
// frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java
private boolean shouldFreezeProcess(ProcessRecord app) {
// 1. 进程状态检查
if (app.getCurProcState() > ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
return false; // 只冻结后台进程
}
// 2. 不能冻结正在进行Binder调用的进程
synchronized (mProcLock) {
if (app.hasPendingTransaction()) {
return false;
}
}
// 3. 不能冻结正在播放音频的进程
if (app.hasAudioPlayback()) {
return false;
}
// 4. 不能冻结有前台服务的进程
if (app.hasForegroundServices()) {
return false;
}
// 5. 不能冻结最近使用过的进程(保护期)
long timeSinceLastInteraction = SystemClock.uptimeMillis() - app.lastInteractionTime;
if (timeSinceLastInteraction < MIN_FREEZE_DELAY) {
return false;
}
return true;
}
核心条件总结:
- ✅ 进程在后台(非前台/可见状态)
- ✅ 没有pending的Binder调用
- ✅ 没有前台Service
- ✅ 没有正在播放音频/视频
- ✅ 距离上次交互超过最小保护时间
4.3 冻结实现原理
进程冻结基于Linux内核的Cgroup Freezer机制实现。
Cgroup Freezer原理
bash
# Cgroup Freezer路径
/sys/fs/cgroup/freezer/
# 冻结一个进程
echo FROZEN > /sys/fs/cgroup/freezer/<group>/freezer.state
# 解冻进程
echo THAWED > /sys/fs/cgroup/freezer/<group>/freezer.state
# 查看当前状态
cat /sys/fs/cgroup/freezer/<group>/freezer.state
# 输出: THAWED (正常运行) 或 FROZEN (已冻结)
Android实现代码
java
// CachedAppOptimizer.java
private void freezeProcess(ProcessRecord app) {
try {
// 通过cgroup freezer冻结进程
// 这会调用到Native层,最终写入cgroup文件
Process.setProcessFrozen(app.pid, app.uid, true);
// 更新进程状态
synchronized (mProcLock) {
app.mOptRecord.setFrozen(true);
app.mOptRecord.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
}
// 记录日志
if (DEBUG_FREEZER) {
Slog.d(TAG, "Froze process: " + app.pid + " " + app.processName);
}
} catch (Exception e) {
Slog.e(TAG, "Unable to freeze " + app.pid + " " + app.processName, e);
}
}
private void unfreezeProcess(ProcessRecord app) {
try {
// 解冻进程
Process.setProcessFrozen(app.pid, app.uid, false);
// 更新状态
synchronized (mProcLock) {
app.mOptRecord.setFrozen(false);
app.mOptRecord.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
}
if (DEBUG_FREEZER) {
Slog.d(TAG, "Unfroze process: " + app.pid + " " + app.processName);
}
} catch (Exception e) {
Slog.e(TAG, "Unable to unfreeze " + app.pid + " " + app.processName, e);
}
}
Native层实现
cpp
// frameworks/base/core/jni/android_util_Process.cpp
static void android_os_Process_setProcessFrozen(JNIEnv *env, jobject clazz,
jint pid, jint uid, jboolean freeze) {
// 构建cgroup路径
std::string freezer_path = StringPrintf("/sys/fs/cgroup/freezer/uid_%d/pid_%d/freezer.state",
uid, pid);
// 写入状态
const char* state = freeze ? "FROZEN" : "THAWED";
int fd = open(freezer_path.c_str(), O_WRONLY);
if (fd < 0) {
ALOGE("Failed to open freezer.state: %s", freezer_path.c_str());
return;
}
write(fd, state, strlen(state));
close(fd);
}
4.4 冻结对稳定性的影响
虽然进程冻结能提升性能和续航,但也可能带来稳定性问题:
潜在问题
1. Binder死锁问题
场景:
进程A(冻结) 持有锁L
↓
进程B 尝试获取锁L
↓
进程B 阻塞等待
↓
如果进程B是系统关键服务
↓
可能触发Watchdog或ANR
示例代码:
java
// 进程A被冻结,持有锁
synchronized (SharedLock.getInstance()) {
// 进程被冻结在这里
}
// 进程B尝试获取同一个锁
synchronized (SharedLock.getInstance()) {
// 永久阻塞,可能导致ANR
}
2. Binder超时问题
进程A 发起Binder调用到进程B
↓
进程B被冻结,无法响应
↓
Binder调用超时 (默认5秒)
↓
进程A可能抛出DeadObjectException
3. 资源泄漏问题
冻结的进程无法释放持有的资源:
- 文件句柄
- 网络连接
- Wake Lock
- Sensor监听
防护措施
系统采取了多重防护:
java
// 1. 冻结前检查
private boolean canFreezeSafely(ProcessRecord app) {
// 检查是否有pending的Binder调用
if (app.hasPendingTransaction()) {
return false;
}
// 检查是否持有WakeLock
if (app.hasWakeLock()) {
return false;
}
// 检查是否有注册的传感器监听
if (app.hasSensorRegistrations()) {
return false;
}
return true;
}
// 2. 自动解冻触发条件
private void unfreezeIfNeeded(ProcessRecord app) {
// 收到Binder调用时自动解冻
if (app.hasIncomingBinderTransaction()) {
unfreezeProcess(app);
}
// 需要前台交互时解冻
if (app.needsUserInteraction()) {
unfreezeProcess(app);
}
// 系统事件触发解冻
if (app.hasSystemEvent()) {
unfreezeProcess(app);
}
}
五、如何获取异常日志
知道了日志的产生机制,现在让我们学习如何高效地获取这些日志。
5.1 获取Logcat日志
基础命令
bash
# 实时查看所有日志
adb logcat
# 清空日志缓冲区
adb logcat -c
# 导出日志到文件
adb logcat -d > logcat.txt
# 指定日志缓冲区
adb logcat -b system # 系统日志
adb logcat -b main # 主日志
adb logcat -b events # 事件日志
adb logcat -b crash # 崩溃日志
# 按优先级过滤
adb logcat *:E # 只显示Error及以上
adb logcat *:W # Warning及以上
adb logcat *:I # Info及以上
# 按Tag过滤
adb logcat -s ActivityManager:V # 只看ActivityManager的日志
adb logcat -s TAG1:I TAG2:D # 多个Tag
高级用法
bash
# 格式化输出
adb logcat -v time # 显示时间
adb logcat -v threadtime # 显示线程时间(推荐)
adb logcat -v long # 详细格式
# 搜索关键字
adb logcat | grep "ANR"
adb logcat | grep -i "crash" # 不区分大小写
# 保存最近的日志(限制大小)
adb logcat -d -t 1000 > recent.log # 最近1000行
# 持续保存到文件
adb logcat -v threadtime > logcat_$(date +%Y%m%d_%H%M%S).txt
# 多个buffer同时导出
adb logcat -b all -d > all_logs.txt
5.2 获取DropBox日志
方法1: dumpsys命令
bash
# 列出所有DropBox日志(只显示header)
adb shell dumpsys dropbox
输出示例:
Drop box contents: 42 entries
Max entries: 1000
anr (text, 245678 bytes, 2024-12-29 03:15:42)
data_app_crash (text, 12345 bytes, 2024-12-29 03:16:01)
system_server_watchdog (text, 89012 bytes, 2024-12-29 03:20:15)
...
# 获取特定类型的日志内容
adb shell dumpsys dropbox --print anr > anr_logs.txt
adb shell dumpsys dropbox --print data_app_crash > crash_logs.txt
# 获取所有日志
adb shell dumpsys dropbox --print > all_dropbox.txt
方法2: 直接读取文件
bash
# 需要root权限
adb root
adb wait-for-device
# 查看文件列表
adb shell ls -lh /data/system/dropbox/
输出示例:
-rw------- 1 system system 245K 2024-12-29 03:15 anr@1703807742123.txt
-rw------- 1 system system 12K 2024-12-29 03:16 data_app_crash@1703807761456.txt
-rw------- 1 system system 87K 2024-12-29 03:20 system_server_watchdog@1703807815789.txt.gz
# 拉取特定文件
adb pull /data/system/dropbox/anr@1703807742123.txt
# 拉取所有DropBox日志
adb pull /data/system/dropbox/ ./dropbox_logs/
方法3: 代码方式
java
// 需要READ_LOGS权限
<uses-permission android:name="android.permission.READ_LOGS" />
public class DropBoxReader {
public void readANRLogs(Context context) {
DropBoxManager dropbox = context.getSystemService(DropBoxManager.class);
if (dropbox == null) {
Log.e(TAG, "DropBoxManager not available");
return;
}
// 读取最近24小时的ANR日志
long timestamp = System.currentTimeMillis() - 24 * 60 * 60 * 1000;
DropBoxManager.Entry entry = null;
try {
while ((entry = dropbox.getNextEntry("anr", timestamp)) != null) {
// 更新时间戳
timestamp = entry.getTimeMillis();
// 读取日志内容(限制最大1MB)
String text = entry.getText(1024 * 1024);
if (text != null) {
// 分析ANR日志
analyzeANRLog(text, timestamp);
}
entry.close();
entry = null;
}
} finally {
if (entry != null) {
entry.close();
}
}
}
private void analyzeANRLog(String log, long timestamp) {
// 提取关键信息
// - 进程名和PID
// - 主线程堆栈
// - CPU使用情况
// ...
}
}
5.3 获取Tombstone日志
bash
# Android 10及以下
adb root
adb shell ls -l /data/tombstones/
adb pull /data/tombstones/tombstone_00
adb pull /data/tombstones/tombstone_01
# Android 11及以上(路径相同,但可能需要不同权限)
adb shell ls -l /data/tombstones/
adb pull /data/tombstones/
# 批量拉取脚本
#!/bin/bash
OUTPUT_DIR="./tombstones_$(date +%Y%m%d_%H%M%S)"
mkdir -p $OUTPUT_DIR
adb root
adb wait-for-device
adb pull /data/tombstones/ $OUTPUT_DIR/
echo "Tombstones saved to $OUTPUT_DIR"
ls -lh $OUTPUT_DIR
5.4 获取ANR traces文件
bash
# Android 10及以下: traces.txt
adb pull /data/anr/traces.txt
# Android 11及以上: ANR信息在DropBox中
adb shell dumpsys dropbox --print anr > anr_with_traces.txt
5.5 一键获取所有日志的脚本
bash
#!/bin/bash
# collect_all_logs.sh - 一键收集所有异常日志
set -e
# 配置
OUTPUT_DIR="./logs_$(date +%Y%m%d_%H%M%S)"
PACKAGE_NAME=$1 # 可选:指定包名
echo "================================"
echo "Android异常日志收集工具"
echo "================================"
echo "输出目录: $OUTPUT_DIR"
echo ""
# 创建输出目录
mkdir -p $OUTPUT_DIR
# 1. 收集Logcat
echo "[1/6] 收集Logcat日志..."
adb logcat -d -v threadtime > $OUTPUT_DIR/logcat.txt
adb logcat -b system -d -v threadtime > $OUTPUT_DIR/logcat_system.txt
adb logcat -b events -d -v threadtime > $OUTPUT_DIR/logcat_events.txt
adb logcat -b crash -d -v threadtime > $OUTPUT_DIR/logcat_crash.txt
echo " ✓ Logcat日志已保存"
# 2. 收集DropBox
echo "[2/6] 收集DropBox日志..."
adb shell dumpsys dropbox > $OUTPUT_DIR/dropbox_list.txt
adb shell dumpsys dropbox --print anr > $OUTPUT_DIR/dropbox_anr.txt 2>/dev/null || echo "No ANR logs"
adb shell dumpsys dropbox --print data_app_crash > $OUTPUT_DIR/dropbox_crash.txt 2>/dev/null || echo "No crash logs"
adb shell dumpsys dropbox --print system_server_watchdog > $OUTPUT_DIR/dropbox_watchdog.txt 2>/dev/null || echo "No watchdog logs"
echo " ✓ DropBox日志已保存"
# 3. 收集Tombstones
echo "[3/6] 收集Tombstone日志..."
adb root 2>/dev/null && sleep 2
mkdir -p $OUTPUT_DIR/tombstones
adb pull /data/tombstones/ $OUTPUT_DIR/tombstones/ 2>/dev/null || echo " ! 无法获取Tombstone(可能需要root)"
# 4. 收集ANR traces (Android 10-)
echo "[4/6] 收集ANR traces..."
adb pull /data/anr/ $OUTPUT_DIR/anr/ 2>/dev/null || echo " ! 无ANR traces文件(Android 11+在DropBox中)"
# 5. 收集系统信息
echo "[5/6] 收集系统信息..."
adb shell dumpsys meminfo > $OUTPUT_DIR/meminfo.txt
adb shell dumpsys cpuinfo > $OUTPUT_DIR/cpuinfo.txt
adb shell dumpsys battery > $OUTPUT_DIR/battery.txt
adb shell ps -A > $OUTPUT_DIR/processes.txt
adb shell getprop > $OUTPUT_DIR/properties.txt
# 6. 如果指定了包名,收集应用信息
if [ -n "$PACKAGE_NAME" ]; then
echo "[6/6] 收集应用信息 ($PACKAGE_NAME)..."
adb shell dumpsys package $PACKAGE_NAME > $OUTPUT_DIR/package_info.txt 2>/dev/null || echo " ! 包名不存在"
adb shell dumpsys activity $PACKAGE_NAME > $OUTPUT_DIR/activity_info.txt 2>/dev/null
else
echo "[6/6] 跳过应用信息收集(未指定包名)"
fi
# 生成摘要报告
echo ""
echo "生成摘要报告..."
cat > $OUTPUT_DIR/README.txt << EOF
Android异常日志收集报告
======================
收集时间: $(date)
设备信息: $(adb shell getprop ro.product.model) ($(adb shell getprop ro.build.version.release))
包名: ${PACKAGE_NAME:-未指定}
目录结构:
├── logcat*.txt - Logcat日志
├── dropbox_*.txt - DropBox异常日志
├── tombstones/ - Native崩溃详情
├── anr/ - ANR traces文件
├── meminfo.txt - 内存信息
├── cpuinfo.txt - CPU使用情况
├── battery.txt - 电量信息
├── processes.txt - 进程列表
├── properties.txt - 系统属性
└── package_info.txt - 应用信息(如果指定)
分析建议:
1. 优先查看 dropbox_anr.txt 和 dropbox_crash.txt
2. 如有Native崩溃,查看 tombstones/ 目录
3. 结合 logcat.txt 查看完整事件时间线
4. 检查 meminfo.txt 和 cpuinfo.txt 了解资源使用情况
EOF
echo ""
echo "================================"
echo "✓ 日志收集完成!"
echo "================================"
echo "输出目录: $OUTPUT_DIR"
echo "文件数量: $(ls -1 $OUTPUT_DIR | wc -l)"
echo "总大小: $(du -sh $OUTPUT_DIR | cut -f1)"
echo ""
echo "使用方法:"
echo " 查看摘要: cat $OUTPUT_DIR/README.txt"
echo " 压缩打包: tar -czf ${OUTPUT_DIR}.tar.gz $OUTPUT_DIR"
echo ""
使用方法:
bash
# 收集所有日志
./collect_all_logs.sh
# 收集特定应用的日志
./collect_all_logs.sh com.example.myapp
# 赋予执行权限
chmod +x collect_all_logs.sh
六、日志存储路径汇总
为了方便查找,这里汇总所有异常日志的存储路径:
Android异常日志路径
├── /data/anr/ # ANR日志目录
│ └── traces.txt # ANR traces (Android 10及以下)
│
├── /data/tombstones/ # Native崩溃日志
│ ├── tombstone_00 # 最新的Tombstone
│ ├── tombstone_01 # 次新的
│ ├── ...
│ └── tombstone_09 # 最多保留10个,循环覆盖
│
├── /data/system/dropbox/ # DropBox异常日志
│ ├── anr@<timestamp>.txt # ANR日志
│ ├── data_app_crash@<timestamp>.txt # 应用崩溃
│ ├── data_app_native_crash@<timestamp>.txt # Native崩溃
│ ├── system_app_crash@<timestamp>.txt # 系统应用崩溃
│ ├── system_server_crash@<timestamp>.txt # SystemServer崩溃
│ ├── system_server_watchdog@<timestamp>.txt.gz # Watchdog
│ ├── system_server_wtf@<timestamp>.txt # WTF日志
│ └── strict_mode@<timestamp>.txt # 严格模式违规
│
├── /data/system/usagestats/ # 使用统计
│ └── ...
│
└── /data/local/tmp/ # 临时日志(adb可直接访问)
└── ...
权限要求:
/data/anr/- 需要root或system权限/data/tombstones/- 需要root或system权限/data/system/dropbox/- 需要root或system权限(但可通过dumpsys读取)/data/local/tmp/- adb shell可直接访问
七、实战案例:基于日志定位线上问题
让我们通过一个真实案例,演示如何利用异常日志快速定位问题。
案例背景
问题现象: 用户反馈某个视频应用在播放视频时偶发性崩溃,但测试环境无法稳定复现。
排查步骤
Step 1: 获取崩溃日志
bash
# 收集最近24小时的崩溃日志
adb shell dumpsys dropbox --print data_app_crash > crash_logs.txt
# 筛选目标应用的崩溃
grep "com.example.videoapp" crash_logs.txt > videoapp_crashes.txt
Step 2: 分析崩溃日志
Process: com.example.videoapp, PID: 12345
java.lang.IllegalStateException: Release() called on uninitialized MediaCodec
at android.media.MediaCodec.native_release(Native Method)
at android.media.MediaCodec.release(MediaCodec.java:2145)
at com.example.videoapp.player.VideoDecoder.cleanup(VideoDecoder.java:156)
at com.example.videoapp.player.VideoPlayer$1.onCompletion(VideoPlayer.java:89)
at android.media.MediaPlayer$EventHandler.handleMessage(MediaPlayer.java:3348)
...
关键发现:
- 崩溃发生在
MediaCodec.release() - 异常信息:
Release() called on uninitialized MediaCodec - 调用路径:视频播放完成回调 → cleanup方法
Step 3: 定位根因
查看相关代码:
java
// VideoDecoder.java
public class VideoDecoder {
private MediaCodec mCodec;
public void init() {
mCodec = MediaCodec.createByCodecName("...");
mCodec.configure(...);
mCodec.start();
}
public void cleanup() {
if (mCodec != null) {
mCodec.release(); // ❌ 问题:未检查codec是否已初始化
mCodec = null;
}
}
}
问题分析:
init()方法可能因为某些原因失败(如codec不支持)init()失败后mCodec不为null,但未完成初始化cleanup()时调用了未初始化codec的release(),导致崩溃
Step 4: 验证修复
java
// 修复后的代码
public class VideoDecoder {
private MediaCodec mCodec;
private boolean mInitialized = false; // ✅ 添加初始化标志
public void init() {
try {
mCodec = MediaCodec.createByCodecName("...");
mCodec.configure(...);
mCodec.start();
mInitialized = true; // ✅ 标记为已初始化
} catch (Exception e) {
Log.e(TAG, "Failed to init codec", e);
mCodec = null;
mInitialized = false;
}
}
public void cleanup() {
if (mCodec != null && mInitialized) { // ✅ 检查是否已初始化
try {
mCodec.stop();
mCodec.release();
} catch (Exception e) {
Log.e(TAG, "Failed to cleanup codec", e);
} finally {
mCodec = null;
mInitialized = false;
}
}
}
}
Step 5: 监控效果
bash
# 部署修复版本后,持续监控崩溃率
adb shell dumpsys dropbox --print data_app_crash | \
grep "com.example.videoapp" | \
grep "MediaCodec" | \
wc -l
# 对比修复前后的崩溃次数
案例总结
关键经验:
- DropBox自动保存的崩溃日志是定位线上问题的关键
- 即使无法复现,也能通过日志分析定位根因
- 堆栈信息清晰地指出了问题代码位置
- 修复后需要持续监控验证效果
八、总结与展望
核心要点回顾
本文深入剖析了Android异常日志系统的底层机制,让我们回顾关键知识点:
1. 日志系统架构
- Logcat:实时运行日志,保存在内存缓冲区
- DropBox:异常事件日志,持久化到文件系统
- Tombstone:Native崩溃详细信息
2. 异常日志生成
- Java异常:UncaughtExceptionHandler捕获并记录
- Native崩溃:信号处理机制,debuggerd生成Tombstone
- ANR:ActivityManagerService主动dump堆栈
3. DropBox核心机制
- 自动接收和保存异常日志
- 基于tag和timestamp的文件组织
- 自动空间管理和配额控制
- 支持应用层查询和监听
4. 进程冻结机制
- 基于Cgroup Freezer实现
- 暂停而非杀死,快速恢复
- 需满足多个安全条件
- 可能导致Binder死锁等稳定性问题
5. 日志获取方法
- adb命令:简单直接,适合临时查看
- dumpsys:无需root,可获取DropBox内容
- 直接读文件:需要root,获取完整信息
- 应用代码:自动化监控和分析
最佳实践建议
对于应用开发者:
- 监听DropBox日志变化,实时上报异常
- 在关键路径添加日志,便于问题定位
- 正确处理进程生命周期,避免冻结相关问题
- 使用Crash监控平台(Bugly/Firebase)自动收集日志
对于系统开发者:
- 了解DropBox配额机制,合理设置保留策略
- 在SystemServer中适当添加WTF日志
- 优化进程冻结策略,平衡性能和稳定性
- 定期清理和归档异常日志
对于测试工程师:
- 掌握日志获取的完整方法
- 建立自动化日志收集脚本
- 学会从日志中快速提取关键信息
- 建立异常日志分析知识库
下篇预告
在掌握了异常日志的获取方法之后,下一篇文章《Native Crash深度分析:工具实战》将带你深入Native崩溃的世界:
- Tombstone文件完全解读: 寄存器、堆栈、内存映射
- 调试工具实战: addr2line、ndk-stack、gdb/lldb
- 符号化技巧: so库符号解析、混淆符号还原
- 常见崩溃模式: 空指针、野指针、栈溢出、内存破坏
- 实战案例: 真实Native崩溃的排查全过程
记住:日志是问题排查的"黑匣子",掌握了日志分析技能,就掌握了快速定位问题的钥匙!
相关文章
作者简介: 多年Android系统开发经验,专注于系统稳定性与性能优化领域。欢迎关注本系列,一起深入Android系统的精彩世界!