iOS-底层探索20:自定义KVO

iOS 底层探索 文章汇总

目录


一、自定义KVO流程

下面通过简单自定义KVO实现系统类似的功能,加深对KVO底层实现逻辑的理解。

目标如下:
  1. 模拟系统的KVO功能
  2. 实现自动移除观察者
  3. 响应式+函数式方式整合KVO使用代码
实现步骤:
  1. 验证是否存在setter方法:观察属性,不观察实例变量、成员变量
  2. 动态生成子类
    2.1 申请类(申请类之后、注册类之前可以添加ivar
    2.2 注册类
  3. 修改 isa 指向
  4. 添加父类setter同名方法
    • 可以添加方法:注册类后新增加的方法存在rwe中(dirty memory)
    • 不能添加属性:ivar只存在ro中(clean memory),因为类加载完毕后其内存大小已经确定
  5. 调用父类 setter
  6. 观察者响应KVO通知


二、自定义KVO实现

NAPerson:用于观察该对象中的属性
@interface NAPerson : NSObject {

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSMutableArray *dataArray;

@end
NAKVOInfo:保存信息
typedef void (^LCJKVOBlock)(NSObject *observer, NSString *keyPath, id oldValue, id newValue);

@interface NAKVOInfo : NSObject

@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, assign)  void *context;
@property (nonatomic, copy) LCJKVOBlock handleBlock;

- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block;

@end

//NAKVOInfo.m
- (instancetype)initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.context  = context;
        self.handleBlock = block;
    }
    return self;
}
NSObject+LCJKVO:通过分类提供添加观察者的方法
@interface NSObject (LCJKVO)

- (void)lcj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block;

@end

//NSObject+LCJKVO.m
static NSString *const kLCJKVOPrefix        = @"LCJKVONotifying_";
static NSString *const kLCJKVOAssiociateKey = @"LCJKVO_AssiociateKey";

@implementation NSObject (LCJKVO)

- (void)lcj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context handleBlock:(LCJKVOBlock)block {
    //1. 验证是否存在setter方法
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    NAKVOInfo *info = [[NAKVOInfo alloc] initWithObserver:observer forKeyPath:keyPath context:context handleBlock:block];
    
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLCJKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        // 保存观察者(分类中主要使用关联对象保存变量信息)(TODO:如果有这个Key就不添加了)
        // 这里不会存在引用关系,更不会出现循环引用
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLCJKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
    
    //2. 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    
    //3. 修改 isa 指向
    object_setClass(self, newClass);
    
    //4. 添加父类setter同名方法 - 不能添加属性:ivar存在ro中(clean memory),因为类加载完毕后其内存大小已经确定
    //                         可以添加方法:方法存在dirty memory
    SEL setterSel = NSSelectorFromString(setterFromGetter(keyPath));
    Method method = class_getInstanceMethod([self class], setterSel);
    const char *type = method_getTypeEncoding(method);
    class_addMethod(newClass, setterSel, (IMP)lcj_setter, type);
}

#pragma mark - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterFromGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有%@的setter方法",keyPath] userInfo:nil];
    }
}

#pragma mark - 创建KVO中间子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLCJKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
    //  2.1 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //  2.2 注册类
    objc_registerClassPair(newClass);
    
    // 添加父类class同名方法
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)lcj_class, classTypes);
    
    // 添加父类dealloc同名方法
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)lcj_dealloc, deallocTypes);
    
    return newClass;
}

// 子类重写的setter imp
static void lcj_setter(id self,SEL _cmd,id newValue) {
    CJLog(@"进入了子类重写的setter方法:%@",newValue);
    NSString *keyPath = getteFromSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    //5. 调用父类 setter
    // 封装objc_msgSendSuper方法
    void (*lcj_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),//= [self class]
    };
    lcj_msgSendSuper(&superStruct, _cmd, newValue);
    
    // TODO:实现KVO自动开关
    
    //6. 观察者响应KVO通知
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLCJKVOAssiociateKey));
    for (NAKVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {//TODO 暂未考虑context
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterFromGetter(NSString *getter) {
    if (getter.length <= 0) { return nil;}
    
    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

// 重写的class方法
Class lcj_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

// 重写的dealloc方法
static void lcj_dealloc(id self,SEL _cmd) {
    // isa 指回父类
    Class superClass = [self class];//已重写了这个方法
    object_setClass(self, superClass);
}

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
static NSString *getteFromSetter(NSString *setter) {
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

@end
ViewController:使用自定义KVO
- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [NAPerson new];
    [self.person lcj_addObserver:self forKeyPath:@"name" context:NULL handleBlock:^(NSObject * _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
        NSLog(@"%@-%@",oldValue,newValue);
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = @"differ";
}


三、扩展一:KVOController使用

#import "TestViewController.h"
#import "NAStudent.h"
#import <FBKVOController.h>

@interface TestViewController ()
@property (nonatomic, strong) NAPerson *person;
@property (nonatomic, strong) FBKVOController *kvoCtrl;
@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [NAPerson new];
    self.person.dataArray = [NSMutableArray arrayWithObject:@"1"];

    self.kvoCtrl = [FBKVOController controllerWithObserver:self];
    [self.kvoCtrl observe:self.person keyPath:@"name" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
    }];

    //集合类型的观察
    [self.kvoCtrl observe:self.person keyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        NSLog(@"****%@****",change);
    }];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = @"differ";
    [[self.person mutableArrayValueForKey:@"dataArray"] addObject:self.person.name];
}

@end

使用KVOController也不需要手动移除观察者,进入其底层代码可看到对kvoCtrl做了弱引用:__weak FBKVOController *_controller;

KVOController流程图如下:


四、扩展一:GNU

由于Apple Foundation层源码不开源导致不清楚底层的实现逻辑,而GNU被认为是Foundation层最接近的源码实现。GNU下载地址 下载GNUstep Base

进入KVO原生添加观察者方法:- (void)addObserver:forKeyPath:options:context:
可以看到是在NSObject(NSKeyValueObserverRegistration)分类中。
GNU项目中搜索NSKeyValueObserverRegistration即可看到KVOGNU源码。


参考

Facebook开源KVO三方框架:KVOController

Advancements in the Objective-C runtime

ro、rw、rwe介绍:iOS-底层探索13:分类的加载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容