SwiftTrace 6.6.0

SwiftTrace 6.6.0

测试已测试
语言语言 组件组件
许可证 NOASSERTION
发布上次发布2020年10月

John Holdsworth 维护。



SwiftTrace

跟踪应用包或框架中非最终类的方法调用。想象一下 Xtrace,但专为 Swift 和 Objective-C 设计。您还可以向非最终 Swift 类的成员函数添加“方面”,以便在方法实现执行前后调用闭包,这反过来可以修改传入的参数或返回值!除了日志记录功能外,随着 Swift 框架的二进制分发即将到来,这可能在类似古代“Swizzling”的方式中使用。

SwiftTrace Example

注意:以下功能不会在通过全模块优化编译的模块的最终或内部类或方法上工作,因为方法的分发将是“直接”的,即链接到调用位置的符号,而不是通过类的 vtable。因此,只有在通过协议引用时,才能跟踪结构体的方法调用,因为它们使用 见证表,可以对其进行修补。

SwiftTrace 可以与 Swift 包管理器一起使用,或通过将以下内容添加到您的项目 Podfile 中作为 CocoaPod 使用:

    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)

如果您在项目的“其他链接器标志”中指定了 "-Xlinker -interposable",则可以一次性跟踪应用程序主包中的所有方法,这在使用以下调用对 SwiftUI 进行分析时非常有用。

    SwiftTrace.traceMainBundleMethods()

如果通过ProTools进行消息传递,可以追踪struct或其他类型的方法,因为这将是通过对所谓的 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以能够记录应用程序的类型之一,有两个步骤。首先,如果该类型仅包含float类型,例如SwiftUI.EdgeInsets,您可能需要扩展该类型以符合SwiftTraceFloatArg。

extension SwiftUI.EdgeInsets: SwiftTraceFloatArg {}

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

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

许多这些API还可用作NSObject的扩展,这在将SwiftTrace作为动态加载捆绑包时很有用,如(InjectionIII)[https://github.com/johnno1962/InjectionIII].

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

这在动态加载捆绑包时很有用,例如在使用(InjectionIII)[https://github.com/johnno1962/InjectionIII]. 而不是包含CocoaPod,您只需要将SwiftTrace.h添加到InjectionIII应用程序捆绑包的桥接头中,并动态加载捆绑包。

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

基准测试

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

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

面向方面编程

您可以使用方法的demangled名称向特定的方法添加方面

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

当调用 TextClass 的方法 "x" 时,这将打印 "ONE";当退出时,将打印 "TWO"。两个参数是 Swizzle,它是一个表示 "Swizzle" 的对象,以及入口或退出栈。入口闭包的全签名为:

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

如果您理解了如何为参数分配寄存器,则可以干扰栈以修改传入的参数,并且在退出方面闭包中可以替换返回值。在好日子里记录(并防止)抛出错误。

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

    stack.intArg1 = 99
    stack.floatArg3 = 77.3

其他类型的参数有些更复杂。它们必须进行类型转换,并且 String 占用两个整寄存器。

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

在退出方面闭包中,设置返回类型更简单,因为它是泛型的

    stack.setReturn(value: "Phew")

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

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

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

如果这似乎很复杂,还有一个 swizzle.arguments 属性,可以用于 onEntry,它包含一个 Array,其中包含类型为 Any 的元素,可以转换为期望的类型。元素 0 是 self

调用接口

现在我们有了跳板基础设施,可以实现 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()))

要确定方法的混淆名称,可以使用此函数获取类的完整列表

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

这个简化的接口有一些限制,它只支持 Double、Float、String、Int、Object、CGRect、CGSize 和 CGPoint 参数。对于不包含浮点值的其他结构化类型,可以将它们符合到 SwiftTraceArg 协议,以便在参数列表中传递它们,或者如果它们只包含浮点值,则符合 SwiftTraceFloatArg。这些值和返回值必须适合 32 字节且不包含浮点值。

它是如何工作的

Swift AnyClass 实例的布局类似于 Objective-C 类,SwiftMeta.swift 中的 ClassMetadataSwift 中有一些额外的数据进行了文档化。在这之后是类的类和实例成员函数的指针表,直到类实例的大小。SwiftTrace 将这些功能指针替换为指向唯一的汇编语言 "跳板" 入口点的指针,该入口点带有相关联的目标函数和数据指针。保存寄存器并调用此函数传递数据指针以记录方法名称。方法名称是通过取消混淆与实现方法功能地址相关联的符号名称来确定的。然后恢复寄存器并将控制传递给原始的方法实现函数。

如果在跟踪过程中遇到无法正常工作的项目,请提交一个问题。这应该是100%可靠的,因为它使用汇编语言跳板而不是Xtrace中的洗牌(Swizzling)。否则,您可以通过Twitter @Injection4Xcode 联系作者。感谢Oliver Letterer为imp_implementationForwardingToSelector项目适配设置跳板。还要感谢@twostrawsUnwrap@artsyeidolon在这些项目测试中广泛使用。

享受吧!