iOS 避免常見崩潰(二)

QiShare發表於2019-03-28

級別: ★★☆☆☆
標籤:「iOS 」「避免常見崩潰」「FBKVOController」「KVO」
作者: WYW
審校: QiShare團隊


前言 專案中可能會用到KVO。關於KVO的基礎使用可以檢視大成哥的iOS KVC與KVO簡介。系統提供的KVO的方式寫起來程式碼較分散,有時候會出問題。

facebook有個開源專案KVOController下文用FBKVOController代指。FBKVOController用起來比較簡單,可以在一定程度避免KVO的常見問題,本文筆者將通過分析FBKVOController,看看FBKVOController是如何避免KVO常見問題的。

使用KVO的常見問題有

  • 新增觀察者的時候,寫錯待觀察的物件屬性名;
  • 多次新增對某物件的屬性的觀察;
  • 忘記移除觀察者,多次移除某觀察者;
  • 移除觀察者的時候,觀察者已釋放;

FBKVOController封裝了系統的KVO,解決上邊提到的相應問題。下邊筆者簡單分析了FBKVOController是如何避免系統KVO相關問題的。筆者將會從如下幾個方面來展開分析FBKVOController。

    1. 系統KVO的簡單使用;
    1. FBKVOController的簡單使用;
    1. FBKVOController的類圖,思維導圖,使用流程圖;
    1. FBKVOController避免寫錯待觀察屬性;
    1. FBKVOController初始化過程;
    1. FBKVOController 觀察某物件的某屬性;
    1. FBKVOController 觀察物件屬性變化;
    1. FBKVOController不需要開發者removeObserver;
    1. FBKVOController的執行緒安全實現方式之互斥鎖;
    1. _FBKVOInfo重寫isEqual:、 hash;
    1. NSMapTable之keyOptions;
    1. NSHashTable之weakObjectsHashTable;

系統KVO的簡單使用

KVO的基礎使用可檢視大成哥的iOS KVC與KVO簡介

筆者下邊貼出的是一個系統方式觀察Person的name屬性的程式碼。

    1. addObserver
    1. 在observeValueForKeyPath方法中檢視person的name屬性的變化情況;
    1. 最後在控制器的dealloc中需要記得removeObserver。
_person = [Person new];
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
_person.name = [NSString stringWithFormat:@"personName:QiShare_%u", arc4random() % 1000];
	
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    NSLog(@"SystemKVOChange:%@", change);
}

- (void)dealloc {
    
    [_person removeObserver:self forKeyPath:@"name"];
}

複製程式碼

FBKVOController的簡單使用

FBKVOController是一個適用於iOS和OS X的簡單的執行緒安全的KVO三方庫。

執行緒安全是通過互斥鎖方式保證的。

FBKVOController的簡單體現在有時我們只需寫一行程式碼即可。

其實FBKVOController也是對系統KVO的封裝。

討論FBKVOController簡單使用的過程中,筆者將以觀察Person類的name屬性為例。 相關程式碼如下:

#import "NSObject+FBKVOController.h"

[self.KVOController observe:_person keyPath:FBKVOKeyPath(_person.name) options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
    NSLog(@"FBKVOChange:%@", change);
}];
複製程式碼

FBKVOController的類圖,思維導圖,使用流程圖

為了便於更容易理解FBKVOController,筆者繪製了FBKVOController的類圖,思維導圖,使用流程圖依次如下:

(思維導圖比較模糊,如有需要請到QiSafeType中下載)

FBKVOClassDiagram.png

FBKVOMind.png

FBKVOFlowChart.png

FBKVOController避免寫錯待觀察屬性;

FBKVOController為了使用過程中,避免寫錯待觀察屬性,設定了2個巨集。

#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))
複製程式碼

筆者仍以觀察Person類的name屬性為例,檢視這2個巨集的使用方式;

這裡的巨集使用了逗號表示式。簡單說逗號表示式的結果就是最右邊的表示式的結果。如(3+5,6+8)的結果為14。

