传统 OC 无感埋点依赖分类 +load 方法,在应用启动早期自动完成方法交换,实现完全无侵入。但 Swift 直接移除了 +load 支持,且默认方法采用静态派发,直接复用旧方案会出现漏统计、交换失效等问题。
- 场景1:自定义业务类埋点(纯 Swift 实现)
仅针对项目内自己编写的类,用 Swift 原生特性即可实现,无需任何 OC 代码:
// 全局单次触发器,启动时自动执行
final class SwizzleTrigger {
static let runOnce: Void = { print("埋点系统启动") }()
}
extension BaseViewController {
// 原方法标记为动态派发
@objc dynamic func viewAppear(_ animated: Bool) {
self.viewAppear(animated)
}
// 原生替换方法
@_dynamicReplacement(for: viewAppear(_:))
private func hookedViewAppear(_ animated: Bool) {
hookedViewAppear(animated)
BuriedPointManager.trackPage("\(Self.self)") // 埋点采集
}
}
// App启动入口触发一次即可
func application(_ app: UIApplication, didFinishLaunchingWithOptions opts: ...) -> Bool {
_ = SwizzleTrigger.runOnce
return true
}
这个方案仅能作用于你自己编写的、标记了
@objc dynamic的方法,无法直接 Hook UIKit 等系统框架的原生方法,因为系统方法没有开启动态派发,@_dynamicReplacement对其完全无效。
- 场景2:系统类全局埋点(工业级稳定方案)
针对 UIKit 系统类(如 UIViewController、UIButton)的全量埋点,用极少量 OC 代码提供 +load 入口,核心逻辑全在 Swift 中实现,是目前业界标准方案:
- 新建极简 OC 分类,仅提供启动入口:
import "YourProject-Swift.h"
@implementation UIViewController (BuriedBoot)
+ (void)load {
static dispatch_once_t once;
dispatch_once(&once, ^{ [SwiftBuriedManager setupSwizzle]; });
}
@end
- Swift 中实现完整埋点逻辑:
import ObjectiveC
final class SwiftBuriedManager: NSObject {
@objc static func setupSwizzle() {
let original = selector(UIViewController.viewWillAppear(_:))
let swizzled = selector(UIViewController.bp_viewWillAppear(_:))
guard let oriM = class_getInstanceMethod(UIViewController.self, original),
let swiM = class_getInstanceMethod(UIViewController.self, swizzled)
else { return }
method_exchangeImplementations(oriM, swiM)
}
}
extension UIViewController {
@objc private func bp_viewWillAppear(_ animated: Bool) {
self.bp_viewWillAppear(animated)
BuriedPoint.reportPage(NSStringFromClass(type(of: self)))
}
}
这里用到的 OC 分类的
+load执行顺序完全遵循 Objective-C Runtime 规则,不同继承链的类之间的分类 +load 执行顺序,完全由 Xcode Compile Sources 里的文件编译顺序决定,不要在不同分类的 +load 之间做逻辑依赖,避免后续调整文件顺序后埋点逻辑失效。
3条必守准则
- 不要在 AppDelegate 中才执行交换,会漏掉启动阶段的页面埋点,导致统计数据失真
- 所有交换逻辑必须用 dispatch_once 包裹,避免重复交换导致方法调用栈混乱
- 埋点采集代码保持轻量,所有上报操作异步执行,不阻塞主线程影响页面滑动性能
极端不推荐的兜底情况
如果你坚决拒绝引入任何 OC 代码,只能在 AppDelegate 的 didFinishLaunchingWithOptions 中直接调用 Swift 封装的 Runtime 交换函数。但这个方案有明确缺陷:执行时机晚于 OC +load,部分在 main 函数之前就初始化的系统类、启动页会完全漏掉埋点,仅适合小型 Demo 项目使用,绝对不建议在生产环境中落地。