Runloop & KVO

runloop

runloop對(duì)于一個(gè)標(biāo)準(zhǔn)的iOS開(kāi)發(fā)來(lái)說(shuō)都不陌生辙谜,應(yīng)該說(shuō)熟悉runloop是標(biāo)配,下面就隨便列幾個(gè)典型問(wèn)題吧

1. app如何接收到觸摸事件的
  1. 首先跷睦,手機(jī)中處理觸摸事件的是硬件系統(tǒng)進(jìn)程 筷弦,當(dāng)硬件系統(tǒng)進(jìn)程識(shí)別到觸摸事件后肋演,會(huì)將這個(gè)事件進(jìn)行封裝抑诸,并通過(guò)machPort,將封裝的事件發(fā)送給當(dāng)前活躍的APP進(jìn)程。
  2. 由于APP的主線程中runloop注冊(cè)了這個(gè)machPort端口爹殊,就是用于接收處理這個(gè)事件的蜕乡,所以這里APP收到這個(gè)消息后,開(kāi)始尋找響應(yīng)鏈梗夸。
  3. 尋找到響應(yīng)鏈后层玲,開(kāi)始分發(fā)事件,它會(huì)優(yōu)先發(fā)送給手勢(shì)集合反症,來(lái)過(guò)濾這個(gè)事件辛块,一旦手勢(shì)集合中其中一個(gè)手勢(shì)識(shí)別了這個(gè)事件,那么這個(gè)事件將不會(huì)發(fā)送給響應(yīng)鏈對(duì)象铅碍。
  4. 手勢(shì)沒(méi)有識(shí)別到這個(gè)事件润绵,事件將會(huì)發(fā)送給響應(yīng)鏈對(duì)象UIResponser
UIResponder

每個(gè)響應(yīng)者都是一個(gè)UIResponder對(duì)象胞谈,即所有派生自UIResponder的對(duì)象尘盼,本身都具備響應(yīng)事件的能力。因此以下類的實(shí)例都是響應(yīng)者:

  • UIView
  • UIViewController
  • UIApplication
  • AppDelegate

響應(yīng)者之所以能響應(yīng)事件烦绳,因?yàn)槠涮峁┝?個(gè)處理觸摸事件的方法:

//手指觸碰屏幕卿捎,觸摸開(kāi)始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移動(dòng)
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指離開(kāi)屏幕,觸摸結(jié)束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//觸摸結(jié)束前径密,某個(gè)系統(tǒng)事件中斷了觸摸午阵,例如電話呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

事件的三徒弟UIResponder、UIGestureRecognizer享扔、UIControl

手勢(shì)識(shí)別器比UIResponder具有更高的事件響應(yīng)優(yōu)先級(jí)5坠稹括细!

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton戚啥、UISegmentedControl奋单、UISwitch等控件都是UIControl的子類。當(dāng)UIControl跟蹤到觸摸事件時(shí)猫十,會(huì)向其上添加的target發(fā)送事件以執(zhí)行action览濒。值得注意的是,UIConotrol是UIView的子類拖云,因此本身也具備UIResponder應(yīng)有的身份贷笛。

關(guān)于UIControl,此處介紹兩點(diǎn):

  1. target-action執(zhí)行時(shí)機(jī)及過(guò)程
  2. 觸摸事件優(yōu)先級(jí)
image.png

UIControl比其父視圖上的手勢(shì)識(shí)別器具有更高的事件響應(yīng)優(yōu)先級(jí)
同一控件上宙项,手勢(shì)識(shí)別器比UIControl

參考文章

2. 為什么只有主線程的runloop是開(kāi)啟的

app啟動(dòng)前會(huì)調(diào)用main函數(shù)乏苦,具體如下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

mian函數(shù)中調(diào)用UIApplicationMain,這里會(huì)創(chuàng)建一個(gè)主線程尤筐,用于UI處理汇荐,為了讓程序可以一直運(yùn)行,所以在主線程中開(kāi)啟一個(gè)runloop盆繁,讓主線程常駐掀淘。

3. 為什么只在主線程刷新UI

