前言
今天這一篇我們來講一下 Runloop和KVO
本章的主要回答的問題如下:
Runloop
- app如何接收到觸摸事件的
- 爲什麼只有主線程的runloop是開啓的
- 爲什麼只在主線程刷新UI
- PerformSelector和runloop的關係
- 如何使線程保活
KVO
- 實現原理
- 如何手動關閉kvo
- 通過KVC修改屬性會觸發KVO麼
- 哪些情況下使用kvo會崩潰,怎麼防護崩潰
- kvo的優缺點
Runloop
作爲一個合格的iOS開發者必須對runloop有一個更深入的瞭解,下面我們來回答一下 相關問題
1.app如何接收到觸摸事件的
回答這個問題前請認真閱讀一下 iOS觸摸事件全家桶
通過上圖可以看出整個流程就是 我們app啓動默認會通過machPort監聽端口的方式 來接受IOHIDEvent 來接收和處理觸摸事件.
2.爲什麼只有主線程的runloop是開啓的
mian()函數中調用UIApplicationMain,這裏會創建一個主線程,用於UI處理,爲了讓程序可以一直運行並接收事件,所以在主線程中開啓一個runloop,讓主線程常駐.
3.爲什麼只在主線程刷新UI
我們所有用到的UI都是來自於UIKit這個基礎庫.因爲objc不是一門線程安全的語言所以存在多線程讀寫不同步的問題,如果使用加鎖的方式操作系統開銷很大,會耗費大量的系統資源(內存、時間片輪轉、cpu處理速度...),加上上面講到的系統事件的接收處理都在主線程,如果UI異步線程的話 還會存在 同步處理事件的問題,所以多點觸摸手勢等一些事件要保持和UI在同一個線程相對是最優解.
另一方面是 屏幕的渲染是 60幀(60Hz/秒), 也就是1秒鐘回調60次的頻率,(iPad Pro 是120Hz/秒),我們的runloop 理想狀態下也會按照時鐘週期 回調60次(iPad Pro 120次), 這麼高頻率的調用是爲了 屏幕圖像顯示能夠垂直同步 不卡頓.在異步線程的話是很難保證這個處理過程的同步更新. 即便能保證的話 相對主線程而言 系統資源開銷 線程調度等等將會佔據大部分資源和在同一個線程只專門幹一件事有點得不償失.
4.PerformSelector和runloop的關係
當調用NSObect的 performSelector:相關的時候,內部會創建一個timer定時器添加到當前線程的runloop中,如果當前線程沒有啓動runloop,則該方法不會被調用.
開發中遇到最多的問題就是這個performSelector: 導致對象的延遲釋放,這裏開發過程中注意一下,可以用單次的NSTimer替代.
詳細可以參考Runloop與performSelector
5.如何使線程保活?
想要線程保活的話就開啓該線程的runloop即可,注意:在NSThread執行的方法中添加while(true){},這樣是模擬runloop的運行原理,結合GCD的信號量,在{}代碼塊中處理任務.
但是注意 開啓runloop的方法要正確
如下代碼
//測試開啓線程
- (void)memoryTest {
for (int i = 0; i < 100000; ++i) {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
[self performSelector:@selector(stopThread) onThread:thread withObject:nil waitUntilDone:YES];
}
}
//線程停止
- (void)stopThread {
CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];
}
//運行線程的runloop 注意 意添加的那個空port,否則會出現內存泄露
- (void)run {
@autoreleasepool {
NSLog(@"current thread = %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
if (!self.emptyPort) {
self.emptyPort = [NSMachPort port];
}
[runLoop addPort:self.emptyPort forMode:NSDefaultRunLoopMode];
[runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];
}
}
//下列代碼用於模擬線程內部做的一些耗時任務
- (void)printSomething {
NSLog(@"current thread = %@", [NSThread currentThread]);
[self performSelector:@selector(printSomething) withObject:nil afterDelay:1];
}
//模擬手動點擊按鈕 讓 runloop停掉
- (void)stopButtonDidClicked:(id)sender {
[self performSelector:@selector(stopRunloop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)stopRunloop {
CFRunLoopStop(CFRunLoopGetCurrent());
}
詳細請參考:iOS開發深入研究Runloop與線程保活
KVO
在開發過程中我們經常使用KVO,下面解答一下KVO相關的問題.
KVO的實現原理
通過runtime
派生子類的方式 複寫相關需要KVO監聽的屬性,在該屬性setter之前和之後調用NSObject的監聽方法,這樣KVO就實現了屬性變換前後的回調.
KVO派生的子類具體格式應該是:NSKVONotifying_+類名
的類 eg: NSKVONotifying_Person
下面示例代碼爲Person類的name添加KVO的模擬實驗
- (void)setName:(NSString *)name{
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
問題來了如何動態創建類呢?
//動態創建XXCustomClass
Class customClass = objc_allocateClassPair([NSObject class], "XXCustomClass", 0);
// 添加實例變量
class_addIvar(customClass, "age", sizeof(int), 0, "i");
// 動態添加方法
class_addMethod(customClass, @selector(hahahha), (IMP)hahahha, "V@:");
//需要實現的方法
void hahahha(id self, SEL _cmd)
{
NSLog(@"hahahha====");
}
- (void)hahahha{
}
//最後註冊到運行時環境
objc_registerClassPair(customClass);
具體原理以及自定義實現KVO可以參考KVO詳解及底層實現
如何手動關閉KVO?
被觀察的對象複寫如下方法 返回NO
即可關閉KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
如果關閉後還想觸發 KVO的話 修改需要手動調用在變量setter的前後 主動調用 willChangeValueForKey:
和didChangeValueForKey:
通過KVC修改屬性會觸發KVO麼?
會的
哪些情況下使用kvo會崩潰,怎麼防護崩潰?
使用不當 會crash,比如:
- 添加和移出不是成對出現且存在多線程添加KVO的情況,經常遇到的crash是移出 - 內存dealloc的時候 或者對象銷燬前沒有正確移出Observer
如何防護?
1.注意移出對象 匹配
2.內存野指針問題,一定要在對象銷燬前移出觀察者 3.可以使用第三方庫BlockKit添加KVO,blockkit內部會自動移除Observer避免crash.
KVO的優缺點
優點:
- 方便兩個對象間同步狀態(keypath)更加方便,一般都是在A類要觀察B類的屬性的變化.
- 非侵入式的得到某內部對象的狀態改變並作出響應.(就是在不改變原來對象類的代碼情況下即可做出對該對象的狀態變化進行監聽)
- 可以嵌入更改前後的兩個時機的狀態. - 可以通過Keypaths對嵌套對象的監聽.
缺點:
- 需要手動移除觀察者,不移除容易造成crash.
- 註冊和移出成對匹配出現.
- keypath參數的類型String, 如果對象的成員變量被重構而變化字符串不會被編譯器識別而報錯.
- 實現觀察的方式是複寫NSObjec的相關KVO的方法,應該更加面向protocol的方式會更好.
總結
這一篇我們講了 runloop和KVO相關的內容,這裏面最負責的當屬runloop如何處理觸摸手勢事件.建議認真研讀相關鏈接文章.這樣纔有一個對runloop更深刻的理解。
推薦
- 更多:iOS面試題大全
- 更多:《BAT面試答案文集.PDF》,獲取可加iOS技術交流圈:937 194 184。
收錄:原文地址