SwiftTrace

追踪应用程序包或框架中非 final 类的 Swift 和 Objective-C 方法调用。可以将其理解为 Swift 和 Objective-C 版本的 Xtrace。您还可以向非 final Swift 类的成员函数添加“切面”,以便在函数实现执行之前或之后调用闭包,从而修改传入的参数或返回值!除了日志记录功能之外,随着 Swift 框架的二进制分发即将到来,也许它会以类似于过去“方法调剂 (Swizzling)”的方式发挥作用。

SwiftTrace Example

注意:这些功能均不能在类或方法上工作,如果类或方法是 final 的,或者是在使用整个模块优化编译的模块中是 internal 的,因为方法的调度将是“直接的”,即链接到调用点的符号,而不是通过类的 vtable。因此,可以追踪对结构体方法的调用,但前提是它们通过协议引用,因为它们使用可以被修补的 witness table (见证表)。

SwiftTrace 可以通过 Swift Package Manager 或 CocoaPod 使用,只需将以下内容添加到您的项目的 Podfile 中

    pod 'SwiftTrace'

项目重建完成后,将 SwiftTrace 导入到应用程序的 AppDelegate 中,并添加类似以下内容到 didFinishLaunchingWithOptions 方法的开头

    SwiftTrace.traceBundle(containing: type(of: self))

这将追踪主应用程序包中定义的所有类。例如,要追踪 RxSwift Pod 中的所有类,请添加以下内容

    SwiftTrace.traceBundle(containing: RxSwift.DisposeBase.self)

这会在 Xcode 调试控制台中生成如上所示的输出。

要追踪 UIKit 等系统框架,您可以使用模式追踪类

    SwiftTrace.traceClasses(matchingPattern:"^UI")

可以使用底层 API 追踪单个类

    SwiftTrace.trace(aClass: MyClass.self)

或者,要追踪特定类的所有实例的方法,包括其超类的方法,请使用以下方法

    SwiftTrace.traceInstances(ofClass: aClass)

或者,要仅追踪特定实例,请使用以下方法

    SwiftTrace.trace(anInstance: anObject)

如果在您的项目的 "Other Linker Flags" 中指定了 "-Xlinker -interposable",则可以一次性追踪应用程序主包中的所有方法,这对于使用以下调用来分析 SwiftUI 很有用

    SwiftTrace.traceMainBundleMethods()

如果结构体或其他类型通过协议发送消息,则可以追踪它们的方法,因为这将通过所谓的 witness table (见证表) 进行间接调用。可以使用类实例指定被追踪的包级别追踪协议。可以通过可选的正则表达式进一步过滤它们。例如,以下内容

SwiftTrace.traceProtocolsInBundle(containing: AClassInTheBundle.self, matchingPattern: "regexp")

例如,要追踪在 SwiftUI 框架中进行的内部调用,您可以使用以下代码

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    SwiftTrace.traceProtocolsInBundle(containing: UIHostingController<HomeView>.self)
    return true
}

可以通过方法名称包含和排除正则表达式来过滤应用哪些追踪。

    SwiftTrace.methodInclusionPattern = "TestClass"
    SwiftTrace.methodExclusionPattern = "init|"+SwiftTrace.defaultMethodExclusions

这些方法必须在您启动追踪之前调用,因为它们在“方法调剂 (Swizzle)”阶段应用。由于通过追踪 UIKit 进行测试,因此存在默认的排除设置。

open class var defaultMethodExclusions: String {
    return """
        \\.getter| (?:retain|_tryRetain|release|_isDeallocating|.cxx_destruct|dealloc|description| debugDescription)]|initWithCoder|\
        ^\\+\\[(?:Reader_Base64|UI(?:NibStringIDTable|NibDecoder|CollectionViewData|WebTouchEventsGestureRecognizer)) |\
        ^.\\[(?:UIView|RemoteCapture) |UIDeviceWhiteColor initWithWhite:alpha:|UIButton _defaultBackgroundImageForType:andState:|\
        UIImage _initWithCompositedSymbolImageLayers:name:alignUsingBaselines:|\
        _UIWindowSceneDeviceOrientationSettingsDiffAction _updateDeviceOrientationWithSettingObserverContext:windowScene:transitionContext:|\
        UIColorEffect colorEffectSaturate:|UIWindow _windowWithContextId:|RxSwift.ScheduledDisposable.dispose| ns(?:li|is)_
        """
}

