iOS-关联(objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects)

 一.关联

 关联是指通过key把两个对象互相关联起来,使得其中一个对象为另一个对象的一部分;

 使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用;

 关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(在垃圾自动回收环境下也不会导致资源不可回收)。

 举个例子:xiaoming是一个Person类,现在我们新建一个dog对象,通过一个绳子,让xiaoming牵着dog,这样他们之间就有了关联,当然xiaoming可以牵着多条狗,也就是关联多个对象,我们如可区分这些关联,我们就可以给它命名,这就是关联时候定义的key

 二.关联的方法

 1.objc_setAssociatedObject

 创建关联的方法,方法全名如下:

 objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

 id object                     :表示关联者,是一个对象,变量名理所当然也是object

 const void *key               :获取被关联者的索引key

 id value                      :被关联者,这里是一个block

 objc_AssociationPolicy policy :关联时采用的协议,有assignretaincopy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC

 

 2.objc_getAssociatedObject

 获取相关联的对象,通过创建的时候设置的key来获取,方法全名如下:

 objc_getAssociatedObject(id object, const void *key)

 id object                     :表示关联者,是一个对象,变量名理所当然也是object

 const void *key               :获取被关联者的索引key

 

 3.objc_removeAssociatedObjects

 用于移除一个对象的所有关联对象,方法全名如下:

 objc_removeAssociatedObjects(id object)

 id object                     :表示关联者,是一个对象,变量名理所当然也是object

 说明:objc_removeAssociatedObjects 函数我们一般是用不上的,因为这个函数会移除一个对象的所有关联对象,将该对象恢复成原始状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil 来移除某个已有的关联对象。示例如下:

 objc_setAssociatedObject(array, &overviewKey, nil, OBJC_ASSOCIATION_ASSIGN);

 三.关联的应用

 1.添加公共属性

 我们常常给一个类添加属性,但出于某些原因,我们需要给这个类添加动态的属性,这样我们就不能通过分类来实现,那么我们就可以通过runtime来实现这个需求了。

示例,我们给一个Person添加属性,代码如下:

Person类代码

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (assign, nonatomic) NSInteger age;

- (void)run;

@end
#import "Person.h"

@implementation Person

- (void)run{
    NSLog(@"age == %ld",_age);
    NSLog(@"%s",__func__);
}

@end
Person类别代码
#import "Person.h"

@interface Person (PersonExtention)

@property (copy, nonatomic) NSString *name;

- (void)song;

@end
#import "Person+PersonExtention.h"
#import <objc/runtime.h>

@implementation Person (PersonExtention)

static char *PersonNameKey = "PersonNameKey";

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, PersonNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name{
    return objc_getAssociatedObject(self, PersonNameKey);
}

- (void)song{
    NSLog(@"name = %@",self.name);
    NSLog(@"%s",__func__);
}

@end
调用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *xiaoming = [[Person alloc]init];
    
    xiaoming.age = 12;
    xiaoming.name = @"xiaoming";
    
    [xiaoming run];
    [xiaoming song];
}
打印结果:

2017-05-12 18:07:42.643 OGJC[1932:138348] age == 12
2017-05-12 18:07:42.644 OGJC[1932:138348] -[Person run]
2017-05-12 18:07:42.644 OGJC[1932:138348] name = xiaoming
2017-05-12 18:07:42.644 OGJC[1932:138348] -[Person(PersonExtention) song]

 2.添加私有成员变量

 应用举例,当我们使用UIAlertView的时候,需要遵循UIAlertView的代理方法,但是当我们一个界面有多个UIAlertView的时候,我们在代理方法里面就需要判断了,或者设置tag值了(本人表示不喜欢设置tag值),然后去处理响应的逻辑,我们要是能把响应逻辑和UIAlertView的创建代码写一块就好了,这样便于我们查看和修改,不容易搞混这样就需要管理来实现了,这样就简单明了了。

 代码如下:

