神经病院 Objective-C Runtime 出院第三天——如何正确使用 Runtime

前言

到了今天终于要"出院"了,要总结一下住院几天的收获,谈谈Runtime到底能为咱们开发带来些什么好处。固然它也是把双刃剑,使用不当的话,也会成为开发路上的一个大坑。html

目录

  • 1.Runtime的优势
    • (1) 实现多继承Multiple Inheritance
    • (2) Method Swizzling
    • (3) Aspect Oriented Programming
    • (4) Isa Swizzling
    • (5) Associated Object关联对象
    • (6) 动态的增长方法
    • (7) NSCoding的自动归档和自动解档
    • (8) 字典和模型互相转换
  • 2.Runtime的缺点

一. 实现多继承Multiple Inheritance

在上一篇文章里面讲到的forwardingTargetForSelector:方法就能知道,一个类能够作到继承多个类的效果,只须要在这一步将消息转发给正确的类对象就能够模拟多继承的效果。git

官方文档上记录了这样一段例子。github

在OC程序中能够借用消息转发机制来实现多继承的功能。 在上图中,一个对象对一个消息作出回应,相似于另外一个对象中的方法借过来或是“继承”过来同样。 在图中,warrior实例转发了一个negotiate消息到Diplomat实例中,执行Diplomat中的negotiate方法,结果看起来像是warrior实例执行了一个和Diplomat实例同样的negotiate方法,其实执行者仍是Diplomat实例。objective-c

这使得不一样继承体系分支下的两个类能够“继承”对方的方法,这样一个类能够响应本身继承分支里面的方法,同时也能响应其余不相干类发过来的消息。在上图中Warrior和Diplomat没有继承关系,可是Warrior将negotiate消息转发给了Diplomat后,就好似Diplomat是Warrior的超类同样。算法

消息转发提供了许多相似于多继承的特性,可是他们之间有一个很大的不一样: 编程

多继承:合并了不一样的行为特征在一个单独的对象中,会获得一个重量级多层面的对象。 vim

消息转发:将各个功能分散到不一样的对象中,获得的一些轻量级的对象,这些对象经过消息经过消息转发联合起来。数组

这里值得说明的一点是,即便咱们利用转发消息来实现了“假”继承,可是NSObject类仍是会将二者区分开。像respondsToSelector:和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。好比上图中一个Warrior对象若是被问到是否能响应negotiate消息:缓存

if ( [aWarrior respondsToSelector:@selector(negotiate)] )复制代码

结果是NO,虽然它可以响应negotiate消息而不报错,可是它是靠转发消息给Diplomat类来响应消息的。安全

若是非要制造假象,反应出这种“假”的继承关系,那么须要从新实现 respondsToSelector:和 isKindOfClass:来加入你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can * * be forwarded to another object and whether that * * object can respond to it. Return YES if it can. */
    }
    return NO;
}复制代码

除了respondsToSelector:和 isKindOfClass:以外,instancesRespondToSelector:中也应该写一份转发算法。若是使用了协议,conformsToProtocol:也同样须要重写。相似地,若是一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。好比一个对象能给它的替代者对象转发消息,它须要像下面这样实现methodSignatureForSelector:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}复制代码

Note: This is an advanced technique, suitable only for situations where no other solution is possible. It is not intended as a replacement for inheritance. If you must make use of this technique, make sure you fully understand the behavior of the class doing the forwarding and the class you’re forwarding to.

须要引发注意的一点,实现methodSignatureForSelector方法是一种先进的技术,只适用于没有其余解决方案的状况下。它不会做为继承的替代。若是您必须使用这种技术,请确保您彻底理解类作的转发和您转发的类的行为。请勿滥用!

二.Method Swizzling

提到Objective-C 中的 Runtime,大多数人第一个想到的可能就是黑魔法Method Swizzling。毕竟这是Runtime里面很强大的一部分,它能够经过Runtime的API实现更改任意的方法,理论上能够在运行时经过类名/方法名hook到任何 OC 方法,替换任何类的实现以及新增任意类。

举的最多的例子应该就是埋点统计用户信息的例子。

假设咱们须要在页面上不一样的地方统计用户信息,常见作法有两种:

  1. 傻瓜式的在全部须要统计的页面都加上代码。这样作简单,可是重复的代码太多。
  2. 把统计的代码写入基类中,好比说BaseViewController。这样虽然代码只须要写一次,可是UITableViewController,UICollectionViewcontroller都须要写一遍,这样重复的代码依旧很多。

基于这两点,咱们这时候选用Method Swizzling来解决这个事情最优雅。

1. Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,咱们能够将Method Swizzling代码写到任何地方,可是只有在这段Method Swilzzling代码执行完毕以后互换才起做用。并且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,咱们能够利用苹果这一特性来实现AOP编程。

Method Swizzling本质上就是对IMP和SEL进行交换。

2.Method Swizzling使用

通常咱们使用都是新建一个分类,在分类中进行Method Swizzling方法的交换。交换的代码模板以下:

#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        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);
        }
    });
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}
@end复制代码

