AppleTrace
iOS / macOS 追踪 → Perfetto

《AppleTrace》使用手册

一个轻量、可嵌入的 tracer:记录 App 的执行时间线——手动 section、Swift 宏、或每一次 objc_msgSend——并直接在浏览器里用 Perfetto 呈现。

简介

AppleTrace 给你的 App 埋点——加 marker、标注 Swift 函数、或 hook 消息发送——把事件时间线写进沙盒里的 trace 片段。appletrace 命令行工具负责把片段从设备上拉下来、合并成一个 trace.json 并在 Perfetto 里打开,查看调用时间线、耗时、线程与计数器。

产生事件有三种方式,按需选用,它们都落进同一条 trace:

全平台

手动 section

用 APTBeginSection/APTEndSection(或 Swift 宏)包住代码。风险最低,精确控制要计时的范围。

arm64 / arm64e

objc_msgSend hook

用 fishhook 式符号重绑自动追踪每一次 OC 消息发送。零改动,仅限 Objective-C。

模拟器 / macOS

Swift 自动 hook

可选的 AppleTraceAuto 桥接 SwiftTrace,零标注追踪 Swift 类层级。

快速开始

最快看到 trace 的方式是内置的示例 App——无需自己写埋点。

  1. 克隆并打开示例。
    bash
    git clone https://github.com/everettjf/AppleTrace.git
    cd AppleTrace
    open sample/AppleTraceSwiftDemo/AppleTraceSwiftDemo.xcodeproj
  2. 先运行一次(真机或模拟器),完成安装。
  3. 一条命令录制——启动、等待、停止、拉取、合并并打开 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 项目,也是新项目最干净的方式)

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

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++

cpp
#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 与宏

swift
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 类层级:

swift
import AppleTraceAuto

#if targetEnvironment(simulator)
AppleTraceAuto.trace(aClass: FeedViewModel.self)   // 进入/退出 → AppleTrace
#endif
🚫

AppleTraceAuto 仅限模拟器 / macOS。SwiftTrace 改写经过指针认证的 vtable 槽,真机上不安全——务必用 #if targetEnvironment(simulator) 包起来。它也看不到 final / 静态派发的方法。宏没有这些限制,是真机上的首选。

教程 · 自动 objc_msgSend Hook

Objective-C app 可以无埋点追踪每一次消息发送。该 hook 是 arm64/arm64e 上 fishhook 式的符号重绑,启动后安装一次:

objective-c
// 例如在 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 能绘制的事件种类:

objective-c
// 在当前线程时间线上打一个点
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);
});
事件APIPerfetto 呈现
SectionAPTBeginSection / APTEndSection线程轨道上的嵌套切片
InstantAPTInstant时间点 marker
CounterAPTCounter曲线轨道
AsyncAPTAsyncBegin / 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:

bash
# 一步到位(已连接设备上开发证书签名的 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                                    # → Perfetto

appletrace 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 退出。

bash
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/AppleTraceSwiftDemoSwiftwithSpan + @Traced/@TraceAll 宏、counter/async,以及 AppleTraceAuto hook
sample/TraceAllMsgDemoObjective-C手动 APTBeginSection section 与自动 objc_msgSend hook

每个引导式 App 都有 Generate Trace 按钮,显示 trace 目录并打印对应的 appletrace 命令。Swift demo 依赖本地包,请从仓库内打开,让 Xcode 自动解析 product。

在越狱设备上追踪任意 App

AppleTraceLoader 插件会把 AppleTrace 注入到你指定的 App,支持 rootless(/var/jb,如 Dopamine)与 roothide 越狱。它根据自身路径推导越狱根目录,同一份二进制两种越狱都能用。

  1. 安装插件包——rootless 用 appletrace-loader_<version>_iphoneos-arm64.deb,roothide 用 …_iphoneos-arm64e.deb:
    bash
    swift run appletrace jb install appletrace-loader_2.1.0_iphoneos-arm64e.deb --host <设备地址>
  2. 录制 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_ENABLED1录制总开关。
APPLETRACE_FORMATbinarybinary 或 text(JSON 行)。APPLETRACE_BINARY=0/1 仍然有效。
APPLETRACE_FLUSH_INTERVAL_MS1000后台刷盘间隔;0 表示关闭。
APPLETRACE_AUTO_HOOK_OBJC_MSGSEND0加载时安装 objc_msgSend hook。
APPLETRACE_TRACE_CLASS_ALLOW / _DENY—逗号分隔的类名前缀白名单 / 黑名单。
APPLETRACE_DATA_DIR沙盒覆盖片段写入目录。
APPLETRACE_BLOCK_SIZE_MB16每个片段的 mmap 块大小(1–256 MB)。
APPLETRACE_KEEP_EXISTING0保留旧 trace 目录而非覆盖。

格式错误的值会记录日志并被忽略,不会悄悄改变行为。

代码内也有运行时控制:APTSetEnabled(BOOL)、APTIsEnabled()、APTGetTraceDirectory()、APTFlush()、APTSyncWait()。

平台支持

模式适用范围
手动 section 与事件所有 iOS/macOS 版本,全语言
Swift 宏(@Traced/@TraceAll/withSpan)模拟器与真机
objc_msgSend hookarm64 与 arm64e
AppleTraceAuto(SwiftTrace)仅模拟器 / macOS
越狱 loaderrootless 与 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 编译。