1. 问题缘起为什么需要批量获取AEE异常DB文件在MTK平台的设备开发与维护过程中AEEAndroid Exception Engine是一个至关重要的系统组件。它负责捕获、记录和分析Android系统及上层应用发生的各种异常比如应用崩溃ANR、系统服务无响应、内核死机等。这些异常信息最终会被封装成一个个.db文件也就是我们常说的AEE数据库文件。对于开发者、测试工程师和售后技术支持来说这些文件是定位问题根因的“第一现场”证据。然而在实际工作中我们很少只面对一个孤立的异常。更多的情况是测试团队在压力测试后反馈了十几台设备出现不同症状的卡顿或重启用户反馈渠道在某个版本升级后集中收到了大量同类崩溃报告或者在分析一个复杂的内存泄漏问题时你需要对比同一设备在不同时间点产生的多个异常快照。这时手动通过ADBAndroid Debug Bridge一台台设备去拉取、或者依赖测试人员一个个文件地收集效率极其低下且容易出错遗漏。因此“如何获取所有异常的AEE db文件”这个需求本质上是一个批量、自动化、精准的数据收集需求。它不是为了解决单个BUG而是为了建立一套高效的异常信息归集流程为后续的批量分析、模式识别和问题聚类打下基础。这通常是资深开发或测试架构师才会深入思考的问题也是提升团队问题排查效率的关键一步。2. AEE DB文件探秘存储位置与命名规则在动手编写脚本之前我们必须先搞清楚目标在哪里长什么样。这是所有自动化操作的前提。2.1 核心存储路径MTK平台的AEE异常DB文件主要存储在设备的以下两个目录中/data/aee_exp/这是最主要的存储位置。绝大多数由系统AEE框架捕获的严重异常如Native Crash、Kernel Panic、Android系统服务死锁等的DB文件都会放在这里。该目录通常需要root权限才能访问。/data/vendor/aee_exp/在一些较新的MTK平台或特定项目配置中部分异常尤其是与供应商Vendor层相关的问题可能会被存储在这个路径下。同样需要root权限。注意虽然理论上应用层的ANRApplication Not Responding跟踪信息也可能被AEE捕获并生成DB文件但更常见的ANR日志是以traces.txt的形式存在于/data/anr/目录。我们的脚本主要聚焦于上述两个AEE专用目录。2.2 文件命名规律AEE DB文件的命名并非随意而是遵循一套约定俗成的规则理解它有助于我们在脚本中做筛选和分类。一个典型的文件名如下SYS_ANDROID_MODEM_EXP_20240315_012345.db我们可以将其拆解为几个部分SYS_ANDROID异常类型前缀。常见的有SYS_ANDROID: Android框架层或Java进程异常。SYS_KERNEL: Linux内核层异常如Oops、Panic。EXP_MODEM: 调制解调器Modem相关异常。EXP_WATCHDOG: 看门狗超时引发的复位异常。MODEM_EXP更具体的异常模块或描述这里是Modem异常。20240315_012345异常发生的时间戳格式通常为年月日_时分秒。这是定位特定时间段异常的关键依据。.db文件扩展名表明这是一个SQLite数据库文件。了解命名规则后我们就可以在脚本中利用正则表达式来匹配和筛选我们感兴趣的文件例如只收集过去24小时内所有内核异常文件。3. 实战构建自动化收集脚本理论清晰后我们来构建一个健壮、实用的自动化脚本。我将提供一个基于Python的解决方案因为它跨平台且库丰富。脚本的核心思路是通过ADB连接设备支持多台递归搜索目标目录根据条件过滤文件然后批量拉取到本地并按设备序列号和日期进行组织。3.1 环境准备与脚本框架首先确保你的开发机上安装了Python3和ADB工具并且ADB已添加到系统环境变量中。我们创建一个名为fetch_aee_db.py的脚本。先搭建框架处理命令行参数比如指定设备、拉取时间范围、目标目录等。#!/usr/bin/env python3 MTK平台AEE异常DB文件批量拉取工具 Author: 资深MTK开发者 import os import re import sys import argparse import subprocess from datetime import datetime, timedelta from pathlib import Path def run_adb_command(device_serial, command): 执行ADB命令支持指定设备 if device_serial: full_cmd fadb -s {device_serial} {command} else: full_cmd fadb {command} try: result subprocess.run(full_cmd, shellTrue, capture_outputTrue, textTrue, timeout30) return result.returncode, result.stdout, result.stderr except subprocess.TimeoutExpired: print(f[ERROR] ADB command timed out: {full_cmd}) return -1, , Timeout def parse_arguments(): 解析命令行参数 parser argparse.ArgumentParser(description批量拉取MTK设备上的AEE异常DB文件) parser.add_argument(--devices, -d, nargs, help指定设备序列号多个用空格分隔。不指定则拉取所有已连接设备。) parser.add_argument(--days, typeint, default7, help拉取最近几天的文件默认7天。) parser.add_argument(--output, -o, default./aee_collection, help本地输出目录默认./aee_collection。) parser.add_argument(--type, -t, choices[all, kernel, android, modem], defaultall, help按异常类型过滤all(全部), kernel(内核), android(框架), modem(调制解调器)。) return parser.parse_args() def main(): args parse_arguments() # 创建输出目录 output_root Path(args.output) output_root.mkdir(parentsTrue, exist_okTrue) print(f[INFO] 输出目录: {output_root.absolute()}) # 获取目标设备列表 target_devices args.devices if args.devices else get_connected_devices() if not target_devices: print([ERROR] 未找到已连接的ADB设备。) sys.exit(1) for device in target_devices: print(f\n[INFO] 正在处理设备: {device}) fetch_from_device(device, args.days, args.type, output_root) if __name__ __main__: main()3.2 核心函数实现设备发现与文件拉取接下来我们实现获取设备列表和针对单台设备拉取的核心逻辑。def get_connected_devices(): 获取所有已连接的ADB设备序列号 retcode, stdout, stderr run_adb_command(None, devices) devices [] if retcode 0: for line in stdout.strip().split(\n)[1:]: # 跳过第一行标题 if line.strip() and device in line: serial line.split(\t)[0] devices.append(serial) return devices def fetch_from_device(device_serial, days_back, filter_type, output_root): 从指定设备拉取文件 # 1. 计算时间戳过滤条件 cutoff_date datetime.now() - timedelta(daysdays_back) # 我们利用文件名中的时间戳来过滤格式如20240315 cutoff_pattern cutoff_date.strftime(%Y%m%d) # 2. 定义搜索路径 search_paths [/data/aee_exp/, /data/vendor/aee_exp/] # 3. 根据类型构建文件名正则表达式 type_pattern_map { all: r.*\.db$, kernel: rSYS_KERNEL_.*\.db$, android: rSYS_ANDROID_.*\.db$, modem: rEXP_MODEM_.*\.db$, } file_pattern type_pattern_map.get(filter_type, type_pattern_map[all]) regex re.compile(file_pattern) # 4. 为当前设备创建子目录 device_dir output_root / device_serial device_dir.mkdir(exist_okTrue) files_found [] for path in search_paths: print(f [INFO] 正在搜索路径: {path}) # 使用find命令递归查找.db文件 find_cmd fshell find {path} -name \*.db\ 2/dev/null retcode, stdout, stderr run_adb_command(device_serial, find_cmd) if retcode ! 0 and stderr: print(f [WARN] 搜索路径 {path} 时可能无权限或路径不存在: {stderr.strip()}) continue for remote_file_path in stdout.strip().split(\n): if not remote_file_path: continue file_name os.path.basename(remote_file_path) # 应用文件名正则过滤 if not regex.match(file_name): continue # 应用时间戳过滤提取文件名中的日期部分 # 假设日期部分在倒数第二个下划线之后扩展名之前 # 例如SYS_ANDROID_MODEM_EXP_20240315_012345.db parts file_name.rstrip(.db).split(_) if len(parts) 2: date_str parts[-2] # 取倒数第二部分作为日期 if date_str.isdigit() and len(date_str) 8: # 确保是8位数字 if date_str cutoff_pattern: # 文件日期早于截止日期跳过 continue files_found.append(remote_file_path) # 5. 批量拉取文件 if not files_found: print(f [INFO] 在设备 {device_serial} 上未找到符合条件的文件。) return print(f [INFO] 找到 {len(files_found)} 个文件开始拉取...) for remote_file in files_found: local_file_name os.path.basename(remote_file) # 简单处理文件名冲突如有同名则添加序号 local_path device_dir / local_file_name counter 1 while local_path.exists(): stem, suffix os.path.splitext(local_file_name) local_path device_dir / f{stem}_{counter}{suffix} counter 1 pull_cmd fpull \{remote_file}\ \{local_path}\ retcode, stdout, stderr run_adb_command(device_serial, pull_cmd) if retcode 0: print(f [OK] 已拉取: {local_file_name} - {local_path}) else: print(f [FAILED] 拉取失败 {remote_file}: {stderr.strip()})这个脚本已经具备了核心功能。你可以通过以下命令使用它# 拉取所有设备最近7天的所有类型AEE DB文件 python fetch_aee_db.py # 拉取指定设备最近3天的内核异常文件并保存到指定目录 python fetch_aee_db.py -d device_serial_1 device_serial_2 --days 3 --type kernel -o ./my_kernel_crashes3.3 权限处理与异常捕获在实际操作中你可能会遇到权限问题。虽然/data/aee_exp/通常需要root但有些设备在userdebug版本或已取得root权限的测试机上可以直接访问。脚本中的find命令已经将错误输出重定向到/dev/null避免了因单个路径无权限而导致的整个脚本中断。更稳健的做法是在尝试拉取之前可以先检查文件是否存在且可读。我们可以添加一个预检查函数def check_file_accessible(device_serial, remote_path): 检查远程文件是否存在且可读 # 使用ls命令并检查返回值 check_cmd fshell ls -l {remote_path} 2/dev/null retcode, stdout, stderr run_adb_command(device_serial, check_cmd) return retcode 0 and stdout.strip() ! 然后在拉取循环中调用它if not check_file_accessible(device_serial, remote_file): print(f [SKIP] 文件不可访问或无权限: {remote_file}) continue # 执行拉取...4. 进阶文件解析与初步分析将文件批量拉取到本地只是第一步。面对几十甚至上百个.db文件我们如何快速定位关键问题这就需要一些初步的自动化分析。AEE的DB文件是SQLite格式我们可以使用Python的sqlite3标准库来读取其中的关键信息。4.1 解析DB文件结构一个典型的AEE DB文件包含多张表其中summary或exception_info表通常存储了异常的概要信息是我们首要关注的对象。由于不同平台版本的表名可能略有差异一个安全的做法是动态探索。我们可以编写一个辅助函数来提取文件的“元信息”import sqlite3 def extract_aee_summary(db_path): 从AEE DB文件中提取摘要信息 summary { file_name: os.path.basename(db_path), timestamp: , exception_type: , process_name: , pid: , reason: , backtrace: } try: conn sqlite3.connect(ffile:{db_path}?modero, uriTrue) # 以只读模式打开 cursor conn.cursor() # 1. 尝试查找关键表 cursor.execute(SELECT name FROM sqlite_master WHERE typetable;) tables [row[0] for row in cursor.fetchall()] target_table None for table in [summary, exception_info, info]: if table in tables: target_table table break if not target_table: conn.close() return summary # 2. 获取表结构动态匹配字段 cursor.execute(fPRAGMA table_info({target_table});) columns [col[1] for col in cursor.fetchall()] # 构建查询语句只选择存在的列 select_cols [] if time in columns: select_cols.append(time) if type in columns: select_cols.append(type) if process in columns: select_cols.append(process) if pid in columns: select_cols.append(pid) if reason in columns: select_cols.append(reason) # 回溯backtrace可能在一个单独的表中这里先简单处理 if backtrace in columns: select_cols.append(backtrace) if select_cols: query fSELECT {, .join(select_cols)} FROM {target_table} LIMIT 1; cursor.execute(query) row cursor.fetchone() if row: for i, col in enumerate(select_cols): summary[col] str(row[i]) if row[i] is not None else conn.close() except sqlite3.Error as e: print(f [ERROR] 解析DB文件 {db_path} 失败: {e}) return summary4.2 生成分析报告有了提取信息的函数我们就可以在拉取脚本的主流程结束后或者单独运行一个分析脚本对所有拉取到的文件进行扫描并生成一份汇总报告如CSV格式便于快速浏览和筛选。def generate_summary_report(collection_root, report_fileaee_summary.csv): 遍历收集的DB文件生成摘要报告 import csv collection_path Path(collection_root) all_summaries [] # 递归查找所有.db文件 for db_file in collection_path.rglob(*.db): print(f[INFO] 正在分析: {db_file}) summary extract_aee_summary(db_file) summary[file_path] str(db_file.relative_to(collection_path)) all_summaries.append(summary) if not all_summaries: print([INFO] 未找到任何DB文件进行分析。) return # 写入CSV fieldnames [file_path, file_name, timestamp, exception_type, process_name, pid, reason] with open(collection_path / report_file, w, newline, encodingutf-8-sig) as csvfile: writer csv.DictWriter(csvfile, fieldnamesfieldnames) writer.writeheader() for s in all_summaries: writer.writerow({k: s.get(k, ) for k in fieldnames}) print(f[SUCCESS] 分析报告已生成: {collection_path / report_file}) print(f 共分析 {len(all_summaries)} 个异常文件。)将这两个函数集成到主脚本中或者在拉取完成后单独运行你就能得到一份清晰的表格列出每个异常文件的时间、类型、进程和原因极大提升了问题初筛的效率。5. 避坑指南与实战心得在实际操作中我踩过不少坑这里分享几个关键点希望能帮你节省时间。5.1 权限与设备状态管理Root权限是前提批量拉取/data/aee_exp/目录下的文件绝大多数情况下需要设备已取得root权限。确保你的测试机是userdebug版本或已成功root。对于正式用户反馈的日志通常无法直接获取此类DB文件需要依赖MTK提供的其他日志抓取工具如META工具、AP日志或开启特定的调试转储设置。ADB连接稳定性在批量操作多台设备时ADB连接可能意外断开。建议在脚本中为每个关键ADB命令添加重试机制和更详细的超时与错误处理。例如在run_adb_command函数中捕获更多异常类型如CalledProcessError并记录到日志文件。设备序列号混淆当同时连接多台相同型号的设备时确保你的脚本能正确区分它们。除了使用序列号也可以在拉取前通过adb shell getprop ro.serialno再次确认并将此信息一并记录到本地文件夹名或报告中。5.2 文件筛选的精确性时间戳过滤的陷阱脚本中基于文件名的日期过滤是近似过滤。因为文件名中的日期是异常发生时间而文件系统的修改时间可能不同。更精确的做法是如果设备权限允许可以结合adb shell ls -l --time-stylefull-iso命令获取文件的精确修改时间进行过滤但这会显著增加ADB命令的复杂度和执行时间。异常类型匹配我们的简单正则匹配可能无法覆盖所有历史或未来MTK平台变异的文件名格式。在生产环境中最好能从项目组或平台方获取一份当前使用的AEE文件命名规范并据此更新正则表达式。一个更保守的策略是如果过滤类型不是all则拉取所有文件后再在本地根据文件名关键词进行二次筛选。5.3 性能与资源考量网络与存储压力AEE DB文件单个可能从几百KB到几十MB不等如果一次性拉取上百个文件会对USB带宽和设备存储I/O造成压力。可以考虑在脚本中添加--limit参数限制单次拉取的文件数量或总大小。或者先拉取文件列表和大小让用户确认后再执行。本地存储组织随着时间推移收集的DB文件会越来越多。脚本中按设备序列号和原始文件名存储的方式可能后期难以管理。一个改进方案是在本地目录中引入日期层级例如./collection/2024-03-15/device_serial/xxx.db。同时可以考虑将解析生成的摘要报告CSV与原始文件关联存储。5.4 解析DB文件的注意事项SQLite版本与文件锁有些AEE DB文件可能在异常发生时仍处于打开状态或者使用了特定的SQLite编译选项导致标准的sqlite3库无法读取。如果遇到database is locked或file is encrypted or is not a database的错误不要轻易放弃。可以尝试使用ADB先将文件复制到设备的临时目录如/sdcard/再从临时目录拉取。有时复制过程会解除原文件的锁。使用更强大的SQLite工具如DB Browser for SQLite一个开源的图形化工具支持查看和修复损坏的数据库手动尝试打开文件。这也是为什么“DB Browser for SQLite”会成为相关搜索热词的原因它在手动分析疑难DB文件时非常有用。关键信息可能在其他表summary表的信息可能比较简略。完整的调用栈backtrace、寄存器信息register、内存映射maps等可能存放在名为backtrace、memory、process_info的单独表中。对于深度分析你需要根据初步报告找到可疑文件然后使用SQLite客户端进行关联查询。6. 扩展思路构建更强大的异常分析流水线对于大型项目或持续集成CI环境我们可以将上述脚本扩展为一个更自动化的流水线定时任务使用cronLinux/Mac或任务计划程序Windows定期如每天凌晨运行收集脚本从连接在服务器上的多台测试设备拉取AEE文件。集中存储与去重将拉取的文件自动上传到公司内部的文件服务器或对象存储如MinIO、S3并计算文件的哈希值如MD5避免重复存储相同的崩溃文件。自动化解析与分类在服务器端运行更强大的解析脚本不仅提取摘要还可以尝试使用简单的规则引擎如基于reason或backtrace中的关键字对异常进行自动分类如“空指针”、“内存不足”、“死锁”。与问题跟踪系统集成当发现高频或新类型的崩溃时可以自动在Jira、GitLab Issues等系统中创建Bug工单并将相关的DB文件作为附件上传初步的分析报告填入问题描述。可视化仪表盘将每日收集的异常数量、类型分布、高频崩溃点等信息展示在 Grafana 或 Kibana 看板上让团队对软件质量有直观的了解。这个流水线将问题发现、收集、初步分析的流程完全自动化让开发人员可以集中精力处理真正需要深入分析的、高优先级的崩溃问题从而大幅提升整个团队的效率。从我个人的经验来看在MTK平台开发中建立一套完善的AEE异常收集与分析机制是提升系统稳定性和问题闭环速度的基石。它把散落在各台设备上的“黑匣子”数据变成了可供挖掘和分析的宝贵资产。