iOS 開發:『Crash 防護系統』(三)KVC 防護

 
1877784-293a0b6572687c1b.jpg
 

本文是 『Crash 防護系統』系列 第三篇。通過本文,您將瞭解到:

  1. KVC Crash 的主要原因
  2. KVC 搜索模式
  3. KVC Crash 防護方案

文中示例代碼在: bujige / YSC-Avoid-Crash


1. KVC Crash 的常見原因

KVC(Key Value Coding),即鍵值編碼,提供一種機制來間接訪問對象的屬性。而不是通過調用 SetterGetter 方法進行訪問。

KVC 日常使用造成崩潰的原因通常有以下幾個:

  1. key 不是對象的屬性,造成崩潰。
  2. keyPath 不正確,造成崩潰。
  3. key 爲 nil,造成崩潰。
  4. value 爲 nil,爲非對象設值,造成崩潰。

常見的使用 KVC 造成崩潰代碼:

/********************* KVCCrashObject.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface KVCCrashObject : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) NSInteger age;

@end

/********************* KVCCrashObject.m 文件 *********************/
#import "KVCCrashObject.h"

@implementation KVCCrashObject

@end


/********************* ViewController.m 文件 *********************/
#import "ViewController.h"
#import "KVCCrashObject.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {    
//    1. key 不是對象的屬性,造成崩潰
//    [self testKVCCrash1];

//    2. keyPath 不正確,造成崩潰
//    [self testKVCCrash2];

//    3. key 爲 nil,造成崩潰
//    [self testKVCCrash4];

//    4. value 爲 nil,爲非對象設值,造成崩潰
//    [self testKVCCrash4];
}

/**
 1. key 不是對象的屬性,造成崩潰
 */
- (void)testKVCCrash1 {
    // 崩潰日誌:[<KVCCrashObject 0x600000d48ee0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key XXX.;
    
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:@"value" forKey:@"address"];
}

/**
 2. keyPath 不正確,造成崩潰
 */
- (void)testKVCCrash2 {
    // 崩潰日誌:[<KVCCrashObject 0x60000289afb0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key XXX.
    
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:@"後廠村路" forKeyPath:@"address.street"];
}

/**
 3. key 爲 nil,造成崩潰
 */
- (void)testKVCCrash3 {
    // 崩潰日誌:'-[KVCCrashObject setValue:forKey:]: attempt to set a value for a nil key

    NSString *keyName;
    // key 爲 nil 會崩潰,如果傳 nil 會提示警告,傳空變量則不會提示警告
    
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:@"value" forKey:keyName];
}

/**
 4. value 爲 nil,造成崩潰
 */
- (void)testKVCCrash4 {
    // 崩潰日誌:[<KVCCrashObject 0x6000028a6780> setNilValueForKey]: could not set nil as the value for the key XXX.
    
    // value 爲 nil 會崩潰
    KVCCrashObject *objc = [[KVCCrashObject alloc] init];
    [objc setValue:nil forKey:@"age"];
}

@end

那麼如何來解決這種崩潰問題呢?

首先我們需要先來了解下 KVC 在執行時,具體的搜索模式。也就是 KVC 內部的執行流程。根據瞭解了 KVC 內部的具體執行流程,我們才能知道在哪個步驟對其進行防護。


2. KVC 搜索模式

2.1 KVC Setter 搜索模式

系統在執行 setValue:forKey: 方法時,會把 keyvalue 作爲輸入參數,並嘗試在接收調用對象的內部,給屬性 key 設置 value 值。通過以下幾個步驟:

  1. 按順序查找名爲 set<Key>:_set<Key>:setIs<Key>: 方法。如果找到方法,則執行該方法,使用輸入參數設置變量,則 setValue:forKey: 完成執行。如果沒找到方法,則執行下一步。
  2. 訪問類的 accessInstanceVariablesDirectly 屬性。如果 accessInstanceVariablesDirectly 屬性返回 YES,就按順序查找名爲 _<key>_is<Key><key>is<Key> 的實例變量,如果找到了對應的實例變量,則使用輸入參數設置變量。則 setValue:forKey: 完成執行。如果未找到對應的實例變量,或者 accessInstanceVariablesDirectly 屬性返回 NO 則執行下一步。
  3. 調用 setValue: forUndefinedKey: 方法,並引發崩潰。

下邊我們通過圖示來展示一下這個流程。

  • 相關代碼:
KVCCrashObject *objc = [[KVCCrashObject alloc] init];
[objc setValue:@"value" forKey:@"name"];
  • KVC setValue:forKey: 搜索模式流程圖:
 
1877784-f493a8e49c8cbe5b.png
 

2.2 KVC Getter 搜索模式