如果您想进一步处理输出,您可以定义自己的自定义追踪子类

    class MyTracer: SwiftTrace.Decorated {

        override func onEntry(stack: inout SwiftTrace.EntryStack) {
            print( ">> "+stack )
        }
    }
    
    SwiftTrace.swizzleFactory = MyTracer.self

由于记录的数据量很快就会变得难以控制,因此您可以通过将追踪与上述函数的可选 subLevels 参数结合使用来控制记录的内容。例如,以下代码在所有 UIKit 上设置一个追踪,但只会记录目标实例的方法调用以及这些方法进行的最多三个级别的调用

    SwiftTrace.traceBundle(containing: UIView.self)
    SwiftTrace.trace(anInstance: anObject, subLevels: 3)

或者,以下代码将记录应用程序的方法和它们对 RxSwift 的调用

    SwiftTrace.traceBundle(containing: RxSwift.DisposeBase.self)
    SwiftTrace.traceMainBundle(subLevels: 3)

如果这看起来很随意,那么规则相当简单。当您添加一个具有非零 subLevels 参数的追踪时,除非在最新追踪的方法中,它们最多在 subLevels 中进行,或者如果它们无论如何都被类或实例 (traceInstances(ofClass:) 和 trace(anInstance:)) 过滤,否则所有先前的追踪都会被抑制。

如果您想扩展 SwiftTrace 以便能够记录您的应用程序的类型之一,则需要两个步骤。首先,您可能需要扩展该类型以符合 SwiftTraceFloatArg,如果它仅包含浮点类型,例如 SwiftUI.EdgeInsets。

extension SwiftUI.EdgeInsets: SwiftTraceFloatArg {}

然后,使用以下 API 添加该类型的处理程序

    SwiftTrace.addFormattedType(SwiftUI.EdgeInsets.self, prefix: "SwiftUI")

许多这些 API 也可以作为 NSObject 的扩展提供,这在 SwiftTrace 通过动态加载 bundle 提供时非常有用,如 (InjectionIII)[https://github.com/johnno1962/InjectionIII] 中所示。

    SwiftTrace.traceBundle(containing: UIView.class)
    // becomes
    UIView.traceBundle()
    
    SwiftTrace.trace(inInstance: anObject)
    // becomes
    anObject.swiftTraceInstance()

当 SwiftTrace 通过动态加载 bundle 提供时,这非常有用,例如在使用 (InjectionIII)[https://github.com/johnno1962/InjectionIII] 时。您无需包含 CocoaPod,只需将 InjectionIII 应用程序的 bundle 中的 SwiftTrace.h 添加到您的 bridging header 并动态加载该 bundle 即可。

   Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()

基准测试

要对应用程序或框架进行基准测试,请追踪其方法,然后您可以使用以下方法之一

   SwiftTrace.sortedElapsedTimes(onlyFirst: 10))
   SwiftTrace.sortedInvocationCounts(onlyFirst: 10))

对象生命周期跟踪

您可以使用 SwiftTrace.LifetimeTracker 类跟踪 Swift 和 Objective-C 类的分配和释放

SwiftTrace.swizzleFactory = SwiftTrace.LifetimeTracker.self
SwiftTrace.traceMainBundleMethods() == 0 {
    print("⚠️ Tracing Swift methods can only work if you have -Xlinker -interposable to your project's \"Other Linker Flags\"")
}
SwiftTrace.traceMainBundle()

每次分配对象时,您将看到一条 .__allocating_init 消息,后跟结果以及自追踪开始以来分配的实时对象的总计数。 每次对象被释放时,您将看到一条 cxx_destruct 消息,后跟该类未完成的对象数量。

如果您想跟踪 Swift 结构体的生命周期,请创建一个标记类,并将一个属性添加到初始化为该类实例的结构体中。

class Marker<What> {}

struct MyView: SwiftUI.View {
    var marker = Marker<MyView>()
}

这个想法基于 LifetimeTracker 项目,作者是 Krzysztof Zabłocki

切面

您可以使用该方法的反混淆名称向特定方法添加切面

    print(SwiftTrace.addAspect(aClass: TestClass.self,
                      methodName: "SwiftTwaceApp.TestClass.x() -> ()",
                      onEntry: { (_, _) in print("ONE") },
                      onExit: { (_, _) in print("TWO") }))