_alertView = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"This is deprecated?" 
delegate:self 
cancelButtonTitle:@"Cancel" 
otherButtonTitles:@"Ok", nil];
    void (^block)(NSInteger) = ^(NSInteger buttonIndex){
        if (buttonIndex == 0) {
            [self doCancel];
        } else {
            [self doOk];
        }
    };
    objc_setAssociatedObject(self.alertView, MyAlertViewKey, block, OBJC_ASSOCIATION_COPY);

#pragma -mark UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    void (^block)(NSInteger) = objc_getAssociatedObject(alertView, MyAlertViewKey);
    block(buttonIndex);
}
这里我们可以举一反三,我们可以用在UIButton上,这里大家可以见我的另一篇博客,讲述关于UIButton的关联的,博客地址如下: 

iOS-OC-Runtime使用小谈


3.实现KVO

如果没找到理想的,就自己动手做一个。既然我们对官方的 API 不太满意,又知道如何去实现一个 KVO,那就尝试自己动手写一个简易的 KVO 玩玩。

 首先,我们创建 NSObject Category,并在头文件中添加两个 API:

typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block;

- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

接下来,实现 PG_addObserver:forKey:withBlock: 方法。逻辑并不复杂:

 检查对象的类有没有相应的 setter 方法。如果没有抛出异常;

 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;

 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;

 添加这个观察者

- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block
{
    // Step 1: Throw exception if its class or superclasses doesn't implement the setter
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        // throw invalid argument exception
    }

    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);

    // Step 2: Make KVO class if this is first time adding observer and 
    //          its class is not an KVO class yet
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }

    // Step 3: Add our kvo setter method if its class (not superclasses) 
    //          hasn't implemented the setter
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }

    // Step 4: Add this observation info to saved observation objects
    PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

再来一步一步细看。

 第一步里,先通过 setterForGetter() 方法获得相应的 setter 的名字(SEL)。也就是把 key 的首字母大写,然后前面加上 set 后面加上 :,这样 key 就变成了 setKey:。然后再用 class_getInstanceMethod 去获得 setKey: 的实现(Method)。如果没有,自然要抛出异常。

 第二步,我们先看类名有没有我们定义的前缀。如果没有,我们就去创建新的子类,并通过 object_setClass() 修改 isa 指针。

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);

    if (clazz) {
        return clazz;
    }

    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);

    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);

    objc_registerClassPair(kvoClazz);

    return kvoClazz;
}

动态创建新的类需要用 objc/runtime.h 中定义的 objc_allocateClassPair() 函数。传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个类。然后就给这个类添加方法,也可以添加变量。这里,我们只重写了 class 方法。哈哈,跟 Apple 一样,这时候我们也企图隐藏这个子类的存在。最后 objc_registerClassPair() 告诉 Runtime 这个类的存在。

 

 第三步,重写 setter 方法。新的 setter 在调用原 setter 方法后,通知每个观察者(调用之前传入的 block ):

static void kvo_setter(id self, SEL _cmd, id newValue)  
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);

    if (!getterName) {
        // throw invalid argument exception
    }

    id oldValue = [self valueForKey:getterName];

    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };

    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;

    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);

    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    for (PGObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

细心的同学会发现我们对 objc_msgSendSuper 进行类型转换。在 Xcode 6 里,新的 LLVM 会对 objc_msgSendSuper 以及 objc_msgSend 做严格的类型检查,如果不做类型转换。Xcode 会抱怨有 too many arguments 的错误。(在 WWDC 2014 的视频 What new in LLVM 中有提到过这个问题。)

 

 最后一步,把这个观察的相关信息存在 associatedObject 里。观察的相关信息(观察者,被观察的 key, 和传入的 block )封装在 PGObservationInfo 类里。

@interface PGObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PGObservingBlock block;

@end

就此,一个基本的 KVO 就可以 work 了。当然,这只是一个一天多做出来的小东西,会有 bug,也有很多可以优化完善的地方。但作为 demo 演示如何利用 Runtime 动态创建类、如何实现 KVO,足已。


说明:

1.KVO实现引用:如何自己动手实现 KVO
2.DEMO下载:http://download.csdn.net/detail/u014220518/9841044

3.转载请注明转自:http://blog.csdn.net/u014220518/article/details/71750875
















已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页