Method Swizzling能够在运行时经过修改类的方法列表中selector对应的函数或者设置交换方法实现,来动态修改方法。能够重写某个方法而不用继承,同时还能够调用原先的实现。因此一般应用于在category中添加一个方法。

3.Method Swizzling注意点

1.Swizzling应该总在+load中执行

Objective-C在运行时会自动调用类的两个方法+load和+initialize。+load会在类初始加载时调用, +initialize方法是以懒加载的方式被调用的,若是程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize方法是永远不会被调用的。因此Swizzling要是写在+initialize方法中,是有可能永远都不被执行。

和+initialize比较+load能保证在类的初始化过程当中被加载。

关于+load和+initialize的比较能够参看这篇文章《Objective-C +load vs +initialize》

2.Swizzling应该老是在dispatch_once中执行

Swizzling会改变全局状态,因此在运行时采起一些预防措施,使用dispatch_once就可以确保代码无论有多少线程都只被执行一次。这将成为Method Swizzling的最佳实践。

这里有一个很容易犯的错误,那就是继承中用了Swizzling。若是不写dispatch_once就会致使Swizzling失效!

举个例子,好比同时对NSArray和NSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会致使NSArray中的Swizzling失效的。

但是为何会这样呢? 缘由是,咱们没有用dispatch_once控制Swizzling只执行一次。若是这段Swizzling被执行屡次,通过屡次的交换IMP和SEL以后,结果可能就是未交换以前的状态。

好比说父类A的B方法和子类C的D方法进行交换,交换一次后,父类A持有D方法的IMP,子类C持有B方法的IMP,可是再次交换一次,就又还原了。父类A仍是持有B方法的IMP,子类C仍是持有D方法的IMP,这样就至关于咩有交换。能够看出,若是不写dispatch_once,偶数次交换之后,至关于没有交换,Swizzling失效!

3.Swizzling在+load中执行时,不要调用[super load]

缘由同注意点二,若是是多继承,而且对同一个方法都进行了Swizzling,那么调用[super load]之后,父类的Swizzling就失效了。

4.上述模板中没有错误

有些人怀疑我上述给的模板可能有错误。在这里须要讲解一下。

在进行Swizzling的时候,咱们须要用class_addMethod先进行判断一下原有类中是否有要替换的方法的实现。

若是class_addMethod返回NO,说明当前类中有要替换方法的实现,因此能够直接进行替换,调用method_exchangeImplementations便可实现Swizzling。

若是class_addMethod返回YES,说明当前类中没有要替换方法的实现,咱们须要在父类中去寻找。这个时候就须要用到method_getImplementation去获取class_getInstanceMethod里面的方法实现。而后再进行class_replaceMethod来实现Swizzling。

这是Swizzling须要判断的一点。

还有一点须要注意的是,在咱们替换的方法- (void)xxx_viewWillAppear:(BOOL)animated中,调用了[self xxx_viewWillAppear:animated];这不是死循环了么?

其实这里并不会死循环。 因为咱们进行了Swizzling,因此其实在原来的- (void)viewWillAppear:(BOOL)animated方法中,调用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的实现。因此不会形成死循环。相反的,若是这里把[self xxx_viewWillAppear:animated];改为[self viewWillAppear:animated];就会形成死循环。由于外面调用[self viewWillAppear:animated];的时候,会交换方法走到[self xxx_viewWillAppear:animated];这个方法实现中来,而后这里又去调用[self viewWillAppear:animated],就会形成死循环了。

因此按照上述Swizzling的模板来写,就不会遇到这4点须要注意的问题啦。

4.Method Swizzling使用场景

Method Swizzling使用场景其实有不少不少,在一些特殊的开发需求中适时的使用黑魔法,能够作法神来之笔的效果。这里就举3种常见的场景。

1.实现AOP

AOP的例子在上一篇文章中举了一个例子,在下一章中也打算详细分析一下其实现原理,这里就一笔带过。

2.实现埋点统计

若是app有埋点需求,而且要本身实现一套埋点逻辑,那么这里用到Swizzling是很合适的选择。优势在开头已经分析了,这里再也不赘述。看到一篇分析的挺精彩的埋点的文章,推荐你们阅读。 iOS动态性(二)可复用并且高度解耦的用户统计埋点实现

3.实现异常保护

平常开发咱们常常会遇到NSArray数组越界的状况,苹果的API也没有对异常保护,因此须要咱们开发者开发时候多多留意。关于Index有好多方法,objectAtIndex,removeObjectAtIndex,replaceObjectAtIndex,exchangeObjectAtIndex等等,这些设计到Index都须要判断是否越界。

常见作法是给NSArray,NSMutableArray增长分类,增长这些异常保护的方法,不过若是原有工程里面已经写了大量的AtIndex系列的方法,去替换成新的分类的方法,效率会比较低。这里能够考虑用Swizzling作。

#import "NSArray+ Swizzling.h"
#import "objc/runtime.h"
@implementation NSArray (Swizzling)
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(swizzling_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)swizzling_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 异常处理
        @try {
            return [self swizzling_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 打印崩溃信息
            NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } else {
        return [self swizzling_objectAtIndex:index];
    }
}
@end复制代码

