iOS中OC语言特性观察
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方法,添加了willChangeValueForKey
与didChangeValueForKey
,并且让实例对象的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。