c語言提供一種特殊的運算子,逗號運算子,優先順序別最低,它將兩個及其以上的式子聯接起來,從左往右逐個計算表示式,整個表示式的值為最後一個表示式的值。如:(3+5,6+8)稱為逗號表示式,其求解過程先表示式1,後表示式2,整個表示式值是表示式2的值,如:(3+5,6+8)的值是14,a=(a=35,a4)的值是60,而(a=35,a4)的值是60, a的值在逗號表示式裡一直是15,最後被逗號表示式賦值為60,a的值最終為60。 摘自360百科

FBKVOKeyPath使用了斷言檢測待觀察物件的屬性:

斷言:當需要在一個值為FALSE時,中斷當前操作的話,可以使用斷言。斷言

#define NSCAssert(condition, desc, ...)

Assertions evaluate a condition and, if the condition evaluates to false, call the assertion handler for the current thread, passing it a format string and a variable number of arguments. 
當condition為false的時候會終端當前操作,並且終端操作的原因會展示為desc。
複製程式碼

以_person.name為例,使用FBKVOKeyPath(_person.name),分析FBKVOKeyPath(KEYPATH)

@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

((void)KEYPATH, NO) 是為了編譯_person.name;
編譯通過了之後const char *fbkvokeypath = strchr(#KEYPATH, '.');
	#keypath巨集返回的是字串"_person.name";
fbkvokeypath是'.'#KEYPATH即"_person.name"中的指標。
fbkvokeypath + 1返回的即"name"。
最後結合@,即為待觀察的Person的屬性@"name"
複製程式碼

以_person.name為例,使用FBKVOClassKeyPath(Person, name),分析FBKVOClassKeyPath(CLASS, KEYPATH);


#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))

((CLASS *)(nil)).KEYPATH 部分是為了編譯_person.name
編譯通過後,#KEYPATH返回的是"name",結合@。
即最後的待觀察物件@"name"

複製程式碼

FBKVOController初始化過程

初始化FBKVOController的過程。

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved {
    self = [super init];
    if (nil != self) {
        _observer = observer;
        NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
        _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
        // 初始化互斥鎖
        pthread_mutex_init(&_lock, NULL);
    }
    return self;
}
複製程式碼

如上程式碼所示:

  • 初始化了_observer = observer,這裡的observer是weak修飾的,是為了避免出現Retain Cycle。

    • 如果是strong修飾observer會出現 控制器 持有 FBKVOController,FBKVOController 持有 observer(控制器),出現Retain Cycle。
  • 初始化了_objectInfosMap

    • _objectInfosMap(NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;,用於儲存待觀察物件,待觀察物件為key,及待觀察物件的屬性,及回撥相關資訊,待觀察物件的屬性及其他資訊為value)。
    • NSMapTable的keyOptions為NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality,可以做到,當key(此處為object)釋放的時候,_objectInfosMap中的相應的key value會自動移除。
    • 關於NSMapTable的keyOptions,下文會提到。

這裡FBKVOController例項,可以直接使用NSObject+FBKVOController.h新增的屬性kvoController(即self.kvoController); 也可以使用如下全能初始化方法建立。

- (instancetype)initWithObserver:(nullable id)observer
                  retainObserved:(BOOL)retainObserved NS_DESIGNATED_INITIALIZER;
複製程式碼
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved {
    self = [super init];
    if (nil != self) {
        _observer = observer;
        
        NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
        _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
        
        pthread_mutex_init(&_lock, NULL);
    }
    return self;
}
複製程式碼

注意:防止Retain Cycle

  • 如果在當前類中,觀察當前類的屬性,傳入的retainObserved引數需要傳入NO。

retainObserved引數用於控制,建立FBKVOController例項的時候,_objectInfosMap對key持有弱引用還是強引用。

NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;,key用於儲存待觀察者object,value用於儲存待觀察物件object的待觀察資訊。

在當前類中觀察當前類的屬性的示例:

[self.KVOControllerNonRetaining observe:self keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
	NSLog(@"FBKVOChange:%@", change);     
}];

複製程式碼

上例如果使用self.KVOController。將會出現