注意,调用这个objc_getClass方法的时候,要先知道类对应的真实的类名才行,NSArray其实在Runtime中对应着__NSArrayI,NSMutableArray对应着__NSArrayM,NSDictionary对应着__NSDictionaryI,NSMutableDictionary对应着__NSDictionaryM。

三. Aspect Oriented Programming

Wikipedia 里对 AOP 是这么介绍的:

An aspect can alter the behavior of the base code by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches).

相似记录日志、身份验证、缓存等事务很是琐碎,与业务逻辑无关,不少地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫横向关注点(Cross-cutting concern),AOP做用就是分离横向关注点(Cross-cutting concern)来提升模块复用性,它能够在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。

接下来分析分析AOP的工做原理。

在上一篇中咱们分析过了,在objc_msgSend函数查找IMP的过程当中,若是在父类也没有找到相应的IMP,那么就会开始执行_class_resolveMethod方法,若是不是元类,就执行_class_resolveInstanceMethod,若是是元类,执行_class_resolveClassMethod。在这个方法中,容许开发者动态增长方法实现。这个阶段通常是给@dynamic属性变量提供动态方法的。

若是_class_resolveMethod没法处理,会开始选择备援接受者接受消息,这个时候就到了forwardingTargetForSelector方法。若是该方法返回非nil的对象,则使用该对象做为新的消息接收者。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(Method:)){
        return otherObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}复制代码

一样也能够替换类方法

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}复制代码

替换类方法返回值就是一个类对象。

forwardingTargetForSelector这种方法属于单纯的转发,没法对消息的参数和返回值进行处理。

最后到了完整转发阶段。

Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。为接下来的完整的消息转发生成一个 NSMethodSignature对象。NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就能够对 NSInvocation 进行处理了。

// 为目标对象中被调用的方法返回一个NSMethodSignature实例
#warning 运行时系统要求在执行标准转发时实现这个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.proxyTarget methodSignatureForSelector:sel];
}复制代码

对象须要建立一个NSInvocation对象,把消息调用的所有细节封装进去,包括selector, target, arguments 等参数,还可以对返回结果进行处理。

AOP的多数操做就是在forwardInvocation中完成的。通常会分为2个阶段,一个是Intercepter注册阶段,一个是Intercepter执行阶段。

1. Intercepter注册

首先会把类里面的某个要切片的方法的IMP加入到Aspect中,类方法里面若是有forwardingTargetForSelector:的IMP,也要加入到Aspect中。

而后对类的切片方法和forwardingTargetForSelector:的IMP进行替换。二者的IMP相应的替换为objc_msgForward()方法和hook过的forwardingTargetForSelector:。这样主要的Intercepter注册就完成了。

2. Intercepter执行

当执行func()方法的时候,会去查找它的IMP,如今它的IMP已经被咱们替换为了objc_msgForward()方法,因而开始查找备援转发对象。

查找备援接受者调用forwardingTargetForSelector:这个方法,因为这里是被咱们hook过的,因此IMP指向的是hook过的forwardingTargetForSelector:方法。这里咱们会返回Aspect的target,即选取Aspect做为备援接受者。

有了备援接受者以后,就会从新objc_msgSend,从消息发送阶段重头开始。

objc_msgSend找不到指定的IMP,再进行_class_resolveMethod,这里也没有找到,forwardingTargetForSelector:这里也不作处理,接着就会methodSignatureForSelector。在methodSignatureForSelector方法中建立一个NSInvocation对象,传递给最终的forwardInvocation方法。

Aspect里面的forwardInvocation方法会干全部切面的事情。这里转发逻辑就彻底由咱们自定义了。Intercepter注册的时候咱们也加入了原来方法中的method()和forwardingTargetForSelector:方法的IMP,这里咱们能够在forwardInvocation方法中去执行这些IMP。在执行这些IMP的先后均可以任意的插入任何IMP以达到切面的目的。

以上就是AOP的原理。

四. Isa Swizzling

前面第二点谈到了黑魔法Method Swizzling,本质上就是对IMP和SEL进行交换。其实接下来要说的Isa Swizzling,和它相似,本质上也是交换,不过交换的是Isa。

在苹果的官方库里面有一个颇有名的技术就用到了这个Isa Swizzling,那就是KVO——Key-Value Observing。

官方文档上对于KVO的定义是这样的:

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

官方给的就这么多,具体实现也没有说的很清楚。那只能咱们本身来实验一下。

KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,确定会调用其setter方法。因此KVO的本质就是监听对象有没有调用被监听属性对应的setter方法。具体实现应该是重写其setter方法便可。

官方是如何优雅的实现重写监听类的setter方法的呢?实验代码以下:

Student *stu = [[Student alloc]init];

    [stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];复制代码

咱们能够打印观察isa指针的指向

Printing description of stu->isa:
Student
Printing description of stu->isa:
NSKVONotifying_Student复制代码

