Objective-C 运行时和 Swift 动态性

1,808 阅读11分钟
  • Objective-C 运行时 Runtime

Objective-C 是一种面向运行时的语言,这意味着方法,变量和类之间的所有链接都推迟到应用程序实际运行的最后一刻,这提供了极大的灵活性,因为可以让开发人员更改这些链接。Swift在大部分情况可看成是面向编译时的语言。因此,在Swift中,安全性更高,但灵活性更低。

Objective-C runtime 是一个库。代表着 对象 的思想,有关面向对象的内容都是在此实现的。 要是用它的功能,倒入库就OK了。

#import <objc/runtime.h>

它主要用C和Assembly编写,并实现了诸如类和对象以及如何调度方法,协议等一大堆东西。运行时负责Objective-C的面向对象编程部分。

runtime.h 里面可以看到

  • 实例的内部结构

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 指向该类的元类 MetaClass
  • super_class 指向该类的父类
  • ivars 变量列表
  • methodLists 方法列表
  • cache 缓存的数据,比如方法等(能够更快的查找方法)
  • protocols 协议列表

对于我们来说,有趣的是变量列表,​​方法列表和协议列表。这些都是可以在运行时更改并从运行时读取的内容。

  • 变量和方法的内部结构

struct objc_ivar {
	char *ivar_name;
	char *ivar_type;
	int ivar_offset;
}

struct objc_method {
	SEL method_name;
	char *method_types;
	IMP method_imp;
}

变量 objc_ivar

  • ivar_name 变量名字
  • ivar_type 变量类型
  • ivar_offset 内存管理里面的偏移量

方法 objc_method:

  • method_name 方法名字,也可以叫做选择器 selector

  • method_types 方法类型的编码字符串

  • method_imp 方法的实现指针地址

  • 动态创建一个类

    // 创建一个类和它的元类
    Class myClass = objc_allocateClassPair([NSObject class], "MyClass", 0);
   
    // 在这可以添加变量,方法,协议
    // ...
    // ...
    
    // 注册类,注册完成以后变量列表就锁定了,但是方法列表和协议列表还可以动态添加
    objc_registerClassPair(myClass);
  • objc_allocateClassPair 创建一个类。第一个参数:它的父类;第二个参数:类的名称;第三个参数:额外的字节,通常设置为0.

  • 创建类以后就可以注册变量,方法,协议了。

  • objc_registerClassPair 把创建好的类注册,此时类就可以正常使用了。该类的行为将与其他Objective-C类相同,并且没有区别。需要注意的是注册后,无法更改变量列表了,但可以更改其他所有内容。

  • 关联属性

如果有一个不属于你类,要是想要扩展它,添加一个功能。那么在 Objective-C 可以使用 Category,在 Swift 中可以使用 extension。但是有一个问题就是你不可以直接添加有存储功能的属性 stored property,只可以直接添加计算属性 computed property。

运行时的另一个特点就是可以向现有的类添加有存储功能的属性,使用 objc_setAssociatedObjectobjc_getAssociatedObject


#import <Foundation/Foundation.h>
#import "objc/runtime.h"

@interface NSObject (AssociatedObject)

@property (nonatomic, copy) NSString *defaultName;

@end

#import "NSObject+AssociatedObject.h"

@implementation NSObject (AssociatedObject)

@dynamic defaultName;

static char associatedKey;

- (void)setDefaultName:(NSString *)defaultName {
    objc_setAssociatedObject(self, &associatedKey, defaultName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)defaultName {
    return objc_getAssociatedObject(self, &associatedKey);
}

@end

测试:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.defaultName = @"happy";
    NSLog(@"name = %@", self.defaultName);
}

输出:

name = happy
  • 方法交叉(Method Swizzling)

  • 方法添加