当调用 TextClass 的方法“x”时,这将打印“ONE”,并在它退出时打印“TWO”。这两个参数分别是 Swizzle (表示“Swizzle”的对象) 和 entry 或 exit 堆栈。 entry 闭包的完整签名是

       onEntry: { (swizzle: SwiftTrace.Swizzle, stack: inout SwiftTrace.EntryStack) in

如果您了解 如何将寄存器分配给参数,则可以探测堆栈以修改传入的参数,对于 exit 切面闭包,您可以替换返回值,并且在顺利的情况下,您可以记录(并阻止)抛出错误。

在闭包中替换输入参数相对简单

    stack.intArg1 = 99
    stack.floatArg3 = 77.3

其他类型的参数稍微复杂一些。 必须进行强制转换,并且 String 占用两个整数寄存器。

    swizzle.rebind(&stack.intArg2).pointee = "Grief"
    swizzle.rebind(&stack.intArg4).pointee = TestClass()

在 exit 切面闭包中,设置返回类型更容易,因为它是一般的

    stack.setReturn(value: "Phew")

当函数抛出异常时,您可以访问 NSError 对象。

    print(swizzle.rebind(&stack.thrownError, to: NSError.self).pointee)

可以将 stack.thrownError 设置为零以取消抛出异常,但您需要设置返回值。

如果这看起来很复杂,则有一个属性 swizzle.arguments 可以在 onEntry 中使用,它包含作为 Array 的参数,该数组包含类型为 Any 的元素,可以将其强制转换为预期的类型。元素 0 是 self

调用接口

现在我们有了一个 trampoline 基础设施,可以为 Swift 实现一个调用 API

    print("Result: "+SwiftTrace.invoke(target: b,
        methodName: "SwiftTwaceApp.TestClass.zzz(_: Swift.Int, f: Swift.Double, g: Swift.Float, h: Swift.String, f1: Swift.Double, g1: Swift.Float, h1: Swift.Double, f2: Swift.Double, g2: Swift.Float, h2: Swift.Double, e: Swift.Int, ff: Swift.Int, o: SwiftTwaceApp.TestClass) throws -> Swift.String",
        args: 777, 101.0, Float(102.0), "2-2", 103.0, Float(104.0), 105.0, 106.0, Float(107.0), 108.0, 888, 999, TestClass()))

为了确定方法的 mangled 名称,您可以使用此函数获取类的完整列表

    print(SwiftTrace.methodNames(ofClass: TestClass.self))

这个简化的接口有局限性,因为它只支持 Double、Float、String、Int、Object、CGRect、CGSize 和 CGPoint 参数。 对于不包含浮点值的其他结构体类型,您可以使它们符合协议 SwiftTraceArg 以便能够在参数列表上传递它们,如果它们只包含浮点数,则可以符合 SwiftTraceFloatArg。 这些值和返回值必须适合 32 字节,并且不能包含浮点数。

工作原理

Swift AnyClass 实例具有类似于 Objective-C 类的布局,并在 SwiftMeta.swift 的 ClassMetadataSwift 中记录了一些额外的数据。 在此数据之后,是类和实例成员函数的 vtable 指针,直到类实例的大小。 SwiftTrace 使用指向唯一汇编语言“trampoline”入口点的指针替换这些函数指针,该入口点具有与其关联的目标函数和数据指针。 保存寄存器并调用此函数,传递数据指针以记录方法名称。 方法名称由与实现方法的函数地址关联的符号名称的反混淆确定。 然后恢复寄存器,并将控制权传递给实现该方法的原始函数。

如果在追踪时遇到无法正常工作的项目,请提交问题。 它应该更可靠,因为它使用汇编语言 trampolines 而不是像 Xtrace 那样进行方法调剂 (Swizzling)。 否则,可以在 Twitter @Injection4Xcode 上联系作者。

感谢 Oliver Letterer 的 imp_implementationForwardingToSelector 项目,该项目经过调整以设置 trampolines,并包含在 MIT 许可下。

该 repo 包含一个非常稍微修改过的非常方便的 https://github.com/facebook/fishhook 版本。 有关其许可详细信息,请参阅源文件和头文件。

还要感谢 @twostrawsUnwrap@artsyeidolon 在测试期间广泛使用。

祝您使用愉快!

$Date: 2022/01/22 $