AppleTrace
iOS / macOS 追踪 → Perfetto

AppleTrace》使用手册

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

简介

AppleTrace 给你的 App 埋点——加 marker、标注 Swift 函数、或 hook 消息发送——把事件时间线写进沙盒里的 trace 片段。一套小的 Python 流水线把这些片段合并成一个 trace.json,直接在 Perfetto 里打开,查看调用时间线、耗时、线程与计数器。

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

全平台

手动 section

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

arm64

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. 运行(模拟器或真机),点击 Generate Trace。App 会跑一段多线程负载,并显示磁盘上的 trace 目录
  3. 在 Mac 上合并片段
    bash
    python3 merge.py -d "<App 中显示的 trace 目录>"
    # → 在片段旁生成 trace.json
  4. 在 Perfetto 打开。进入 ui.perfetto.dev,把 trace.json 拖进去即可。
💡

想更快?sh go.sh "<trace 目录>" 一步完成合并并打开 Perfetto。

安装

环境要求

  • Xcode、Python 3、浏览器(Perfetto 在 ui.perfetto.dev)。
  • ldid 仅在重签 loader 时需要;pytest 仅跑测试时需要。

Swift Package(Swift 项目,也是新项目最干净的方式)

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

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
    // ... 工作 ...
}
⚠️

事件按线程缓冲,在达到阈值、调用 APTFlush()、或线程退出时落盘。读取 trace 前务必调用 APTFlush()(比如进入后台时),否则短小的运行可能看起来是空的。

教程 · 追踪 Swift

objc_msgSend hook 看不到 Swift 的静态 / vtable / witness 派发,所以 Swift 走源码级埋点。加入 Swift 包后 import AppleTrace

作用域 span 与宏

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

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 上 fishhook 式的符号重绑,启动后安装一次:

objective-c
// 例如在 application:didFinishLaunchingWithOptions: 早期
if (APTInstallObjcMsgSendHook()) {
    NSLog(@"AppleTrace: objc_msgSend hook 已安装");
}

每次被追踪的发送会变成 [类]方法 的 begin/end 对。完整示例见 sample/TraceAllMsgDemo,其中验证了浮点参数、结构体返回与 super 派发的处理。

⚠️

仅限 arm64。该 hook 在 arm64e 上会硬报错(调用方通过认证的 GOT 入口到达 objc_msgSend)。请构建纯 arm64 切片。

教程 · 事件类型

除了 section,AppleTrace 还记录 Perfetto 能绘制的事件种类:

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

Swift 封装一一对应:traceInstanttraceCounterasyncBeginasyncEnd

在 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 · 合并

bash
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/appletracedata

3 · 浏览

trace.json 拖进 ui.perfetto.dev。用 W/S 缩放、A/D 平移,搜索框可跳转到切片。

二进制片段(可选,更小更快)

设置 APPLETRACE_BINARY=1 改写紧凑的二进制片段格式。merge.py 对两种格式都透明解码——工作流不变。

示例 App

示例语言演示
sample/AppleTraceSwiftDemoSwiftwithSpan + @Traced/@TraceAll 宏、counter/async,以及 AppleTraceAuto hook
sample/TraceAllMsgDemoObjective-C手动 APTBeginSection section 与自动 objc_msgSend hook

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

环境变量

变量默认作用
APPLETRACE_ENABLEDtrue录制总开关。
APPLETRACE_BINARYfalse写二进制片段而非文本。
APPLETRACE_DATA_DIR沙盒覆盖片段写入目录。
APPLETRACE_BLOCK_SIZE_MB16每个片段的 mmap 块大小(1–256 MB)。
APPLETRACE_KEEP_EXISTINGfalse保留旧 trace 目录而非覆盖。

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

平台支持

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