在上面的方法结构中,可以看到它只是一个包含方法名称,类型,实现地址的一个简单数据结构。那么在使用 Runtime 添加的时候给提供相关参数就行了。

    // 获得方法名
    Method flyMethod = class_getInstanceMethod(self.class, @selector(gotoFly));
    // 找到实现地址
    IMP flyImp = method_getImplementation(flyMethod);
    // 设置类型
    const char *types = method_getTypeEncoding(flyMethod); 
    // 方法添加到某个类
    class_addMethod(MyClass, @selector(gotoFly), flyImp, types);

那么方法的调用情况则是这样:

    // gotoFly 可以这样调用
    [self gotoFly];
    
    // gotoFly 也可以这样调用
    [self performSelector:@selector(gotoFly)];
    
    // 上面的内部实际调用
    objc_msgSend(self, @selector(gotoFly));
  • 方法替换

但是如果想要替换一个方法的实现。在 Runtime 中经常用到的就是 Method Swizzling

+ (void)load {

    // 保持线程安全,只调用一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class]; // 确实在当前类
        
        SEL originalSelector = @selector(gotoFly);
        SEL swizzledSelector = @selector(gotoWalk);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        /* 如果当前类没有 原方法的 IMP,说明在从父类继承过来的方法实现,
         * 需要在当前类中添加一个 originalSelector 方法,
         * 但是用 替换方法 swizzledMethod 去实现它
         */
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            // 原方法的 IMP 添加成功后,修改 替换方法的 IMP 为 原始方法的 IMP
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            // 交换实现地址
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)gotoFly {
    NSLog(@"I can fly!");
}

- (void)gotoWalk {
    NSLog(@"I can walk!");
}

测试:

- (void)addButtons {
    UIButton *flyButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    [flyButton setTitle:@"fly" forState:UIControlStateNormal];
    [flyButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [flyButton addTarget:self action:@selector(gotoFly) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:flyButton];
    
    UIButton *walkButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 200, 100, 100)];
    [walkButton setTitle:@"walk" forState:UIControlStateNormal];
    [walkButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [walkButton addTarget:self action:@selector(gotoWalk) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:walkButton];
}

点击 fly 按钮输出:I can walk!

点击 walk 按钮输出:I can fly!

  • 消息转发

一个方法能被正确的调用那很好,没问题。但是如果调用一个不存在的方法在默认则会抛出一个方法未识别的移除,app直接崩溃了。此时苹果还是提供了消息转发功能来处理未识别的方法的。

下面一共有三个机会处理:

// 1
+(BOOL)resolveInstanceMethod:(SEL)sel{
	// 添加一个实例方法,然后 return YES。 然后之前的方法会再调用一遍.
}

// 2
- (id)forwardingTargetForSelector:(SEL)aSelector{
	// 返回一个处理该方法的实例.
}

// 3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
	// 为调用创建一个签名,进入 forwardInvocation
}

- (void)forwardInvocation:(NSInvocation *)invocation {
	// 根据上面的签名,旋转一个对象执行方法:
	// [invocation invokeWithTarget:target];
}
  • 动态方法解析

调用一个不存在的方法时,运行时会首先调用 resolveInstanceMethod (如果是类方法则是 resolveClassMethod)处理。这时就有机会动态添加一个方法来执行,返回 true,然后原始方法就会再次调用一遍。

