iOS KVO 自己實(shí)現(xiàn)(利用Runtime)

一、KVO 實(shí)現(xiàn)機(jī)制

Apple 的文檔有簡(jiǎn)單提到過 KVO 的實(shí)現(xiàn)


Key-Value Observing Implementation Details

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.

  • 被觀察對(duì)象的 isa 指針會(huì)指向一個(gè)中間類,而不是原來真正的類
  • Mike Ash 早在 2009 年就做了這么個(gè)探究
  1. 觀察一個(gè)對(duì)象時(shí),一個(gè)新的類會(huì)動(dòng)態(tài)被創(chuàng)建;
  2. 這個(gè)類繼承自該對(duì)象的原本的類,并重寫了被觀察屬性的 setter 方法;
  3. 重寫的 setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后,通知所有觀察對(duì)象值的更改;
  4. 把這個(gè)對(duì)象的 isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個(gè)對(duì)象的類是什么 ) 指向這個(gè)新創(chuàng)建的子類;
  5. 完成新創(chuàng)建的子類的實(shí)例。

二、KVO 缺陷

  • 比如,你只能通過重寫 -observeValueForKeyPath:ofObject:change:context: 方法來獲得通知;
  • 想要提供自定義的 selector ,不行;
  • 想要傳一個(gè) block ,不行;
  • 父類同樣監(jiān)聽同一個(gè)對(duì)象的同一個(gè)屬性 ->還要處理父類的情況 ;
  • 有時(shí)候,不知道父類是不是對(duì)這個(gè)消息有興趣。-addObserver:forKeyPath:options:context: 傳進(jìn)去一個(gè)父類不知道的 context

注:有不少人都覺得官方 KVO 不好使的。

  1. Mike Ash 的 Key-Value Observing Done Right;
  2. 以及獲得不少分享討論的 KVO Considered Harmful
  3. 所以在實(shí)際開發(fā)中 KVO 使用的情景并不多,更多時(shí)候還是用 Delegate 或 NotificationCenter。

三、自己實(shí)現(xiàn) KVO

  • 1、創(chuàng)建 NSObject 的 Category,并在頭文件中添加兩個(gè) API:
- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block
{
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        
        return;
    }
    
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    
    // if not an KVO class yet
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    // add our kvo setter if this class (not superclasses) doesn't implement the setter?
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
    
    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];
}
  1. 檢查對(duì)象的類有沒有相應(yīng)的 setter 方法。如果沒有拋出異常;
  2. 檢查對(duì)象 isa 指向的類是不是一個(gè) KVO 類。如果不是,新建一個(gè)繼承原來類的子類,并把 isa 指向這個(gè)新建的子類;
  3. 檢查對(duì)象的 KVO 類重寫過沒有這個(gè) setter 方法。如果沒有,添加重寫的 setter 方法;
  4. 添加這個(gè)觀察者
  • 通過 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;
}
  • 重寫 setter 方法。新的 setter 在調(diào)用原 setter 方法后,通知每個(gè)觀察者(調(diào)用之前傳入的 block )
static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
        return;
    }
    
    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);
            });
        }
    }
}
  • 觀察的相關(guān)信息存在 associatedObject 里。觀察的相關(guān)信息(觀察者,被觀察的 key, 和傳入的 block )封裝在 PGObservationInfo 類里
@interface PGObservationInfo : NSObject

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

@end

參考:
完整的例子可以從這里下載:ImplementKVO
KVO Implementation

Creating Classes at Runtime in Objective-C

Key-Value Observing Done Right

By your command

Associated Objects

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 到祖國(guó)的首都-北京,若不是借JM比賽的光,相必我也不會(huì)獨(dú)獨(dú)來一趟。 “第九屆盛世中華中國(guó)青少年藝術(shù)人才選拔活動(dòng)全國(guó)...
    白卉閱讀 344評(píng)論 4 4
  • 茶園小坐茗柯香,品至怡然潤(rùn)肺腸。滌俗何須是龍井,平陽天韻有黃湯。 2018年3月13日,松柏先生受邀到浙江子久文化...
    松柏先生閱讀 542評(píng)論 0 0
  • 2017-01-13- 2017-01-23 卡住了……卡住……了……卡……住……了……用這篇文章調(diào)一下姿勢(shì),繼續(xù)...
    ChatGPTplugin閱讀 1,288評(píng)論 2 2
  • 工作區(qū)域是辦公空間設(shè)計(jì)的核心部分。這里是個(gè)人能力發(fā)揮、團(tuán)隊(duì)協(xié)調(diào)合作的重要生產(chǎn)空間??臻g形式以開敞式為主,依據(jù)工作種...
    alsk35閱讀 1,290評(píng)論 0 0
  • 怎么說呢,人和人的相處真的是很奇妙,本來不怎么相干的兩個(gè)人因?yàn)橐粋€(gè)同樣的興趣愛好成了好朋友,愿意和對(duì)方說說心里話,...
    柚子樹檸檬草閱讀 200評(píng)論 1 0

友情鏈接更多精彩內(nèi)容