原因一:安全+效率
UIKit不是線程安全的,UI操作涉及到渲染訪問(wèn)各種View對(duì)象的屬性油昂,如果異步操作下會(huì)存在讀寫問(wèn)題革娄,而為其加鎖則會(huì)耗費(fèi)大量資源并拖慢運(yùn)行速度。

另一方面因?yàn)檎麄€(gè)程序的起點(diǎn)UIApplication是在主線程進(jìn)行初始化冕碟,所有的用戶事件都是在主線程上進(jìn)行傳遞(如點(diǎn)擊拦惋、拖動(dòng)),所以view只能在主線程上才能對(duì)事件進(jìn)行響應(yīng)安寺。而在渲染方面由于圖像的渲染需要以60幀的刷新率在屏幕上 同時(shí)更新厕妖,在非主線程異步化的情況下無(wú)法確定這個(gè)處理過(guò)程能夠?qū)崿F(xiàn)同步更新。

4. PerformSelector和runloop的關(guān)系
一 基礎(chǔ)用法

performSelecor響應(yīng)了OC語(yǔ)言的動(dòng)態(tài)性:延遲到運(yùn)行時(shí)才綁定方法我衬。當(dāng)我們?cè)谑褂靡韵路椒〞r(shí):

[obj performSelector:@selector(xxx)];
[obj performSelector:@selector(xxx:) withObject:@"xxx"];
[obj performSelector:@selector(xxx: with:) withObject:@"xxx" withObject:@"xxx"];

編譯階段并不會(huì)去檢查方法是否有效存在叹放,只會(huì)給出警告。所以在實(shí)際開(kāi)發(fā)中挠羔,為了避免運(yùn)行時(shí)突然報(bào)錯(cuò)找不到方法等問(wèn)題井仰,少使用performSelector方法。

二 延遲執(zhí)行
[obj performSelector:@selector(xxx:) withObject:@"xxx" afterDelay:4.f];

NSTimer:當(dāng)一個(gè)NSTimer注冊(cè)到Runloop后破加,Runloop會(huì)重復(fù)的在相應(yīng)的時(shí)間點(diǎn)注冊(cè)事件俱恶,當(dāng)然Runloop為了節(jié)省資源并不會(huì)在準(zhǔn)確的時(shí)間點(diǎn)觸發(fā)事件。
performSelector:withObject:afterDelay:其實(shí)就是在內(nèi)部創(chuàng)建了一個(gè)NSTimer,然后會(huì)添加到當(dāng)前線程的Runloop中合是。所以當(dāng)該方法添加到子線程中時(shí)了罪,需要格外的注意兩個(gè)地方:

① 在子線程中執(zhí)行會(huì)不會(huì)調(diào)用test方法

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
});

會(huì)發(fā)現(xiàn)test方法并沒(méi)有被調(diào)用,因?yàn)樽泳€程中的runloop默認(rèn)是沒(méi)有啟動(dòng)的狀態(tài)聪全。使用run方法開(kāi)啟當(dāng)前線程的runloop泊藕,但是一定要注意run方法和執(zhí)行該延遲方法的順序。

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
         [[NSRunLoop currentRunLoop] run];
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
});

會(huì)發(fā)現(xiàn)即便添加了run方法难礼,但是test方法還是沒(méi)有被調(diào)用娃圆,在最后打印當(dāng)前線程的runloop,會(huì)發(fā)現(xiàn):

timers = <CFArray 0x6000002a8100 [0x109f67bb0]>{type = mutable-small, count = 1, values = (
    0 : <CFRunLoopTimer 0x6000001711c0 [0x109f67bb0]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 544280547 (1.98647892 @ 3795501066754), callout = (Delayed Perform) lZLearningFromInterviewController test (0x105ea0d9c / 0x104b2e2c0) (), context = <CFRunLoopTimer context 0x600000470080>}

子線程的runloop中確實(shí)添加了一個(gè)CFRunLoopTimer的事件蛾茉,但是到最后都不會(huì)被執(zhí)行讼呢。
將run方法和performSelector延遲方法調(diào)換順序后運(yùn)行:

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
        [[NSRunLoop currentRunLoop] run];
});