例子如下:


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *testButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
    [testButton setTitle:@"test" forState:UIControlStateNormal];
    [testButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [testButton addTarget:self action:@selector(testButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:testButton];
}
- (void)testButtonAction {
    NSLog(@"call testButtonAction");

    // fakeNews 方法不存在
    [self performSelector:@selector(fakeNews)];
}

+ (BOOL) resolveInstanceMethod:(SEL)sel
{
    NSLog(@"call resolveInstanceMethod");
    if (sel == @selector(fakeNews))
    {
        class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicMethodIMP(id self, SEL _cmd)
{
    NSLog(@"call dynamicMethodIMP");
}

@end

此时执行 fakeMethod 方法,由于 fakeMethod 不存在,当时动态添加了方法实现 dynamicMethodIMP。所以输出:

2020-04-05 17:58:20.722564+0800 RuntimeOC[8797:2588206] call testButtonAction
2020-04-05 17:58:20.722720+0800 RuntimeOC[8797:2588206] call resolveInstanceMethod
2020-04-05 17:58:20.722842+0800 RuntimeOC[8797:2588206] call dynamicMethodIMP

签名参数 v@: 怎样设置?可以查看 Type Encodings

  • 换个对象处理方法

如果在 resolveInstanceMethod 那没有创建新方法,那么可以使用 forwardingTargetForSelector。只需返回要在其上调用该方法的目标,然后将在该目标上调用选择器

例子如下:

@interface DispatchObject : NSObject

- (void)fakeNews;

@end

@implementation DispatchObject

- (void)fakeNews {
    NSLog(@"BBC ? fake news");
}

@end

// ---------------------------------------------------

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *testButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
    [testButton setTitle:@"test" forState:UIControlStateNormal];
    [testButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [testButton addTarget:self action:@selector(testButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:testButton];
}

- (void)testButtonAction {
    NSLog(@"call testButtonAction");

    // fakeNews 方法不存在
    [self performSelector:@selector(fakeNews)];
}

+ (BOOL) resolveInstanceMethod:(SEL)sel
{
    NSLog(@"call resolveInstanceMethod");
//    if (sel == @selector(fakeNews))
//    {
//        class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
//        return YES;
//    }
    
    // 如果返回 true 并且添加了方法处理,那么就不会执行 forwardingTargetForSelector 了。
    // 否则 true 或者 false 都会执行 forwardingTargetForSelector。

    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"call forwardingTargetForSelector");
    if (aSelector == @selector(fakeNews)) {
        // 通过返回 DispatchObject 的实例处理该方法。记得不要返回self,不然就无限循环了。
        return [[DispatchObject alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

输出:

2020-04-05 17:58:56.322710+0800 RuntimeOC[8840:2589207] call testButtonAction
2020-04-05 17:58:56.322853+0800 RuntimeOC[8840:2589207] call resolveInstanceMethod
2020-04-05 17:58:56.322940+0800 RuntimeOC[8840:2589207] call forwardingTargetForSelector
2020-04-05 17:58:56.323036+0800 RuntimeOC[8840:2589207] BBC ? fake news
  • 签名加调用

如果上面两步 resolveInstanceMethodforwardingTargetForSelector 都没有处理。还有最后一个机会拯救这个找不到都方法:实现 methodSignatureForSelectorforwardInvocation

@interface DispatchObject : NSObject

- (void)fakeNews;

@end

@implementation DispatchObject

- (void)fakeNews {
    NSLog(@"BBC ? fake news");
}

@end

// ---------------------------------------------------

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *testButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 300, 100, 100)];
    [testButton setTitle:@"test" forState:UIControlStateNormal];
    [testButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [testButton addTarget:self action:@selector(testButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:testButton];
}

- (void)testButtonAction {
    NSLog(@"call testButtonAction");

    // fakeNews 方法不存在
    [self performSelector:@selector(fakeNews)];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"call methodSignatureForSelector");
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fakeNews"]) {
        // 签名,为 forwardInvocation 做准备
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"call forwardInvocation");
    SEL aSelector = [anInvocation selector];
    
    DispatchObject *news = [[DispatchObject alloc] init];
    if ([news respondsToSelector:aSelector]) {
        [anInvocation invokeWithTarget:news];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

@end

输出:

2020-04-05 17:57:16.928061+0800 RuntimeOC[8723:2586776] call testButtonAction
2020-04-05 17:57:16.928215+0800 RuntimeOC[8723:2586776] call methodSignatureForSelector
2020-04-05 17:57:16.928340+0800 RuntimeOC[8723:2586776] call forwardInvocation
2020-04-05 17:57:16.928455+0800 RuntimeOC[8723:2586776] BBC ? fake news
  • KVC 和 KVO

在 iOS 的 Foundation 库里面。 键值编码(KVC)和键值观察(KVO)就是基于 Runtime 搭建的。KVC 和 KVO 允许我们将 UI 绑定到数据。

  • KVC

@property (nonatomic, strong) NSNumber *age;

[myClass valueForKey:@"age"];
[myClass setValue:@(4) forKey:@"age"];

如代码所示,可以直接用属性名设置或者获取值。

  • KVO

[myClass addObserver:self
    forKeyPath:@"age"
    options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
    context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context{
	// 监听的属性更改了。
}

当观察值改变时,它将通过调用 observeValueForKeyPath 此方法通知观察者。

上面 age 属性 KVO 的实现大致是这样的:

  • AClass 类有一个 obj 对象
  • 在运行的时候检测到对象 obj 属性比如 age 被监听以后。Runtime 会动态创建一个 AClass 的子类 NSKVONotifying_AClass
  • NSKVONotifying_AClass 重写 setAge
- (void)setAge:(NSString *)newAge { 
      [self willChangeValueForKey:@"age"];    // KVO 在调用存取方法之前总调用 
      [super setValue:newAge forKey:@"age"];  // 调用父类的存取方法 
      [self didChangeValueForKey:@"age"];     // KVO 在调用存取方法之后总调用
}
  • Runtime 使用 isa-swizzling 重置 objisa 指针,从 AClass 指向 NSKVONotifying_AClass 来实现 AClass 类属性值 age 改变后的监听。
  • Objective-C 中的方法查找先通过 isa 找到当前的类,然后查看到当前类否实现方法,如果没有再去 superClass 查找
  • 由于上面 isa 的指向类修改改为 NSKVONotifying_AClass。所以调用 setAge 的时候就能即保证通知能够发出去,又能使原有的类数据得到更改且里面的代码保持干净。

Objcetive-C KVO 的一个Demo KVOOC

  • Swift

Swift作为一种语言通常是强类型的。它是静态类型,Swift中的默认类型非常安全的。Swift中存在的任何动态都可以通过Objective-C运行时获得。

对于动态性,Swift 提供了 @objc@dynamic 来修饰。@objc 将 Swift API公开给 Objective-C 运行时,但仍不能保证编译器不会尝试对其进行优化。使用 @dynamic 修饰符时,不需要使用,@objc 因为它是隐含的。

下面是一些在 Swift 里 Runtime 的一些实现:

  • Swift extension 关联属性的例子:

extension NSObject {
    
    // 保持命名空间的干净
    private struct AssociatedKeys {
        static var defaultNameKey = "dq_defaultNameKey"
    }
    
    var defaultName: String? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.defaultNameKey) as? String
        } set {
            objc_setAssociatedObject(self, &AssociatedKeys.defaultNameKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    
}
  • Swift 的 Method Swizzling

由于现在 Objective-C 中的 loadinitialize 方法在 Swift 方法中都有所限制,然后 dispatch_once 也在 Swift 中废弃了。具体的可以查看 how-to-implement-method-swizzling-swift

  • Swift 的消息转发

    // 1
    override class func resolveInstanceMethod(_ sel: Selector!) -> Bool {
        return super.resolveInstanceMethod(sel)
    }
    
    // 2
    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return super.forwardingTarget(for: aSelector)
    }

在 Swift 中,只提供了动态添加方法 resolveInstanceMethod 和找新对象处理 forwardingTarget。另外一个方法签名和调用的不支持。

  • Swift KVO 和 KVC

在Swift中,KVO和KVC的功能弱得多。您正在观察的对象必须继承自NSObject Objective-C类型。您要观察的变量必须声明为dynamic。您需要对观察到的事情非常具体。

这里有一个KVO Swift 的demo KVOSwift

参考

Apple Key Value Observing Articles

Apple using_key-value_observing_in_swift

Realm The Objective-C Runtime & Swift Dynamism

NSHipster Swift & the Objective-C Runtime

iOS Runtime详解