RunLoop
runloop对于一个标准的iOS开发来说都不陌生,应该说熟悉runloop是标配,下面就随便列几个典型问题吧。
1、app如何接收到触摸事件的
系统响应阶段
1、手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
2、IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port(通信端口,用于本地通信)传递给SpringBoad进程。
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。
3、SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调。
此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件,因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。
若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;
若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。
APP响应阶段
1.APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
2.source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
3.source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing,此处开始便是与我们平时开发相关的工作了。
4.寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了,事件除了被响应者消耗,还能被手势识别器或是target-action模式捕捉并消耗掉。
5.触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么最终也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
总结:
1、手接触到屏幕会产生触摸事件,IOKit会把触摸事件封装成IOHIDEvent。
2、然后通过Mach Port 转发事件给SpringBoard。
3、SpringBoard 会判断当前是否有应用在前台,没有前台应用SpringBoard自己处理,如果有应用在前台,会把事件转发给前台的App。
4、App 接收到SpringBoard 传递来的事件,主线程被唤醒,触发Source1(Source1 是苹果注册用来监听Mach Port消息的),Source1 回调又触发 Source0,把IOHIDEvent封装成 UIEvent 并把事件添加到UIApplication 事件队列中,事件开始从UIApplication开始寻找最佳的响应者。
5、在响应链上要么被某个响应者对象或者手势消耗掉,如果没有找到响应者,会一直顺着响应链向上查找,找到顶层的UIApplication,UIApplication默认不会做任何的响应。
References:
2、为什么只有主线程的runloop是开启的
2.1、在iOS中,主线程的runloop默认开启是如何实现的?
每个线程都有唯一与之对应的runloop,runloop保存在全局的字段里,线程为Key、runloop作为value,线程刚创建的时候并没有runloop对象,runloop对象会在第一次获取它的时候创建。
在程序的入口Main函数中,UIApplicationMain函数中Apple 的文档是这样写的
It also sets up the main event loop, including the application’s run loop, and begins processing events.
也就是说在UIApplicationMain这个函数中会主动获取runloop,所以会在主线程中开启runloop。
2.2、为什么主线程默认需要开启runloop,子线程不需要呢?
2.2.1、主线程开启runloop让主线程成为常驻线程,不然直接返回了,程序就没法运行了。
2.2.2、主线程的 runloop它负责处理app存活期间的大部分事件,如用户交互等,使用户行为能够得到响应。
2.2.3、屏幕想要维持每秒60次的刷新,也是主线程的runloop驱动的。
3、为什么只在主线程刷新UI
1、在子线程无法保证线程安全。
在UIKit中,很多类中大部分的属性都被修饰为nonatomic,假设UITableView在其他线程去移除了一个cell,而在另一个线程却对这个cell所在的index进行一些操作,这时候可能就会引发crash。
2、如果把UIKit设计成线程安全的,在性能上也不会有很好的表现
因为加锁解锁而耗费大量的时间。事实上并发编程也没有因为UIKit是线程不安全而变得困难,我们所需要做的只是要确保UI操作在主线程进行就可以了。
3、在子线程修改UI,可能导致UI的渲染不能同步
而每一个view的变化的修改并不是立刻变化,相反的会在当前run loop的结束的时候统一进行重绘,这样设计的目的是为了能够在一个runloop里面处理好所有需要变化的view,包括resize、hide、reposition等等,所有view的改变都能在同一时间生效,这样能够更高效的处理绘制,这个机制被称为绘图循环。
如果在后台线程可以更新UI,后台线程的修改不在一个runloop里面,就不能统一的更新UI,就会导致有些UI改变了 ,另一些线程修改的UI没有改变。
References:
4、PerformSelector和runloop的关系
4.1、如果是调用以下3个方法,这是给消息接收者发送一个消息,跟runloop没有关系
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2
4.2、如果是调用下面的方法,这些方法会设置了一个计时器来在当前线程的runloop中执行 aSelector 消息。定时器被配置为在Mode参数指定的模式下运行。当计时器触发时,线程尝试将消息从运行循环中取出并执行选择器。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
This method sets up a timer to perform the
aSelectormessage on the current thread’s run loop. The timer is configured to run in the modes specified by themodesparameter. When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in one of the specified modes; otherwise, the timer waits until the run loop is in one of those modes.
4.3、通过下面的代码看子线程中的runloop和performSelector的关系
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"1");
[self observer1];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[self performSelector:@selector(testPerform) withObject:nil afterDelay:0];//
[runloop run];
NSLog(@"3");
});
}
- (void)testPerform{
NSLog(@"2");
}
子线程默认不开启runloop,如果上面的代码中没有,只会打印1 3,不会打印 testPerform 中的2。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
如果添加上面获取runloop 、开启 runloop的方法,会打印123。
添加[self performSelector:@selector(testPerform) withObject:nil afterDelay:0];方法后,会往runloop中添加一个计时器,具体看下面代码执行时的打印
没有执行performSelector前runloop打印如下 timers = (null)
执行performSelector之后,runloop打印如下,timers = <CFArray 0x600002e519e0
5、如何使线程保活
//
// KeepThreadAliveViewController.m
// OffcnApp
//
// Created by zhouluyao on 2022/1/14.
//
#import "KeepThreadAliveViewController.h"
#import "UIButton+quick.h"
@interface KeepThreadAliveViewController ()
@property (strong, nonatomic) NSThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation KeepThreadAliveViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self setupUI];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
// NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)
// [[NSRunLoop currentRunLoop] run];
/*
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers
*/
}];
[self.thread start];
}
- (void)setupUI {
UIButton *taskBtn = [UIButton buttonWithTitle:@"执行任务" titleColor:[UIColor redColor] font:[UIFont systemFontOfSize:15] target:self action:@selector(taskBtnClick)];
taskBtn.frame = CGRectMake(50, 200, 100, 50);
[self.view addSubview:taskBtn];
UIButton *stopBtn = [UIButton buttonWithTitle:@"终止线程" titleColor:[UIColor blueColor] font:[UIFont systemFontOfSize:15] target:self action:@selector(stopBtnClick)];
stopBtn.frame = CGRectMake(50, 400, 100, 50);
[self.view addSubview:stopBtn];
#import "UIButton+quick.h"
}
- (void)taskBtnClick {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)stopBtnClick {
// 在子线程调用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 设置标记为NO
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
@end
具体步骤:
1、在线程执行的过程中,往当前线程所在的runloop里面添加一个NSPort对象,调用让线程一直有事情做。
self.thread = [[NSThread alloc] initWithBlock:^{
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
2、需要使用保活线程执行任务的时候,直接调用。
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
3、想要销毁线程的时候,停止runloop,并把是否停止的标记设置为停止
// 设置标记为NO
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
6、什么情况下需要在子线程中开启runloop
6.1、runloop适用于您希望与线程进行更多交互的情况。
没必要开启runloop的情况:
如果您使用线程执行一些长时间运行的预定任务。
需要启动一个runloop的情况:
1、使用 input sources 中的 Port 和 Custom和其他线程通信。
2、在线程中使用计时器
3、使用 performSelector: withObject: afterDelay:方法
4、保留线程执行定期任务
a run loop is necessary, and if it is, configure and start it yourself. You do not need to start a thread’s run loop in all cases. For example, if you use a thread to perform some long-running and predetermined task, you can probably avoid starting the run loop. Run loops are intended for situations where you want more interactivity with the thread. For example, you need to start a run loop if you plan to do any of the following:
- Use ports or custom input sources to communicate with other threads.
- Use timers on the thread.
- Use any of the
performSelector… methods in a Cocoa application.- Keep the thread around to perform periodic tasks
6.2、runloop会收到两种不同的source。
输入源传递异步事件,通常是来自跨进程跨线程的消息。
定时器源传递同步事件,在预定时间或重复间隔发生。
A run loop receives events from two different types of sources.
Input sources deliver asynchronous events, usually messages from another thread or from a different application.
Timer sources deliver synchronous events, occurring at a scheduled time or repeating interval.
References:
KVO
同runloop一样,这也是标配的知识点了,同样列出几个典型问题
先在下图中看下gnustep中KVO实现的底层的存储结构
被观察对象添加监听的过程:
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOInfo *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;
setup();
// Use the original class
r = replacementForClass([self class]);
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: forwarder];
}
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOPathInfo *pathInfo;
GSKVOObservation *observation;
unsigned count;
if ([anObserver respondsToSelector:
@selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
{
return;
}
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo == nil)
{
pathInfo = [GSKVOPathInfo new];
// use immutable object for map key
aPath = [aPath copy];
NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
[pathInfo release];
[aPath release];
}
observation = nil;
pathInfo->allOptions = 0;
count = [pathInfo->observations count];
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver)
{
o->context = aContext;
o->options = options;
observation = o;
}
pathInfo->allOptions |= o->options;
}
if (observation == nil)
{
observation = [GSKVOObservation new];
GSAssignZeroingWeakPointer((void**)&observation->observer,
(void*)anObserver);
observation->context = aContext;
observation->options = options;
[pathInfo->observations addObject: observation];
[observation release];
pathInfo->allOptions |= options;
}
}
@interface GSKVOObservation : NSObject
{
@public
NSObject *observer; // Not retained (zeroing weak pointer)
void *context;
int options;
}
@end
/* An instance of thsi records the observations for a key path and the
* recursion state of the process of sending notifications.
*/
@interface GSKVOPathInfo : NSObject
{
@public
unsigned recursion;
unsigned allOptions;
NSMutableArray *observations;
NSMutableDictionary *change;
}
@interface GSKVOInfo : NSObject
{
NSObject *instance; // Not retained.
NSRecursiveLock *iLock;
NSMapTable *paths;
}
1、当调用 addObserver:forKeyPath:options:context:方法时,被观察对象会把自己的地址作为key,从infoTable(NSMapTable*)中获取value(GSKVOInfo*),获取不到就创建infoTable设置key、value。
2、GSKVOInfo*结构体中主要包含了paths(NSMapTable *) 和 instance (被观察者对象),这个instance是not retained 的。
3、当观察者对被观察对象的同一个keyPath进行观察时,就会把这些观察者对象封装成一个GSKVOObservation对象添加到observations数组中。
4、GSKVOObservation对象包含观察者observer,context 以及options。
5、存储结构对observer是not retained 的、对被观察者对象instance也是not retained,也就说存储结构是不会对监听者持有、也不会对被观察对象持有。
被观察对象添加监听的属性值改变时的回调过程:
- (void) didChangeValueForKey: (NSString*)aKey
{
GSKVOPathInfo *pathInfo;
GSKVOInfo *info;
info = (GSKVOInfo *)[self observationInfo];
if (info == nil)
{
return;
}
pathInfo = [info lockReturningPathInfoForKey: aKey];
if (pathInfo != nil)
{
if (pathInfo->recursion == 1)
{
id value = [self valueForKey: aKey];
if (value == nil)
{
value = null;
}
[pathInfo->change setValue: value
forKey: NSKeyValueChangeNewKey];
[pathInfo->change setValue:
[NSNumber numberWithInt: NSKeyValueChangeSetting]
forKey: NSKeyValueChangeKindKey];
[pathInfo notifyForKey: aKey ofInstance: [info instance] prior: NO];
}
}
}
- (void) notifyForKey: (NSString *)aKey ofInstance: (id)instance prior: (BOOL)f
{
unsigned count;
id oldValue;
id newValue;
if (f == YES)
{
if ((allOptions & NSKeyValueObservingOptionPrior) == 0)
{
return; // Nothing to do.
}
[change setObject: [NSNumber numberWithBool: YES]
forKey: NSKeyValueChangeNotificationIsPriorKey];
}
else
{
[change removeObjectForKey: NSKeyValueChangeNotificationIsPriorKey];
}
/* Retain self so that we won't be deallocated during the
* notification process.
*/
[self retain];
count = [observations count];
while (count-- > 0)
{
GSKVOObservation *o = [observations objectAtIndex: count];
[o->observer observeValueForKeyPath: aKey
ofObject: instance
change: change
context: o->context];
}
}
1、被观察对象的keyPath发生改变时会调用didChangeValueForKey:。
2、在didChangeValueForKey:方法中会根据被观察对象的地址 和 keyPath 找到所有监听被观察对象指定keyPath的GSKVOObservation,这个GSKVOObservation是被放在数组中的。
3、遍历数组中的GSKVOObservation,从GSKVOObservation这个结构中拿到observer,context以及在存储KVO的数组结构中找到instance 和 change 回调给观察者。
4、回调的方式是通过调用观察者自己实现的 observeValueForKeyPath:ofObject:change:context:方法。
1、实现原理
先看Apple 对KVO的解释 Observing Programming Guide
Automatic key-value observing is implemented using a technique called isa-swizzling.
The
isapointer, 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.
You should never rely on the
isapointer to determine class membership. Instead, you should use theclassmethod to determine the class of an object instance.
KVO使用的是一种isa交换的技术,当对一个实例对象的属性添加监听的时候,这个实例对象的isa指向的类对象就发生变化了,实例对象的isa指向了一个一个中间类,而不是这个实例对象真实的类对象。
@interface Person1 : NSObject
@property (nonatomic, assign) int age;
@end
1.1、对实例对象p1添加观察者,p1的isa发生变化了吗?
- (void)testKVO {
Person1 *p1 = [Person1 new];
Person1 *p2 = [Person1 new];
//添加监听之后 实例对象的isa指向就发生了变化
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:@"p1"];
}
打印结果:
(lldb) p p1->isa
(Class) $0 = NSKVONotifying_Person1
(lldb) p p2->isa
(Class) $1 = Person1
结论:
实例对象p1的isa指向就发生了变化,变成了NSKVONotifying_Person1。
1.2、对实例对象p1移除观察者,实例对象的isa指向会变成原来的吗?
- (void)testKVO {
Person1 *p1 = [Person1 new];
Person1 *p2 = [Person1 new];
//添加监听之后 实例对象的isa指向就发生了变化
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:@"p1"];
//移除观察者之后,实例对象的isa指向会变成原来的吗?
[p1 removeObserver:self forKeyPath:@"age"];
}
打印结果:
(lldb) p p1->isa
(Class) $0 = NSKVONotifying_Person1
(lldb) p p1->isa
(Class) $1 = Person1
结论:
实例对象p1移除观察者之后,实例对象的isa指向又会变成Person1。
1.3、对实例对象p1添加监听,方法的实现有什么变化?
- (void)testKVO {
Person1 *p1 = [Person1 new];
Person1 *p2 = [Person1 new];
//添加监听之后 方法的实现有什么变化
NSLog(@"setAge: Method(IMP) p1 -%p,p2 -%p",[p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector:@selector(setAge:)]);
}
打印结果:
2021-08-22 12:13:25.374693+0800 OffcnApp[53897:8562396] setAge: Method(IMP) p1 -0x10d19379f,p2 -0x10b88f0d0
(lldb) p (IMP) 0x10d19379f
(IMP) $0 = 0x000000010d19379f (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP) 0x10b88f0d0
(IMP) $1 = 0x000000010b88f0d0 (OffcnApp`-[Person1 setAge:] at KVO_KVCViewController.m:15)
结论:
实例对象p1的方法实现也发生了变化,[Person1 setAge:]变成了_NSSetIntValueAndNotify
1.4、打印出增加监听后的派生类和之前类中所有的方法?
- (void)testKVO {
Person1 *p1 = [Person1 new];
Person1 *p2 = [Person1 new];
//打印出增加监听后的派生类和之前类中所有的方法?
[self printMethodListWithClass:object_getClass(p1)];
NSLog(@"-----------------------------------------");
[self printMethodListWithClass:object_getClass(p2)];
}
- (void)printMethodListWithClass:(Class)cls {
NSMutableString *methods = [NSMutableString string];
unsigned int count;
Method * methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methods appendFormat:@"%@", [NSString stringWithFormat:@"%@ \n",methodName]];
}
NSLog(@"class- %@\nmethods-%@",NSStringFromClass(cls),methods);
}
打印结果:
2021-08-24 09:57:50.186128+0800 OffcnApp[18144:9473469] class- NSKVONotifying_Person1
methods-setAge:
class
dealloc
_isKVOA
-----------------------------------------
2021-08-24 09:57:50.186256+0800 OffcnApp[18144:9473469] class- Person1
methods-willChangeValueForKey:
didChangeValueForKey:
setAge:
age
结论:
简单分析下重写这些方法的作用:
-setAge :中间类会对添加过监听的属性重写实现
class - 重写这个方法,是为了伪装苹果自动为我们生成的中间类;
dealloc - 应该是处理对象销毁之前的一些收尾工作;
_isKVOA - 告诉系统使用了。
Refrences:
2、如何手动关闭KVO
下面代码还是以监听到Int类型属性改变为例
- (void) setterInt: (unsigned int)val
{
NSString *key;
Class c = [self class];
void (*imp)(id,SEL,unsigned int);
imp = (void (*)(id,SEL,unsigned int))[c instanceMethodForSelector: _cmd];
key = newKey(_cmd);
if ([c automaticallyNotifiesObserversForKey: key] == YES)
{
// pre setting code here
[self willChangeValueForKey: key];
(*imp)(self, _cmd, val);
// post setting code here
[self didChangeValueForKey: key];
}
else
{
(*imp)(self, _cmd, val);
}
RELEASE(key);
}
可以看到 [c automaticallyNotifiesObserversForKey: key] 的值为NO的话,会直接调用setter方法的实现,而不会调用 [self willChangeValueForKey: key]; 、 [self didChangeValueForKey: key];也就不会触发监听者的回调。
被观察的对象复写如下方法 返回NO即可关闭KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
如果关闭后还想触发 KVO的话 修改需要手动调用在变量setter的前后 主动调用 willChangeValueForKey:和didChangeValueForKey:。
3、通过KVC修改属性会触发KVO么
会触发KVO。
先回顾一下KVC赋值的流程
1、首先会按照setKey、_setKey的顺序查找方法,若找到方法,则直接调用方法并赋值;
2、未找到方法,则调用+ (BOOL)accessInstanceVariablesDirectly;
3、若accessInstanceVariablesDirectly方法返回YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常;
4、若accessInstanceVariablesDirectly方法返回NO,则直接抛出异常;
没有声明属性,只有一个变量,使用KVC赋值会触发KVO吗?
Person1
@interface Person1 : NSObject
{
@public
int isAge;
}
@end
@implementation Person1
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
点击控制器屏幕设置KVC
点击控制器屏幕设置KVC
//1、监听
- (void)viewDidLoad {
[super viewDidLoad];
Person1 *p1 = [Person1 new];
[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:@"p1"];
}
//2、点击屏幕触发
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.p1 setValue:@(22) forKey:@"age"];
}
//3、实现监听
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath -%@, object - %@,change -%@",keyPath,object,change);
}
结论:
KVC会在[NSObject(NSKeyValueCoding) setValue:forKey:]方法中调用_NSSetValueAndNotifyForKeyInIvar这个方法,从上面图中的汇编代码可以看到在设置变量前调用了willChangeValueForKey:、设置变量后调用了didChangeValueForKey:,所以也会触发KVO的回调。
4、哪些情况下使用KVO会崩溃,怎么防护崩溃
1、没有添加监听进行移除、或者添加过一次,移除了多次
Person1 *p1 = [Person1 new];
//[p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:@"p1"];
[p1 removeObserver:self forKeyPath:@"age"];
Crash(Uncaught)-========uncaughtException异常错误报告========
name:NSRangeException
reason:
Cannot remove an observer <KVO_KVCViewController 0x7fdeebf1e070> for the key path "age" from <Person1 0x600003c7db80> because it is not registered as an observer.
2、监听者没有实现方法 -observeValueForKeyPath:ofObject:change:context:
Crash(Uncaught)-========uncaughtException异常错误报告========
name:NSInternalInconsistencyException
reason:
<KVO_KVCViewController: 0x7fd6f5512b60>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: age
Observed object: <Person1: 0x600002b8b0a0>
Change: {
kind = 1;
new = 22;
}
Context: 0x1008fd9c8
callStackSymbols:
3、对NSArray 、NSSet 添加监听 移除监听会导致崩溃
self.array = [NSMutableArray arrayWithObjects:@"1",@"2",@"3",@"4",@"5",@"6", nil];
[self.array addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionNew context:@"array"];
Crash(Uncaught)-========uncaughtException异常错误报告========
name:NSInvalidArgumentException
reason:
[<__NSArrayM 0x600002decdb0> addObserver:forKeyPath:options:context:] is not supported. Key path: count
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
[NSException raise: NSGenericException
format: @"[%@-%@]: This class is not observable",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
[NSException raise: NSGenericException
format: @"[%@-%@]: This class is not observable",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
@implementation NSSet (NSKeyValueObserverRegistration)
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
[NSException raise: NSGenericException
format: @"[%@-%@]: This class is not observable",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
[NSException raise: NSGenericException
format: @"[%@-%@]: This class is not observable",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
@end
5、KVO的优缺点
优点:
1、可以无侵入的观察某个对象属性值的变化
2、可以监听属性值变化的过程,新值、旧值一块监听
3、被观察对象可以通过keyPathsForValuesAffectingValueForKey:实现组合监听,也就是只要age、name一个属性变化就会触发_info属性的变化。
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *info;
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqualToString:@"info"]) {
return [[NSSet alloc] initWithArray:@[@"age", @"name"]];
}
else {
return [[NSSet alloc] init];
}
}
- (NSString *)info {
return [NSString stringWithFormat:@"name:%@_age:%d", _name, _age];
}