系統在執行 valueForKey: 方法時,會將給定的 key 作爲輸入參數,在調用對象的內部進行以下幾個步驟:

  1. 按順序查找名爲 get<Key><key>is<Key>_<key> 的訪問方法。如果找到,調用該方法,並繼續執行步驟 5。否則繼續向下執行步驟 2。
  2. 搜索形如 countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes: 的方法。
    • 如果實現了 countOf<Key> 方法,並且實現了 objectIn<Key>AtIndex:<key>AtIndexes: 這兩個方法的任意一個方法,系統就會以 NSArray 爲父類,動態生成一個類型爲 NSKeyValueArray 的集合類對象,並調用上邊的實現方法,將結果直接返回。
    • 如果對象還實現了形如 get<Key>:range: 的方法,系統也會在必要的時候自動調用。
    • 如果上述操作不成功則繼續向下執行步驟 3。
  3. 如果上邊兩步失敗,系統就會查找形如 countOf<Key>enumeratorOf<Key>memberOf<Key>: 的方法。系統會自動生成一個 NSSet 類型的集合類對象,該對象響應所有 NSSet 方法並將結果返回。如果查找失敗,則執行步驟 4。
  4. 如果上邊三步失敗,系統就會訪問類的 accessInstanceVariablesDirectly 方法。
    • 如果返回 YES,就按順序查找名爲 _<key>_is<Key><key>is<Key> 的實例變量。如果找到了對應的實例變量,則直接獲取實例變量的值。並繼續執行步驟 5。
    • 如果返回 NO,或者未找到對應的實例變量,則繼續執行步驟 6。
  5. 分爲三種情況:
    • 如果檢索到的屬性值是對象指針,則直接返回結果。
    • 如果檢索到的屬性值是 NSNumber 支持的基礎數據類型,則將其存儲在 NSNumber 實例中並返回該值。
    • 如果檢索到的屬性值是 NSNumber 不支持的數據類型,則轉換爲 NSValue 對象並返回該對象。
  6. 如果一切都失敗了,調用 valueForUndefinedKey:,並引發崩潰。

3. KVC Crash 防護方案

  • 從 2.1 KVC Setter 搜索模式 和 2.2 KVC Getter 搜索模式 可以看出:
    • setValue:forKey: 執行失敗會調用 setValue: forUndefinedKey: 方法,並引發崩潰。
    • valueForKey: 執行失敗會調用 valueForUndefinedKey: 方法,並引發崩潰。

所以,爲了進行 KVC Crash 防護,我們就需要重寫 setValue: forUndefinedKey: 方法和 valueForUndefinedKey: 方法。重寫這兩個方法之後,就可以防護 1. key 不是對象的屬性2. keyPath 不正確 這兩種崩潰情況了。

那麼 3. key 爲 nil,造成崩潰 的情況,該怎麼防護呢?**

我們可以利用 Method Swizzling 方法,在 NSObject 的分類中將 setValue:forKey:ysc_setValue:forKey: 進行方法交換。然後在自定義的方法中,添加對 key 爲 nil 這種類型的判斷。

注意:這裏我看到另外一個開發者不是很建議 Hook 掉系統的 setValue:forKey: 方法,說是爲了儘可能少的對系統方法產生邏輯判斷。這裏我也持保留意見。小夥伴可以親自試驗一下。
作者文章鏈接:iOS 中的 crash 防護(二)KVC 造成的 crash

還有最後一種 4. value 爲 nil,爲非對象設值,造成崩潰 的情況。

NSKeyValueCoding.h 文件中,有一個 setNilValueForKey: 方法。上邊的官方註釋給了我們答案。

在調用 setValue:forKey: 方法時,系統如果查找到名爲 set<Key>: 方法的時候,會去檢測 value 的參數類型,如果參數類型爲 NSNmber 的標量類型或者是 NSValue 的結構類型,但是 value 爲 nil 時,會自動調用 setNilValueForKey: 方法。這個方法的默認實現會引發崩潰。

所以爲了防止這種情況導致的崩潰,我們可以通過重寫 setNilValueForKey: 來解決。

至此,上文提到的 KVC 使用不當造成的四種類型崩潰就都解決了。下面我們來看下具體實現代碼。

  • 具體防護代碼:
/********************* NSObject+KVCDefender.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface NSObject (KVCDefender)

@end

/********************* NSObject+KVCDefender.m 文件 *********************/
#import "NSObject+KVCDefender.h"
#import "NSObject+MethodSwizzling.h"

@implementation NSObject (KVCDefender)

// 不建議攔截 `setValue:forKey:` 方法
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        // 攔截 `setValue:forKey:` 方法,替換自定義實現
        [NSObject yscDefenderSwizzlingInstanceMethod:@selector(setValue:forKey:)
                                       withMethod:@selector(ysc_setValue:forKey:)
                                        withClass:[NSObject class]];

    });
}

- (void)ysc_setValue:(id)value forKey:(NSString *)key {
    if (key == nil) {
        NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
        NSLog(@"%@", crashMessages);
        return;
    }

    [self ysc_setValue:value forKey:key];
}

- (void)setNilValueForKey:(NSString *)key {
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setNilValueForKey]: could not set nil as the value for the key %@.",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages : [<%@ %p> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key: %@,value:%@'",NSStringFromClass([self class]),self,key,value];
    NSLog(@"%@", crashMessages);
}

- (nullable id)valueForUndefinedKey:(NSString *)key {
    NSString *crashMessages = [NSString stringWithFormat:@"crashMessages :[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key: %@",NSStringFromClass([self class]),self,key];
    NSLog(@"%@", crashMessages);
    
    return self;
}

@end

經過測試,剛纔因爲使用 KVC 不當造成崩潰都已經被解決了。


參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章