经过打印,咱们能够很明显的看到,被观察的对象的isa变了,变成了NSKVONotifying_Student这个类了。

在@interface NSObject(NSKeyValueObserverRegistration) 这个分类里面,苹果定义了KVO的方法。

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;复制代码

KVO在调用addObserver方法以后,苹果的作法是在执行完addObserver: forKeyPath: options: context: 方法以后,把isa指向到另一个类去。

在这个新类里面重写被观察的对象四个方法。class,setter,dealloc,_isKVOA。

1. 重写class方法

重写class方法是为了咱们调用它的时候返回跟重写继承类以前一样的内容。

static NSArray * ClassMethodNames(Class c)
{
    NSMutableArray * array = [NSMutableArray array];
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }

    free(methodList);
    return array;
}

int main(int argc, char * argv[]) {

    Student *stu = [[Student alloc]init];

    NSLog(@"self->isa:%@",object_getClass(stu));
    NSLog(@"self class:%@",[stu class]);
    NSLog(@"ClassMethodNames = %@",ClassMethodNames(object_getClass(stu)));
    [stu addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

    NSLog(@"self->isa:%@",object_getClass(stu));
    NSLog(@"self class:%@",[stu class]);
    NSLog(@"ClassMethodNames = %@",ClassMethodNames(object_getClass(stu)));
}复制代码

打印结果

self->isa:Student
self class:Student
ClassMethodNames = (
".cxx_destruct",
name,
"setName:"
)

self->isa:NSKVONotifying_Student
self class:Student
ClassMethodNames = (
"setName:",
class,
dealloc,
"_isKVOA"
)复制代码

这里也能够看出,这是object_getClass方法和class方法的区别。

2. 重写setter方法

在新的类中会重写对应的set方法,是为了在set方法中增长另外两个方法的调用:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key复制代码

在didChangeValueForKey:方法再调用

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context复制代码

这里有几种状况须要说明一下:

1)若是使用了KVC 若是有访问器方法,则运行时会在setter方法中调用will/didChangeValueForKey:方法;

若是没用访问器方法,运行时会在setValue:forKey方法中调用will/didChangeValueForKey:方法。

因此这种状况下,KVO是奏效的。

2)有访问器方法 运行时会重写访问器方法调用will/didChangeValueForKey:方法。 所以,直接调用访问器方法改变属性值时,KVO也能监听到。

3)直接调用will/didChangeValueForKey:方法。

综上所述,只要setter中重写will/didChangeValueForKey:方法就可使用KVO了。

3. 重写dealloc方法

销毁新生成的NSKVONotifying_类。

4. 重写_isKVOA方法

这个私有方法估计多是用来标示该类是一个 KVO 机制声称的类。

Foundation 到底为咱们提供了哪些用于 KVO 的辅助函数。打开 terminal,使用 nm -a 命令查看 Foundation 中的信息:

nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation复制代码

里面包含了如下这些KVO中可能用到的函数:

00000000000233e7 t __NSSetDoubleValueAndNotify
00000000000f32ba t __NSSetFloatValueAndNotify
0000000000025025 t __NSSetIntValueAndNotify
000000000007fbb5 t __NSSetLongLongValueAndNotify
00000000000f33e8 t __NSSetLongValueAndNotify
000000000002d36c t __NSSetObjectValueAndNotify
0000000000024dc5 t __NSSetPointValueAndNotify
00000000000f39ba t __NSSetRangeValueAndNotify
00000000000f3aeb t __NSSetRectValueAndNotify
00000000000f3512 t __NSSetShortValueAndNotify
00000000000f3c2f t __NSSetSizeValueAndNotify
00000000000f363b t __NSSetUnsignedCharValueAndNotify
000000000006e91f t __NSSetUnsignedIntValueAndNotify
0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify
00000000000f3766 t __NSSetUnsignedLongValueAndNotify
00000000000f3890 t __NSSetUnsignedShortValueAndNotify
00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar
00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey复制代码

Foundation 提供了大部分基础数据类型的辅助函数(Objective C中的 Boolean 只是 unsigned char 的 typedef,因此包括了,但没有 C++中的 bool),此外还包括一些常见的结构体如 Point, Range, Rect, Size,这代表这些结构体也能够用于自动键值观察,但要注意除此以外的结构体就不能用于自动键值观察了。对于全部 Objective C 对象对应的是 __NSSetObjectValueAndNotify 方法。

KVO即便是苹果官方的实现,也是有缺陷的,这里有一篇文章详细了分析了KVO中的缺陷,主要问题在KVO的回调机制,不能传一个selector或者block做为回调,而必须重写-addObserver:forKeyPath:options:context:方法所引起的一系列问题。并且只监听一两个属性值还好,若是监听的属性多了, 或者监听了多个对象的属性, 那有点麻烦,须要在方法里面写不少的if-else的判断。

最后,官方文档上对于KVO的实现的最后,给出了须要咱们注意的一点是,永远不要用用isa来判断一个类的继承关系,而是应该用class方法来判断类的实例。

五. Associated Object 关联对象