所以在子線程中兩者的順序必須是先執(zhí)行performSelector延遲方法之后再執(zhí)行run方法。因?yàn)閞un方法只是嘗試想要開(kāi)啟當(dāng)前線程中的runloop谦炬,但是如果該線程中并沒(méi)有任何事件(source悦屏、timer里覆、observer)的話耍鬓,并不會(huì)成功的開(kāi)啟.

② test方法中執(zhí)行的線程

 [self performSelector:@selector(test) withObject:nil afterDelay:2];

如果在子線程中調(diào)用該performSelector延遲方法,會(huì)發(fā)現(xiàn)調(diào)用該延遲方法的子線程和test方法中執(zhí)行的子線程是同一個(gè)韧涨,也就是說(shuō):
對(duì)于該performSelector延遲方法而言稚机,如果在主線程中調(diào)用幕帆,那么test方法也是在主線程中執(zhí)行获搏;如果是在子線程中調(diào)用赖条,那么test也會(huì)在該子線程中執(zhí)行。

在回答完延遲方法之后常熙,會(huì)將該方法和performSelector:withObject:作對(duì)比纬乍,那么performSelector:withObject:在不添加到子線程的Runloop中時(shí)是否能執(zhí)行?

performSelector:withObject:只是一個(gè)單純的消息發(fā)送裸卫,和時(shí)間沒(méi)有一點(diǎn)關(guān)系仿贬。所以不需要添加到子線程的Runloop中也能執(zhí)行。

三 異步執(zhí)行

如何在不使用GCD和NSOperation的情況下墓贿,實(shí)現(xiàn)異步線程茧泪?

  1. NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[NSThread detachNewThreadSelector:@selector(test) toTarget:self withObject:nil];
[NSThread detachNewThreadWithBlock:^{

        NSLog(@"block中的線程 ---- %@",[NSThread currentThread]);
}];
  1. performSelectorInBackground
 [self performSelectorInBackground:@selector(test) withObject:nil];

該方法一目了然,開(kāi)啟新的線程在后臺(tái)執(zhí)行test方法

  1. performSelector:onThread:在指定線程執(zhí)行
waitUntilDone 當(dāng)這個(gè)參數(shù)為YES,時(shí)表示當(dāng)前runloop循環(huán)中的時(shí)間馬上響應(yīng)這個(gè)事件聋袋,
如果為NO則runloop會(huì)將這個(gè)事件加入runloop隊(duì)列在合適的時(shí)間執(zhí)行這個(gè)事件

[self performSelector:@selector(test) 
onThread:[NSThread currentThread] 
withObject:nil 
waitUntilDone:YES];

要注意队伟,在執(zhí)行performSelector:onThread:withObject:waitUntilDone方法時(shí)候,如果是在另外一個(gè)線程執(zhí)行幽勒,必須保證另外的線程是有一個(gè)runloop.具體的使用可以參考AFNetworking.

四 多參傳遞

performSelector如何進(jìn)行多值傳輸嗜侮?

  1. NSArray或者NSDictionary或者自定義Model的形式
//封裝參數(shù)
    NSDictionary *dic = @{@"param1":@"this is a string",@"param2":@[@2,@3,@3],@"param3":@123};
//調(diào)用方法
    [self performSelector:@selector(testFunctionWithParams:) withObject:dic];

- (void)testFunctionWithParams:(NSDictionary *)paramDic {
    NSLog(@"%s dic:%@",__FUNCTION__, paramDic);
}
  1. objc_msgSend:
{
    NSNumber *age = [NSNumber numberWithInt:20];
    NSString *name = @"李周";
    NSString *gender = @"女";
    NSArray *friends = @[@"謝華華",@"亞呼呼"];

    SEL selector = NSSelectorFromString(@"getAge:name:gender:friends:");
    NSArray *array = @[age,name,gender,friends];

    ((void(*)(id,SEL,NSNumber*,NSString*,NSString*,NSArray*)) objc_msgSend)(self,selector,age,name,gender,friends);

}

- (void)getAge:(NSNumber *)age name:(NSString *)name gender:(NSString *)gender friends:(NSArray *)friends
{
    NSLog(@"%d----%@---%@---%@",[age intValue],name,gender,friends[0]);
}
  1. 用NSInvocation傳遞
