iOS 避免常見崩潰(一)

QiShare發表於2019-02-28

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

筆者最近看了部分引起App Crash的常見情況,這次先討論下操作集合型別(如NSArray,NSDictionary等)時,防止常見崩潰(如避免從陣列中取值時越界、往字典中插入為nil的value等)的內容。

  • 為了避免崩潰,操作集合類物件時,設定值和取值的時候,可以考慮使用如下方法:
      1. 使用分類新增的安全性方法
      1. 使用交換系統方法和我們做了安全性處理的方法

安全操作集合類物件:新增分類方法

當操作集合類物件的時候,可以使用我們新增的安全取值的分類。

新增分類方法部分,先從NSString* 和NSNumber* 的integerValue談起。

  • (1)如在NSObject的分類中新增的qi_safeIntegerValue用於替換平時用的integerValue的方法。
- (NSInteger)qi_safeIntegerValue {
    
    if ([self isKindOfClass:[NSNumber class]]) {
        return [((NSNumber *)self) integerValue];
    } else if([self isKindOfClass:[NSString class]]) {
        return [((NSString *)self) integerValue];
    } else {
        return kCustomErrorCode;
    }
}
複製程式碼

使用qi_safeIntegerValue的方式為:

    id number = @(1);
    [number qi_safeIntegerValue];
複製程式碼
  • (2)如在NSObject的分類中新增了qi_safeArrayObjectAtIndex用於替換NSArray* 的- (ObjectType)objectAtIndex:(NSUInteger)index;
- (id)qi_safeArrayObjectAtIndex:(NSUInteger)index {
    
    if (![self isKindOfClass:[NSArray class]]) {
        return nil;
    }
    
    if (index < 0 || index >= ((NSArray *)self).count) {
        return nil;
    }
    
    return [(NSArray *)self objectAtIndex:index];
}
複製程式碼

使用qi_safeArrayObjectAtIndex的方式為:

    NSArray *qiArr = @[@1];
    [qiArr qi_safeArrayObjectAtIndex:0];
    [qiArr qi_safeArrayObjectAtIndex:1];
複製程式碼
  • 更多相關內容,可檢視Demo QiSafeType

不過只使用分類,不適於我們使用字面量語法取值的情況。如對於陣列來說,如果我們想要使用qiArr[0]這樣的字面量語法取值,使用當前的分類的方式就不適用了。此時就需要結合runtime使用交換qiArr[0]呼叫的系統方法和我們自己新增的安全取值的方法來達到字面量安全取值的目的。

安全操作集合類物件:方法交換

安全操作集合類物件,方法交換部分,筆者會以字面量操作NSArray聊聊相關內容。

以字面量的方式宣告陣列 及取值:

// 宣告陣列的時候 插入nil
NSString *nilValue = nil;
NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];

NSString *nilValue = nil;
NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];
NSLog(@"qiArr:%@", qiArr);

// 從陣列中取值 故意越界取值
NSLog(@"qiArr[0]:%@ qiArr[1]:%@ qiArr[2]:%@",qiArr[0],qiArr[1],qiArr[2]);

// 輸出結果如下:
qiArr:(
    qishare0,
    qiShare2
)

qiArr[0]:qishare0 qiArr[1]:qiShare2 qiArr[2]:(null)
複製程式碼
  • 交換方法的操作需要引入#import <objc/runtime.h>,主要會用到下邊的API。
// Returns a pointer to the data structure describing a given class method for a given class.
// 返回一個描述指定類cls和給定類方法的資料結構的指標 其實返回值也是一個Method
// 獲取指定類Cls和指定方法sel的對應的 類 Method
Method class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

// Returns a specified instance method for a given class.
// 返回指定的類cls及相應selector的例項方法
// 獲取指定類Cls和指定方法sel的對應的 例項 Method
Method class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
   
// 交換Method m1和Method m2的實現 
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼
  • 交換方法需要待交換方法SEL,及待交換的類Class

以下內容筆者會以字面量的方式宣告qiArr NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];為例進行分析,所需的class及要交換的SEL

  • (1)待交換的方法

就待交換的方法而言,宣告qiArr的時候。首先要確定呼叫的NSArray哪個方法。呼叫的是+ (instancetype)arrayWithObjects:(const ObjectType _Nonnull [_Nonnull])objects count:(NSUInteger)cnt;;這個可以通過自己寫一個陣列的子類來證實這件事,也可以通過交換方法後呼叫的方法來反面驗證。NSArray的子類的寫法可以參考QiSafeType中的QiSubArray的寫法。

關於繼承NSArray:Any subclass of NSArray must override the primitive instance methods count and objectAtIndex:. These methods must operate on the backing store that you provide for the elements of the collection. For this backing store you can use a static array, a standard NSArray object, or some other data type or mechanism. You may also choose to override, partially or fully, any other NSArray method for which you want to provide an alternative implementation.

+ (instancetype)arrayWithObjects:(const ObjectType _Nonnull [_Nonnull])objects count:(NSUInteger)cnt;是一個類方法,所以方法交換部分,筆者也寫了一個安全宣告陣列的類方法。+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt

筆者寫的待交換的+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt的方法體如下:

+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt {
    
    id instance = nil;
    id safeObjs[cnt];
    NSUInteger j = 0;
    for (NSUInteger i = 0; i < cnt; i ++) {
        if (!objects[i]) {
            continue;
        }
        safeObjs[j++] = objects[i];
    }
    instance = [self qisafeArrayWithObjects:safeObjs count:j];
    return instance;
}
複製程式碼

