Dynamic Swift
As we all know, Swift is a static language, and all attributes are determined at compile time. The distribution of methods is completed statically, similar to the vtable of C++, which can greatly improve the performance of Swift. According to statistics, the speed of Swift static distribution is about 1.1 nanoseconds. The dynamic dispatch time of ObjC is 4.9 nanoseconds, which is about 5 times faster than Swift, so Swift performs better than ObjC because Swift does a lot of static optimization.
ObjC, as we all know, is the opposite of Swift. ObjC is a dynamic language, and all methods and properties are distributed and bound dynamically. This gives us many benefits, such as the ability to replace a method globally, or to replace the type of the sample at runtime, etc. AOP programming can be implemented easily with ObjC.
I once thought that Swift is really a static language. I might be really reluctant to admit the ObjC history of Swift in my heart, because I prefer to treat Swift as a new language. In this context, I think Swift is a completely static language. I also think that in order to use the ObjC runtime, we need to inherit NSObject from Swift. However, it turns out that this idea is not entirely static, Swift can also distribute dynamically, and Swift can distribute dynamically without inheriting NSObject (an Apple egg? Looking at the Swift documentation, Apple said so, but I chose to ignore it…)
ObjC Runtime does something
We all know what ObjC Runtime does. It provides us with a unified data structure that describes what a class, an instance, really is, and we clearly know that all classes of ObjC must inherit from NSObject.
More about ObjC Runtime is beyond the scope of this article.
Does Swift have a runtime?
The answer is yes. However, Swift does not provide an API for runtime access, and since Swift itself is a static language, its runtime is nothing like dynamic. Fortunately, Swift supports dynamic, so what does that mean? Swift has its own runtime that handles the pure Swift language and certainly does a lot of statically optimized work, so if we want to support Swift dynamic, we need to use objC’s runtime mechanism, and Swift itself is compatible with objC’s runtime.
This is a Swift class:
public class SwiftClass {
public var val: Int
public init(
_ val: Int)
{
self.val = val
}
public func echo(
_ val: Int) -> Int
{
return val
}
@objc
public dynamic func dynamicEcho(
_ val: Int) -> Int
{
return val
}
}
func objc_classes(of cls: AnyClass) -> [AnyClass] {
var clss: [AnyClass] = []
var cls: AnyClass? = cls
while let _cls = cls {
clss.append(_cls
cls = class_getSuperclass(_cls)
}
return clss
}Copy the code
Create an instance:
let classIns = SwiftClass(110)Copy the code
Take a look at the dynamicIns type:
print(objc_classes(of: object_getClass(classIns)!) ) // [__lldb_expr_3.SwiftClass, SwiftObject]Copy the code
You can clearly see that the root class of dynamicIns in the ObjC runtime is SwiftObject. This is exactly what we expected, as Swift accommodates the transition from Swift classes to objC runtime classes for us.
Let’s look at the “stuff” of the SwiftObject class:
let swiftObjectClass = objc_getClass("SwiftObject".withCString { $0 }) print(swiftObjectClass as Any) public func objc_methods(of class: AnyClass) -> [String] { var count: UInt32 = 0 let methodList = class_copyMethodList(class, &count) return (0.. <count).compactMap { idx in methodList.map { method_getName($0.advanced(by: Int(idx)).pointee).description } } } public func objc_properties(of class: AnyClass) -> [String] { var count: UInt32 = 0 let propertyList = class_copyPropertyList(class, &count) return (0.. <count).compactMap { idx in propertyList.map { String(cString: property_getName($0.advanced(by: Int(idx)).pointee)) } } } public func objc_ivars(of class: AnyClass) -> [String] { var count: UInt32 = 0 let ivarList = class_copyIvarList(class, &count) return (0.. <count).compactMap { idx in ivarList.flatMap { ivar_getName($0.advanced(by: Int(idx)).pointee).map({ String(cString: $0) }) } } } public func objc_protocols(of class: AnyClass) -> [String] { var count: UInt32 = 0 let protocolList = class_copyProtocolList(class, &count) return (0.. <count).compactMap { idx in String(cString: protocol_getName(protocolList! [Int(idx)])) } } print(objc_methods(of: swiftObjectClass as! AnyClass)) // ["class", "isKindOfClass:", "release", "isEqual:", "self", "performSelector:", "performSelector:withObject:", "performSelector:withObject:withObject:", "isProxy", "isMemberOfClass:", "conformsToProtocol:", "respondsToSelector:", "retain", "autorelease", "retainCount", "zone", "hash", "superclass", "description", "debugDescription", "dealloc", "methodForSelector:", "doesNotRecognizeSelector:", "allowsWeakReference", "retainWeakReference", "isDeallocating", "tryRetain", "isNSArray", "isNSNumber", "isNSDictionary", "isNSSet", "isNSOrderedSet", "isNSString", "cfTypeID", "isNSValue", "isNSDate", "isNSData", "isNSObject", "copyDescription", "isNSCFConstantString", "isNSTimeZone"] print(objc_properties(of: swiftObjectClass as! AnyClass)) // ["hash", "superclass", "description", "debugDescription"] print(objc_ivars(of: swiftObjectClass as! AnyClass)) // ["isa", "refCounts"] print(objc_protocols(of: swiftObjectClass as! AnyClass)) // ["NSObject"] print(objc_methods(of: object_getClass(classIns)!) )Copy the code
So, SwiftObject implements an ObjC runtime type of the protocol NSObject. Does SwiftObject support dynamic distribution? Let’s try it:
print((classIns as AnyObject).perform? (NSSelectorFromString("class")) as Any) // print((classIns as AnyObject).perform? (NSSelectorFromString("echo:"), with: 110)) // Error. print((classIns as AnyObject).perform? (NSSelectorFromString("dynamicEcho:"), with: 110) as Any)Copy the code
As you can see, SwiftObject implements NSObject’s basic protocol methods at the ObjC runtime, and we can dynamically distribute methods from the NSObject protocol on Swift objects at runtime. And the method or property that we declared @objc Dynamic. That is, Swift declared types still exist in ObjC runtime as ObjC compliant, except that methods or properties declared in Swift are statically optimized by Swift to improve performance, while statically optimized methods and properties are not present in ObjC runtime.
Here’s another example:
public struct SwiftStruct { public let val: Int public func echo( _ val: Int) -> Int { return val } // @objc // Error: @objc can only be used with members of classes, @objc protocols, and concrete extensions of classes // public dynamic func dynamicEcho( // Error: Only members of classes may be dynamic // _ val: Int) -> Int // { // return val // } } public enum SwiftEnum { case a, b, c } let structVal = SwiftStruct(val: 110) let stdIntVal = 110 let stdBoolVal = false let stdArrVals = ["1", "2", "3"] let stdDicVals = ["1": 1, "2": 2] let stdSetVals: Set<Int> = [1, 2, 3] print(objc_classes(of: object_getClass(structVal)!) ) // [_SwiftValue, NSObject] print(objc_methods(of: object_getClass(structVal)!) ) // ["isEqual:", "hash", "description", "debugDescription", "dealloc", "copyWithZone:", "swiftTypeMetadata", "swiftTypeName", "_swiftValue"] print(objc_classes(of: object_getClass(SwiftEnum.a)!) ) print(objc_classes(of: object_getClass(stdIntVal)!) ) // [__NSCFNumber, NSNumber, NSValue, NSObject] print(objc_classes(of: object_getClass(stdBoolVal)!) ) // [__NSCFBoolean, NSNumber, NSValue, NSObject] print(objc_classes(of: object_getClass(stdArrVals)!) ) // [Swift.SwiftDeferredNSArray, Swift.SwiftNativeNSArrayWithContiguousStorage, Swift._SwiftNativeNSArray, _SwiftNativeNSArrayBase, NSArray, NSObject] print(objc_classes(of: object_getClass(stdDicVals)!) ) // [Swift.SwiftDeferredNSDictionary<Swift.String, Swift.Int>, Swift.SwiftNativeNSDictionary, _SwiftNativeNSDictionaryBase, NSDictionary, NSObject] print(objc_classes(of: object_getClass(stdSetVals)!) ) // [Swift.SwiftDeferredNSSet<Swift.Int>, Swift.SwiftNativeNSSet, _SwiftNativeNSSetBase, NSSet, NSObject]Copy the code
As you can see, Swift’s struct types all have compatible types in ObjC, and the root type of all types is ObjC’s NSObject class. We can also distribute dynamically to these types by calling ObjC runtime registered methods, but we cannot distribute dynamically to our own Swift methods because the Struct structure type does not support dynamic.
For Swift bridging types, such as Int, Double, Set, Array, Dictionary, they all have corresponding bridge types in ObjC runtime, such as Int/Double for NSNumber, Set corresponds to the NSSet type, Array corresponds to NSArray, and Dictionary corresponds to NSDiction. For these types, we should avoid bridging as much as possible, because bridging does not have the static optimization of Swift.
For Swift specific types, the ObjC runtime class is _SwiftValue, which integrates with NSObject and provides some Swift specific methods for Swift optimization.
Provides dynamic dispatch capability for Swift types
Let’s take a structure as an example and create a new structure:
public struct DynamicStruct { public var val: Int public func echo( _ val: Int) -> Int { print("Calling echo..." ) return val } internal func dispatchEcho( _ obj: AnyObject, val: Int) -> Int { return echo(val) } }Copy the code
Generate an instance:
let dynamicVal = DynamicStruct(val: 110)Copy the code
Add ObjC runtime methods to dynamicVal:
let block: @convention(block) (AnyObject, Int) -> Int = dynamicVal.dispatchEcho
let imp = imp_implementationWithBlock(unsafeBitCast(block, to: AnyObject.self))
let dynamicValCls = object_getClass(dynamicVal)!
class_addMethod(dynamicValCls, NSSelectorFromString("objcEcho:"), imp, "@24@0:8@16")Copy the code
Dynamically distribute using ObjC:
print((dynamicVal as AnyObject).perform(NSSelectorFromString("objcEcho:"), with: Int(1000))! .takeRetainedValue()) // Calling echo... / / 1000Copy the code
The result is, as expected, the ability to dynamically distribute ObjC to the structure type.
Let’s get back to business
Does Swift have a runtime? Yes, all types in Swift (structs, enums, classes) have corresponding classes to describe in the ObjC runtime, which finally answers a question I’ve been puzzling about.
Make a summary
Swift is a static language. All methods and attributes declared in Swift are determined at static compile time. Swift also supports dynamic binding and dynamic distribution. Swift’s dynamic features will be implemented using ObjC Runtime, fully compatible with ObjC.
@objc, @objcmembers, dynamic
As mentioned earlier, Swift’s dynamic distribution depends on ObjC’s runtime system. In order to expose Swift’s method attributes and even types to ObjC, we need to declare @objc, which can be accessed in ObjC. However, Properties or methods declared as @objc may be optimized by Swift for static calls, not necessarily distributed dynamically. To use dynamic features, you need to declare dynamic so that dynamic features can be fully used. @objcmembers is similar to @objc, except that:
-
Classes declared by @objcMembers implicitly add @objc flags to all properties or methods
-
Classes declared as @objc need to inherit from NSObject, whereas @objcMembers need not inherit from NSObject
A few points to note:
-
In Swift 3.x, classes that inherit from NSObject implicitly add the @objc flag to all public attributes or methods, while private b attributes or methods need to be added manually
-
In Swift 4.x, classes that inherit from NSObject do not implicitly add @objc
-
@objc can also change the Name of the property or method exposed to objC: @objc(Name)
Swift’s dilemma
Swift is designed to be a static language, but it has no choice but to be compatible with historical Cocoa libraries and never get rid of ObjC. This situation should be Swift’s norm in the next few years. Such a compromise provides us with ideas for Swift to be dynamic.
Although Swift is compatible with the ObjC runtime, we don’t know what Swift’s purpose is, to be compatible with historical Cocoa libraries. Or is this an Easter egg from Swift?
Perhaps this is Swift’s Easter egg, or even its ability to open up more dynamics in the future.
The possibility of AOP
It is Swift’s compatibility with the ObjC runtime that opens up the possibility of implementing AOP for the Swift platform. AOP implementations rely on intercept pattern. Swift supports two methods of method invocation, static invocation and dynamic dispatch. Therefore, to implement method interception in Swift, either the compiler provides support or you have to start at runtime, whereas all types in Swift, Are compatible with the ObjC runtime for us.