这意味着全部方法、变量、类之间的link,都会推迟到应用实际运行的最后一刻才会创建。
这将给开发人员极高的灵活性,由于咱们能够修改这些link。git
所以在 Swift 当中,灵活性受到了限制,不过您会所以获得更多的安全性。github
咱们一般所说的 Objective-C 「动态性」,每每都是指 KVO。虽然还有其他的函数,可是这些是最多见、最经常使用的。这也就是人们所说的,Swift 缺失的部分。编程
而 KVO是Foundation框架基于运行时实现的一个特性。所以本文先从Objective-C 的运行时 开始描述。安全
本质上是一个库。它负责了 “Objective” 这个部分,app
#import <objc/runtime.h>
它主要由 C 和汇编编写而成,其实现了诸如类、对象、方法调度、协议等等这些东西。它是彻底开源的,而且开源了很长一段时间了。框架
typedef struct objc_class *Class; struct objc_object { Class isa; };
对象只与一个类创建引用关联,也就是这个 isa 的意思所在。编程语言
struct objc_class { Class isa; Class super_class; const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols; };
类当中一样有 isa 这个值。除了 NSObject 这个类以外,super_class 的值永远不会为 nil,由于 Objective-C 当中的其他类都是以某种方式继承自 NSObject 的.ide
更多的应该是关注变量列表 (ivars)、方法列表 (methodLists) 和这个协议列表 (protocols)。函数
这些就是咱们能在运行时修改和读取的。能够看到,对象其实本质上是一个很是简单的结构体,类一样也是。咱们能够借助运行时函数,从而在运行时动态建立类。单元测试
BJC_EXPORT Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
Class myClass = objc_allocateClassPair([NSObject class], "MyClass", 0); // 在这里添加变量、方法和协议 objc_registerClassPair(myClass); // 当类注册以后,变量列表将会被锁定 [[myClass alloc] init];
所建立的这个类和其他的 Objective-C 类毫无区别.
利用 Objective-C 运行时函数:allocateClassPair 建立Class。咱们为其提供一个 isa,在本例当中咱们提供了 NSObject,而后为其命名。第三个参数则是额外字节的定义,一般咱们都直接赋值 0 便可。随后咱们就能够添加变量、方法以及协议了,以后就注册这个 ClassPair。注册以后,咱们就没法修改变量列表了,其他的内容仍然能够修改。
[myObject isMemberOfClass:NSObject.class]; [myObject respondsToSelector:@selector(doStuff:)]; // isa == class class_respondsToSelector(myObject.class, @selector(doStuff:));
respondsToSelector,其接受 Selector 和类为参数。
BJC_EXPORT BOOL class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
在编写 XCTestCase 的时候,须要完成 setUp 和 tearDown 的设定,随后才能编写相关的 test 函数。
当测试运行的时候,系统会自行遍历全部的测试函数,并自动运行。
unsigned int count; Method *methods = class_copyMethodList(myObject.class, &count); //Ivar *list = class_copyIvarList(myObject.class,&count); for(unsigned i = 0; i < count; i++) { SEL selector = method_getName(methods[i]); NSString *selectorString = NSStringFromSelector(selector); if ([selectorString containsString:@"test"]) { [myObject performSelector:selector]; } } free(methods);
struct objc_ivar { char *ivar_name; char *ivar_type; int ivar_offset; } struct objc_method { SEL method_name; char *method_types; IMP method_imp; }
Method doStuff = class_getInstanceMethod(self.class, @selector(doStuff)); IMP doStuffImplementation = method_getImplementation(doStuff); const char *types = method_getTypeEncoding(doStuff); //“v@:@" class_addMethod(myClass.class, @selector(doStuff:), doStuffImplementation, types);
这个例子具体的方法实现部分咱们取了个巧,由于咱们使用了既有的 doStuff 方法,
所以可以很简单地获取其方法实现和方法类型。
不过咱们还能够用其余方法来完成:交换方法的实现。可使用运行时当中最著名的动态特性:方法混淆 (swizzling)。
咱们可使用 [self doStuff] 或者 [self performSelector:@selector(doStuff)] 来进行调用,实际上在运行时级别,它们都是借助 objc_msgSend 向对象发送了一个消息。
[self doStuff]; [self performSelector:@selector(doStuff)]; objc_msgSend(self, @selector(message));
扩展一个不是本身建立的类,想要向其中添加函数。Swift 的扩展与之很是类似。
类别的一个问题便在于,它没法添加存储属性。您能够添加一个计算属性,可是存储属性是没法添加的。
运行时的另外一个特性即是:
咱们能够借助 setAssociatedObject 和 getAssociatedObject 这两个函数,向既有的类当中添加存储属性。
@implementation NSObject (AssociatedObject) @dynamic associatedObject; - (void)setAssociatedObject:(id)object { objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)associatedObject { return objc_getAssociatedObject(self, @selector(associatedObject)); }
可是若是调用方法所在的对象为 nil 的时候,咱们就会获得一个异常,应用便会崩溃。但事实证实,在崩溃以前会预留几个步骤,从而容许咱们对某个不存在的函数进行一些操做。
// 1 +(BOOL)resolveInstanceMethod:(SEL)sel{ // 添加实例方法并返回 YES 的一次机会,它随后会再次尝试发送消息 } // 2 - (id)forwardingTargetForSelector:(SEL)aSelector{ // 返回能够处理 Selector 的对象 } // 3 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ // 您须要实现它来建立 NSInvocation } - (void)forwardInvocation:(NSInvocation *)invocation { // 在您所选择的目标上调用 Selector [invocation invokeWithTarget:target]; }
ps:试图桥接两个不一样的框架的时候,这个功能便很是有用
当调用了某个不存在的方法时,运行时首先会调用一个名为 resolveInstanceMethod 的类方法,
若是所调用的方法是类方法的话,则为调用 resolveClassMethod。
这时候咱们便有机会来添加方法了,即上面提到的利用运行时动态添加方法
若是不想建立新方法的话,第一步返回了NO,
还有 forwardingTargetForSelector。能够直接返回须要调用方法的目标对象便可,以后这个对象就会调用 Selector。
第三步骤:forwardInvocation
略为复杂的 forwardInvocation。
若是您须要这么作,那么还须要实现 methodSignatureForSelector。
全部的调用过程都被封装到 NSInvocation 对象当中,以后你即可以使用特定的对象进行调用了。
交换方法的实现
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(doSomething); SEL swizzledSelector = @selector(mo_doSomething); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
2种实现方式:
一、class_addMethod originalSelector 的时候使用swizzledMethod的IMP、TYP
以后进行class_replaceMethod,来达到交换的目的。
即先换掉originalSelector的IMP,再换掉swizzledMethod的IMP,达到exchange 的目的
二、直接使用exchange 方法
当类加载以后,会调用一个名为 load 的类函数。因为咱们只打算混淆一次,所以咱们须要使用 dispatch_once。接着咱们即可以获得该方法,而后使用 class_replaceMethod 或者 method_exchangeImplementations 来替换方法。
之因此想要混淆,是由于它能够用于日志记录和 Mock 测试。例如上报用户打开的界面所在VC的名称,就可使用swizzling 统一处理
Foundation 框架实现了基于运行时的一个特性:
键值编码 (key-value-coding, KVC) 以及键值观察 (key-value observing, KVO)。
KVC 和 KVO 容许咱们将 UI 和数据进行绑定。这也是 Rx 以及其余响应式框架实现的基础。
ps:并且MVVM的实现又能够借助“V-VM”第三方绑定框架进行实现
@property (nonatomic, strong) NSNumber *number; [myClass valueForKey:@"number"]; [myClass setValue:@(4) forKey:@"number"];
能够将属性名称做为键,来获取属性值或者设置属性值.
[myClass addObserver:self forKeyPath:@"number" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil]; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ // Respond to observation. }
在观察的值发生变动以后,KVO 会调用此方法当即通知观察者。经过这个方法,咱们即可以按需更新 UI。
咱们一般所说的 Objective-C 「动态性」,每每都是指 KVO。虽然还有其他的函数,可是这些是最多见、最经常使用的。这也就是人们所说的,Swift 缺失的部分。
Swift 是一种强类型语言。类型静态,也就是说 Swift 的默认类型是很是安全的。
若是须要的话,不安全类型也是存在的,Swift 中的动态性能够经过 Objective-C 运行时来得到。
可是 Swift 开源并迁移到 Linux 以后,因为 Linux 上的 Swift 并不提供 Objective-C 运行时,事情就。。
Swift 当中存在有这两个修饰符 @objc 和 @dynamic,此外咱们一样还能够访问 NSObject。@objc 将您的 Swift API 暴露给 Objective-C 运行时,可是它仍然不能保证编译器会尝试对其进行优化。
若是您真的想使用动态功能的话,就须要使用 @dynamic。(一旦您使用了 @dynamic 修饰符以后,就不须要添加 @objc 了,由于它已经隐含在其中。)
// 1 override class func resolveInstanceMethod(_ sel: Selector!) -> Bool { // 添加实例方法并返回 true 的一次机会,它随后会再次尝试发送消息 } // 2 override func forwardingTarget(for aSelector: Selector!) -> Any? { // 返回能够处理 Selector 的对象 } // 3 - Swift 不支持 NSInvocation
load 在 Swift 再也不会被调用,所以咱们须要在 initialize 中进行混淆。
在 Objective-C 当中,咱们使用 dispatch_once,可是自 Swift 3 以后,dispatch_once 便不复存在于 Swift 当中了。事情变得略为复杂。
if self is MyClass { // YAY } let myString = "myString"; let mirror = Mirror(reflecting: myString) print(mirror.subjectType) // “String" let string = String(reflecting: type(of: myString)) // Swift.String // No native method introspection
is 替代了 isMemberOfClass
若是打算为 Linux 编写单元测试的时候,就没法自动遍历全部的函数。您必须实现 static var allTests,而后手动列出全部的测试函数。这很。。。。
KVO 的魅力在于,您能够在不是本身所建立的类当中使用它,也能够只对您想要监听变化的类使用。所观察的对象必需要继承自 NSObject,而且使用一个 Objective-C 类型。所观察的变量必需要生命为 dynamic。致使你必需要对想要观察的事务了如指掌。
只能使用基于协议来观察对象,语言自身是没有原生的解决方案的。或者使用一些符合 Swift 风格的方法来暴露一些运行时函数的 ObjectiveKit 的开源库