Home > other >  Swift swizzling init function
Swift swizzling init function

Time:01-21

I'm trying to capture initialisation of InputStream with file URL in Swift. I've successfully implemented such capture in Objective-C for NSInputStream class. The issue is that after exchanging initialisers, swizzled method is not triggered. Furthermore, after method exchange, calling swizzled method directly does not lead to its execution, meaning, its implementation was successfully exchanged. I'd like to know, what are the reasons of such odd behaviour, as method is swizzled successfully, but it's not clear, which method was replaced by swizzled one. Provided sample with current implementation of InputStream swizzling in Swift.

private let swizzling: (AnyClass, Selector, Selector) -> () = { forClass, originalSelector, swizzledSelector in
  if let originalMethod = class_getInstanceMethod(forClass, originalSelector),
     let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)  {
      method_exchangeImplementations(originalMethod, swizzledMethod)
  }
}

extension InputStream {
  @objc dynamic func swizzledInit(url: URL) -> InputStream? {
      print("Swizzled constructor")
      return self.swizzledInit(url: url)
  }
  
  static func Swizzle() {
      swizzling(InputStream.self, #selector(InputStream.init(url:)), #selector(swizzledInit(url:)))
  }
}

CodePudding user response:

From the InputStream documentation:

NSInputStream is an abstract superclass of a class cluster consisting of concrete subclasses of NSStream that provide standard read-only access to stream data.

The key here: when you create an InputStream, the object you get back will not be of type InputStream, but a (private) subclass of InputStream. Much like the Foundation collection types (NSArray/NSMutableArray, NSDictionary/NSMutableDictionary, etc.), the parent type you interface with is not the effective type you're working on: when you alloc one of these types, the returned object is usually of a private subclass.

In most cases, this is irrelevant implementation detail, but in your case, because you're trying to swizzle an initializer, you do actually care about the value returned from alloc, since you're swizzling an initializer which is never getting called.

In the specific case of InputStream, the value returned from [NSInputStream alloc] is of the private NSCFInputStream class, which is the effective toll-free bridged type shared with CoreFoundation. This is private implementation detail that may change at any time, but you can swizzle the initializer on that class instead:

guard let class = NSClassFromString("NSCFInputStream") else {
    // Handle the fact that the class is absent / has changed.
    return
}

swizzling(class, #selector(InputStream.init(url:)), #selector(swizzledInit(url:)))

Note that if you're submitting an app for App Store Review, it is possible that the inclusion of a private class name may affect the review process.

  •  Tags:  
  • Related