Associated Objects是Objective-C 2.0中Runtime的特性之一。众所周知,在 Category 中,咱们没法添加@property,由于添加了@property以后并不会自动帮咱们生成实例变量以及存取方法。那么,咱们如今就能够经过关联对象来实如今 Category 中添加属性的功能了。

1. 用法

借用这篇经典文章Associated Objects里面的例子来讲明一下用法。

// NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end

// NSObject+AssociatedObject.m
@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));
}复制代码

这里涉及到了3个函数:

OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

OBJC_EXPORT void objc_removeAssociatedObjects(id object)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);复制代码

来讲明一下这些参数的意义:

1.id object 设置关联对象的实例对象

2.const void *key 区分不一样的关联对象的 key。这里会有3种写法。

使用 &AssociatedObjectKey 做为key值

static char AssociatedObjectKey = "AssociatedKey";复制代码

使用AssociatedKey 做为key值

static const void *AssociatedKey = "AssociatedKey";复制代码

使用@selector

@selector(associatedKey)复制代码

3种方法均可以,不过推荐使用更加简洁的第三种方式。

3.id value 关联的对象

4.objc_AssociationPolicy policy 关联对象的存储策略,它是一个枚举,与property的attribute 相对应。

Behavior @property Equivalent Description
OBJC_ASSOCIATION_ASSIGN @property (assign) / @property (unsafe_unretained) 弱引用关联对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 强引用关联对象,且为非原子操
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 复制关联对象,且为非原子操做
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 强引用关联对象,且为原子操做
OBJC_ASSOCIATION_COPY @property (atomic, copy) 复制关联对象,且为原子操做

这里须要注意的是标记成OBJC_ASSOCIATION_ASSIGN的关联对象和 @property (weak) 是不同的,上面表格中等价定义写的是 @property (unsafe_unretained),对象被销毁时,属性值仍然还在。若是以后再次使用该对象就会致使程序闪退。因此咱们在使用OBJC_ASSOCIATION_ASSIGN时,要格外注意。

According to the Deallocation Timeline described in WWDC 2011, Session 322(~36:00), associated objects are erased surprisingly late in the object lifecycle, inobject_dispose(), which is invoked by NSObject -dealloc.

关于关联对象还有一点须要说明的是objc_removeAssociatedObjects。这个方法是移除源对象中全部的关联对象,并非其中之一。因此其方法参数中也没有传入指定的key。要删除指定的关联对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 便可。

objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);复制代码

关联对象3种使用场景

1.为现有的类添加私有变量 2.为现有的类添加公有属性 3.为KVO建立一个关联的观察者。

2.源码分析
(一) objc_setAssociatedObject方法
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}复制代码

这个函数里面主要分为2部分,一部分是if里面对应的new_value不为nil的时候,另外一部分是else里面对应的new_value为nil的状况。

当new_value不为nil的时候,查找时候,流程以下:

首先在AssociationsManager的结构以下

class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }

    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};复制代码

在AssociationsManager中有一个spinlock类型的自旋锁lock。保证每次只有一个线程对AssociationsManager进行操做,保证线程安全。AssociationsHashMap对应的是一张哈希表。

AssociationsHashMap哈希表里面key是disguised_ptr_t。

disguised_ptr_t disguised_object = DISGUISE(object);复制代码

经过调用DISGUISE( )方法获取object地址的指针。拿到disguised_object后,经过这个key值,在AssociationsHashMap哈希表里面找到对应的value值。而这个value值ObjcAssociationMap表的首地址。

在ObjcAssociationMap表中,key值是set方法里面传过来的形参const void *key,value值是ObjcAssociation对象。

ObjcAssociation对象中存储了set方法最后两个参数,policy和value。

因此objc_setAssociatedObject方法中传的4个形参在上图中已经标出。

如今弄清楚结构以后再来看源码,就很容易了。objc_setAssociatedObject方法的目的就是在这2张哈希表中存储对应的键值对。

先初始化一个 AssociationsManager,获取惟一的保存关联对象的哈希表 AssociationsHashMap,而后在AssociationsHashMap里面去查找object地址的指针。

若是找到,就找到了第二张表ObjectAssociationMap。在这张表里继续查找object的key。

if (i != associations.end()) {
    // secondary table exists
    ObjectAssociationMap *refs = i->second;
    ObjectAssociationMap::iterator j = refs->find(key);
    if (j != refs->end()) {
        old_association = j->second;
        j->second = ObjcAssociation(policy, new_value);
    } else {
        (*refs)[key] = ObjcAssociation(policy, new_value);
    }
}复制代码

若是在第二张表ObjectAssociationMap找到对应的ObjcAssociation对象,那就更新它的值。若是没有找到,就新建一个ObjcAssociation对象,放入第二张表ObjectAssociationMap中。

再回到第一张表AssociationsHashMap中,若是没有找到对应的键值

ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();复制代码