思想:在宣告字面量陣列的時候,先遍歷指定的陣列,過濾掉為空的物件,剩餘的存放到另一個陣列中。再呼叫自己新增的宣告陣列的類方法(因為我們新增的方法和系統的方法進行了方法交換,所以這裡實質是呼叫的系統的宣告陣列的方法。)

  • (2)待交換的類

宣告qiArr的時候待交換的方法為類方法,所以待交換的類為[NSArray class]

就待交換的方法而言。以qiArr[0]為例。要確定呼叫的陣列的那個方法。呼叫的是- (ObjectType)objectAtIndex:(NSUInteger)index;這個可以通過自己寫一個陣列的子類來證實這件事,也可以通過交換方法後呼叫的方法來反面驗證。

上述問題明確了之後,我們就可以在NSArray的分類的+ (void)load方法中根據指定的類[NSArray class]把+ (instancetype)arrayWithObjects:(const ObjectType _Nonnull [_Nonnull])objects count:(NSUInteger)cnt;+ (instancetype)qisafeArrayWithObjects:(const id _Nonnull [_Nonnull])objects count:(NSUInteger)cnt進行方法交換了。

關鍵程式碼如下:

    Method originMethod = class_getClassMethod([NSArray class], @selector(arrayWithObjects:count:));
    Method alterMethod = class_getClassMethod([NSArray class], @selector(qisafeArrayWithObjects:count:));
    method_exchangeImplementations(originMethod, alterMethod);
複製程式碼

以上部分,筆者解釋了,NSArray以字面量的方式宣告陣列的方法交換部分的內容,下邊筆者將繼續和大家聊一下關於NSArray字面量取值的內容。其實NSArray運作在抽象工廠設計模式下。抽象工廠模式是類簇在iOS下的的一種實現。眾多常用類,如NSString,NSArray,NSDictionary,NSNumber都運作在抽象工廠模式下,引自從NSArray看類簇
以下部分,大家也可以直接看Demo中的寫法自行分析。

筆者想通過以下程式碼說明,建立出的NSArray*的例項(qiArr)的class,和建立qiArr的方式及qiArr中包含的物件的個數有關。

NSString *nilValue = nil;
// 宣告陣列的時候 插入nil
NSArray *qiArr = @[@"qishare0", nilValue, @"qiShare2"];
NSLog(@"qiArr:%@", qiArr);
NSLog(@"qiArr[0]:%@ qiArr[1]:%@ qiArr[2]:%@",qiArr[0],qiArr[1],qiArr[2]);

id tempValue = nil;
    
NSArray *qiArr0 = @[];
tempValue = qiArr0[0];
tempValue = qiArr0[1];
    
NSArray *qiArr1 = @[@1];
tempValue = qiArr1[0];
tempValue = qiArr1[1];
    
NSArray *qiArr2 = @[@1, @2];
tempValue = qiArr2[1];
tempValue = qiArr2[2];
    
NSLog(@"qiArr0 class:%@", NSStringFromClass([qiArr0 class]));
NSLog(@"qiArr1 class:%@", NSStringFromClass([qiArr1 class]));
NSLog(@"qiArr2 class:%@", NSStringFromClass([qiArr2 class]));
    
NSArray *qiArr3 = [NSArray arrayWithObjects:@"1", @"2", @"3", nil];
    
NSLog(@"qiArr3 class:%@", NSStringFromClass([qiArr3 class]));

 qiArr0 class:__NSArray0
 qiArr1 class:__NSSingleObjectArrayI
 qiArr2 class:__NSArrayI
 qiArr3 class:__NSArrayI
複製程式碼

有了上述內容,我們就可以在通過字面量語法故意越界訪問陣列物件的時候,根據崩潰的提示,新增上相應的方法交換。

關鍵程式碼如下:

	// __NSArray0
	Method originArr0ObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), @selector(objectAtIndex:));
    Method alterArr0ObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSArray0"), @selector(qiSafeArr0ObjectAtIndex:));
    method_exchangeImplementations(originArr0ObjectAtIndexMethod, alterArr0ObjectAtIndexMethod);

   // __NSSingleObjectArrayI
	Method originSingleObjArrIObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSSingleObjectArrayI"), @selector(objectAtIndex:));
    Method alterSingleObjArrIObjectAtIndexMethod = class_getInstanceMethod(NSClassFromString(@"__NSSingleObjectArrayI"), @selector(qiSafeSingleObjArrIObjectAtIndex:));
    method_exchangeImplementations(originSingleObjArrIObjectAtIndexMethod, alterSingleObjArrIObjectAtIndexMethod);
    
    // 注意__NSArrayI呼叫字面量語法的時候呼叫的系統方法為`- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx API_AVAILABLE(macos(10.8), ios(6.0), watchos(2.0), tvos(9.0));` 和上述型別的NSArray待交換的方法不同,這一點可以故意測試崩潰,檢視崩潰的提示得出。
    
    // __NSArrayI
    Method originArrIObjAtIndexedSubMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndexedSubscript:));
    Method alterArrIObjAtIndexedSubMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(qiSafeArrIObjAtIndexedSubscript:));
    method_exchangeImplementations(originArrIObjAtIndexedSubMethod, alterArrIObjAtIndexedSubMethod);

複製程式碼

Demo

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

參考學習網址


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

iOS 避免常見崩潰(一)

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

推薦文章:
iOS訊息轉發
iOS 自定義拖拽式控制元件:QiDragView
iOS 自定義卡片式控制元件:QiCardView
iOS Wireshark抓包
iOS Charles抓包
初探TCP
IP、UDP初探
奇舞週刊

相關文章