self(QiSafeKVOController) 持有 KVOController(FBKVOController)

KVOController(FBKVOController) 持有 _objectInfosMap

_objectInfosMap 持有 self(QiSafeKVOController)

的迴圈引用的問題。

FBKVOController 觀察某物件的某屬性;

FBKVOController觀察的物件,及觀察的物件的keyPath,option,block等資訊都儲存在了_FBKVOInfo例項中。 筆者以

- (void)observe:(nullable id)object
        keyPath:(NSString *)keyPath
        options:(NSKeyValueObservingOptions)options
          block:(FBKVONotificationBlock)block;
複製程式碼

分析觀察某物件某屬性的過程。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block {
    
    // 對keyPath block 及 待觀察物件object的簡單校驗
    NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    if (nil == object || 0 == keyPath.length || NULL == block) {
        return;
    }
    
    // 建立儲存觀察者資訊的info(_FBKVOInfo例項) 儲存觀察者self  keyPath options 及值改變的block
    _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
    
    // 使用info觀察object
    [self _observe:object info:info];
}
複製程式碼
  • 在觀察物件屬性的時候,FBKVOController用到了_FBKVOInfo的例項,_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];用於儲存待觀察物件的屬性及回撥資訊。

- (void)_observe:(id)object info:(_FBKVOInfo *)info {
    // 互斥鎖加鎖
    pthread_mutex_lock(&_lock);
    
    // 檢視_objectInfosMap中是否已經新增過object對應的資訊
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    
    // 檢視infos中是否已經新增過info資訊 這裡的檢視方式是按照object的keypath的hash值確定的
    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
        // 檢視與待觀察物件object相應的infos中,已經新增過info資訊,解鎖返回
        pthread_mutex_unlock(&_lock);
        return;
    }

    //  _objectInfosMap之前沒有新增過對待觀察物件object的資訊,建立用於儲存object相應的infos資訊的內容
    if (nil == infos) {
        infos = [NSMutableSet set];
        [_objectInfosMap setObject:infos forKey:object];
        
    }

    // 同樣會呼叫hash 新增info資訊到infos中
    [infos addObject:info];
    // 解鎖
    pthread_mutex_unlock(&_lock);
    
    [[_FBKVOSharedController sharedController] observe:object info:info];
}

複製程式碼
  • 如下程式碼可以避免重複觀察某物件的某屬性。
    // 檢視與待觀察物件object的屬性資訊 是否已經新增過info資訊
    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
        // 檢視與待觀察物件object相應的infos中,已經新增過info資訊,解鎖返回
        pthread_mutex_unlock(&_lock);
        return;
    }