此时就不存在第二张表ObjectAssociationMap了,这时就须要新建第二张ObjectAssociationMap表,来维护对象的全部新增属性。新建完第二张ObjectAssociationMap表以后,还须要再实例化 ObjcAssociation对象添加到 Map 中,调用setHasAssociatedObjects方法,代表当前对象含有关联对象。这里的setHasAssociatedObjects方法,改变的是isa_t结构体中的第二个标志位has_assoc的值。(关于isa_t结构体的结构,详情请看第一天的解析)

// release the old value (outside of the lock).
 if (old_association.hasValue()) ReleaseValue()(old_association);复制代码

最后若是老的association对象有值,此时还会释放它。

以上是new_value不为nil的状况。其实只要记住上面那2张表的结构,这个objc_setAssociatedObject的过程就是更新 / 新建 表中键值对的过程。

再来看看new_value为nil的状况

// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i !=  associations.end()) {
    ObjectAssociationMap *refs = i->second;
    ObjectAssociationMap::iterator j = refs->find(key);
    if (j != refs->end()) {
        old_association = j->second;
        refs->erase(j);
    }
}复制代码

当new_value为nil的时候,就是咱们要移除关联对象的时候。这个时候就是在两张表中找到对应的键值,并调用erase( )方法,便可删除对应的关联对象。

(二) objc_getAssociatedObject方法
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}复制代码

objc_getAssociatedObject方法 很简单。就是经过遍历AssociationsHashMap哈希表 和 ObjcAssociationMap表的全部键值找到对应的ObjcAssociation对象,找到了就返回ObjcAssociation对象,没有找到就返回nil。

(三) objc_removeAssociatedObjects方法
void objc_removeAssociatedObjects(id object) {
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}复制代码

在移除关联对象object的时候,会先去判断object的isa_t中的第二位has_assoc的值,当object 存在而且object->hasAssociatedObjects( )值为1的时候,才会去调用_object_remove_assocations方法。

_object_remove_assocations方法的目的是删除第二张ObjcAssociationMap表,即删除全部的关联对象。删除第二张表,就须要在第一张AssociationsHashMap表中遍历查找。这里会把第二张ObjcAssociationMap表中全部的ObjcAssociation对象都存到一个数组elements里面,而后调用associations.erase( )删除第二张表。最后再遍历elements数组,把ObjcAssociation对象依次释放。

以上就是Associated Object关联对象3个函数的源码分析。

六.动态的增长方法

在消息发送阶段,若是在父类中也没有找到相应的IMP,就会执行resolveInstanceMethod方法。在这个方法里面,咱们能够动态的给类对象或者实例对象动态的增长方法。

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"method1"]) {
        class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
    }

    return [super resolveInstanceMethod:sel];
}复制代码

关于方法操做方面的函数还有如下这些

// 调用指定方法的实现
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 经过引用返回方法的返回值类型字符串
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 经过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );复制代码

这些方法其实平时不须要死记硬背,使用的时候只要先打出method开头,后面就会有补全信息,找到相应的方法,传入对应的方法便可。

七.NSCoding的自动归档和自动解档

如今虽然手写归档和解档的时候很少了,可是自动操做仍是用Runtime来实现的。

- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
}

- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}复制代码

手动的有一个缺陷,若是属性多起来,要写好多行类似的代码,虽然功能是能够完美实现,可是看上去不是很优雅。

用runtime实现的思路就比较简单,咱们循环依次找到每一个成员变量的名称,而后利用KVC读取和赋值就能够完成encodeWithCoder和initWithCoder了。

#import "Student.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Student

- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar var = vars[i];
        const char *name = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:name];

        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}
@end复制代码

class_copyIvarList方法用来获取当前 Model 的全部成员变量,ivar_getName方法用来获取每一个成员变量的名称。

八.字典和模型互相转换

1.字典转模型

1.调用 class_getProperty 方法获取当前 Model 的全部属性。 2.调用 property_copyAttributeList 获取属性列表。 3.根据属性名称生成 setter 方法。 4.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC)

+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
    id objc = [[self alloc] init];
    for (NSString *key in aDictionary.allKeys) {
        id value = aDictionary[key];

        /*判断当前属性是否是Model*/
        objc_property_t property = class_getProperty(self, key.UTF8String);
        unsigned int outCount = 0;
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
        objc_property_attribute_t attribute = attributeList[0];
        NSString *typeString = [NSString stringWithUTF8String:attribute.value];

        if ([typeString isEqualToString:@"@\"Student\""]) {
            value = [self objectWithKeyValues:value];
        }

        //生成setter方法,并用objc_msgSend调用
        NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
        SEL setter = sel_registerName(methodName.UTF8String);
        if ([objc respondsToSelector:setter]) {
            ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
        }
        free(attributeList);
    }
    return objc;
}复制代码

这段代码里面有一处判断typeString的,这里判断是防止model嵌套,好比说Student里面还有一层Student,那么这里就须要再次转换一次,固然这里有几层就须要转换几回。

