app如何接收到触摸事件的
iOS app接收触摸事件的过程如下:
-
系统响应阶段:
-
APP响应触摸事件:
-
触摸事件的响应者:
-
寻找最佳响应者(Hit-Testing):
以上是iOS app接收触摸事件的基本流程和机制。
Learn more:
为什么只有主线程的runloop是开启的
主线程的runloop是开启的主要原因是为了让程序可以一直运行并接收事件。在iOS开发中,主线程负责处理UI相关的操作,包括用户交互、界面更新等。为了保证主线程能够一直运行并响应事件,需要在主线程中开启一个runloop。
以下是关于为什么只有主线程的runloop是开启的的一些解释:
-
UIApplicationMain函数:在iOS应用程序的入口函数main()中,会调用UIApplicationMain函数来创建一个主线程,并且在主线程中开启一个runloop。这样可以保证主线程一直处于运行状态,能够接收和处理用户的触摸事件、定时器事件等[1]。
-
线程安全:Objective-C不是一门线程安全的语言,多线程读写不同步可能会导致数据竞争和内存访问错误。为了避免这种情况,将UI操作限制在主线程中,通过主线程的runloop来处理UI事件,可以保证线程安全性[2]。
-
屏幕刷新频率:iOS设备的屏幕刷新频率通常是60帧/秒(60Hz),也就是每秒钟刷新60次。为了保证界面的流畅性和响应性,主线程的runloop需要以相同的频率进行调用,以便及时更新界面。如果将UI操作放在其他线程中,很难保证与屏幕刷新的同步,可能会导致界面卡顿或不同步的情况发生[2]。
总结起来,只有主线程的runloop是开启的是为了保证UI操作的线程安全性和界面的流畅性。通过在主线程中开启runloop,可以让主线程一直处于运行状态,能够接收和处理用户的触摸事件,并及时更新界面。
Learn more:
为什么只在主线程刷新UI
在iOS开发中,只能在主线程刷新UI的原因有以下几点:
-
UIKit线程不安全:UIKit是一个线程不安全的类,涉及到UI操作时,可能会存在读写问题。如果在后台线程中对UI进行操作,可能会导致数据竞争和不可预测的结果。为了避免这种情况,iOS要求在主线程上刷新UI [1][2].
-
主线程是UI事件的传递线程:整个iOS应用程序的起点是UIApplication,在主线程上进行初始化。所有的用户事件,如点击、拖动等,都是在主线程上进行传递。因此,只有在主线程上才能对这些事件进行响应。如果在其他线程上刷新UI,可能会导致用户交互无法正常响应 [2].
-
渲染流程需要同步更新:iOS的渲染流程要求以60帧的刷新率在屏幕上同时更新。如果在非主线程异步刷新UI,无法确定这个处理过程能够实现同步更新,可能会导致画面刷新完,但UI还没有被渲染的情况发生。为了保证画面的同步更新,iOS要求在主线程上刷新UI [2].
综上所述,iOS要求在主线程上刷新UI是为了保证UI框架的线程安全,避免可能会导致UI问题的不必要的麻烦和风险 [1].
Learn more:
PerformSelector和runloop的关系
PerformSelector和runloop之间有一定的关系。下面是关于这个关系的一些要点:
-
PerformSelector是NSObject类提供的方法,用于在指定的线程中执行某个方法。这些方法会被添加到runloop中,以便在适当的时候执行。
-
Runloop是一个事件循环,用于处理各种事件(例如用户输入、定时器触发等)。每个线程都有一个与之关联的runloop,用于处理该线程的事件。
-
PerformSelector方法会将要执行的方法添加到当前线程的runloop中。当runloop运行时,会根据设定的条件(例如延迟时间、运行模式等)来触发执行相应的方法。
-
如果在子线程中使用PerformSelector方法,需要手动启动该子线程的runloop,否则方法可能不会被执行。可以通过调用runloop的run方法来启动runloop。
-
PerformSelector方法添加的方法会在runloop的特定阶段被执行,例如在timers阶段或者sources阶段。具体执行的阶段取决于方法添加的时机和所设置的延迟时间。
-
如果runloop在执行完添加的方法后没有其他任务需要处理,它可能会退出并结束运行。因此,如果希望runloop持续运行,可以添加其他事件源或定时器来保持其活跃状态。
综上所述,PerformSelector方法通过将方法添加到runloop中,实现了在指定线程中延迟执行某个方法的功能。
Learn more:
- Runloop与performSelector - 掘金
- 【runloop】performSelector方法探究_runloop perform-CSDN博客
- RunLoop与PerformSelector - 简书
如何使线程保活
线程保活就是不让线程退出,所以往简单说就是搞个 “while(1)” 自己实现一套处理流程,事件派发就可以了;但 iOS 中有 runloop,所以我们就无须大费周章。 TODO: C 语言嵌入式如何实现线程池和保活。
runloop 线程保活前提就是有事情要处理,这里指 timer,source0,source1 事件。
所以有如下几种方式,方式一:
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer 定时任务");
}];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addTimer:timer forMode:NSDefaultRunLoopMode];
[runloop run];
方式二:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
方式三:
- (IBAction)testRunLoopKeepAlive:(id)sender {
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(start) object:nil];
[self.myThread start];
}
- (void)start {
self.finished = NO;
do {
// Runs the loop until the specified date, during which time it processes data from all attached input sources.
[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
} while (!self.finished);
}
- (IBAction)closeRunloop:(id)sender {
self.finished = YES;
}
- (IBAction)executeTask:(id)sender {
[self performSelector:@selector(doTask) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)doTask {
NSLog(@"执行任务在线程:%@",[NSThread currentThread]);
}
If no input sources or timers are attached to the run loop, this method exits immediately and returns
NO; otherwise, it returns after either the first input source is processed orlimitDateis reached. Manually removing all known input sources and timers from the run loop does not guarantee that the run loop will exit immediately. macOS may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
所以上面的方式并非完美,只要没有源,runloop 直接就被退出了,但是因为包了一个while (!self.finished),所以相当于退出->起->退出-> 起。
Note: 如果runloop中没有处理事件,这里一直会退出然后起runloop,就算设置了
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];也没用,但是执行过一次[self performSelector:@selector(doTask) onThread:self.myThread withObject:nil waitUntilDone:NO],那么程序就卡在[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]这一行了。
Learn more:
KVO 实现原理
KVO(Key-Value Observing)是一种iOS中的观察者模式的实现方式,用于监听对象属性值的改变。下面将详细介绍KVO的实现原理。
-
KVO基础知识
-
KVO的实现原理
-
派生类中重写的方法
- 派生类重写了父类的setter方法、class方法、dealloc方法、_isKVOA方法[1]。
-
验证派生类中重写的方法
- 可以通过打印派生类的方法列表来验证派生类中重写的方法。使用class_copyMethodList函数获取方法列表,然后遍历方法列表获取方法名[1].
Learn more:
如何手动关闭kvo
在iOS中,可以通过以下方法手动关闭KVO(键值观察):
- 重写被观察对象的
automaticallyNotifiesObserversForKey方法,返回NO。这会告诉系统不要自动发送KVO通知[1]。 - 重写被观察对象的
automaticallyNotifiesObserversOf<Key>方法,返回NO。这里的<Key>是被观察属性的名称,例如automaticallyNotifiesObserversOfName[1]。
需要注意的是,关闭KVO后,需要手动在赋值前后添加willChangeValueForKey和didChangeValueForKey,才能确保能够收到观察通知[1]。
以下是一个示例代码:
class MyObject: NSObject {
var myProperty: String = "" {
willSet {
self.willChangeValue(forKey: "myProperty")
}
didSet {
self.didChangeValue(forKey: "myProperty")
}
}
override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
return false
}
override class func automaticallyNotifiesObserversOfMyProperty() -> Bool {
return false
}
}
在上面的示例中,MyObject类重写了automaticallyNotifiesObservers(forKey:)方法和automaticallyNotifiesObserversOfMyProperty()方法,将返回值设置为false,从而手动关闭了KVO。在myProperty属性的willSet和didSet中,手动调用了willChangeValue(forKey:)和didChangeValue(forKey:)方法,以确保能够收到观察通知。
Learn more:
通过KVC修改属性会触发KVO么
通过KVC修改属性会触发KVO。KVC(KeyValueCoding)是一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。KVO(Key-Value Observing)是一种观察者模式,用于监听对象属性值的改变。
KVC的赋值过程如下:
- 首先会按照setKey、_setKey的顺序查找方法,若找到方法,则直接调用方法并赋值。
- 如果未找到方法,则调用+ (BOOL)accessInstanceVariablesDirectly方法。
- 如果accessInstanceVariablesDirectly方法返回YES,则按照_key、_isKey、key、isKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常。
- 如果accessInstanceVariablesDirectly方法返回NO,则直接抛出异常。
KVO的实现原理是通过Runtime API动态生成一个子类,并将实例对象的isa指向这个子类。当修改实例对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,该函数会依次调用willChangeValueForKey方法、父类原来的setter方法和didChangeValueForKey方法,最终触发监听器的监听方法[1]。
所以,通过KVC修改属性会触发KVO,因为KVC在修改属性时会调用willChangeValueForKey方法和didChangeValueForKey方法,从而触发KVO的监听方法。
Learn more:
- ios 通过kvc修改属性会触发kvo_底层原理 - KVO/KVC-CSDN博客
- iOS KVC和KVO-腾讯云开发者社区-腾讯云
- Objective-C基础之二(深入理解KVO、KVC) - 掘金
哪些情况下使用kvo会崩溃,怎么防护崩溃
在使用KVO(Key-Value Observing)时,可能会出现以下情况导致崩溃:
-
KVO添加次数和移除次数不匹配:
- 移除了未注册的观察者,导致崩溃。
- 重复移除多次,移除次数多于添加次数,导致崩溃。
- 重复添加多次,虽然不会崩溃,但是发生改变时,也同时会被观察多次。
- 添加和删除的顺序不一致导致崩溃 [1].
-
被观察者提前被释放,被观察者在dealloc时仍然注册着KVO,导致崩溃。
- 例如,被观察者是局部变量的情况(iOS 10及之前会崩溃) [1].
-
添加了观察者,但未实现observeValueForKeyPath:ofObject:change:context:方法,导致崩溃 [1].
-
添加或者移除时keypath为nil,导致崩溃 [1].
为了防止这些崩溃,可以采取以下防护方法:
-
对KVO的添加和移除方法进行swizzle(方法交换):
- 在添加KVO时,将KVO的相关信息存储到KVO缓存容器中。
- 在移除KVO时,先检查KVO缓存容器中是否有匹配的KVO信息,如果有,则调用系统的方法进行删除,如果没有,则不处理。
- 这样可以保证KVO缓存和系统KVO列表的一致性,避免多次删除导致的崩溃问题 [1].
-
在observeValueForKeyPath:ofObject:change:context:方法中使用try-catch语句:
- 在调用原方法之前加上try-catch语句,捕获异常,避免因为未实现该方法而导致的崩溃 [1].
-
在添加和移除KVO时,对keypath进行判空:
- 在addObserver:forKeyPath:options:context:、removeObserver:forKeyPath:和removeObserver:forKeyPath:context:方法中,对keypath进行判空处理,避免因为keypath为nil而导致的崩溃 [1].
-
FBKVOController 对 KVO 机制进行了额外的一层封装,框架不但可以自动帮我们移除观察者,还提供了 block 或者 selector 的方式供我们进行观察处理。不可否认的是,FBKVOController 为我们的开发提供了很大的便利性。但是相对而言,这种方式对项目代码的侵入性比较大,必须依靠编码规范来强制约束团队人员使用这种方式。
综上所述,通过对KVO的添加和移除方法进行swizzle,使用try-catch语句处理observeValueForKeyPath:ofObject:change:context:方法,以及对keypath进行判空处理,可以有效防止KVO使用中可能导致的崩溃问题。
Learn more:
kvo的优缺点
优点:
- 运用了设计模式:观察者模式
- 支持多个观察者观察同一属性,或者一个观察者监听不同属性。
- 开发人员不需要实现属性值变化了发送通知的方案,系统已经封装好了,大大减少开发工作量;
- 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SDK对象)的实现;
- 能够提供观察的属性的最新值以及先前值;
- 用key paths来观察属性,因此也可以观察嵌套对象;
- 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
- 观察的属性键值硬编码(字符串),编译器不会出现警告以及检查;
- 由于允许对一个对象进行不同属性观察,所以在唯一回调方法中,会出现地狱式
if-else if - else分支处理情况;
References: