目錄
觀察者模式
通知機制
基本使用
注冊觀察者
創建一個通知
發送通知
通知與多線程
使用異步發送通知
NSNotificationQueue通知隊列
在子線程中運行觀察者函數
實現原理
named表
nameless表
wildcard表
添加觀察者
發送通知
移除通知?
KVO機制
基本使用
一對多
實現原理
KVO Crash
同一個類的不同對象在使用KVO時,是否會創造出不同的中間子類
單例模式
基本使用
實現原理
懶漢式
餓漢式
代理模式
關于block和代理的選擇
其他設計模式
設計模式原則
設計模式一覽
觀察者模式
iOS中常見的觀察者模式有通知機制、KVO機制,這種模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主體對象,這個主體對象在狀態上發生變化時,會通知所有觀察者對象,使他們能自動更新自己。簡單地說就是,A需要響應B的變化,就注冊A為觀察者,當B發生變化時通知A,告知B發生了變化,這個也叫經典觀察者模式
通知機制
基本使用
通知機制的基本使用包括三個步驟:注冊觀察者、創建通知、發送通知
注冊觀察者
//方式一:觀察者接收到通知后執行任務的代碼在發送通知的線程中執行
[[NSNotification defaultCenter] addObserver:self selector:@selector(changeString:) name:@"ChangeStringWithInfo" object:nil]; //object用來指定發送通知的對象
?
//方式二:觀察者接收到通知后執行任務的代碼在指定的操作隊列中執行
self.observer = [notificationCenter addObserverForName:@"ChangeStringWithoutInfo" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull notification) {NSLog(@"%@", notification.userInfo[@"string"]);}];
?
//使用block創建時需要用一個id類型的屬性來保存方法的返回值 因為這個方法沒有把self注冊為observer,而是注冊了一個匿名對象,返回的就是這個匿名對象 如果不把這個對象保存起來的話,就無法在dealloc中remove這個觀察者,block就會一直保存在通知中心,導致內存泄漏
- (void)dealloc {[[NSNotification defaultCenter] removeObserver:self.observer];
}
在iOS9以后,注冊的觀察者已經不用在dealloc中移除了,系統會自動將其移除,但是一定不要忘記使用block注冊觀察者返回的對象還是需要在dealloc中移除,不然block和匿名的觀察者就會一直保存在通知中心,導致內存泄漏(注意block不要強引用self,否則會導致循環引用)
創建一個通知
//1.不帶信息NSNotification* notification = [NSNotification notificationWithName:@"ChangeStringWithoutInfo" object:self];
//2.帶信息NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys:self.string,@"string", nil];NSNotification* notification1 = [NSNotification notificationWithName:@"ChangeStringWithInfo" object:self userInfo:userInfo];
發送通知
//1.先創建好通知再直接發送通知[[NSNotificationCenter defaultCenter] postNotification:notification1];//2.不用先創建通知,這個方法自己創建并發送[[NSNotificationCenter defaultCenter] postNotificationName:@"ChangeStringWithoutInfo" object:self];[[NSNotificationCenter defaultCenter] postNotificationName:@"ChangeStringWithoutInfo" object:self userInfo:userInfo];});
通知與多線程
通知的多線程要從兩個方面來看,一個是發送通知的操作,另一個是觀察者的執行操作,如果并發隊列中異步派發通知的發送操作,那么如果是同步觀察者,也只會阻塞每條通知各自的線程,但會開辟多個線程,觀察者函數的執行實現異步的效果,如果是異步觀察者那觀察者函數執行則肯定是異步的。別的情況下觀察者函數的執行都是同步的(這里除黑體別管了,要理解底層的原理,NSNotification的接口只會決定觀察者是異步還是同步,只決定通知的發送會不會被阻塞,別的要根據通知發送的異步同步派發和串行并發隊列去具體情況具體分析)
通知的發送是同步的,意味著如果是同步觀察者,那么觀察者函數的執行會阻塞通知的發送,如果是異步觀察者,那么觀察者的函數會被放到一個隊列中等待執行,函數的執行不會阻塞通知的發送。同步觀察者其實就是觀察者要執行的代碼是在發送通知的線程上執行的(queue參數為nil),而異步觀察者則表明函數是在非當前線程的指定隊列中執行的,函數執行是否并發取決于隊列的類型和提交方式(queue設置為非當前線程的隊列)
有一條規則:同步或異步觀察者只能決定通知發送是否被阻塞,而觀察者函數的執行要綜合考慮通知發送和函數執行的隊列類型和派發方式
一個通知發送時會做這些事:
-
查找所有匹配的觀察者
-
執行所有同步觀察者
-
提交所有異步觀察者到目標隊列
使用異步發送通知
NSNotificationQueue通知隊列
通過NSNotificationQueue通知隊列,可以實現異步的發送通知,通過它不會直接把通知發給通知中心,而是先發給這個隊列,再由這個隊列決定在當前runLoop結束的時候或者空閑的時候轉發給通知中心,再由通知中心轉發給注冊的觀察者。通過這個隊列可以合并重復的通知,以便只發送一個通知
NSNotificationQueue遵循FIFO的順序,當一個通知移動到NSNotificationQueue的最前面,它就被發送給notification Center,然后notification Center再將通知轉發給注冊了該通知的監聽者
每一個線程都有一個默認的NSNotificationQueue,這個NSNotificationQueue和通知中心聯系在一起。當然我們也可以自己創建NSNotificationQueue,可以為一個線程創建多個NSNotificationQueue。
通過類方法創建NSNotificationQueue實例:
NSNotificationQueue* notificationQueue = [NSNotificationQueue defaultQueue];
接著通過方法將通知送到通知隊列
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
?
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
在這個方法中,postingStyle是一個枚舉量,表明隊列發送通知的策略
typedef NS_ENUM(NSUInteger, NSPostingStyle) {NSPostWhenIdle = 1,NSPostASAP = 2,NSPostNow = 3
};
-
NSPostWhenIdle 在空閑時發送,即當本線程的runloop空閑時就發送通知到通知中心
-
NSPostASAP 盡可能快地發送通知,即當前通知或者timer的回調執行完畢后發送通知到通知中心
-
NSPostNow 多個相同的通知合并之后馬上發送
coalesceMask這個參數表明多個通知的合并方式,也是一個枚舉類型,當向隊列中發送多個重復的通知時,有時我們不希望這些通知全部發送給通知中心,這時就可以使用這個枚舉類型的參數表明通知的合并策略
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {NSNotificationNoCoalescing = 0,NSNotificationCoalescingOnName = 1,NSNotificationCoalescingOnSender = 2
};
-
NSNotifacationNoCoalescing 不管是否重復,不合并
-
NSNotifacationCoalescingOnName 按照通知的名字,如果名字重復就移除重復的
-
NSNotifacationCoalescingOnSender 按照發送方,如果多個通知的發送方是一樣的,就只保留一個
modes這個參數用來指定runloop的mode,指定mode后,只有當前線程的runloop在這個特定的mode下才能將通知發送到通知中心
這里當NSPostingStyle的類型是NSPostWhenIdle和NSPostASAP時是異步的,但是類型是NSPostNow時時同步的
在子線程中運行觀察者函數
把觀察者函數放到新開的子線程(GCD異步派發+串行隊列)中運行也可以實現類似異步的效果,因為每一個同步的通知函數中都只需要開辟新的線程,很快就可以結束,函數執行在新的線程中,只會阻塞新的線程
- (void)test {dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);dispatch_async(queue, ^{ ? ?// 異步執行 + 串行隊列NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Handle notification and sleep 3s");sleep(3);});
}
實現原理
通知機制的核心是一個與線程關聯的單例對象叫通知中心,這個單例類中主要定義了兩個結構體,一個用來存儲所有注冊通知信息的表,另一個用來保存單個注冊信息的節點
typedef struct NCTbl {Observation ? ? ? *wildcard; ?// 添加觀察者時既沒有傳入 NotificationName ,又沒有傳入object,就會加在這個鏈表上,它里邊的觀察者可以接收所有的系統通知GSIMapTable ? ? ? nameless; ? // 添加觀察者時沒有傳入 NotificationName 的表GSIMapTable ? ? ? named; ? ? ?// 添加觀察者時傳入了 NotificationName 的表
} NCTable
?
typedef struct Obs {id ? ? ? ?observer; ? // 觀察者對象SEL ? ? ? selector; ? // 方法信息struct Obs ? ?*next; ? ? ?// 指向下一個節點int ? ? ? retained; ? /* Retain count for structure. */struct NCTbl ?*link; ? ? ?/* Pointer back to chunk table */
} Observation;
三個表的結構各有不同:
named表
named表中,通知的名字作為表的key,而value中由于還要保存object的信息,因此得用一張表來保存object和Observer的對應關系,當有多個觀察者時Observer用一個鏈表來保存
當參數object為nil時,系統會根據nil生成一個key,這個key對應的value(鏈表)保存的就是傳入了通知名字沒有傳入object的所有觀察者
nameless表
nameless表沒有通知名,因此不用外層的鍵值對應關系:
wildcard表
這個表就是既沒有通知名字也沒有object,所以他會在nameless基礎上再脫去一層鍵值關系,直接存所有可以相應所有通知的觀察者的信息
添加觀察者
?
- (void) addObserver: (id)observerselector: (SEL)selectorname: (NSString*)nameobject: (id)object
{Observation ? ? ? ?*list;Observation ? ? ? ?*o;GSIMapTable ? ? ? ?m;GSIMapNode ? ? ? ?n;
// observer為空時的報錯if (observer == nil)[NSException raise: NSInvalidArgumentExceptionformat: @"Nil observer passed to addObserver ..."];
// selector為空時的報錯if (selector == 0)[NSException raise: NSInvalidArgumentExceptionformat: @"Null selector passed to addObserver ..."];
// observer不能響應selector時的報錯if ([observer respondsToSelector: selector] == NO){[NSException raise: NSInvalidArgumentExceptionformat: @"[%@-%@] Observer '%@' does not respond to selector '%@'",NSStringFromClass([self class]), NSStringFromSelector(_cmd),observer, NSStringFromSelector(selector)];}
// 給表上鎖lockNCTable(TABLE);
// 建立一個新Observation,存儲這次注冊的信息o = obsNew(TABLE, selector, observer);// 如果有nameif (name) {// 在named表中 以name為key尋找valuen = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);// named表中沒有找到對應的valueif (n == 0) {// 新建一個表m = mapNew(TABLE);// 由于這是對給定名稱的首次觀察,因此我們對該名稱進行了復制,以便在map中無法對其進行更改(來自GNUStep的注釋)name = [name copyWithZone: NSDefaultMallocZone()];// 新建表作為name的value添加在named表中GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);GS_CONSUMED(name)} else { //named表中有對應的value// 取出對應的valuem = (GSIMapTable)n->value.ptr;}// 將observation添加到正確object的列表中// 獲取添加完后name對應的value的object對應的鏈表n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);// n是object的valueif (n == 0) { // 如果object對應value沒有數據o->next = ENDOBS;// 將o作為object的value鏈表的頭結點插入GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);} else { // 如果有object對應的value那么就直接添加到原練表的尾部// 在鏈表尾部加入olist = (Observation*)n->value.ptr;o->next = list->next;list->next = o;}// 這個else if 就是沒有name有object的Observation,對object進行的操作相同,} else if (object) {// 直接獲取object對應的value鏈表n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);if (n == 0) { // 這個對應鏈表如果沒有數據o->next = ENDOBS;// 將該observation作為頭節點插入GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);} else { // 有數據,將obsevation直接插在原鏈表的后面list = (Observation*)n->value.ptr;o->next = list->next;list->next = o;}} else {// 既沒有name又沒有object,就加在WILDCARD鏈表中o->next = WILDCARD;WILDCARD = o;}// 解鎖unlockNCTable(TABLE);
}
自然語言描述:
-
先檢查參數是否為空,observer為空或者selector為空或者observer不能響應selector時報錯
-
首先給表上鎖,根據要插入的類信息建立一個新Observation,存儲這次注冊的信息,然后開始對表操作:
-
如果有name,在named表中以name為key查找,找到了就先取出來一會兒用,如果沒有找到,就創一個新的表加到name表里并用指針記錄,這時就獲取到了這個表,然后在這個表中找到object對應的鏈表,接著就把觀察者信息加到這個表里,object對應為空就把新節點作為頭節點,不為空就加到鏈表末尾
-
如果沒有name但是有object,就在nameless表里找,直接根據object找對應的鏈表,然后與之前相同,沒數據就作為頭節點,有數據就存到鏈表末尾
-
如果沒有name也沒有object,就加到wildcard表里,直接存到鏈表末頭部
-
-
然后給表解鎖
發送通知
- (void) postNotification: (NSNotification*)notification {if (notification == nil) {[NSException raise: NSInvalidArgumentExceptionformat: @"Tried to post a nil notification."];}[self _postAndRelease: RETAIN(notification)];
}
?
- (void) postNotificationName: (NSString*)nameobject: (id)object {[self postNotificationName: name object: object userInfo: nil];
}
?
?
- (void) postNotificationName: (NSString*)nameobject: (id)objectuserInfo: (NSDictionary*)info {GSNotification ? ? ? ?*notification;
?notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());notification->_name = [name copyWithZone: [self zone]];notification->_object = [object retain];notification->_info = [info retain];[self _postAndRelease: notification];
}
發送通知最后都是調用_postAndRelease,有一個是直接用傳進來的NSNotification,另外兩個是根據傳進來的信息創建一個新的NSNotification,所以重點就在于_postAndRelease?
- (void) _postAndRelease: (NSNotification*)notification {Observation ? ? ? ?*o;unsigned ? ? ? ?count;NSString ? ? ? ?*name = [notification name];id ? ? ? ? ? ? ? ?object;GSIMapNode ? ? ? ?n;GSIMapTable ? ? ? ?m;GSIArrayItem ? ? ? ?i[64];GSIArray_t ? ? ? ?b;GSIArray ? ? ? ?a = &b;// name為空的報錯,注冊時可以注冊無名,注冊無名就等于說是所有的通知都能接收,但是發送通知時不可以if (name == nil) {RELEASE(notification);[NSException raise: NSInvalidArgumentExceptionformat: @"Tried to post a notification with no name."];}object = [notification object];
?GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);lockNCTable(TABLE);// 查找所有未指定name或object的觀察者,加在a數組中,即將wildcard表中的數據都加在新建鏈表中for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next){GSIArrayAddItem(a, (GSIArrayItem)o);}// 查找與通知的object相同但是沒有name的觀察者,加在a數組中if (object) {// 在nameless中找object對應的數據節點n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);if (n != 0) { // 將其加入到新建鏈表中o = purgeCollectedFromMapNode(NAMELESS, n);while (o != ENDOBS) {GSIArrayAddItem(a, (GSIArrayItem)o);o = o->next;}}}
?// 查找name的觀察者,但觀察者的非零對象與通知的object不匹配時除外,加在a數組中if (name) {// 先匹配namen = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));if (n) { // m指向name匹配到的數據m = (GSIMapTable)n->value.ptr;} else {m = 0;}if (m != 0) { // 如果上述name查找到了數據// 首先,查找與通知的object相同的觀察者n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);if (n != 0) { // 找到了與通知的object相同的觀察者,就加入到新建鏈表中o = purgeCollectedFromMapNode(m, n);while (o != ENDOBS) {GSIArrayAddItem(a, (GSIArrayItem)o);o = o->next;}}
?if (object != nil) {// 接著是沒有object的觀察者,都加在新建鏈表中n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);if (n != 0) { // 如果沒有object并且有數據,就把其中的數據加到新建鏈表中o = purgeCollectedFromMapNode(m, n);while (o != ENDOBS) {GSIArrayAddItem(a, (GSIArrayItem)o);o = o->next;}}}}}unlockNCTable(TABLE);
?// 發送通知,給之前新建鏈表中的所有數據count = GSIArrayCount(a);while (count-- > 0) {o = GSIArrayItemAtIndex(a, count).ext;if (o->next != 0) {NS_DURING {// 給observer發送selector,讓其處理[o->observer performSelector: o->selectorwithObject: notification];}NS_HANDLER {BOOL ? ? ? ?logged;// 嘗試將通知與異常一起報告,但是如果通知本身有問題,我們只記錄異常。NS_DURINGNSLog(@"Problem posting %@: %@", notification, localException);logged = YES;NS_HANDLERlogged = NO;NS_ENDHANDLERif (NO == logged){ NSLog(@"Problem posting notification: %@", localException);} ?}NS_ENDHANDLER}}lockNCTable(TABLE);GSIArrayEmpty(a);unlockNCTable(TABLE);
?RELEASE(notification);
}
自然語言描述:
-
首先檢測name是否為空,為空就釋放notification并報錯,然后創建數組用來存儲觀察者,接著給表上鎖,開始查找所有沒指定name或者object的觀察者(wildcard表中的所有數據)加到數組中
-
然后查找object相同但是沒有name的觀察者,加到數組中(也就是在nameless表里查找object對應的那個鏈表,然后遍歷鏈表加到新建的鏈表數組中)
-
接著name對應的觀察者(在name表里找),找到name的數據后查找key與通知的object相同的觀察者并放到新數組里,再找沒有object為nil的那個鏈表,把里面的所有數據都存到新數組里,然后給表解鎖
-
最后遍歷得到的新數組,讓觀察者performSelector去執行對應的函數,然后清空新數組并release通知
移除通知?
- (void) removeObserver: (id)observer {if (observer == nil)return;
?[self removeObserver: observer name: nil object: nil];
}
?
- (void) removeObserver: (id)observername: (NSString*)nameobject: (id)object {// 當其要移除的信息都為空時,直接返回if (name == nil && object == nil && observer == nil)return;
?lockNCTable(TABLE);// name和object都為nil,就在wildcard鏈表里刪除對應observer的注冊信息if (name == nil && object == nil) {WILDCARD = listPurge(WILDCARD, observer);}// name為空時if (name == nil) {GSIMapEnumerator_t ? ? ? ?e0;GSIMapNode ? ? ? ? ? ? ? ?n0;// 首先嘗試刪除為此object對應的所有命名項目// 在named表中e0 = GSIMapEnumeratorForMap(NAMED);n0 = GSIMapEnumeratorNextNode(&e0);while (n0 != 0) {GSIMapTable ? ? ? ? ? ? ? ?m = (GSIMapTable)n0->value.ptr;NSString ? ? ? ? ? ? ? ?*thisName = (NSString*)n0->key.obj;
?n0 = GSIMapEnumeratorNextNode(&e0);if (object == nil) { // 如果object為空,直接清除named表// 清空named表GSIMapEnumerator_t ? ? ? ?e1 = GSIMapEnumeratorForMap(m);GSIMapNode ? ? ? ? ? ? ? ?n1 = GSIMapEnumeratorNextNode(&e1);
?while (n1 != 0) {GSIMapNode ? ? ? ?next = GSIMapEnumeratorNextNode(&e1);
?purgeMapNode(m, n1, observer);n1 = next;}} else {// 以object為key找到對應鏈表,清空該鏈表GSIMapNode ? ? ? ?n1;n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);if (n1 != 0) {purgeMapNode(m, n1, observer);}}if (m->nodeCount == 0) {mapFree(TABLE, m);GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);}}// 開始操作nameless表if (object == nil) { // object為空時// 清空nameless表e0 = GSIMapEnumeratorForMap(NAMELESS);n0 = GSIMapEnumeratorNextNode(&e0);while (n0 != 0) {GSIMapNode ? ? ? ?next = GSIMapEnumeratorNextNode(&e0);
?purgeMapNode(NAMELESS, n0, observer);n0 = next;}} else { // object不為空// 找到對應的observer鏈表,清空該鏈表n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);if (n0 != 0) {purgeMapNode(NAMELESS, n0, observer);}}} else { // name不為空GSIMapTable ? ? ? ? ? ? ? ?m;GSIMapEnumerator_t ? ? ? ?e0;GSIMapNode ? ? ? ? ? ? ? ?n0;
?n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));// 如果沒有和這個name相同的key,直接返回if (n0 == 0) {unlockNCTable(TABLE);return; ? ? ? ? ? ? ? ?/* Nothing to do. ? ? ? */}m = (GSIMapTable)n0->value.ptr; // 找到name作為key對應的數據信息
?if (object == nil) {// 如果object為nil,就清空剛才找到的name對應的數據信息e0 = GSIMapEnumeratorForMap(m);n0 = GSIMapEnumeratorNextNode(&e0);
?while (n0 != 0) {GSIMapNode ? ? ? ?next = GSIMapEnumeratorNextNode(&e0);
?purgeMapNode(m, n0, observer);n0 = next;}} else {// 如果object不為空,清空object對應的鏈表n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);if (n0 != 0) {purgeMapNode(m, n0, observer);}}// 因為其中的數據清除完了,所以記得清除named表中的作為key的nameif (m->nodeCount == 0) {mapFree(TABLE, m);GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));}}unlockNCTable(TABLE);
}
自然語言描述:
-
如果要移除的信息都為空直接返回
-
然后給表上鎖,如果name和object都為nil,就在wildcard鏈表里刪除對應observer的注冊信息
-
當name為空時,首先在name表中操作,如果object為空的話,就直接遍歷named表,找到其中value的value與參數相等的為觀察者并移除,如果object不為空,就找到object為key對應的鏈表,在鏈表中移除該觀察者;然后在nameless表里操作,如果object為空,就在nameless表里移除觀察者,如果object不為空,就先找到對應的鏈表,再移除鏈表中對應的觀察者
-
name不為空時,如果沒找到和name相同的key,直接返回,如果找到了而且object為空,就清空name對應的數據中相應的Obserber,如果object不為空,就清空object對應的鏈表中相應的Observer,如果某個name對應的數據清除完了,就清除name表中作為key的name,操作完解鎖
KVO機制
KVO
來監聽對象屬性的變化,并及時做出響應的一種機制,即當指定的被觀察的對象的屬性被修改后,KVO
會自動通知相應的觀察者,它與NSNotificationCenter的區別如下:
相同點
-
1、兩者的實現原理都是
觀察者模式
,都是用于監聽 -
2、都能
實現一對多
的操作
不同點
-
1、
KVO
只能用于監聽對象屬性的變化,并且屬性名都是通過NSString
來查找,編譯器不會幫你檢測對錯和補全,純手敲比較容易出錯 -
2、
NSNotification
的發送監聽(post
)的操作我們可以控制,KVO
由系統控制 -
3、
KVO
可以記錄新舊值
變化
基本使用
KVO的使用分三步:注冊觀察者、實現回調、移除觀察者
[self.subViewController addObserver:self forKeyPath:@"string" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {if ([keyPath isEqualToString:@"string"]) {NSLog(@"KVO:%@", change);}
}
- (void)dealloc {[self.subViewController removeObserver:self forKeyPath:@"name"];
}
這里參數context是用來區分不同對象的同名屬性的,它是一個void*類型
void *subViewControllerContext = &subViewControllerContext;
如果需要指定某些時候觸發KVO,別的時候不觸發,可以使用手動觸發KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {if ([key isEqualToString:@"string"]) {return NO;}return [super automaticallyNotifiesObserversForKey:key];
}//返回NO,不自動觸發KVO
[self.subViewController willChangeValueForKey:@"string"];
self.subViewController.string = @"希珀";
[self.subViewController didChangeValueForKey:@"string"];
//手動觸發
一對多
有時候一個屬性的值取決于當前或者其他對象的一個或多個屬性的值,比如人的FullName取決于fisrtName和lastName
- (NSString *)fullName {return [NSString stringWithFormat:@"%@ %@",_firstName, _lastName];
}
?
//方法一
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];if([key isEqualToString:@"fullName"]) {NSArray* affectingKeys = @[@"lastName", @"firstName"];keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];}return keyPaths;
}
?
//方法二
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName
{return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
實現原理
KVO的底層是通過isa-swizzling實現的,在注冊KVO觀察者時,將修改觀察對象的isa指針,指向中間類而不是真實類
注冊KVO觀察者后,會將isa指針指向原類的子類NSKVONotifing_NameOfCalss,在這個動態子類中觀察的是setter方法,子類會重寫父類中的dealloc方法(使dealloc后isa指回原類,不會銷毀這個動態子類),重寫基類中的class方法和_isKVOA方法,以確保重寫的class方法可以指回原類,還會重寫父類對應屬性的setter方法,在這個方法中會在修改屬性值前后調用willChangeValue和didChangeValueForKey這兩個方法,會觸發監聽者的響應方法
動態子類會根據觀察屬性的automaticallyNotifiesObserversForKey
的布爾值來決定是否生成
KVO Crash
KVO Crash,通常是KVO的被觀察者dealloc時仍然注冊著KVO導致的crash,添加KVO時重復添加觀察者或重復移除觀察者引起的。 一個被觀察的對象上有若干個觀察者,每個觀察者又有若干條keypath。如果觀察者和keypath的數量一多,很容易不清楚被觀察的對象整個KVO關系,導致被觀察者在dealloc的時候,仍然殘存著一些關系沒有被注銷,同時還會導致KVO注冊者和移除觀察者不匹配的情況發生。尤其是多線程的情況下,導致KVO重復添加觀察者或者移除觀察者的情況,這種類似的情況通常發生的比較隱蔽,很難從代碼的層面上排查。
可以讓觀察對象持有一個KVO的delegate,所有和KVO相關的操作均通過delegate來進行管理,delegate通過建立一張MAP表來維護KVO的整個關系,這樣做的好處有2個: 1:如果出現KVO重復添加觀察或者移除觀察者(KVO注冊者不匹配的)情況,delegate可以直接阻止這些非正常的操作。 2:被觀察對象dealloc之前,可以通過delegate自動將與自己有關的KVO關系都注銷掉,避免了KVO的被觀察者dealloc時仍然注冊著KVO導致的crash
同一個類的不同對象在使用KVO時,是否會創造出不同的中間子類
willChangeValue和didChangeValueForKey這兩個方法是怎么觸發監聽者的響應方法的?
這兩個問題可以同時回答。
單例模式
單例模式表明某個類只能生成一個實例,并且提供一個全局的訪問入口訪問這個實例。iOS常見的單例類有這些:UIApplication(應用程序實例類)、NSNotificationCenter(消息中心類)、NSFileManager(文件管理類)、NSUserDefaults(應用程序設置)、NSURLCache(請求緩存類)、NSHTTPCookieStorage(應用程序cookies池)
基本使用
@implementation GCDSingleton
?
static GCDSingleton *singleton;
+(instancetype)sharedInstacne {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{singleton = [[super allocWithZone:nil] init];});return singleton;
}
?
+(instancetype) allocWithZone:(struct _NSZone *)zone {return [self sharedInstacne];
}
?
-(instancetype)copyWithZone:(NSZone *)zone {return singleton;
}
?
@end
實現原理
dispatch_once是通過dispatch_once_f來實現單例類的,&oncetoken作為一個全局靜態變量,可以視為一個開關,在函數中會進行原子操作,以避免多線程的問題,確認線程安全后會進行block里代碼的執行并在執行完畢后廣播,執行完會把開關的狀態改變,把開關關上,第二次進入這個函數時就會判斷到這個開關的值,如果是關上的就直接返回,如果開關的值沒關但是多線程的鎖上鎖了,就會進行無限期等待,等待開鎖。
懶漢式
像一個懶漢,需要的時候再加載
static LazyModeSingleton *instance = nil;
?
+(instancetype)shareInstance {if (!instance) {@synchronized (self) {if (!instance) {instance = [[self alloc] init];}}}return instance;
}
?
+(instancetype)allocWithZone:(struct _NSZone *)zone {if (!instance) {@synchronized (self) {if (!instance) {instance = [super allocWithZone:nil];}}}return instance;
}
餓漢式
像一個餓漢,程序一開始就加載
static LazyModeSingleton *instance = nil;
+(void)load {instance = [[self alloc] init];
}
?
+(instancetype)allocWithZone:(struct _NSZone *)zone {return instance;
}
?
+(instancetype)sharedInstance {return instance;
}
代理模式
代理模式是一種常見的設計模式,用于在對象之間進行通信和交互。在iOS開發中,代理模式經常被使用。
代理模式的核心是委托,其中一個對象(委托方)將一些任務委托給另一個對象(代理方)來完成。代理方負責執行委托方指定的任務,并將結果返回給委托方。
在iOS開發中,通常通過定義協議(Protocol)來使用代理模式。委托方定義一個協議,聲明一組可選或必需的方法。代理方實現該協議,并將自身設置為委托方的代理。然后,委托方可以調用代理去執行協議中的函數并給函數傳參
iOS常見的代理模式有:UITableViewDelegate 和 UITableViewDataSource、UIWebViewDelegate 和 WKNavigationDelegate、UITextFieldDelegate等等
-
協議:用來指定代理雙方可以做什么,必須做什么。
-
代理:根據指定的協議,完成委托方需要實現的功能。
-
委托:根據指定的協議,指定代理去完成什么功能。
代理需要使用weak屬性關鍵字,以免造成循環引用,自定義的協議在使用的時候需要先判斷方法是否實現再調用函數
非正式協議通過category來實現
比如我們可以用代理模式給UITableView瘦身
typedef void (^selectCell) (NSIndexPath *indexPath);
//代理對象的協議需要聲明在.h文件中
@interface TableViewDelegateObj : NSObject <UITableViewDelegate, UITableViewDataSource>
//創建代理對象實例,并將數據列表傳進去
//代理對象通過block將消息向外界傳遞
//return返回實例對象
+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList selectBlock:(selectCell)selectBlock;
代理對象.m文件
#import "TableViewDelegateObj.h"
@interface TableViewDelegateObj () @property (nonatomic, strong) NSArray ? *dataList;
@property (nonatomic, copy) ? selectCell selectBlock;
?
@end@implementation TableViewDelegateObj
?
+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList selectBlock:(selectCell)selectBlock {return [[[self class] alloc] initTableViewDelegateWithDataList:dataListselectBlock:selectBlock];
}
?
?
- (instancetype)initTableViewDelegateWithDataList:(NSArray *)dataList selectBlock:(selectCell)selectBlock {self = [super init];if (self) {self.dataList = dataList;self.selectBlock = selectBlock;}return self;
}
?
?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {static NSString *identifier = @"cell";UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];if (!cell) {cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];}cell.textLabel.text = self.dataList[indexPath.row];return cell;
}
?
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {return self.dataList.count;
}
?
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {[tableView deselectRowAtIndexPath:indexPath animated:NO];// 將點擊事件通過block的方式傳遞出去self.selectBlock(indexPath);
}
?
@end
關于block和代理的選擇
多個消息傳遞時,應該使用delegate而不是block;一個委托對象的代理屬性只有一個代理對象,如果想要委托對象調用多個代理對象的回調應該用block而不是代理;如果是單例對象最好不要用delegate
其他設計模式
設計模式原則
S 單一職責原則告訴我們實現類要職責單一;
O 開閉原則是總綱,它告訴我們要對擴展開放,對修改關閉;
L 里氏替換原則告訴我們不要破壞繼承體系;
L 迪米特法則告訴我們要降低耦合度;
I 接口隔離原則告訴我們在設計接口的時候要精簡單一;
D 依賴倒置原則告訴我們要面向接口編程;