稳定性性能系列之四——异常日志机制与进程冻结:问题排查的黑匣子

异常日志机制与进程冻结:问题排查的黑匣子

日志是问题排查的"黑匣子",也是系统健康状态的"体检报告"。

引言:凌晨3点的日志抢救行动

记得那是一个周五的凌晨3点,我被一阵急促的电话铃声惊醒。电话那头是值班同事焦急的声音:"线上用户反馈系统频繁重启,但是现场已经来不及抓日志了,怎么办?"

我迅速清醒过来:"别慌,看看DropBox里有没有system_server_watchdog的日志!" 几分钟后,同事兴奋地回复:"找到了!有5条Watchdog日志!" 就是靠着这些自动保存的异常日志,我们在一个小时内定位到了问题:一个第三方系统服务死锁导致Watchdog触发系统重启。

这次经历让我深刻体会到:异常日志就是问题排查的"黑匣子",它在系统出问题的那一刻自动记录下关键信息,为我们后续的分析提供了宝贵的线索。

但问题来了:

  • 这些日志是如何自动生成的?
  • 它们保存在哪里?
  • 如何才能有效地获取和利用这些日志?
  • 新的进程冻结机制又是怎么回事?

本文将带你深入Android异常日志系统的底层机制,从日志打印到存储,从获取到分析,建立完整的异常日志知识体系。读完本文,你将能够:

  1. 理解Android异常日志的完整生命周期
  2. 掌握DropBox日志系统的工作原理和使用方法
  3. 了解进程冻结(Freeze)机制及其对稳定性的影响
  4. 学会高效获取各类异常日志的方法和技巧
  5. 具备基于日志快速定位问题的能力

一、异常日志系统概览

在深入具体机制之前,让我们先建立一个全局视角,了解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 日志系统架构:从产生到存储

让我们看看一条异常日志是如何从产生到最终落盘的:

核心组件说明:

  1. Logd守护进程: 负责接收和管理Logcat日志,维护多个日志缓冲区
  2. DropBoxManagerService: 系统服务,专门负责异常日志的接收、存储和查询
  3. 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();
        }
    }
}

配额控制策略:

  1. 时间限制: 默认保留3天内的日志
  2. 文件数量: 最多保留1000个文件
  3. 空间限制: 不超过5MB或磁盘空间的10%
  4. 清理策略: 优先删除最旧的日志

四、进程冻结(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;
        }
    }
}

问题分析:

  1. init()方法可能因为某些原因失败(如codec不支持)
  2. init()失败后mCodec不为null,但未完成初始化
  3. 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

# 对比修复前后的崩溃次数

案例总结

关键经验:

  1. DropBox自动保存的崩溃日志是定位线上问题的关键
  2. 即使无法复现,也能通过日志分析定位根因
  3. 堆栈信息清晰地指出了问题代码位置
  4. 修复后需要持续监控验证效果

八、总结与展望

核心要点回顾

本文深入剖析了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,获取完整信息
  • 应用代码:自动化监控和分析

最佳实践建议

对于应用开发者:

  1. 监听DropBox日志变化,实时上报异常
  2. 在关键路径添加日志,便于问题定位
  3. 正确处理进程生命周期,避免冻结相关问题
  4. 使用Crash监控平台(Bugly/Firebase)自动收集日志

对于系统开发者:

  1. 了解DropBox配额机制,合理设置保留策略
  2. 在SystemServer中适当添加WTF日志
  3. 优化进程冻结策略,平衡性能和稳定性
  4. 定期清理和归档异常日志

对于测试工程师:

  1. 掌握日志获取的完整方法
  2. 建立自动化日志收集脚本
  3. 学会从日志中快速提取关键信息
  4. 建立异常日志分析知识库

下篇预告

在掌握了异常日志的获取方法之后,下一篇文章《Native Crash深度分析:工具实战》将带你深入Native崩溃的世界:

  • Tombstone文件完全解读: 寄存器、堆栈、内存映射
  • 调试工具实战: addr2line、ndk-stack、gdb/lldb
  • 符号化技巧: so库符号解析、混淆符号还原
  • 常见崩溃模式: 空指针、野指针、栈溢出、内存破坏
  • 实战案例: 真实Native崩溃的排查全过程

记住:日志是问题排查的"黑匣子",掌握了日志分析技能,就掌握了快速定位问题的钥匙!


相关文章


作者简介: 多年Android系统开发经验,专注于系统稳定性与性能优化领域。欢迎关注本系列,一起深入Android系统的精彩世界!


🎉 感谢关注,让我们一起深入Android系统的精彩世界!

找到我 : 个人主页

相关推荐
冰_河14 分钟前
QPS从300到3100:我靠一行代码让接口性能暴涨10倍,系统性能原地起飞!!
java·后端·性能优化
阿巴斯甜11 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker12 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952713 小时前
Andorid Google 登录接入文档
android
黄林晴14 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android