几个出名的开源库JSONModel、MJExtension等都是经过这种方式实现的(利用runtime的class_copyIvarList获取属性数组,遍历模型对象的全部成员属性,根据属性名找到字典中key值进行赋值,固然这种方法只能解决NSString、NSNumber等,若是含有NSArray或NSDictionary,还要进行第二步转换,若是是字典数组,须要遍历数组中的字典,利用objectWithDict方法将字典转化为模型,在将模型放到数组中,最后把这个模型数组赋值给以前的字典数组)

2.模型转字典

这里是上一部分字典转模型的逆步骤:

1.调用 class_copyPropertyList 方法获取当前 Model 的全部属性。 2.调用 property_getName 获取属性名称。 3.根据属性名称生成 getter 方法。 4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC)

//模型转字典
-(NSDictionary *)keyValuesWithObject{
    unsigned int outCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (int i = 0; i < outCount; i ++) {
        objc_property_t property = propertyList[i];

        //生成getter方法,并用objc_msgSend调用
        const char *propertyName = property_getName(property);
        SEL getter = sel_registerName(propertyName);
        if ([self respondsToSelector:getter]) {
            id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);

            /*判断当前属性是否是Model*/
            if ([value isKindOfClass:[self class]] && value) {
                value = [value keyValuesWithObject];
            }

            if (value) {
                NSString *key = [NSString stringWithUTF8String:propertyName];
                [dict setObject:value forKey:key];
            }
        }

    }
    free(propertyList);
    return dict;
}复制代码

中间注释那里的判断也是防止model嵌套,若是model里面还有一层model,那么model转字典的时候还须要再次转换,一样,有几层就须要转换几回。

不过上述的作法是假设字典里面再也不包含二级字典,若是还包含数组,数组里面再包含字典,那还须要多级转换。这里有一个关于字典里面包含数组的demo.

九.Runtime缺点

看了上面八大点以后,是否是感受Runtime很神奇,能够迅速解决不少问题,然而,Runtime就像一把瑞士小刀,若是使用得当,它会有效地解决问题。但使用不当,将带来不少麻烦。在stackoverflow上有人已经提出这样一个问题:What are the Dangers of Method Swizzling in Objective C?,它的危险性主要体现如下几个方面:

  • Method swizzling is not atomic

Method swizzling不是原子性操做。若是在+load方法里面写,是没有问题的,可是若是写在+initialize方法中就会出现一些奇怪的问题。

  • Changes behavior of un-owned code

若是你在一个类中重写一个方法,而且不调用super方法,你可能会致使一些问题出现。在大多数状况下,super方法是指望被调用的(除非有特殊说明)。若是你使用一样的思想来进行Swizzling,可能就会引发不少问题。若是你不调用原始的方法实现,那么你Swizzling改变的太多了,而致使整个程序变得不安全。

  • Possible naming conflicts

命名冲突是程序开发中常常遇到的一个问题。咱们常常在类别中的前缀类名称和方法名称。不幸的是,命名冲突是在咱们程序中的像一种瘟疫。通常咱们会这样写Method Swizzling

@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end

@implementation NSView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end复制代码

这样写看上去是没有问题的。可是若是在整个大型程序中还有另一处定义了my_setFrame:方法呢?那又会形成命名冲突的问题。咱们应该把上面的Swizzling改为如下这种样子:

@implementation NSView (MyViewAdditions)

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end复制代码

虽然上面的代码看上去不是OC(由于使用了函数指针),可是这种作法确实有效的防止了命名冲突的问题。原则上来讲,其实上述作法更加符合标准化的Swizzling。这种作法可能和人们使用方法不一样,可是这种作法更好。Swizzling Method 标准定义应该是以下的样子:

typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end复制代码
  • Swizzling changes the method's arguments

这一点是这些问题中最大的一个。标准的Method Swizzling是不会改变方法参数的。使用Swizzling中,会改变传递给原来的一个函数实现的参数,例如:

[self my_setFrame:frame];复制代码

会变转换成

objc_msgSend(self, @selector(my_setFrame:), frame);复制代码

objc_msgSend会去查找my_setFrame对应的IMP。一旦IMP找到,会把相同的参数传递进去。这里会找到最原始的setFrame:方法,调用执行它。可是这里的_cmd参数并非setFrame:,如今是my_setFrame:。原始的方法就被一个它不期待的接收参数调用了。这样并很差。

这里有一个简单的解决办法,上一条里面所说的,用函数指针去实现。参数就不会变了。

  • The order of swizzles matters

调用顺序对于Swizzling来讲,很重要。假设setFrame:方法仅仅被定义在NSView类里面。

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];复制代码

当NSButton被swizzled以后会发生什么呢?大多数的swizzling应该保证不会替换setFrame:方法。由于一旦改了这个方法,会影响下面全部的View。因此它会去拉取实例方法。NSButton会使用已经存在的方法去从新定义setFrame:方法。以致于改变了IMP实现不会影响全部的View。相同的事情也会发生在对NSControl进行swizzling的时候,一样,IMP也是定义在NSView类里面,把NSControl 和 NSButton这上下两行swizzle顺序替换,结果也是相同的。

当调用NSButton的setFrame:方法,会去调用swizzled method,而后会跳入NSView类里面定义的setFrame:方法。NSControl 和 NSView对应的swizzled method不会被调用。