複製程式碼
  • _FBKVOShareController中的如下方法封裝了系統的KVO,及改變FBKVOInfo的_FBKVOInfoStateInitial狀態為_FBKVOInfoStateObserving
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info {
    if (nil == info) {
        return;
    }
    
    // 儲存待觀察物件的資訊 到_infos中
    pthread_mutex_lock(&_mutex);
    [_infos addObject:info];
    pthread_mutex_unlock(&_mutex);
    
    // 系統的方式新增觀察者
    [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
    
    
    if (info->_state == _FBKVOInfoStateInitial) {
        // 改變要觀察的物件的info的觀察狀態為 _FBKVOInfoStateObserving
        info->_state = _FBKVOInfoStateObserving;
    } else if (info->_state == _FBKVOInfoStateNotObserving) {
        // 這部分內容筆者沒有復現
        // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
        // and the observer is unregistered within the callback block.
        // at this time the object has been registered as an observer (in Foundation KVO),
        // so we can safely unobserve it.
        // 系統方式移除觀察者
        [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
}

複製程式碼

FBKVOController 觀察物件屬性變化

在FBKVOController中有如下系統KVO的observeValueForKeyPath方法及block回撥程式碼。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
    NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
    
    _FBKVOInfo *info;
    
    {
        // lookup context in registered infos, taking out a strong reference only if it exists
        pthread_mutex_lock(&_mutex);
        info = [_infos member:(__bridge id)context];
        pthread_mutex_unlock(&_mutex);
    }
    
    if (nil != info) {
        
        // take strong reference to controller
        FBKVOController *controller = info->_controller;
        if (nil != controller) {
            
            // take strong reference to observer
            id observer = controller.observer;
            if (nil != observer) {
                
                // dispatch custom block or action, fall back to default action
                if (info->_block) {
                    NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
                    // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
                    if (keyPath) {
                        NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
                        [mChange addEntriesFromDictionary:change];
                        changeWithKeyPath = [mChange copy];
                    }
                    info->_block(observer, object, changeWithKeyPath);
                } else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                    [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
                } else {
                    [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
                }
            }
        }
    }
}
複製程式碼

以下程式碼實現了簡單校驗info->controller、觀察者、info->block,校驗無誤的情況下,進行block回撥,實現回撥到控制器中的block的部分。

if (nil != info) {
        
        // take strong reference to controller
        FBKVOController *controller = info->_controller;
        if (nil != controller) {
            
            // take strong reference to observer
            id observer = controller.observer;
            if (nil != observer) {
                
                // dispatch custom block or action, fall back to default action
                if (info->_block) {
                    NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
                    // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
                    if (keyPath) {
                        NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
                        [mChange addEntriesFromDictionary:change];
                        changeWithKeyPath = [mChange copy];
                    }
                    info->_block(observer, object, changeWithKeyPath);
                }
複製程式碼

FBKVOController觀察物件

FBKVOController觀察的物件,及觀察的物件的keyPath,option,block等資訊都儲存在了FBKVOInfo例項中。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block {
    // 使用斷言 對keyPath block 的簡單校驗
    NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
	// 對keyPath block 及 待觀察物件object的簡單校驗
    if (nil == object || 0 == keyPath.length || NULL == block) {
        return;
    }
    
    // 建立儲存觀察者資訊的info(_FBKVOInfo例項) 儲存觀察者self  keyPath options 及值改變的block
    _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
    
    // 使用info觀察object
    [self _observe:object info:info];
}
複製程式碼

- (void)_observe:(id)object info:(_FBKVOInfo *)info {
    // 互斥鎖加鎖
    pthread_mutex_lock(&_lock);
    
    // 檢視_objectInfosMap中是否已經新增過object對應的資訊
    NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    
    // 檢視與待觀察物件object相應的infos中 是否已經新增過info資訊
    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
        // 檢視與待觀察物件object相應的infos中,已經新增過info資訊,解鎖返回
        pthread_mutex_unlock(&_lock);
        return;
    }

    //  _objectInfosMap之前沒有新增過對待觀察物件object的資訊,建立用於儲存object相應的infos資訊的內容
    if (nil == infos) {
        infos = [NSMutableSet set];
        [_objectInfosMap setObject:infos forKey:object];
        
    }

    // 同樣會呼叫hash 新增info資訊到infos中
    [infos addObject:info];
    // 解鎖
    pthread_mutex_unlock(&_lock);
    
    [[_FBKVOSharedController sharedController] observe:object info:info];
}
複製程式碼

封裝系統KVO,改變info->_state。

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info {
    if (nil == info) {
        return;
    }
    
    // 儲存待觀察物件的資訊 到_infos中
    pthread_mutex_lock(&_mutex);
    [_infos addObject:info];
    pthread_mutex_unlock(&_mutex);
    
    // **系統的方式新增觀察者**
    [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
    
    
    if (info->_state == _FBKVOInfoStateInitial) {
        // 改變要觀察的物件的info的觀察狀態為 _FBKVOInfoStateObserving
        info->_state = _FBKVOInfoStateObserving;
    } else if (info->_state == _FBKVOInfoStateNotObserving) {
        // 這部分內容筆者沒有復現
        // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
        // and the observer is unregistered within the callback block.
        // at this time the object has been registered as an observer (in Foundation KVO),
        // so we can safely unobserve it.
        // 系統方式移除觀察者
        [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
}

複製程式碼

儲存待觀察的object及FBKVOInfo資訊到_objectInfosMap,為了避免多次觀察某物件的同一屬性,在儲存操作前有個簡單的校驗.

    _FBKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
        // 檢視與待觀察物件object相應的infos中,已經新增過info資訊,解鎖返回
        pthread_mutex_unlock(&_lock);
        return;
    }

複製程式碼

FBKVOController不需要開發者removeObserver

在控制器銷燬的時候,FBKVOController的例項也會銷燬,FBKVOController在實現檔案中重寫了dealloc,依次移除之前objectsInfosMap中的需要移除觀察者的object的觀察者。

- (void)dealloc {
    [self unobserveAll];
    // 銷燬互斥鎖
    pthread_mutex_destroy(&_lock);
}
複製程式碼
- (void)unobserveAll {
    [self _unobserveAll];
}
複製程式碼
- (void)_unobserveAll {
    // 互斥鎖加鎖
    pthread_mutex_lock(&_lock);
    
    // copy一份_objectInfosMap
    NSMapTable *objectInfoMaps = [_objectInfosMap copy];
    
    //  清空_objectInfosMap中的觀察者object及觀察的infos資訊
    [_objectInfosMap removeAllObjects];
    
    // 解鎖
    pthread_mutex_unlock(&_lock);
    
    // 獲取單例
    _FBKVOSharedController *shareController = [_FBKVOSharedController sharedController];
    
    // 依次取消objectInfoMaps中observer的資訊
    for (id object in objectInfoMaps) {
        // 取消觀察每一個註冊了觀察的object及相應的觀察的資訊
        NSSet *infos = [objectInfoMaps objectForKey:object];
        [shareController unobserve:object infos:infos];
    }
}
複製程式碼
- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos {
    // 如果沒有待移除的object相關的info資訊了, return
    if (0 == infos.count) {
        return;
    }
    
    // _infos移除infos中的info資訊
    /**
     _infos中存放的是觀察的所有object的info資訊
     infos儲存的是當前的object的info資訊
     info指的的infos中的每個info(_FBKVOInfo *)資訊
     */
    
    pthread_mutex_lock(&_mutex);
    for (_FBKVOInfo *info in infos) {
        [_infos removeObject:info];
    }
    pthread_mutex_unlock(&_mutex);
    
    // 移除info指定的keyPath及context資訊的觀察者 並且info的狀態為_FBKVOInfoStateNotObserving
    for (_FBKVOInfo *info in infos) {
        if (info->_state == _FBKVOInfoStateObserving) {
            [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
        }
        info->_state = _FBKVOInfoStateNotObserving;
    }
}
複製程式碼

FBKVOController的執行緒安全實現方式之互斥鎖;

  • FBKVOController的執行緒安全是通過互斥鎖實現的

mutext(MUTual EXclusion)是一種互斥設定。 用於保護共享資料免於併發訪問、設定出現問題。還有一些其他挑剔的情況。

在FBKVOController中,在操作_objectInfosMap,_infos的時候使用了互斥鎖。

就FBKVOController的例項變數_objectInfosMap而言。

在初始化FBKVOController的時候,初始化了互斥鎖;在讀寫objectInfosMap之前鎖定了互斥鎖;在讀寫完objectInfosMap之後,解鎖了互斥鎖;在FBKVOController銷燬的時候銷燬了互斥鎖。

// 使用互斥鎖需要匯入pthread.h
#import <pthread.h>

pthread_mutex_t _lock;

// 初始化互斥鎖
pthread_mutex_init(&_lock, NULL);

// 鎖定互斥鎖
pthread_mutex_lock(&_lock);

// 解鎖互斥鎖
pthread_mutex_unlock(&_lock);

// 銷燬互斥鎖
pthread_mutex_destroy(&_lock);
    
複製程式碼
// 初始化互斥鎖
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_init(pthread_mutex_t * __restrict,
		const pthread_mutexattr_t * _Nullable __restrict);
		

// 鎖定互斥鎖
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_lock(pthread_mutex_t *);


// 解除鎖定互斥鎖
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_unlock(pthread_mutex_t *);


// 銷燬互斥鎖
__API_AVAILABLE(macos(10.4), ios(2.0))
int pthread_mutex_destroy(pthread_mutex_t *);


複製程式碼

互斥鎖的使用可以檢視:使用互斥鎖

關於鎖的更多內容可以檢視大成哥的:iOS 多執行緒之執行緒安全

_FBKVOInfo重寫isEqual:、 hash

_FBKVOInfo重寫了isEqual: 和hash方法。

_FBKVOInfo重寫isEqual:和hash方法的原因是,FBKVOController想要自己去控制2個_FBKVOInfo的例項是否相等。這裡_FBKVOInfo是根據的 _keypath的hash值判斷是否相等的。

- (NSUInteger)hash {
  return [_keyPath hash];
}

- (BOOL)isEqual:(id)object {
  if (nil == object) {
    return NO;
  }
  if (self == object) {
    return YES;
  }
  if (![object isKindOfClass:[self class]]) {
    return NO;
  }
  return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}
複製程式碼

If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual: in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash in your subclass.

如果兩個物件是相等的,他們一定有相同的hash值。尤其重要的是,如果我們打算把子類例項放到一個集合物件中,並且在子類中重寫了isEqual方法的時候,請確保也在子類中重寫了hash方法。

NSMapTable之keyOptions;

FBKVOController使用了NSMapTable儲存要監聽的物件。 NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;

  • _objectInfosMap的key為要觀察的物件

  • _objectInfosMap的value儲存了FBKVOInfo的NSMutableSet資訊.

  • _objectInfosMap 初始化。
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    
    // _objectInfosMap 初始化
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}
複製程式碼

A collection similar to a dictionary, but with a broader range of available memory semantics.

Declaration @interface NSMapTable<__covariant KeyType, __covariant ObjectType> : NSObject Discussion

  • The map table is modeled after NSDictionary with the following differences: Keys and/or values are optionally held “weakly” such that entries are removed when one of the objects is reclaimed.

  • Its keys or values may be copied on input or may use pointer identity for equality and hashing.

  • It can contain arbitrary pointers (its contents are not constrained to being objects).

NSMapTable 是一中類似於NSDictionary的集合,不過NSMapTable有更廣範的記憶體語義。 NSMapTable在NSDictionary的基礎上做了部分修整,NSMapTable相比較NSDictionary有如下不同的地方:

  • NSMapTable的keys或者values是可選地持有weak型別物件的。當NSMapTable中的weak類性的key或者value釋放的時候,相應的鍵值對會自動從NSMapTable中移除。

  • NSMapTable可以在新增鍵值對的時候進行拷貝操作,可以通過指標進行相等性和雜湊檢查。

  • NSMapTable可以包含任意指標(她的內容不限於物件)。

NSMapTable關於keyOptions相關程式碼:

    // weakKeyStrongObjectsMapTable
    NSMapTable *weakKeyStrongObjectsMapTable = [NSMapTable weakToStrongObjectsMapTable];
    /** // 相當於
    NSPointerFunctionsOptions weakOptions = NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality;
    NSPointerFunctionsOptions strongOptions = NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality;
    weakKeyStrongObjectsMapTable = [NSMapTable mapTableWithKeyOptions:weakOptions valueOptions:strongOptions];
     */
    
    NSObject *key0 = [NSObject new];
    NSObject *obj0 = [NSObject new];
    [weakKeyStrongObjectsMapTable setObject:obj0 forKey:key0];
    NSLog(@"weakKeyStrongObjectsMapTable:%@", weakKeyStrongObjectsMapTable);
    /*
     weakKeyStrongObjectsMapTable:NSMapTable {
     [3] <NSObject: 0x600001711180> -> <NSObject: 0x600001711190>
     }
     */
    key0 = nil;
    NSLog(@"key0 =nil, weakKeyStrongObjectsMapTable:%@", weakKeyStrongObjectsMapTable);
    /*
     key0 =nil, weakKeyStrongObjectsMapTable:NSMapTable {
     }
     */
    
    // weakKeyWeakObjsMapTable
    NSObject *key1 = [NSObject new];
    NSObject *obj1 = [NSObject new];
    NSObject *key2 = [NSObject new];
    NSObject *obj2 = [NSObject new];
    
    NSMapTable *weakKeyWeakObjsMapTable = [NSMapTable weakToWeakObjectsMapTable];
    // 相當於
    //  weakKeyWeakObjsMapTable = [NSMapTable mapTableWithKeyOptions:weakOptions valueOptions:weakOptions];
    [weakKeyWeakObjsMapTable setObject:obj1 forKey:key1];
    [weakKeyWeakObjsMapTable setObject:obj2 forKey:key2];
    NSLog(@"weakKeyWeakObjsMapTable:%@", weakKeyWeakObjsMapTable);
    /*
     weakKeyWeakObjsMapTable:NSMapTable {
     [3] <NSObject: 0x600001711180> -> <NSObject: 0x600001710fa0>
     [10] <NSObject: 0x6000017111a0> -> <NSObject: 0x6000017110b0>
     }
     */
    
    key1 = nil;
    NSLog(@"key1 = nil, weakKeyWeakObjsMapTable:%@", weakKeyWeakObjsMapTable);
    /*
     key1 = nil, weakKeyWeakObjsMapTable:NSMapTable {
     [10] <NSObject: 0x6000017111a0> -> <NSObject: 0x6000017110b0>
     }
     */
    
    obj2 = nil;
    [weakKeyWeakObjsMapTable setObject:obj1 forKey:key1];
    NSLog(@"obj2 = nil, weakKeyWeakObjsMapTable:%@", weakKeyWeakObjsMapTable);
    /*
     obj2 = nil, weakKeyWeakObjsMapTable:NSMapTable {
     }
     */

複製程式碼

以weakToStrongObjectsMapTable建立的NSMapTable的例項weakKeyStrongObjectsMapTable,當新增的key key1銷燬時,相應的key1 obj1的鍵值對會自動移除。

NSHashTable之weakObjectsHashTable

NSHashTable A collection similar to a set, but with broader range of available memory semantics.

The hash table is modeled after NSSet with the following differences:

  • It can hold weak references to its members.

  • Its members may be copied on input or may use pointer identity for equality and hashing.

  • It can contain arbitrary pointers (its members are not constrained to being objects).

NSHashTable NSHashTable是類似於NSSet的集合,不過NSHashTable有更加廣泛的記憶體語義。

NSHashTable是在NSSet的基礎上做的調整,相比較NSSet,NSHashTable有如下不同之處:

  • NSHashTable可以持有成員的弱引用。

  • NSHashTable可以在加入成員時執行copy操作,可以通過isEqual:和hash檢測成員的雜湊值和相等性。

  • NSHashTable可以存放任意的指標(NSHashTable的成員不限於物件)。

相關程式碼:

	// NSHashTable
	NSHashTable *hashTable = [NSHashTable weakObjectsHashTable];
	// 相當於
	[NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality];
	NSObject *hashObj = [NSObject new];
	[hashTable addObject:hashObj];
	NSLog(@"hashTable:%@", hashTable);
	/*
	 hashTable:NSHashTable {
	 [11] <NSObject: 0x600002528af0>
	 }
	 */
	hashObj = nil;
	NSLog(@"hashObj = nil, hashTable:%@", hashTable);
	/*
	 hashObj = nil, hashTable:NSHashTable {
	 }
	 */
複製程式碼

對於weakObjectsHashTable建立的NSHashTable例項hashTable,當hashTable中新增的obj,銷燬後,hashTable中的之前新增的obj,會自動移除。

Demo

  • 更多相關內容,可檢視Demo QiSafeType

參考學習網址


小編微信:可加並拉入《QiShare技術交流群》。

iOS 避免常見崩潰(二)

關注我們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號)

推薦文章:
iOS 避免常見崩潰(一)
演算法小專欄:選擇排序
iOS Runloop(一)
iOS 常用除錯方法:LLDB命令
iOS 常用除錯方法:斷點
iOS 常用除錯方法:靜態分析
iOS訊息轉發
奇舞週刊

相關文章