//可以傳多個(gè)參數(shù)的方法
- (id)performSelector:(SEL)selector withObjects:(NSArray *)objects
{
    // 方法簽名(方法的描述)
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector];
    if (signature == nil) {
        
        //可以拋出異常也可以不操作。
    }
    
    // NSInvocation : 利用一個(gè)NSInvocation對(duì)象包裝一次方法調(diào)用(方法調(diào)用者、方法名锈颗、方法參數(shù)顷霹、方法返回值)
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    
    // 設(shè)置參數(shù)
    NSInteger paramsCount = signature.numberOfArguments - 2; // 除self、_cmd以外的參數(shù)個(gè)數(shù)
    paramsCount = MIN(paramsCount, objects.count);
    for (NSInteger i = 0; i < paramsCount; i++) {
        id object = objects[i];
        if ([object isKindOfClass:[NSNull class]]) continue;
        [invocation setArgument:&object atIndex:i + 2];
    }
    
    // 調(diào)用方法
    [invocation invoke];
    
    // 獲取返回值
    id returnValue = nil;
    if (signature.methodReturnLength) { // 有返回值類型击吱,才去獲得返回值
        [invocation getReturnValue:&returnValue];
    }
    
    return returnValue;
}

//調(diào)用方法
NSArray *paramArray = @[@"112",@[@2,@13],@12];
    [self performSelector:@selector(textFunctionWithParam:param2:param3:) withObjects:paramArray];


//要調(diào)用的方法
-(void)textFunctionWithParam:(NSString *)param1 param2:(NSArray *)param2 param3:(NSInteger)param3 {
    NSLog(@"param1:%@, param2:%@, param3:%ld",param1, param2, param3);
}
5. 如何使線程绷艿恚活
  • 在NSThread執(zhí)行的方法中添加while(true){},這樣是模擬runloop的運(yùn)行原理覆醇,結(jié)合GCD的信號(hào)量绅喉,在{}中處理任務(wù)。參考這篇文章
  • 采用runloop的方式叫乌。參考這篇文章

KVO

1. 實(shí)現(xiàn)原理

KVO 的實(shí)現(xiàn)也依賴于 Objective-C 強(qiáng)大的 Runtime

簡(jiǎn)單概述下 KVO 的實(shí)現(xiàn):
當(dāng)你觀察一個(gè)對(duì)象時(shí)柴罐,一個(gè)新的類NSKVONotifying_XXX會(huì)動(dòng)態(tài)被創(chuàng)建。這個(gè)類繼承自該對(duì)象的原本的類憨奸,并重寫了被觀察屬性的setter 方法革屠。自然,重寫的 setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前之后排宰,通知所有觀察對(duì)象值的更改似芝。最后把這個(gè)對(duì)象的isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個(gè)對(duì)象的類是什么 ) 指向這個(gè)新創(chuàng)建的子類,對(duì)象就神奇的變成了新創(chuàng)建的子類的實(shí)例

setter方法前后會(huì)調(diào)用:

  • willChangeValueForKey
  • didChangeValueForKey
2. 如何手動(dòng)關(guān)閉kvo
  • 重寫被觀察對(duì)象的automaticallyNotifiesObserversForKey方法板甘,返回NO
  • 重寫automaticallyNotifiesObserversOf<key>党瓮,返回NO

注意:關(guān)閉kvo后,需要手動(dòng)在賦值前后添加willChangeValueForKeydidChangeValueForKey盐类,才可以收到觀察通知寞奸。

參考這篇文章

3. 通過(guò)KVC修改屬性會(huì)觸發(fā)KVO么

會(huì)

