用法一: Unrecognized Selector類型crash防護(Unrecognized Selector)
先介紹下class_addMethod這個方法:
/**
Class _Nullable cls: 你要添加方法的那個類;
SEL _Nonnull name:name都說可以隨便取, 但有些場景隨便取會有問題(如下例:),一些取添加或替換方法的名稱SEL;
IMP _Nonnull imp:新方法的 IMP;
const char * _Nullable types:新方法的返回值及參數(shù)
*/
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
開發(fā)中經(jīng)常會遇到這個問題------“unrecognized selector sent to instance 0x7faa2a132c0” 而導致cash. 為了防止crash我們可以用class_addMethod給找不到對應方法即將crash的消息添加一個與之對應的方法來防止其因為找不到相應方法而crash.
在OC中找不到對相應的實現(xiàn)方法時, 有補救機制 即 會先調用動態(tài)決議方法 該方法解決不了問題 再調用重定向方法; 若都解決不了再 cash.
動態(tài)決議方法:(這是給類利用class_addMethod添加函數(shù)的機會...)
- (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
- (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
重定向方法:
- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
- (void)forwardInvocation:(NSInvocation *)anInvocation;
補救機制(攔截調用)的整個流程即Objective——C的消息轉發(fā)機制。其具體流程如下圖:

下面上代碼: 給一個類的對象調用一個未實現(xiàn)的方法 然后用runtime 在動態(tài)決議方法中為其添加實現(xiàn), 防止crash
/* Person 類 */
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel)); // 打?。?eat
Method addMethod = class_getInstanceMethod([self class], @selector(addMethod));
// 這里 SEL _Nonnull name 必須寫 sel, 否則還是會cash...
class_addMethod([self class], sel, method_getImplementation(addMethod), method_getTypeEncoding(addMethod));
return true;
}
/*
* 這個方法也可以解決cash問題, 意思是轉給新的對象去執(zhí)行這個方法...
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector: %@",NSStringFromSelector(aSelector));
if (aSelector == @selector(eat)) {
// 創(chuàng)建新的對象
StubProxyObject * stub = [[StubProxyObject alloc] init];
return stub;
}
return [super forwardingTargetForSelector:@selector(addMethod)];
}
*/
- (void) addMethod {
NSLog(@"addMethod");
}
@end
// 調用 Person未實現(xiàn)方法eat
Person * person = [[Person alloc] init];
[person performSelector:@selector(eat)];
添加后就不會cash;
用法二: 替換系統(tǒng)的方法:
代碼示例: 創(chuàng)建 UIViewController 分類, 然后替換 - (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
#import "UIViewController+Extention.h"
#import <objc/runtime.h>
@implementation UIViewController (Extention)
+ (void)load {
Class class = [self class];
// 保證方法替換只執(zhí)行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originSelector = @selector(dismissViewControllerAnimated:completion:);
SEL swizzledSelector = @selector(customDismissViewController);
Method oringinMethod = class_getInstanceMethod(class, originSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char * type = method_getTypeEncoding(swizzledMethod);
BOOL didAddMethod = class_addMethod(class, originSelector, swizzledIMP, type);
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(oringinMethod), method_getTypeEncoding(oringinMethod));
} else {
method_exchangeImplementations(oringinMethod, swizzledMethod);
}
});
}
- (void) customDismissViewController {
NSLog(@"customDismissViewController");
// 這里調用不會有 死循環(huán)....
[self customDismissViewController];
}
還可以寫為:
+ (void)load {
Class class = [self class];
SEL originSelector = @selector(dismissViewControllerAnimated:completion:);
SEL swizzledSelector = @selector(customDismissViewController);
Method originMethod = class_getInstanceMethod(class, originSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 確保兩個方法都得獲取到
if (!originMethod || !swizzledMethod) {
return;
}
// 交換跟上面情況一樣
// method_exchangeImplementations(originMethod, swizzledMethod);
// 這種只是作替換,既自定義方法里 不能再調用 [self customDismissViewController];
class_replaceMethod(class, originSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// class_replaceMethod(class, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}
- (void) customDismissViewController {
NSLog(@"customDismissViewController");
// 這里不能調, 不然會產生死循環(huán)...
// [self customDismissViewController];
}
應用場景: 如何通過不去手動修改每個UIImage的imageNamed:方法就可以實現(xiàn)為該方法中加入版本判斷語句? 可以為 UIImage建一個分類(UIImage+Category), 替換系統(tǒng)的 imageNamed: 方法; 注意: 替換方法后,最后要調用一下自己定義替換的方法, 讓其有加載圖片的功能...
用法三: 在不同類之間實現(xiàn)Method Swizzling
示例: Person類有一個實例方法 - (void)run:(CGFloat)speed, 目前需要Hook該方法對速度大于20才執(zhí)行 run, 利用另一個類的方法交換來實現(xiàn):
Person類
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Person : NSObject
- (void) run: (CGFloat)speed;
@end
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
- (void) run: (CGFloat)speed {
NSLog(@"person --- %f",speed);
}
@end
StubProxyObject 類
#import "StubProxyObject.h"
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@implementation StubProxyObject
+(void)load {
Class originClass = NSClassFromString(@"Person");
Class swizzledClass = [self class];
SEL originSelector = NSSelectorFromString(@"run:");
SEL swizzledSelector = @selector(stub_run:);
Method originMethod = class_getInstanceMethod(originClass, originSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
// 向Person類中新添加 stub_run: 方法
BOOL addMethod = class_addMethod(originClass, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (!addMethod) {
return;
}
// 獲取當前 Person 中新添加的方法 stub_run:的Method指針
Method newSwizzledMethod = class_getInstanceMethod(originClass, swizzledSelector);
if (!newSwizzledMethod) {
return;
}
BOOL didAddMethod = class_addMethod(originClass, originSelector, method_getImplementation(newSwizzledMethod), method_getTypeEncoding(newSwizzledMethod));
if (didAddMethod) {
class_replaceMethod(originClass, swizzledSelector , method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod, newSwizzledMethod);
}
}
- (void) stub_run: (CGFloat) merter {
NSLog(@"StubProxyObject --- stub_run");
if (merter > 20) {
[self stub_run:merter];
}
}
@end
然后在其它地方 Person 對象調用 run: 方法時:
Person * person = [Person new];
[person run:30];
控制臺會打印:
StubProxyObject --- stub_run
person --- 30.000000
用法四: 給分類添加屬性:
我們知道, 分類中是無法設置屬性的,如果在分類的聲明中寫@property, 能為其生成get 和 set 方法的聲明,但無法生成成員變量,就是說雖然點語法能調用出來,但程序執(zhí)行后會crash...
示例: 給NSObject添加分類(NSObject+Category)設置屬性
#import <Foundation/Foundation.h>
@interface NSObject (Extention)
@property (nonatomic, strong) NSString * name;
@end
#import "NSObject+Extention.h"
#import <objc/runtime.h>
static char * const nameKey = "nameKey";
@implementation NSObject (Extention)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, nameKey);
}
@end
用法五: 利用runtime進行解歸檔操作
首先得知道三個方法:
- 獲得某個類的所有成員變量(outCount 會返回成員變量的總數(shù))
Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
- 獲得成員變量的名字
const char *ivar_getName(Ivar v)
- 獲得成員變量的類型
const char *ivar_getTypeEndcoding(Ivar v)
利用runtime 獲取所有屬性來重寫歸檔解檔方法(對于類中屬性比較多時, 用runtime來解歸檔比較方便)
// 設置不需要歸解檔的屬性
- (NSArray *)ignoredNames {
return @[@"_aaa",@"_bbb",@"_ccc"];
}
// 歸檔調用方法
- (void)encodeWithCoder:(NSCoder *)aCoder {
// 獲取所有成員變量
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
// 獲得成員變量的名字
const char * name = ivar_getName(ivar);
// 將每個成員變量名轉換為NSString對象類型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不需要歸檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 通過成員變量名,取出成員變量的值
id value = [self valueForKeyPath:key];
// 再將值歸檔
[aCoder encodeObject:value forKey:key];
// 這兩步就相當于 [aCoder encodeObject:@(self.age) forKey:@"_age"];
}
free(ivars);
}
// 解檔方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
// 獲取所有成員變量
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
// 獲得成員變量的名字
const char * name = ivar_getName(ivar);
// 將每個成員變量名轉換為NSString對象類型
NSString *key = [NSString stringWithUTF8String:name];
// 忽略不需要解檔的屬性
if ([[self ignoredNames] containsObject:key]) {
continue;
}
// 根據(jù)變量名解檔取值,無論是什么類型
id value = [aDecoder decodeObjectForKey:key];
// 取出的值再設置給屬性
[self setValue:value forKey:key];
// 這兩步就相當于以前的 self.age = [aDecoder decodeObjectForKey:@"_age"];
}
free(ivars);
}
return self;
}
若項目中解歸檔的類比較多時, 就可以考慮 NSObject 分類來寫上述邏輯了....
NSObject+Extension.h
#import <Foundation/Foundation.h>
@interface NSObject (Extention)
@property (nonatomic, strong) NSString * name;
- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder;
- (void)decode:(NSCoder *)aDecoder;
@end
NSObject+Extention.m
#import "NSObject+Extension.h"
#import <objc/runtime.h>
@implementation NSObject (Extension)
// 歸檔調用方法
- (void)encode:(NSCoder *)aCoder {
// 一層層父類往上查找,對父類的屬性執(zhí)行歸解檔方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有實現(xiàn)該方法再去調用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [self valueForKeyPath:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
// 解檔方法
- (void)decode:(NSCoder *)aDecoder {
// 一層層父類往上查找,對父類的屬性執(zhí)行歸解檔方法
Class c = [self class];
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有實現(xiàn)該方法再去調用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
@end
在需要歸解檔的對象中實現(xiàn)下面方法即可:
// 設置需要忽略的屬性
- (NSArray *)ignoredNames {
return @[@"_aaa"];
}
// 在系統(tǒng)方法內來調用我們的方法
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
[self decode:aDecoder];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self encode:aCoder];
}
然而多個類需要解歸檔時, 上面的代碼還是重復的, 所以我們直接可以定義個宏, 在需要的類里直接一句宏就搞定了,這也是MJExtention里一句宏搞定解歸檔的實現(xiàn)原理;
在 NSObject+Extension.h 里, 我們定義一個宏:
#define YQCodingImplementation \
-(void)encodeWithCoder:(NSCoder *)aCoder\
{\
[self encode:aCoder];\
}\
-(instancetype)initWithCoder:(NSCoder *)aDecoder\
{\
if (self = [super init]) {\
[self decode:aDecoder];\
}return self; \
}
然后在需要的類里:
#import "Person.h"
#import "NSObject+HZCoding.h"
@implementation Person
// 歸檔 解檔 , 一句宏就可以了...
YQCodingImplementation
@end
用法六: 利用runtime 獲取所有屬性來進行字典轉模型
可以看參考文章, 里面寫的比較詳細了....
參考文章: http://m.itdecent.cn/p/ab966e8a82e2
http://www.cocoachina.com/ios/20161102/17920.html