iOS中OC语言特性观察

标签: 编程学习 iOS学习
KVO

又叫键值观察,可以⽤于监听某个对象属性值的改变 基本使用

@interface KVOController ()
@property(nonatomic, strong) TestModel *testModel;
@end

@implementation KVOController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];

    self.testModel = [TestModel new];
    self.testModel.title = @"Hello";

    [self.testModel addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"------ %@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.testModel.title = @"World";
}

- (void)dealloc {
    //注意最后移除监听
    [self.testModel removeObserver:self forKeyPath:@"title"];
}

@end

使用 setter 方法改变值 kvo 才会生效,使用 setValue:forKey: 改变值 kvo 也会生效,因为内部使用的是 setter 方法,成员变量直接修改值的话需要手动调用 kvo 才会生效

/// 手动触发kvo
[self.testModel willChangeValueForKey:@"title"];

//在这里直接修改值

[self.testModel didChangeValueForKey:@"title"];

原理是当设置KVO监听的时候,系统利用 Runtime API动态生成一个子类 NSKVONotifying_(原类名),重写了set与get方法,添加了willChangeValueForKeydidChangeValueForKey,并且让实例对象的isa指向这个全新的子类,当修改实例对象的属性时,会调用 Foundation 的 _NSSet * ValueAndNotify函数,内部会触发监听器的监听方法。我们可以使用 object_getClass 去获取这个系统生成的中间类。

KVC

又称键值编码,可以通过⼀个key来访问某个属性,常用的 API 如下

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;

基本使用

@implementation KVCController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];

    KvcModel *kvcModel = [[KvcModel alloc] init];

    [kvcModel setValue:@"标题" forKey:@"title"];
//    [kvcModel setValue:@"test" forKeyPath:@"cat.name"]; // 可以访问下一层级

    NSLog(@"--> %@", [kvcModel valueForKey:@"title"]);
}
@end

使用KVC是会触发KVO的。这里有一个重点,就是KVC设值和取值的过程 setValue:forKey: 的过程

调用 setValue:forKey: 的时候会先按照 setKey: -> _setKey 的顺序查找,如果没有找到方法,则会查看 accessInstanceVariablesDirectly 的值,如果返回的是NO,则按照没有找到成员变量处理,直接调用 setValue:forUndefinedKey: 并抛出异常,如返回的是YES(其实accessInstanceVariablesDirectly⽅法的默认返回值就是YES),则会按照 _key -> _isKey -> key -> isKey 的顺序额去查找成员变量,如果找不到就抛出异常。

valueForKey:的过程

调用 valueForKey: 的时候会先按照 getKey -> key -> isKey -> _key 的顺序查找方法,如果没有找到还是看 accessInstanceVariablesDirectly 的值,步骤与上面设值类似,如果是 YES 则按照 _key -> _isKey -> key -> isKey 的顺序去查找,如果找不到成员变量就调用 valueForUndefinedKey: 并抛出异常。

分类

我们开发的时候都会用到分类Category,所有分类的方法最终都会通过 Runtime 动态的将方法合并到类对象或元类对象中去。 作用: ① 声明私有方法; ② 分解庞大的类文件; ③ 把 framework 的私有方法公开等。 特点: ① 运行时决议; ② 可以为系统类添加分类;

我们可以在苹果开源的 objc-runtime-new.h 源码中看到category的结构

