《AppleTrace》使用手册
一个轻量、可嵌入的 tracer:记录 App 的执行时间线——手动 section、Swift 宏、或每一次 objc_msgSend——并直接在浏览器里用 Perfetto 呈现。
简介
AppleTrace 给你的 App 埋点——加 marker、标注 Swift 函数、或 hook 消息发送——把事件时间线写进沙盒里的 trace 片段。一套小的 Python 流水线把这些片段合并成一个 trace.json,直接在 Perfetto 里打开,查看调用时间线、耗时、线程与计数器。
产生事件有三种方式,按需选用,它们都落进同一条 trace:
手动 section
用 APTBeginSection/APTEndSection(或 Swift 宏)包住代码。风险最低,精确控制要计时的范围。
objc_msgSend hook
用 fishhook 式符号重绑自动追踪每一次 OC 消息发送。零改动,仅限 Objective-C。
Swift 自动 hook
可选的 AppleTraceAuto 桥接 SwiftTrace,零标注追踪 Swift 类层级。
快速开始
最快看到 trace 的方式是内置的示例 App——无需自己写埋点。
- 克隆并打开示例。
bash
git clone https://github.com/everettjf/AppleTrace.git cd AppleTrace open sample/AppleTraceSwiftDemo/AppleTraceSwiftDemo.xcodeproj - 运行(模拟器或真机),点击 Generate Trace。App 会跑一段多线程负载,并显示磁盘上的 trace 目录。
- 在 Mac 上合并片段:
bash
python3 merge.py -d "<App 中显示的 trace 目录>" # → 在片段旁生成 trace.json - 在 Perfetto 打开。进入 ui.perfetto.dev,把
trace.json拖进去即可。
想更快?sh go.sh "<trace 目录>" 一步完成合并并打开 Perfetto。
安装
环境要求
- Xcode、Python 3、浏览器(Perfetto 在 ui.perfetto.dev)。
ldid仅在重签 loader 时需要;pytest仅跑测试时需要。
Swift Package(Swift 项目,也是新项目最干净的方式)
dependencies: [
.package(url: "https://github.com/everettjf/AppleTrace.git", branch: "master"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "AppleTrace", package: "AppleTrace"),
// 可选,仅限模拟器/macOS:
.product(name: "AppleTraceAuto", package: "AppleTrace"),
]),
]嵌入 framework(Objective-C / C / C++)
打开 appletrace/appletrace.xcodeproj 构建 framework,把 appletrace.framework 嵌入你的 target。可参考 sample/TraceAllMsgDemo。
教程 · 手动埋点
手动 section 是推荐的基线——适配所有 iOS/macOS 版本,产出命名清晰的切片。
Objective-C
#import <appletrace/appletrace.h>
- (void)viewDidLoad {
APTBegin; // 自动命名为 "[类名 viewDidLoad]"
[super viewDidLoad];
APTEnd;
}
- (void)loadFeed {
APTBeginSection("network"); // 自定义 section 名
// ... 工作 ...
APTEndSection("network");
}APTBegin/APTEnd 用类名+方法名自动命名;要自定义就用 APTBeginSection/APTEndSection。同线程内按 LIFO 嵌套。
C / C++
#include <appletrace/appletrace.h>
void safer() {
APTScopeSection("decode"); // RAII:作用域结束自动 end
// ... 工作 ...
}事件按线程缓冲,在达到阈值、调用 APTFlush()、或线程退出时落盘。读取 trace 前务必调用 APTFlush()(比如进入后台时),否则短小的运行可能看起来是空的。
教程 · 追踪 Swift
objc_msgSend hook 看不到 Swift 的静态 / vtable / witness 派发,所以 Swift 走源码级埋点。加入 Swift 包后 import AppleTrace。
作用域 span 与宏
import AppleTrace
// 作用域 span——即使 throw / 提前返回也会闭合:
withSpan("loadFeed") { try? loadFeed() }
// 标注函数。section 以 #function 命名,对 final 类、struct、protocol
// 方法都生效——begin/end 在编译期插入函数体,绕开了派发问题。
@Traced
func decodeImage() { /* ... */ }
// @TraceAll 给每个有函数体的方法都加上 @Traced:
@TraceAll
final class FeedViewModel {
func reload() { /* 已追踪 */ }
func render() { /* 已追踪 */ }
}
APTFlush() // 或 AppleTrace.flush()零标注自动追踪
可选的 AppleTraceAuto 桥接 SwiftTrace,无需标注即可 hook 类层级:
import AppleTraceAuto
#if targetEnvironment(simulator)
AppleTraceAuto.trace(aClass: FeedViewModel.self) // 进入/退出 → AppleTrace
#endifAppleTraceAuto 仅限模拟器 / macOS。SwiftTrace 改写经过指针认证的 vtable 槽,真机上不安全——务必用 #if targetEnvironment(simulator) 包起来。它也看不到 final / 静态派发的方法。宏没有这些限制,是真机上的首选。
教程 · 自动 objc_msgSend Hook
Objective-C app 可以无埋点追踪每一次消息发送。该 hook 是 arm64 上 fishhook 式的符号重绑,启动后安装一次:
// 例如在 application:didFinishLaunchingWithOptions: 早期
if (APTInstallObjcMsgSendHook()) {
NSLog(@"AppleTrace: objc_msgSend hook 已安装");
}每次被追踪的发送会变成 [类]方法 的 begin/end 对。完整示例见 sample/TraceAllMsgDemo,其中验证了浮点参数、结构体返回与 super 派发的处理。
仅限 arm64。该 hook 在 arm64e 上会硬报错(调用方通过认证的 GOT 入口到达 objc_msgSend)。请构建纯 arm64 切片。
教程 · 事件类型
除了 section,AppleTrace 还记录 Perfetto 能绘制的事件种类:
// 在当前线程时间线上打一个点
APTInstant("cache_miss");
// 随时间变化的数值 → counter 曲线(内存、FPS、队列深度…)
APTCounter("resident_mb", 142.5);
// 跨线程/队列的工作,按 (name, id) 匹配 → 异步弧
uint64_t reqID = 42;
APTAsyncBegin("image_load", reqID);
dispatch_async(queue, ^{
APTAsyncEnd("image_load", reqID);
});| 事件 | API | Perfetto 呈现 |
|---|---|---|
| Section | APTBeginSection / APTEndSection | 线程轨道上的嵌套切片 |
| Instant | APTInstant | 时间点 marker |
| Counter | APTCounter | 曲线轨道 |
| Async | APTAsyncBegin / APTAsyncEnd | 可跨线程的弧 |
Swift 封装一一对应:traceInstant、traceCounter、asyncBegin、asyncEnd。
在 Perfetto 查看
1 · 拉取 trace 片段
片段写在 <app 沙盒>/Library/appletracedata。
- 模拟器:目录就在 Mac 本地,示例 App 会显示路径。
- 真机:Xcode ▸ Window ▸ Devices and Simulators ▸ Download Container,或
xcrun devicectl device copy from … --domain-type appDataContainer --source Library/appletracedata --destination ./trace。
2 · 合并
python3 merge.py -d /path/to/appletracedata # → trace.json
# 或统一 CLI:
python3 scripts/appletrace_cli.py open /path/to/appletracedata
# 或合并并打开 Perfetto:
sh go.sh /path/to/appletracedata3 · 浏览
把 trace.json 拖进 ui.perfetto.dev。用 W/S 缩放、A/D 平移,搜索框可跳转到切片。
二进制片段(可选,更小更快)
设置 APPLETRACE_BINARY=1 改写紧凑的二进制片段格式。merge.py 对两种格式都透明解码——工作流不变。
示例 App
| 示例 | 语言 | 演示 |
|---|---|---|
sample/AppleTraceSwiftDemo | Swift | withSpan + @Traced/@TraceAll 宏、counter/async,以及 AppleTraceAuto hook |
sample/TraceAllMsgDemo | Objective-C | 手动 APTBeginSection section 与自动 objc_msgSend hook |
每个引导式 App 都有 Generate Trace 按钮,显示 trace 目录并打印合并/Perfetto 命令。Swift demo 依赖本地包,请从仓库内打开,让 Xcode 自动解析 product。
环境变量
| 变量 | 默认 | 作用 |
|---|---|---|
APPLETRACE_ENABLED | true | 录制总开关。 |
APPLETRACE_BINARY | false | 写二进制片段而非文本。 |
APPLETRACE_DATA_DIR | 沙盒 | 覆盖片段写入目录。 |
APPLETRACE_BLOCK_SIZE_MB | 16 | 每个片段的 mmap 块大小(1–256 MB)。 |
APPLETRACE_KEEP_EXISTING | false | 保留旧 trace 目录而非覆盖。 |
代码内也有运行时控制:APTSetEnabled(BOOL)、APTIsEnabled()、APTGetTraceDirectory()、APTFlush()、APTSyncWait()。
平台支持
| 模式 | 适用范围 |
|---|---|
| 手动 section 与事件 | 所有 iOS/macOS 版本,全语言 |
Swift 宏(@Traced/@TraceAll/withSpan) | 模拟器与真机 |
objc_msgSend hook | arm64(非 arm64e) |
AppleTraceAuto(SwiftTrace) | 仅模拟器 / macOS |
为什么 Swift 需要源码级追踪
Swift 为性能绕开消息派发:struct/final 方法与全模块优化的调用走静态派发;类方法走 vtable;protocol 方法走 witness table。它们都不经过 objc_msgSend,所以自动 hook 只能看到很薄的 @objc dynamic 表面。宏直接在源码插桩,因此覆盖全部四种派发。
FAQ 与排查
trace 是空的 / 只有元数据
writer 按线程批量缓冲。读取前调用 APTFlush()(如进入后台、或场景结束时)。示例 App 已替你 flush。
构建报错:SDK does not contain 'libarclite'
部署目标过旧。把 IPHONEOS_DEPLOYMENT_TARGET 设为 12.0 或更高——新版 Xcode 移除了 pre-12 的 ARC 兼容库。
Swift app 启动崩溃 Library not loaded: @rpath/SwiftTrace.framework
app target 需要 LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks,dyld 才能在真机上找到内嵌的动态 framework。
AppleTraceAuto 某些方法追踪不到
SwiftTrace 无法 hook final / 静态派发的方法,且仅限模拟器/macOS。这类请用 @Traced/@TraceAll 宏。
命令行 xcodebuild 找不到宏插件的 SwiftSyntax
去掉 -sdk iphonesimulator,只用 -destination;-sdk 会把 host 宏插件错误地按目标 SDK 编译。