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

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

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

引言:凌晨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系统的精彩世界!

找到我 : 个人主页

相关推荐
低调小一1 天前
深度复盘:KMP 在字节跳动的工程化落地实践
android·kotlin
歪楼小能手1 天前
Android16系统go版关闭重力旋转开关后缺失手动旋转屏幕悬浮按钮
android·java·平板
崇山峻岭之间1 天前
Matlab学习记录37
android·学习·matlab
e***98571 天前
Bug侦破大会:破解技术悬案的终极策略
bug
stevenzqzq1 天前
Android 协程 Channel 菜鸟教程
android·channel
遗悲风1 天前
PHP伪协议全面解析:原理、常用场景、攻防实战与安全防护
android·安全·php
撩得Android一次心动1 天前
Android Lifecycle 全面解析:掌握生命周期管理的艺术(源码篇)
android·lifecycle
stevenzqzq1 天前
android fow 限流
android·限流·flow
缘友一世1 天前
精粤X99-TI D4 PLUS大板使用多显卡BIOS设置
bug·gpu·硬件·主板·x99
码农幻想梦1 天前
实验四 mybatis动态sql及逆向工程
sql·性能优化·mybatis