struct category_t {
    const char *name; // 类名
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法列表
    struct method_list_t *classMethods; // 类方法列表
    struct protocol_list_t *protocols;  // 协议列表
    struct property_list_t *instanceProperties; // 属性列表
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if(isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
}

可以添加的内容 ① 实例方法; ② 类方法; ③ 协议; ④ 属性(非实例变量,实例变量需要使用关联对象去添加)

实现原理 编译之后底层结构为 struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息,在程序运行的时候,runtime 会将 category 的数据合并到类信息中(类对象、元类对象),其实是先合并,然后将合并后的分类数据插入到类原来的数据前面,而当有多个分的的时候,取决于编译的先后顺序,造成“覆盖”的假象。

+load⽅法会在runtime加载类、分类时调⽤,每个类、分类的+load,在程序运⾏过程中只调⽤⼀次;+initialize⽅法会在类第⼀次接收到消息时调⽤

load 与 initialize 的区别 调用方式:load 是根据函数地址直接调用的,initialize 是通过 objc_msgSend 调用的。 调用时刻:load 是 runtime 加载类、分类的时候调用的,只会调用一次;initialize 是类第一接收到信息的时候调用的,每个类只会 initialize 一次,但是作为父类的时候 initialize 可能会被调用多次。 调用顺序:load 是先调用类的 load,其次再调用分类的 load,先编译的类,优先调用 load,调用子类的 load 之前,会先调用父类的 load,分类的 load 也遵循先编译先调用;而 initialize 则是先初始化父类的,再初始化子类,每个类只会初始化一次,子类会自动调用父类的,而且有可能子类最终调用的就是父类的,如果子类没有实现,则会调用父类的(所以父类的可能会调用很多次),但不代表父类初始化多次,如果分类实现了 initialize,则会覆盖本身的。

扩展

Extension 扩展,作用如下: ① 声明私有属性; ② 私有方法; ③ 私有成员变量。

特点如下: ① 编译时决议; ② 只以声明的形式存在,多数情况下寄生在宿主类的 .m 文件中; ③ 不能给系统类添加扩展。

扩展与分类的区别: Extension 在编译的时候,它的数据就已经包含在类信息中了; Category 是在运行时才会将数据合并到类信息中。

关联属性

前面我们提到分类无法添加成员变量,因为类底层结构的成员变量是存储在一个只读的结构体属性中的,而且成员变量会影响实例对象的大小,而实例对象在创建之后就确定了大小,所以也是不能用分类去添加成员变量的。但是我们可以使用 runtime 的关联对象来实现成员变量的添加

void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy) // 添加关联对象
id objc_getAssociatedObject(id object, const void * key) // 获得关联对象
void objc_removeAssociatedObjects(id object) //移除所有的关联对象

如果设置关联对象为 nil,就相当于是移除了关联对象

KEY 写法有很多种,下面举几个例子

//第一种
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, MyKey);

//第二种
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, &MyKey);

//第三种:使⽤属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");

//第四种:使⽤get⽅法的@selecor作为key,@selector获取的就是方法名称
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @selector(getter));

策略

关联对象的策略 objc_AssociationPolicy,其实跟属性修饰词差不多

原理 关联对象并不存储在被关联的对象本身的内存中,而是存储在同一个全局容器中,该容器是由 AssociationsManager 管理并在 AssociationsHashMap 中存储的。

代理

准确的说代理是一种软件设计模式,由代理对象、委托者、协议组成。iOS中以 @protocol 的形式出现,传递方式是一对一传递

@protocol TestDelegate <NSObject>
@optional
- (void)testDelegate;
@end

@interface TestModel : NSObject
@property(nonatomic, weak) id<TestDelegate> delegate;
@end

@implementation TestModel

- (void) delegateTest {
    if(self.delegate && [self.delegate respondsToSelector:@selector(testDelegate)]) {
        [self.delegate testDelegate];
    }
}
@end

注意 ① 代理实现了不同类之间的数据交互,只有某一事件触发才会被调用,在使用代理时,代理者要遵循代理,设置代理,实现代理方法,只有设置了代理,才能调用代理方法。 ② delegate 通常使用 weak 来修饰,这是为了规避循环引用。

通知

是使用观察者模式来实现的用于跨层传递消息的机制,传递方式是一对多传递

#define kTouchBeginNotification @"kTouchBeginNotification" //点击

@implementation TestController

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(log) name:kTouchBeginNotification object:nil];
}

- (void)log {
    NSLog(@"点击了");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[NSNotificationCenter defaultCenter] postNotification:kTouchBeginNotification];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kTouchBeginNotification object:nil];
}
@end

注意 页面销毁的时候要移除监听

原理 其实就是全局维护一个 map,以通知的 key 为键,以订阅者列表为值,当发送通知的之后去遍历订阅者列表一一调用,这个类似于之前我们用flutter 实现的 eventBus。