NSButton 和 NSControl各自调用各自的 swizzling方法,相互不会影响。

可是咱们改变一下调用顺序,把NSView放在第一位调用。

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];复制代码

一旦这里的NSView先进行了swizzling了之后,状况就和上面大不相同了。NSControl的swizzling会去拉取NSView替换后的方法。相应的,NSControl在NSButton前面,NSButton也会去拉取到NSControl替换后的方法。这样就十分混乱了。可是顺序就是这样排列的。咱们开发中如何能保证不出现这种混乱呢?

再者,在load方法中加载swizzle。若是仅仅是在已经加载完成的class中作了swizzle,那么这样作是安全的。load方法能保证父类会在其任何子类加载方法以前,加载相应的方法。这就保证了咱们调用顺序的正确性。

  • Difficult to understand (looks recursive)

看着传统定义的swizzled method,我认为很难去预测会发生什么。可是对比上面标准的swizzling,仍是很容易明白。这一点已经被解决了。

  • Difficult to debug

在调试中,会出现奇怪的堆栈调用信息,尤为是swizzled的命名很混乱,一切方法调用都是混乱的。对比标准的swizzled方式,你会在堆栈中看到清晰的命名方法。swizzling还有一个比较难调试的一点, 在于你很难记住当前确切的哪一个方法已经被swizzling了。

在代码里面写好文档注释,即便你认为这段代码只有你一我的会看。遵循这个方式去实践,你的代码都会没问题。它的调试也没有多线程的调试困难。

最后

通过在“神经病院”3天的修炼以后,对OC 的Runtime理解更深了。

关于黑魔法Method swizzling,我我的以为若是使用得当,仍是很安全的。一个简单而安全的措施是你仅仅只在load方法中去swizzle。和编程中不少事情同样,不了解它的时候会很危险可怕,可是一旦明白了它的原理以后,使用它又会变得很是正确高效。

对于多人开发,尤为是改动过Runtime的地方,文档记录必定要完整。若是某人不知道某个方法被Swizzling了,出现问题调试起来,十分蛋疼。

若是是SDK开发,某些Swizzling会改变全局的一些方法的时候,必定要在文档里面标注清楚,不然使用SDK的人不知道,出现各类奇怪的问题,又要被坑很久。

在合理使用 + 文档完整齐全 的状况下,解决特定问题,使用Runtime仍是很是简洁安全的。

平常可能用的比较多的Runtime函数可能就是下面这些

//获取cls类对象全部成员ivar结构体
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//获取cls类对象name对应的实例方法结构体
Method class_getInstanceMethod(Class cls, SEL name)
//获取cls类对象name对应类方法结构体
Method class_getClassMethod(Class cls, SEL name)
//获取cls类对象name对应方法imp实现
IMP class_getMethodImplementation(Class cls, SEL name)
//测试cls对应的实例是否响应sel对应的方法
BOOL class_respondsToSelector(Class cls, SEL sel)
//获取cls对应方法列表
Method *class_copyMethodList(Class cls, unsigned int *outCount)
//测试cls是否遵照protocol协议
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
//为cls类对象添加新方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
//替换cls类对象中name对应方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
//为cls添加新成员
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
//为cls添加新属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
//获取m对应的选择器
SEL method_getName(Method m)
//获取m对应的方法实现的imp指针
IMP method_getImplementation(Method m)
//获取m方法的对应编码
const char *method_getTypeEncoding(Method m)
//获取m方法参数的个数
unsigned int method_getNumberOfArguments(Method m)
//copy方法返回值类型
char *method_copyReturnType(Method m)
//获取m方法index索引参数的类型
char *method_copyArgumentType(Method m, unsigned int index)
//获取m方法返回值类型
void method_getReturnType(Method m, char *dst, size_t dst_len)
//获取方法的参数类型
void method_getArgumentType(Method m, unsigned int index, char *dst, size_t dst_len)
//设置m方法的具体实现指针
IMP method_setImplementation(Method m, IMP imp)
//交换m1,m2方法对应具体实现的函数指针
void method_exchangeImplementations(Method m1, Method m2)
//获取v的名称
const char *ivar_getName(Ivar v)
//获取v的类型编码
const char *ivar_getTypeEncoding(Ivar v)
//设置object对象关联的对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取object关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除object关联的对象
void objc_removeAssociatedObjects(id object)复制代码

这些API看上去很差记,其实使用的时候不难,关于方法操做的,通常都是method开头,关于类的,通常都是class开头的,其余的基本都是objc开头的,剩下的就看代码补全的提示,看方法名基本就能找到想要的方法了。固然很熟悉的话,能够直接打出指定方法,也不会依赖代码补全。

还有一些关于协议相关的API以及其余一些不经常使用,可是也可能用到的,就须要查看Objective-C Runtime官方API文档,这个官方文档里面详细说明,平时不懂的多看看文档。

最后请你们多多指教。

Ps.这篇干货有点多,还好都写完了。顺利出院了!

相关文章
相关标签/搜索