《AppleTrace》使用手册
一个轻量、可嵌入的 tracer:记录 App 的执行时间线——手动 section、Swift 宏、或每一次 objc_msgSend——并直接在浏览器里用 Perfetto 呈现。
简介
AppleTrace 给你的 App 埋点——加 marker、标注 Swift 函数、或 hook 消息发送——把事件时间线写进沙盒里的 trace 片段。appletrace 命令行工具负责把片段从设备上拉下来、合并成一个 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 - 先运行一次(真机或模拟器),完成安装。
- 一条命令录制——启动、等待、停止、拉取、合并并打开 Perfetto:
bash
swift run appletrace record --bundle-id com.everettjf.AppleTraceSwiftDemo \ --env APPLETRACE_AUTORUN=1 --open
在模拟器上,点击 Generate Trace,再执行 App 中显示的 swift run appletrace open <dir>。trace 只交给你浏览器里的 Perfetto,不会上传。
安装
环境要求
- Xcode 16+(Swift 6)、macOS 12+、浏览器(Perfetto 在 ui.perfetto.dev)。App 需要 iOS 15+ / macOS 12+。
ldid、xz仅在构建越狱安装包时需要;Python 仅用于旧版merge.py。
Swift Package(Swift 项目,也是新项目最干净的方式)
dependencies: [
.package(url: "https://github.com/everettjf/AppleTrace.git", from: "2.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "AppleTrace", package: "AppleTrace"),
// 可选,仅限模拟器/macOS:
.product(name: "AppleTraceAuto", package: "AppleTrace"),
]),
]嵌入 framework(Objective-C / C / C++)
使用 release 中的 appletrace.xcframework,或打开 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
APTScopeSection("inner"); // 同一作用域可以用多个
// ... 工作 ...
}事件按线程缓冲,每秒(APPLETRACE_FLUSH_INTERVAL_MS)、进入后台时、线程退出和进程退出时写入内存映射文件——即使 App 被杀,trace 也能读出来。只有在进程内读取 trace 时才需要调用 APTFlush()。
教程 · 追踪 Swift
objc_msgSend hook 看不到 Swift 的静态 / vtable / witness 派发,所以 Swift 走源码级埋点。加入 Swift 包后 import AppleTrace。
作用域 span 与宏
import AppleTrace
// 作用域 span——即使 throw / 提前返回也会闭合。字面量名字匹配 StaticString
// 重载,直接传给 C 内核,不分配内存。
withSpan("loadFeed") { try? loadFeed() }
// 异步工作可能在另一个线程恢复,用 async span:
let feed = try await withAsyncSpan("fetchFeed") { try await api.feed() }
// 标注函数。begin/end 在编译期插入函数体,对 final 类、struct、actor、
// protocol 方法都生效。
@Traced
func decodeImage() { /* span 名 "decodeImage()" */ }
// @TraceAll 给每个有函数体的方法和构造器加上 @Traced,名字带类型限定:
// "FeedViewModel.reload(force:)"。
@TraceAll
final class FeedViewModel {
func reload(force: Bool) { /* 已追踪 */ }
func refresh() async throws { /* async span——跨 await 也安全 */ }
@NotTraced func hotPath() { /* 不追踪 */ }
}
@Traced("feed.decode") // 显式命名
func decode() { /* ... */ }async 函数使用 async span。section 必须在同一线程开始和结束,但 await 之后可能换线程恢复;因此 @Traced 对 async 函数生成带每次调用唯一 id 的 async begin/end,Perfetto 会单独画一条轨道。
零标注自动追踪
可选的 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/arm64e 上 fishhook 式的符号重绑,启动后安装一次:
// 例如在 application:didFinishLaunchingWithOptions: 早期
if (APTInstallObjcMsgSendHook()) {
NSLog(@"AppleTrace: objc_msgSend hook 已安装");
}
// Swift:AppleTrace.installObjCMsgSendHook()
// 完全不改代码:启动时设置 APPLETRACE_AUTO_HOOK_OBJC_MSGSEND=1
// (`appletrace record --hook` 就是这么做的)每次被追踪的发送会变成 [类]方法 的 begin/end 对。完整示例见 sample/TraceAllMsgDemo,其中验证了浮点参数、结构体返回与 super 派发的处理。
已支持 arm64e。后端会为 chained-fixup __auth_got 槽重新进行 PAC 签名,已在 iPhone 17 Pro(iOS 27)和 iPhone 11(iOS 26.0.1)上验证。之后 dlopen 加载的镜像也会被重绑定。
部署目标为 iOS 14+ / macOS 11+ 时,clang 会把部分 alloc、init、class、self 消息发送改为直接调用 objc_alloc_init / objc_opt_*,因此它们不会作为 hook 事件出现。
教程 · 事件类型
除了 section,AppleTrace 还记录 Perfetto 能绘制的事件种类:
// 在当前线程时间线上打一个点
APTInstant("cache_miss");
// 随时间变化的数值 → counter 曲线(内存、FPS、队列深度…)
APTCounter("resident_mb", 142.5);
// 跨线程/队列的工作,按 (name, id) 匹配 → 异步弧
uint64_t reqID = APTNewAsyncID(); // 进程内唯一
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、newAsyncID、asyncBegin、asyncEnd、withAsyncSpan。Swift 的 async span 会把当前任务名(Swift 6.2 的 Task(name:),iOS 26 / macOS 26 及以上)记录为 task 参数;Objective-C 中可用 APTAsyncBeginWithTask(name, id, label)。
在 Perfetto 查看
片段写在 <app 沙盒>/Library/appletracedata,其余交给 appletrace CLI:
# 一步到位(已连接设备上开发证书签名的 App):
swift run appletrace record --bundle-id com.example.MyApp --duration 15 --hook --open
# 或分步:
swift run appletrace pull --bundle-id com.example.MyApp -o mytrace # devicectl 拷贝
swift run appletrace merge mytrace # → mytrace/trace.json
swift run appletrace open mytrace # → Perfettoappletrace open 在 127.0.0.1 上提供一个小页面,通过 Perfetto 的 postMessage 接口把 trace 交给浏览器里的 ui.perfetto.dev——文件不会上传。也可以自己把 trace.json 拖进去。用 W/S 缩放、A/D 平移,搜索框可跳转到切片。
二进制与文本片段
默认写二进制片段(热路径上没有字符串格式化,体积约为文本的一半)。设置 APPLETRACE_FORMAT=text 可写人类可读的 JSON 行。appletrace merge(以及旧版 merge.py)两种都能解码,包括被杀进程留下的片段。
CI 性能门禁
appletrace summary 输出每个 span 的次数、总耗时、自身耗时、平均、p50、p95 和最大耗时;appletrace diff 对比两份 trace 或已保存的基线,出现性能回退时以状态 2 退出。
appletrace summary run1 run2 run3 --only MyApp. -o perf/baseline.json # 保存基线
appletrace diff perf/baseline.json current.json \
--metric p50 --threshold 20% --min-delta 2ms --only MyApp. # 回退时退出码为 2两边各录多次运行,并且只对自己的 span 设门禁。CI 指南里有模拟器录制脚本和 GitHub Actions 配置。
示例 App
| 示例 | 语言 | 演示 |
|---|---|---|
sample/AppleTraceSwiftDemo | Swift | withSpan + @Traced/@TraceAll 宏、counter/async,以及 AppleTraceAuto hook |
sample/TraceAllMsgDemo | Objective-C | 手动 APTBeginSection section 与自动 objc_msgSend hook |
每个引导式 App 都有 Generate Trace 按钮,显示 trace 目录并打印对应的 appletrace 命令。Swift demo 依赖本地包,请从仓库内打开,让 Xcode 自动解析 product。
在越狱设备上追踪任意 App
AppleTraceLoader 插件会把 AppleTrace 注入到你指定的 App,支持 rootless(/var/jb,如 Dopamine)与 roothide 越狱。它根据自身路径推导越狱根目录,同一份二进制两种越狱都能用。
- 安装插件包——rootless 用
appletrace-loader_<version>_iphoneos-arm64.deb,roothide 用…_iphoneos-arm64e.deb:bashswift run appletrace jb install appletrace-loader_2.1.0_iphoneos-arm64e.deb --host <设备地址> - 录制 App——以开启追踪的状态重启 App,结束后通过 SSH 拉取并合并:
bash
swift run appletrace jb record com.example.App --host <设备地址> \ --duration 15 --hook --allow "EX,App" --open
也可以分步:jb enable <bundle-id> --hook,操作 App,jb pull <bundle-id>,open appletracedata-<bundle-id>,最后 jb disable <bundle-id>。enable 会写入 <jbroot>/var/mobile/Library/AppleTrace/<bundle-id>.plist;loader 只处理有该文件的 App。
SSH 需要密钥登录(ssh-copy-id)。通过 USB 时可用 iproxy 2222 22 转发端口,再传 --host 127.0.0.1 --port 2222。只有 jb install 需要 root;如果只能以 mobile 登录,请用 Sileo/Zebra 安装 .deb,其余命令加 --user mobile。
loader 会在 App 的 tmp/AppleTraceLoader.log 写一行状态(jb pull 会打印),包括 hook 重绑定了多少个 GOT 槽位。
被封存的 App:iOS 26 上 Apple 自带 App 的只读 __DATA_CONST 被映射为永久不可写区域,hook 无法重绑定其 GOT(日志中 main_executable_slots=0)。这些 App 照常运行;第三方 App 不受影响。
环境变量
| 变量 | 默认 | 作用 |
|---|---|---|
APPLETRACE_ENABLED | 1 | 录制总开关。 |
APPLETRACE_FORMAT | binary | binary 或 text(JSON 行)。APPLETRACE_BINARY=0/1 仍然有效。 |
APPLETRACE_FLUSH_INTERVAL_MS | 1000 | 后台刷盘间隔;0 表示关闭。 |
APPLETRACE_AUTO_HOOK_OBJC_MSGSEND | 0 | 加载时安装 objc_msgSend hook。 |
APPLETRACE_TRACE_CLASS_ALLOW / _DENY | — | 逗号分隔的类名前缀白名单 / 黑名单。 |
APPLETRACE_DATA_DIR | 沙盒 | 覆盖片段写入目录。 |
APPLETRACE_BLOCK_SIZE_MB | 16 | 每个片段的 mmap 块大小(1–256 MB)。 |
APPLETRACE_KEEP_EXISTING | 0 | 保留旧 trace 目录而非覆盖。 |
格式错误的值会记录日志并被忽略,不会悄悄改变行为。
代码内也有运行时控制:APTSetEnabled(BOOL)、APTIsEnabled()、APTGetTraceDirectory()、APTFlush()、APTSyncWait()。
平台支持
| 模式 | 适用范围 |
|---|---|
| 手动 section 与事件 | 所有 iOS/macOS 版本,全语言 |
Swift 宏(@Traced/@TraceAll/withSpan) | 模拟器与真机 |
objc_msgSend hook | arm64 与 arm64e |
AppleTraceAuto(SwiftTrace) | 仅模拟器 / macOS |
| 越狱 loader | rootless 与 roothide 越狱(arm64 / arm64e) |
为什么 Swift 需要源码级追踪
Swift 为性能绕开消息派发:struct/final 方法与全模块优化的调用走静态派发;类方法走 vtable;protocol 方法走 witness table。它们都不经过 objc_msgSend,所以自动 hook 只能看到很薄的 @objc dynamic 表面。宏直接在源码插桩,因此覆盖全部四种派发。
FAQ 与排查
trace 是空的 / 只有元数据
writer 每秒、进入后台和退出时都会刷盘。如果在 App 运行时拉取,先等一秒(appletrace record 会自动等),或调用 APTFlush()。另外确认 APPLETRACE_ENABLED 不是 0。
部署目标相关的构建错误
AppleTrace 2.0 需要 iOS 15+ / macOS 12+,这是当前 Xcode 接受的最低版本。
@Traced async 函数显示在单独的轨道上,而不是线程上
这是预期行为:await 之后可能换线程,所以 async 函数使用 async span。
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 编译。