本文是 『Crash 防禦系統』系列 第一篇。 這個系列將會介紹如何設計一套 APP Crash 防禦系統。這套系統採用 AOP(面向切面編程)的設計思想,利用 Objective-C語言的運行時機制,在不侵入原有項目代碼的基礎之上,經過在 APP 運行時階段對崩潰因素的的攔截和處理,使得 APP 可以持續穩定正常的運行。html
經過本文,您將瞭解到:ios
- Crash 防禦系統開篇
- 防禦原理簡介和常見 Crash
- Method Swizzling 方法的封裝
- Unrecognized Selector 防禦 4.1 unrecognized selector sent to instance(找不到對象方法的實現) 4.2 unrecognized selector sent to class(找不到類方法實現)
文中示例代碼在: bujige / YSC-Avoid-Crashgit
APP 的崩潰問題,一直以來都是開發過程當中重中之重的問題。平常開發階段的崩潰,發現後還可以當即處理。可是一旦發佈上架的版本出現問題,就須要緊急加班修復 BUG,再更新上架新版本了。在這個過程當中, 說不定會由於崩潰而致使關鍵業務中斷、用戶存留率降低、品牌口碑變差、生命週期價值降低等,最終致使流失用戶,影響到公司的發展。github
固然,避免崩潰問題的最好辦法就是不產生崩潰。在開發的過程當中就要儘量地保證程序的健壯性。可是,人又不是機器,不可能不犯錯。不可能存在沒有 BUG 的程序。可是若是可以利用一些語言機制和系統方法,設計一套防禦系統,使之可以有效的下降 APP 的崩潰率,那麼不只 APP 的穩定性獲得了保障,並且最重要的是能夠減小沒必要要的加班。編程
這套 Crash 防禦系統被命名爲:『YSCDefender(防衛者)』。Defender 也是路虎旗下最硬派的越野車系。在電影《Tomb Raider》裏面,由 Angelina Jolie 飾演的英國女探險家 Lara Croft,所駕駛的就是一臺 Defender。Defender 也是我比較喜歡的車之一。數組
不過呢,這不重要。。。我就是爲這個項目起了個花裏胡哨的名字,並給這個名字賦予了一些無聊的意義。。。bash
Objective-C
語言是一門動態語言,咱們能夠利用 Objective-C
語言的 Runtime
運行時機制,對須要 Hook
的類添加 Category(分類)
,在各個分類的 +(void)load;
中經過 Method Swizzling
攔截容易形成崩潰的系統方法,將系統原有方法與添加的防禦方法的 selector(方法選擇器)
與 IMP(函數實現指針)進行對調。而後在替換方法中添加防禦操做,從而達到避免以及修復崩潰的目的。ide
經過 Runtime 機制能夠避免的常見 Crash :函數
- unrecognized selector sent to instance(找不到對象方法的實現)
- unrecognized selector sent to class(找不到類方法實現)
- KVO Crash
- KVC Crash
- NSNotification Crash
- NSTimer Crash
- Container Crash(集合類操做形成的崩潰,例如數組越界,插入 nil 等)
- NSString Crash (字符串類操做形成的崩潰)
- Bad Access Crash (野指針)
- Threading Crash (非主線程刷 UI)
- NSNull Crash
這一篇咱們先來說解下 unrecognized selector sent to instance(找不到對象方法的實現)
和 unrecognized selector sent to class(找不到類方法實現)
形成的崩潰問題。ui
因爲這幾種常見 Crash 的防禦都須要用到 Method Swizzling 技術。因此咱們能夠爲 NSObject 新建一個分類,將 Method Swizzling 相關的方法封裝起來。
/********************* NSObject+MethodSwizzling.h 文件 *********************/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (MethodSwizzling)
/** 交換兩個類方法的實現 * @param originalSelector 原始方法的 SEL * @param swizzledSelector 交換方法的 SEL * @param targetClass 類 */
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;
/** 交換兩個對象方法的實現 * @param originalSelector 原始方法的 SEL * @param swizzledSelector 交換方法的 SEL * @param targetClass 類 */
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass;
@end
/********************* NSObject+MethodSwizzling.m 文件 *********************/
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (MethodSwizzling)
// 交換兩個類方法的實現
+ (void)yscDefenderSwizzlingClassMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
swizzlingClassMethod(targetClass, originalSelector, swizzledSelector);
}
// 交換兩個對象方法的實現
+ (void)yscDefenderSwizzlingInstanceMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector withClass:(Class)targetClass {
swizzlingInstanceMethod(targetClass, originalSelector, swizzledSelector);
}
// 交換兩個類方法的實現 C 函數
void swizzlingClassMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getClassMethod(class, originalSelector);
Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
// 交換兩個對象方法的實現 C 函數
void swizzlingInstanceMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
複製代碼
若是被調用的對象方法沒有實現,那麼程序在運行中調用該方法時,就會由於找不到對應的方法實現,從而致使 APP 崩潰。好比下面這樣的代碼:
UIButton *testButton = [[UIButton alloc] init];
[testButton performSelector:@selector(someMethod:)];
複製代碼
testButton
是一個 UIButton
對象,而 UIButton
類中並無實現 someMethod:
方法。因此向 testButoon
對象發送 someMethod:
方法,就會致使 testButoon
對象沒法找到對應的方法實現,最終致使 APP 的崩潰。
那麼有辦法解決這類由於找不到方法的實現而致使程序崩潰的方法嗎?
咱們從『 iOS 開發:『Runtime』詳解(一)基礎知識』知道了消息轉發機制中三大步驟:消息動態解析、消息接受者重定向、消息重定向。經過這三大步驟,可讓咱們在程序找不到調用方法崩潰以前,攔截方法調用。
大體流程以下:
+resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機會提供一個函數實現。咱們能夠經過重寫這兩個方法,添加其餘函數實現,並返回 YES, 那運行時系統就會從新啓動一次消息發送的過程。若返回 NO 或者沒有添加其餘函數實現,則進入下一步。forwardingTargetForSelector:
,Runtime 就會調用這個方法,容許咱們將消息的接受者轉發給其餘對象。若是這一步方法返回 nil
,則進入下一步。methodSignatureForSelector:
方法獲取函數的參數和返回值類型。
methodSignatureForSelector:
返回了一個 NSMethodSignature
對象(函數簽名),Runtime 系統就會建立一個 NSInvocation 對象,並經過 forwardInvocation:
消息通知當前對象,給予這次消息發送最後一次尋找 IMP 的機會。methodSignatureForSelector:
返回 nil
。則 Runtime 系統會發出 doesNotRecognizeSelector:
消息,程序也就崩潰了。這裏咱們選擇第二步(消息接受者重定向)來進行攔截。由於 -forwardingTargetForSelector
方法能夠將消息轉發給一個對象,開銷較小,而且被重寫的機率較低,適合重寫。
具體步驟以下:
-ysc_forwardingTargetForSelector:
方法;-forwardingTargetForSelector:
和 -ysc_forwardingTargetForSelector:
進行方法交換。實現代碼以下:
#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (SelectorDefender)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 攔截 `-forwardingTargetForSelector:` 方法,替換自定義實現
[NSObject yscDefenderSwizzlingInstanceMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ysc_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
// 自定義實現 `-ysc_forwardingTargetForSelector:` 方法
- (id)ysc_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 獲取 NSObject 的消息轉發方法
Method root_forwarding_method = class_getInstanceMethod([NSObject class], forwarding_sel);
// 獲取 當前類 的消息轉發方法
Method current_forwarding_method = class_getInstanceMethod([self class], forwarding_sel);
// 判斷當前類自己是否實現第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 若是沒有實現第二步:消息接受者重定向
if (!realize) {
// 判斷有沒有實現第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getInstanceMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getInstanceMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 若是沒有實現第三步:消息重定向
if (!realize) {
// 建立一個新類
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"出問題的類,出問題的對象方法 == %@ %@", errClassName, errSel);
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 若是類不存在 動態建立一個類
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 註冊類
objc_registerClassPair(cls);
}
// 若是類沒有對應的方法,則動態添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息轉發到當前動態生成類的實例對象上
return [[cls alloc] init];
}
}
return [self ysc_forwardingTargetForSelector:aSelector];
}
// 動態添加的方法實現
static int Crash(id slf, SEL selector) {
return 0;
}
@end
複製代碼
同對象方法同樣,若是被調用的類方法沒有實現,那麼一樣也會致使 APP 崩潰。
例如,有這樣一個類,聲明瞭一個 + (id)aClassFunc;
的類方法, 可是並無實現,就像下邊的 YSCObject
這樣。
/********************* YSCObject.h 文件 *********************/
#import <Foundation/Foundation.h>
@interface YSCObject : NSObject
+ (id)aClassFunc;
@end
/********************* YSCObject.m 文件 *********************/
#import "YSCObject.h"
@implementation YSCObject
@end
複製代碼
若是咱們直接調用 [YSCObject aClassFunc];
就會致使崩潰。
找不到類方法實現的解決方法和以前相似,咱們能夠利用 Method Swizzling 將 +forwardingTargetForSelector:
和 +ysc_forwardingTargetForSelector:
進行方法交換。
#import "NSObject+SelectorDefender.h"
#import "NSObject+MethodSwizzling.h"
#import <objc/runtime.h>
@implementation NSObject (SelectorDefender)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 攔截 `+forwardingTargetForSelector:` 方法,替換自定義實現
[NSObject yscDefenderSwizzlingClassMethod:@selector(forwardingTargetForSelector:)
withMethod:@selector(ysc_forwardingTargetForSelector:)
withClass:[NSObject class]];
});
}
// 自定義實現 `+ysc_forwardingTargetForSelector:` 方法
+ (id)ysc_forwardingTargetForSelector:(SEL)aSelector {
SEL forwarding_sel = @selector(forwardingTargetForSelector:);
// 獲取 NSObject 的消息轉發方法
Method root_forwarding_method = class_getClassMethod([NSObject class], forwarding_sel);
// 獲取 當前類 的消息轉發方法
Method current_forwarding_method = class_getClassMethod([self class], forwarding_sel);
// 判斷當前類自己是否實現第二步:消息接受者重定向
BOOL realize = method_getImplementation(current_forwarding_method) != method_getImplementation(root_forwarding_method);
// 若是沒有實現第二步:消息接受者重定向
if (!realize) {
// 判斷有沒有實現第三步:消息重定向
SEL methodSignature_sel = @selector(methodSignatureForSelector:);
Method root_methodSignature_method = class_getClassMethod([NSObject class], methodSignature_sel);
Method current_methodSignature_method = class_getClassMethod([self class], methodSignature_sel);
realize = method_getImplementation(current_methodSignature_method) != method_getImplementation(root_methodSignature_method);
// 若是沒有實現第三步:消息重定向
if (!realize) {
// 建立一個新類
NSString *errClassName = NSStringFromClass([self class]);
NSString *errSel = NSStringFromSelector(aSelector);
NSLog(@"出問題的類,出問題的類方法 == %@ %@", errClassName, errSel);
NSString *className = @"CrachClass";
Class cls = NSClassFromString(className);
// 若是類不存在 動態建立一個類
if (!cls) {
Class superClsss = [NSObject class];
cls = objc_allocateClassPair(superClsss, className.UTF8String, 0);
// 註冊類
objc_registerClassPair(cls);
}
// 若是類沒有對應的方法,則動態添加一個
if (!class_getInstanceMethod(NSClassFromString(className), aSelector)) {
class_addMethod(cls, aSelector, (IMP)Crash, "@@:@");
}
// 把消息轉發到當前動態生成類的實例對象上
return [[cls alloc] init];
}
}
return [self ysc_forwardingTargetForSelector:aSelector];
}
// 動態添加的方法實現
static int Crash(id slf, SEL selector) {
return 0;
}
@end
複製代碼
將 4.1 和 4.2 結合起來就能夠攔截全部未實現的類方法和對象方法了。具體實現可參考代碼: bujige / YSC-Avoid-Crash
- 本文做者: 行走少年郎
- 本文連接: www.jianshu.com/p/aeecc4b46…
- 版權聲明: 本文章採用 CC BY-NC-SA 3.0 許可協議。轉載請在文字開頭註明『本文做者』和『本文連接』!