4. 哪些情況下使用kvo會(huì)崩潰,怎么防護(hù)崩潰
  1. 多次重復(fù)移除同一個(gè)屬性在跳,移除了未注冊(cè)的觀察者
    解決辦法:根據(jù)實(shí)際情況枪萄,增加一個(gè)添加keyPath的標(biāo)記,在dealloc中根據(jù)這個(gè)標(biāo)記猫妙,刪除觀察者瓷翻。
  2. dealloc 沒(méi)有移除 kvo 觀察者
    解決方案:創(chuàng)建一個(gè)中間對(duì)象,將其作為某個(gè)屬性的觀察者割坠,然后dealloc的時(shí)候去做移除觀察者齐帚,而調(diào)用者是持有中間對(duì)象的,調(diào)用者釋放了彼哼,中間對(duì)象也釋放了对妄,dealloc 也就移除觀察者了;
  3. 添加了觀察者沪羔,但未實(shí)現(xiàn) observeValueForKeyPath:ofObject:change:context:方法饥伊,導(dǎo)致崩潰象浑;
  4. 添加或者移除時(shí) keypath == nil,導(dǎo)致崩潰琅豆;

以下解決方案出自 iOS 開(kāi)發(fā):『Crash 防護(hù)系統(tǒng)』(二)KVO 防護(hù) 一文愉豺。
其實(shí)還可以將觀察者observer委托給另一個(gè)類去完成,這個(gè)類弱引用被觀察者茫因,當(dāng)這個(gè)類銷毀的時(shí)候蚪拦,移除觀察者對(duì)象,參考KVOController冻押。

5. kvo的優(yōu)缺點(diǎn)

缺點(diǎn)補(bǔ)充:

  • 只能通過(guò)重寫 -observeValueForKeyPath:ofObject:change:context:方法來(lái)獲得通知驰贷。
  • 不同通過(guò)指定selector的方式獲取通知。
  • 不能通過(guò)block的方式獲取通知洛巢。

參考這篇文章

6. 如何去實(shí)現(xiàn)帶Block的KVO括袒?

首先,我們創(chuàng)建 NSObject 的 Category稿茉,并在頭文件中添加兩個(gè) API:

typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);

@interface NSObject (KVO)

- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block;

- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;

@end

接下來(lái)锹锰,實(shí)現(xiàn) ·PG_addObserver:forKey:withBlock: ·方法。邏輯并不復(fù)雜:

  1. 檢查對(duì)象的類有沒(méi)有相應(yīng)的 setter 方法漓库。如果沒(méi)有拋出異常恃慧;
  2. 檢查對(duì)象 isa 指向的類是不是一個(gè) KVO 類。如果不是渺蒿,新建一個(gè)繼承原來(lái)類的子類痢士,并把 isa 指向這個(gè)新建的子類;
  3. 檢查對(duì)象的 KVO 類重寫過(guò)沒(méi)有這個(gè) setter 方法茂装。如果沒(méi)有怠蹂,添加重寫的 setter 方法;
  4. 添加這個(gè)觀察者
- (void)PG_addObserver:(NSObject *)observer
                forKey:(NSString *)key
             withBlock:(PGObservingBlock)block
{
    // Step 1: Throw exception if its class or superclasses doesn't implement the setter
    SEL setterSelector = NSSelectorFromString(setterForGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        // throw invalid argument exception
    }
    
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    
    // Step 2: Make KVO class if this is first time adding observer and 
    //          its class is not an KVO class yet
    if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
        clazz = [self makeKvoClassWithOriginalClassName:clazzName];
        object_setClass(self, clazz);
    }
    
    // Step 3: Add our kvo setter method if its class (not superclasses) 
    //          hasn't implemented the setter
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    }
// Step 4: Add this observation info to saved observation objects
    PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

第一步里训唱,先通過(guò) setterForGetter()方法獲得相應(yīng)的 setter 的名字(SEL)褥蚯。也就是把 key 的首字母大寫,然后前面加上 set 后面加上 :况增,這樣 key 就變成了 setKey:。然后再用 class_getInstanceMethod 去獲得 setKey: 的實(shí)現(xiàn)(Method)训挡。如果沒(méi)有澳骤,自然要拋出異常。

第二步澜薄,我們先看類名有沒(méi)有我們定義的前綴为肮。如果沒(méi)有,我們就去創(chuàng)建新的子類肤京,并通過(guò) object_setClass() 修改 isa 指針颊艳。

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
    NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    // class doesn't exist yet, make it
    Class originalClazz = object_getClass(self);
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    // grab class method's signature so we can borrow it
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

動(dòng)態(tài)創(chuàng)建新的類需要用 objc/runtime.h 中定義的 objc_allocateClassPair() 函數(shù)茅特。傳一個(gè)父類,類名棋枕,然后額外的空間(通常為 0)白修,它返回給你一個(gè)類。然后就給這個(gè)類添加方法重斑,也可以添加變量兵睛。這里,我們只重寫了 class 方法窥浪。哈哈祖很,跟 Apple 一樣,這時(shí)候我們也企圖隱藏這個(gè)子類的存在漾脂。最后 objc_registerClassPair() 告訴 Runtime 這個(gè)類的存在假颇。

第三步,重寫 setter 方法骨稿。新的 setter 在調(diào)用原 setter 方法后拆融,通知每個(gè)觀察者(調(diào)用之前傳入的 block )

static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    
    if (!getterName) {
        // throw invalid argument exception
    }
    
    id oldValue = [self valueForKey:getterName];
    
    struct objc_super superclazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    // cast our pointer so the compiler won't complain
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, which is original class's setter method
    objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
    
    // look up observers and call the blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
    for (PGObservationInfo *each in observers) {
        if ([each.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                each.block(self, getterName, oldValue, newValue);
            });
        }
    }
}

細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)我們對(duì) objc_msgSendSuper 進(jìn)行類型轉(zhuǎn)換。在 Xcode 6 里啊终,新的 LLVM 會(huì)對(duì) objc_msgSendSuper 以及 objc_msgSend 做嚴(yán)格的類型檢查镜豹,如果不做類型轉(zhuǎn)換。Xcode 會(huì)抱怨有 too many arguments 的錯(cuò)誤蓝牲。(在 WWDC 2014 的視頻 What new in LLVM 中有提到過(guò)這個(gè)問(wèn)題趟脂。)

最后一步,把這個(gè)觀察的相關(guān)信息存在 associatedObject 里例衍。觀察的相關(guān)信息(觀察者昔期,被觀察的 key, 和傳入的 block )封裝在 PGObservationInfo 類里。

@interface PGObservationInfo : NSObject

@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PGObservingBlock block;

@end

就此佛玄,一個(gè)基本的 KVO 就可以 work 了硼一。當(dāng)然,這只是一個(gè)一天多做出來(lái)的小東西梦抢,會(huì)有 bug,也有很多可以優(yōu)化完善的地方奥吩。

完整的例子可以從這里下載:ImplementKVO

7. KVOController的原理哼蛆?

Facebook開(kāi)源框架

KVO 存在的問(wèn)題
KVO 本身寫起來(lái)并不友好,存在一些問(wèn)題:

  • 需要手動(dòng)移除觀察者
  • 處理觀察事件需要和注冊(cè)觀察事件割裂開(kāi)

那么如何解決呢霞赫?

沒(méi)有什么是一個(gè)中間變量不能解決的腮介。可以創(chuàng)建一個(gè)實(shí)例端衰,觀察的事件由它分發(fā)叠洗,在其 dealloc 方法中移除觀察者甘改。這樣就不用在外部業(yè)務(wù)方法中移除了。KVOController 也是這么做的灭抑。

總結(jié)
整個(gè)庫(kù)的流程是:

  1. KVOController觀察的對(duì)象作為其 NSMapTable 屬性 _objectInfomap 的十艾,把整個(gè)回調(diào)環(huán)境組成的對(duì)象 KVOInfo 作為保存起來(lái)。同時(shí)通過(guò)一個(gè)單例的 KVOSharedController 執(zhí)行具體的注冊(cè)與監(jiān)聽(tīng)方法名挥。

  2. KVO 自動(dòng)取消監(jiān)聽(tīng)的核心在于讓 KVOController 這類的中間類的生命周期和被監(jiān)聽(tīng)的 object 同步疟羹,而不是和 Observer 同步。因?yàn)橘骶螅挥性诒槐O(jiān)聽(tīng)對(duì)象回收的時(shí)候取消監(jiān)聽(tīng)才能真正避免 crash 的危險(xiǎn)榄融。

NSMapTable
NSMapTable 相比較 NSDictionary 的優(yōu)勢(shì)有:

  • NSDictionary 必須是 key-obj 的形式,key 必須是滿足 NSCopying 協(xié)議的救湖;NSMapTable 則是 obj-obj 的形式
  • NSDictionary 的 obj 是強(qiáng)引用愧杯;NSMapTable 的 key 和 value 都可以自己決定是強(qiáng)引用還是弱引用。如果弱引用回收后鞋既,會(huì)自動(dòng)刪除力九。
[[NSMapTable alloc] initWithKeyOptions:keyOptions 
valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality 
capacity:0];

NSMapTable 的選項(xiàng)
NSMapTableStrongMemory (a “memory option”)
NSMapTableWeakMemory (a “memory option”)
NSMapTableObjectPointerPersonality (a “personality option”)
NSMapTableCopyIn (a “copy option”)

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市邑闺,隨后出現(xiàn)的幾起案子跌前,更是在濱河造成了極大的恐慌,老刑警劉巖陡舅,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抵乓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡靶衍,警方通過(guò)查閱死者的電腦和手機(jī)灾炭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)颅眶,“玉大人蜈出,你說(shuō)我怎么就攤上這事√涡铮” “怎么了铡原?”我有些...
    開(kāi)封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)煤杀。 經(jīng)常有香客問(wèn)我眷蜈,道長(zhǎng),這世上最難降的妖魔是什么沈自? 我笑而不...
    開(kāi)封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮辜妓,結(jié)果婚禮上枯途,老公的妹妹穿的比我還像新娘忌怎。我一直安慰自己,他們只是感情好酪夷,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布榴啸。 她就那樣靜靜地躺著,像睡著了一般晚岭。 火紅的嫁衣襯著肌膚如雪鸥印。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天坦报,我揣著相機(jī)與錄音库说,去河邊找鬼。 笑死片择,一個(gè)胖子當(dāng)著我的面吹牛潜的,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播字管,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼啰挪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了嘲叔?” 一聲冷哼從身側(cè)響起亡呵,我...
    開(kāi)封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎硫戈,沒(méi)想到半個(gè)月后锰什,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掏愁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年歇由,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片果港。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沦泌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出辛掠,到底是詐尸還是另有隱情谢谦,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布萝衩,位于F島的核電站回挽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏猩谊。R本人自食惡果不足惜千劈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望牌捷。 院中可真熱鬧墙牌,春花似錦涡驮、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至虽风,卻和暖如春棒口,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辜膝。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工无牵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人内舟。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓合敦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親验游。 傳聞我的和親對(duì)象是個(gè)殘疾皇子充岛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • 面向?qū)ο蟮娜筇卣?并作簡(jiǎn)單的介紹。 面向?qū)ο蟮娜齻€(gè)基本特征是:封裝耕蝉、繼承崔梗、多態(tài)。 1.封裝是面向?qū)ο蟮奶卣髦?...
    xiny123閱讀 1,437評(píng)論 0 6
  • 設(shè)計(jì)模式是什么垒在? 你知道哪些設(shè)計(jì)模式蒜魄,并簡(jiǎn)要敘述? 設(shè)計(jì)模式是一種編碼經(jīng)驗(yàn)场躯,就是用比較成熟的邏輯去處理某一種類型的...
    iOS菜鳥(niǎo)大大閱讀 709評(píng)論 0 1
  • 面向?qū)ο蟮娜筇匦裕悍庋b谈为、繼承、多態(tài) OC內(nèi)存管理 _strong 引用計(jì)數(shù)器來(lái)控制對(duì)象的生命周期踢关。 _weak...
    運(yùn)氣不夠技術(shù)湊閱讀 1,108評(píng)論 0 10
  • 翻譯來(lái)源: RunLoops Run Loops RunLoops是與線程緊密相關(guān)的基礎(chǔ)架構(gòu)的一部分伞鲫,簡(jiǎn)稱運(yùn)行循環(huán)...
    AlexCorleone閱讀 570評(píng)論 0 1
  • 反射機(jī)制實(shí)在【運(yùn)行狀態(tài)】中,對(duì)于任意一個(gè)類签舞,都能夠知道這個(gè)類的所有屬性和方法秕脓,對(duì)于任意一個(gè)對(duì)象,都能夠調(diào)用它的任意...
    孔嘚嘚兒閱讀 254評(píng)論 0 1