runtime是OC中一個很重要的概念,通過這個機制可以幫我們做很多事情。
不給出demo的文章都是耍流氓
demo截圖如下:

首先我們先創(chuàng)建一個Person類。.h和.m文件如下:
#import <Foundation/Foundation.h>
@interface Person : NSObject <NSCoding>
@property (nonatomic, assign) int age; // 屬性變量
- (void)func1;
- (void)func2;
- (void)sayHello1:(NSString *)name;
@end
#import "Person.h"
#import <objc/runtime.h>
@interface Person ()
@property (nonatomic, copy) NSString *name;
@end
@implementation Person {
NSString *instanceName;
}
- (instancetype)init
{
self = [super init];
if (self) {
_name = @"Tom";
instanceName = @"Jim";
_age = 12;
}
return self;
}
// 需要歸檔哪些屬性! 常規(guī)方法
//- (void)encodeWithCoder:(NSCoder *)aCoder
//{
// [aCoder encodeObject:_name forKey:@"name"];
// [aCoder encodeInt:_age forKey:@"age"];
//}
// 解檔
//- (instancetype)initWithCoder:(NSCoder *)aDecoder
//{
// self = [super init];
// if (self) {
// _name = [aDecoder decodeObjectForKey:@"name"];
// _age = [aDecoder decodeIntForKey:@"age"];
// }
// return self;
//}
// 使用runtime來歸檔、解檔
- (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 拿到每個成員變量
Ivar ivar = ivars[i];
// 拿名稱
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
// 歸檔 -- 利用KVC
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 拿到每一個成員變量
Ivar ivar = ivars[i];
// 拿名稱
const char * name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
// 解檔
id value = [aDecoder decodeObjectForKey:key];
// 利用KVC設(shè)置值
[self setValue:value forKey:key];
}
}
return self;
}
- (void)func1
{
NSLog(@"執(zhí)行了func1方法");
}
- (void)func2
{
NSLog(@"執(zhí)行了func2方法");
}
// 測試消息轉(zhuǎn)發(fā)
- (void)sayHello1:(NSString *)name
{
NSLog(@"Hello, I am a person");
}
- (NSString *)description
{
return [NSString stringWithFormat:@"name:%@ age:%d", self.name, self.age];
}
@end
1.獲取所有變量
Ivar、class_copyIvarList、ivar_getName、ivar_getTypeEncoding
objc_property_t、class_copyPropertyList、property_getName
- (IBAction)getAllVariable:(id)sender
{
unsigned int count = 0;
// 獲取類的一個包含所有變量的列表,Ivar是runtime聲明的一個宏,是實例變量的意思
Ivar *allVariables = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 遍歷每一個變量,包括名稱和類型 (此處沒有星號"*"),
Ivar ivar = allVariables[i];
const char *variablename = ivar_getName(ivar); // 獲取成員變量名稱
const char *variableType = ivar_getTypeEncoding(ivar); // 獲取成員變量類型
NSLog(@"(Name: %s)----(Type:%s)", variablename, variableType);
}
/*
2017-07-05 13:07:00.735 RuntimeDemo[3353:127039] (Name: instanceName)----(Type:@"NSString")
2017-07-05 13:07:00.735 RuntimeDemo[3353:127039] (Name: _age)----(Type:i)
2017-07-05 13:07:00.736 RuntimeDemo[3353:127039] (Name: _name)----(Type:@"NSString")
Ivar,一個指向objc_ivar結(jié)構(gòu)體指針,包含了變量名、變量類型等信息??梢钥吹剿接袑傩訽name instanceName都能夠訪問到了。在有些項目中,為了對某些私有屬性進行隱藏,某些.h文件中沒有出現(xiàn)相應(yīng)的顯式創(chuàng)建,而是如上面的person類中,在.m中進行私有創(chuàng)建,但是我們可以通過runtime這個有效的方法,訪問到所有包括這些隱藏的私有變量。
*/
NSLog(@"測試一下class_copyPropertyList的區(qū)別");
objc_property_t *allProperties = class_copyPropertyList([Person class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = allProperties[i];
const char *char_f = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:char_f];
NSLog(@"property = %@", propertyName);
}
/*
2017-07-05 11:55:16.961 RuntimeDemo[3187:98751] property = name
2017-07-05 11:55:16.961 RuntimeDemo[3187:98751] property = age
如果單單需要獲取屬性列表,可以使用函數(shù):class_copyPropertyList(),instanceName作為實例變量是不被獲取的,而class_copyIvarList()函數(shù)則能夠返回實例變量和屬性變量的所有成員變量。
*/
free(allVariables);
free(allProperties);
}
2. 獲取Person所有方法
Method、class_copyMethodList、SEL、method_getName、sel_getName
這里獲取方法列表,所有.m文件顯式實現(xiàn)的方法都會被找到,當然也包括setter+getter方法了。Method是runtime聲明的一個宏,表示對一個方法的描述。method_getName是獲取SEL,即獲取方法選擇器@selector()。還可以通過sel_getName以字符串獲取sel的name,也即@selector()中的方法名稱。
- (IBAction)getAllMethod:(id)sender
{
unsigned int count;
// 獲取方法列表,所有在.m文件顯式實現(xiàn)的方法都會被找到,包括setter+getter方法;
Method *allMethods = class_copyMethodList([Person class], &count);
for (int i = 0; i < count; i++) {
// Method,為runtime聲明的一個宏,表示對一個方法的描述
Method md = allMethods[i];
// 獲取SEL:SEL類型,即獲取方法選擇器@selector()
SEL sel = method_getName(md);
// 得到sel的方法名:以字符串格式獲取sel的name,也即@selector()中的方法名稱
const char *methodname = sel_getName(sel);
NSLog(@"(Method:%s)", methodname);
}
}
/*
控制臺輸出:
2017-07-05 13:17:13.380 RuntimeDemo[3392:134673] (Method:age)
2017-07-05 13:17:13.381 RuntimeDemo[3392:134673] (Method:func1)
2017-07-05 13:17:13.386 RuntimeDemo[3392:134673] (Method:func2)
2017-07-05 13:17:13.386 RuntimeDemo[3392:134673] (Method:setAge:)
2017-07-05 13:17:13.386 RuntimeDemo[3392:134673] (Method:.cxx_destruct)
2017-07-05 13:17:13.386 RuntimeDemo[3392:134673] (Method:description)
2017-07-05 13:17:13.386 RuntimeDemo[3392:134673] (Method:name)
2017-07-05 13:17:13.386 RuntimeDemo[3392:134673] (Method:setName:)
2017-07-05 13:17:13.387 RuntimeDemo[3392:134673] (Method:init)
控制臺輸出了包括set和get等方法名。
分析:Method是一個指向objc_method結(jié)構(gòu)體指針,表示對類中的某個方法的描述。
在api中的定義typedef struct objc_method *Method;
而objc_method結(jié)構(gòu)體如下:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
method_name:方法選擇器@selector(),類型為SEL。相同名字的方法下,即使在不同類中定義,它們的方法選擇器也相同。
method_types:方法類型,是個char指針,存儲著方法的參數(shù)類型和返回值類型。
method_imp: 指向方法的具體實現(xiàn)的指針,數(shù)據(jù)類型為IMP,本質(zhì)上是一個函數(shù)指針。
SEL:數(shù)據(jù)類型,表示方法選擇器,可以理解為對方法的一種包裝。在每個方法都有一個與之對應(yīng)的SEL類型的數(shù)據(jù),根據(jù)一個SEL數(shù)據(jù)"@selector(方法名)"就可以找到對應(yīng)的方法地址,進而調(diào)用方法。
因此可以通過:獲取Method結(jié)構(gòu)體->得到SEL選擇器的名稱->得到對應(yīng)的方法名,這樣的方式認識OC中關(guān)于方法的定義。
*/
3.改變person的_name變量屬性
Ivar、class_copyIvarList、object_setIvar
- (IBAction)changeVariable:(id)sender {
NSLog(@"改變前的person:%@", self.person);
unsigned int count = 0;
Ivar *allList = class_copyIvarList([Person class], &count);
Ivar ivv = allList[2];
object_setIvar(self.person, ivv, @"Mike"); // name屬性Tom被強制改為Mike。
NSLog(@"改變之后的person: %@", self.person);
}
4.添加新屬性
objc_setAssociatedObject、objc_getAssociatedObject
id objc_getAssociatedObject(id object, const void *key)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
這里第一個參數(shù)是需要添加屬性的對象,第二個參數(shù)是屬性的key,必須是C語言字符串,第三個參數(shù)是屬性的值,類型為id,所以此處先將height轉(zhuǎn)為NSNumber類型,在分類中,即使使用@property定義了,也只是生產(chǎn)set+get方法,而不會生成_變量名,分類中是不允許定義變量的。
#import "Person.h"
@interface Person (PersonCategory)
@property (nonatomic, assign) float height; // 新屬性
@end
#import "Person+PersonCategory.h"
#import <objc/runtime.h>
const char * str = "myKey"; // 作為key,字符常量 必須是C語言字符串
@implementation Person (PersonCategory)
- (void)setHeight:(float)height
{
NSNumber *num = [NSNumber numberWithFloat:height];
/*
第一個參數(shù)是需要添加屬性的對象;
第二個參數(shù)是屬性的key;
第三個參數(shù)是屬性的值,類型必須為id,所以此處height先轉(zhuǎn)為NSNumber類型;
第四個參數(shù)是使用策略,是一個枚舉值,類似@property屬性創(chuàng)建時設(shè)置的關(guān)鍵字,可從命名看出各枚舉的意義
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
*/
objc_setAssociatedObject(self, str, num, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// 提取屬性的值
- (float)height
{
NSNumber *number = objc_getAssociatedObject(self, str);
return [number floatValue];
}
@end
- (IBAction)addVariable:(id)sender {
self.person.height = 12; // 給新屬性height賦值
NSLog(@"添加的新屬性height = %f", [self.person height]); // 訪問新屬性
}
/*
點擊按鈕四、再點擊按鈕一、二獲取類的屬性、方法
2017-07-05 14:14:23.648 RuntimeDemo[3640:165606] 12.000000
2017-07-05 14:14:28.026 RuntimeDemo[3640:165606] (Name: instanceName)----(Type:@"NSString")
2017-07-05 14:14:28.026 RuntimeDemo[3640:165606] (Name: _age)----(Type:i)
2017-07-05 14:14:28.026 RuntimeDemo[3640:165606] (Name: _name)----(Type:@"NSString")
2017-07-05 14:14:28.027 RuntimeDemo[3640:165606] 測試一下class_copyPropertyList的區(qū)別
2017-07-05 14:14:28.027 RuntimeDemo[3640:165606] property = height
2017-07-05 14:14:28.027 RuntimeDemo[3640:165606] property = name
2017-07-05 14:14:28.027 RuntimeDemo[3640:165606] property = age
2017-07-05 14:14:28.886 RuntimeDemo[3640:165606] (Method:age)
2017-07-05 14:14:28.886 RuntimeDemo[3640:165606] (Method:func1)
2017-07-05 14:14:28.886 RuntimeDemo[3640:165606] (Method:func2)
2017-07-05 14:14:28.887 RuntimeDemo[3640:165606] (Method:setAge:)
2017-07-05 14:14:28.887 RuntimeDemo[3640:165606] (Method:.cxx_destruct)
2017-07-05 14:14:28.887 RuntimeDemo[3640:165606] (Method:description)
2017-07-05 14:14:28.887 RuntimeDemo[3640:165606] (Method:name)
2017-07-05 14:14:28.887 RuntimeDemo[3640:165606] (Method:setName:)
2017-07-05 14:14:28.888 RuntimeDemo[3640:165606] (Method:init)
2017-07-05 14:14:28.888 RuntimeDemo[3640:165606] (Method:height)
2017-07-05 14:14:28.888 RuntimeDemo[3640:165606] (Method:setHeight:)
可以看到分類的新屬性可以在person對象中對新屬性height進行訪問賦值。
獲取person類屬性時,依然沒有height的存在,但是卻有height和setHeight這兩個方法;因為在分類中,即使使用@property定義了,也只是生產(chǎn)set+get方法,而不會生成_變量名,分類中是不允許定義變量的。
使用runtime中objc_setAssociatedObject()和objc_getAssociatedObject()方法,本質(zhì)上只是為對象person添加了對height的屬性關(guān)聯(lián),但是達到了新屬性的作用;
使用場景:假設(shè)imageCategory是UIImage類的分類,在實際開發(fā)中,我們使用UIImage下載圖片或者操作過程需要增加一個URL保存一段地址,以備后期使用。這時可以嘗試在分類中動態(tài)添加新屬性MyURL進行存儲。
*/
5.添加新的方法(這種方法等價于對person類添加Category對方法進行擴展)
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
這里第一個參數(shù)表示Class cls類型,第二個參數(shù)表示待調(diào)用的方法名稱,第三個參數(shù)IMP表示一個函數(shù)指針,這里表示指定具體的實現(xiàn)。
- (IBAction)addMethod:(id)sender {
/* 動態(tài)添加方法:
第一個參數(shù)表示 Class cls 類型;
第二個參數(shù)表示待調(diào)用的方法名稱;
第三個參數(shù) (IMP)myAddingFunction,IMP一個函數(shù)指針,這里表示指定具體實現(xiàn)方法myAddingFunction;
第四個參數(shù)表示方法的參數(shù),0代表沒有參數(shù);
*/
class_addMethod([Person class], @selector(NewMethod), (IMP)myAddingFunction, 0);
[self.person performSelector:@selector(NewMethod)];
}
// 具體的實現(xiàn)(方法的內(nèi)部都默認包含兩個參數(shù)Class和SEL方法,被稱為隱式參數(shù)。)
int myAddingFunction(id self, SEL _cmd) {
NSLog(@"已新增方法:NewMethod");
return 1;
}
6.交換兩種方法之后(功能對調(diào))
Method、class_getInstanceMethod、method_exchangeImplementation
要使用dispatch_once執(zhí)行方法交換,方法交換要求線程安全,而且保證在任何情況下只能交換一次。
回顧下NSObject類的+load和+initialize這兩個方法吧。在程序啟動時,Runtime會去加載所有的類。在這一時期,如果類或者類的分類實現(xiàn)了+load方法,則會去調(diào)用這個方法。而+initialize方法是在類或子類第一次接收消息之前會被調(diào)用,這包括類的實例對象或者類對象。如果類一直沒有被用到,則這個方法不會被調(diào)用?;谶@兩個方法的特殊性,我們可以將類使用時所需要的一些前置條件在這兩個方法中處理。不過,如果可能,應(yīng)該盡量放在+initialize中。因為+load方法是在程序啟動時調(diào)用,勢必會影響到程序的啟動時間。而+initialize方法可以說是懶加載調(diào)用,只有用到才會去執(zhí)行。
- (IBAction)replaceMethod:(id)sender {
Method method1 = class_getInstanceMethod([self.person class], @selector(func1));
Method method2 = class_getInstanceMethod([self.person class], @selector(func2));
// 交換方法
method_exchangeImplementations(method1, method2);
[self.person func1];
}
- (void)func1
{
NSLog(@"執(zhí)行了func1方法");
}
- (void)func2
{
NSLog(@"執(zhí)行了func2方法");
}
/*
輸出
2017-07-06 17:22:39.399 RuntimeDemo[10188:825945] 執(zhí)行了func2方法
交換方法的使用場景:項目中的某個功能,在項目中需要多次被引用,當項目的需求發(fā)生改變時,要使用另一種功能代替這個功能,且要求不改變舊的項目,也就是不改變原來方法實現(xiàn)的前提下。那么,我們可以在分類中,再寫一個新的方法,符合新的需求的方法,然后交換兩個方法的實現(xiàn)。這樣,在不改變項目的代碼,而只是增加了新的代碼的情況下,就完成了項目的改進,很好地體現(xiàn)了該項目的封裝性與利用率。
注:交換兩個方法的實現(xiàn)一般寫在類的load方法里面,因為load方法會在程序運行前加載一次。
*/
7.獲取協(xié)議列表
Protocol、class_copyProtocolList、protocol_getName
- (IBAction)fetchProtocolList:(id)sender {
unsigned int count = 0;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self.person class], &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned i = 0; i < count; i++) {
Protocol *protocol = protocolList[i];
const char *protocolName = protocol_getName(protocol);
[mutableList addObject:[NSString stringWithUTF8String:protocolName]];
}
NSLog(@"獲取到的協(xié)議列表為:%@", mutableList);
}
/*
2017-07-06 17:23:36.327 RuntimeDemo[10188:825945] 獲取到的協(xié)議列表為:(
NSCoding
)
*/
8. 序列化相關(guān),歸檔&解檔
這里實際上還是獲取到所有屬性值,然后使用KVC來設(shè)置值。注意是KVC。
// 需要歸檔哪些屬性! 常規(guī)方法
//- (void)encodeWithCoder:(NSCoder *)aCoder
//{
// [aCoder encodeObject:_name forKey:@"name"];
// [aCoder encodeInt:_age forKey:@"age"];
//}
// 解檔
//- (instancetype)initWithCoder:(NSCoder *)aDecoder
//{
// self = [super init];
// if (self) {
// _name = [aDecoder decodeObjectForKey:@"name"];
// _age = [aDecoder decodeIntForKey:@"age"];
// }
// return self;
//}
// 使用runtime來歸檔、解檔
- (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 拿到每個成員變量
Ivar ivar = ivars[i];
// 拿名稱
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
// 歸檔 -- 利用KVC
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 拿到每一個成員變量
Ivar ivar = ivars[i];
// 拿名稱
const char * name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
// 解檔
id value = [aDecoder decodeObjectForKey:key];
// 利用KVC設(shè)置值
[self setValue:value forKey:key];
}
}
return self;
}
9. 實現(xiàn)字典和模型的自動轉(zhuǎn)換
優(yōu)秀的JSON轉(zhuǎn)模型第三方庫JSONModel、YYModel等都利用runtime對屬性進行獲取,賦值操作,要比KVC進行模型轉(zhuǎn)換更強大,更有效率。在YYModel的源碼可以看出,YY對NSObject的內(nèi)容進行了又一次封裝,添加了許多描述內(nèi)容。其中YYClassInfo是對Class進行了再次封裝,而YYClassIvarInfo、YYClassMethodInfo、YYCIPropertyInfo分別是對Class的Ivar、Method和property進行了封裝和描述。在提取Class的相關(guān)信息時都運用了Runtime。
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
if (ivars) {
NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
_ivarInfos = ivarInfos;
for (unsigned int i = 0; i < ivarCount; i++) {
YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
if (info.name) ivarInfos[info.name] = info;
}
free(ivars);
}
以下是一個簡陋版的字典轉(zhuǎn)模型方案
@interface NSObject (ZCModel)
+(instancetype) setModelWithDict:(NSDictionary*)dict;
@end
@implementation NSObject (ZCModel)
+(instancetype) setModelWithDict:(NSDictionary*)dict{
Class cls = [self class];
id Model = [[self alloc]init];
unsigned int count;
//提取Class的property列表
objc_property_t *property_t = class_copyPropertyList(cls, &count);
//遍歷列表,對每個property分別處理
for (int i =0; i< count; i++) {
objc_property_t property = property_t[i];
NSString *key = [NSString stringWithUTF8String:property_getName(property)];
id value = dict[key];
if (!value) continue;
//提取property的attribute信息
NSString *attribure = [NSString stringWithUTF8String:property_getAttributes(property)];
//從attribute信息中提取其class的信息
if ([attribure hasPrefix:@"T@"]) {
NSRange range = [attribure rangeOfString:@"\","];
NSString *typeString = [attribure substringWithRange:NSMakeRange(3, range.location - 3)];
NSLog(@"the property class is %@",typeString);
//對非NS開頭的class處理為嵌套的model,對model進行遞歸,轉(zhuǎn)為模型
if (![typeString hasPrefix:@"NS"]) {
Class modelClass = NSClassFromString(typeString);
value = [modelClass setModelWithDict:value];
}
}
//將字典中的值設(shè)置給模型
[Model setValue:value forKeyPath:key];
}
free(property_t);
return Model;
}
@end
10. 實現(xiàn)跳轉(zhuǎn)功能,
objc_getClass、objc_allocateClassPair、objc_registerClassPair
- (IBAction)push:(id)sender {
NSDictionary *dic = @{@"class" : @"TestViewController",
@"property" : @{
@"name" : @"guohongwei",
@"name1" : @"guohongwei",
@"phoneNum" : @"1234567890"
}};
// leim
NSString *class = [NSString stringWithFormat:@"%@", dic[@"class"]];
const char *className = [class cStringUsingEncoding:NSASCIIStringEncoding];
// 從一個字符串返回一個類
Class newClass = objc_getClass(className);
if (!newClass) {
// 創(chuàng)建一個類
Class superClass = [NSObject class];
newClass = objc_allocateClassPair(superClass, className, 0);
// 注冊你創(chuàng)建的這個類
objc_registerClassPair(newClass);
}
// 創(chuàng)建對象
id instance = [[newClass alloc] init];
// 對該對象賦值屬性
NSDictionary *propertys = dic[@"property"];
[propertys enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
//檢測這個對象是否存在該屬性, 如果沒有檢查的話,會crash,報錯如下:reason: '[<TestViewController 0x7f88e1403f60> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name1.'
if ([self checkIsExistPropertyWithInstance:instance verifyPropertyName:key]) {
// 利用kvc賦值
[instance setValue:obj forKey:key];
}
}];
// 跳轉(zhuǎn)到對應(yīng)的控制器
[self.navigationController pushViewController:instance animated:YES];
}
// TestViewController *test = [[TestViewController alloc] init];
// [self.navigationController pushViewController:test animated:YES];
- (BOOL)checkIsExistPropertyWithInstance:(id)instance verifyPropertyName:(NSString *)verifyPropertyName
{
unsigned int outCount, i;
// 獲取對象里的屬性列表
objc_property_t *properties = class_copyPropertyList([instance class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
// 屬性名轉(zhuǎn)為字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判斷該屬性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
11. 消息轉(zhuǎn)發(fā)(resolveInstanceMethod:)
一個方法的聲明必定會有與之對應(yīng)的實現(xiàn),如果調(diào)用了只有聲明沒有實現(xiàn)的方法會導致程序crash,而實現(xiàn)并非只有中規(guī)中矩的在.m里寫上相同的方法名再在內(nèi)部寫實現(xiàn)代碼。
當調(diào)用[receiver message]時,會觸發(fā)id objc_msgSend(id self, SEL op, ...)這個函數(shù)。
receiver通過isa指針找到當前對象的class,并在class中尋找op,如果找到,調(diào)用op,如果沒找到,到super_class中繼續(xù)尋找,如此循環(huán)直到NSObject(引自引文)。 如果NSObject中仍然沒找到,程序并不會立即crash,而是按照優(yōu)先級執(zhí)行下列三個方法(下列方法優(yōu)先級依次遞減,高優(yōu)先級方法消息轉(zhuǎn)發(fā)成功不會再執(zhí)行低優(yōu)先級方法):
- 1 + resolveInstanceMethod:(SEL)sel // 對應(yīng)實例方法
- resolveClassMethod:(SEL)sel // 對應(yīng)類方法
- 2 - (id)forwardingTargetForSelector:(SEL)aSelector
- 3 - (void)forwardInvocation:(NSInvocation *)anInvocation
// 注意sayHello方法并沒有實現(xiàn)哦,如果直接調(diào)用的話是會崩潰的
- (IBAction)testResolveInstanceMethod:(id)sender {
[self sayHello:@"runtime"];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(sayHello:)) {
class_addMethod([self class], sel, (IMP)say, "v@:@");
}
return [super resolveInstanceMethod:sel];
}
void say(id self, SEL _cmd, NSString *name) {
NSLog(@"Hello %@", name);
}
12. 消息轉(zhuǎn)發(fā)(forwardingTargetForSelector:)
- (IBAction)testForwardingTargetForSelector:(id)sender {
[self sayHello1:@"runtime"];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello1:)) {
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}
12. 消息轉(zhuǎn)發(fā)(forwardInvocation:)
- (IBAction)testForwardInvacation:(id)sender {
[self sayHello1:@"runtime"];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [Person instanceMethodSignatureForSelector:aSelector];
}
return methodSignature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
Person *person = [Person new];
if ([person respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:person];
}
}
13 熱修復
JSPatch是個優(yōu)秀開源項目,只需要在項目里引入極小的引擎文件就可以使用JavaScript調(diào)用任何OC的原生接口,替換任意的OC原生方法。目前主要用于下發(fā)JS腳本替換原生OC代碼,實時修復線上bug,更多詳情可以閱讀JSPatch技術(shù)文檔。JSPatch能做到通過JS調(diào)用和改寫OC方法最根本的原因是OC是動態(tài)語言,OC上所有方法的調(diào)用和類的生成都通過OC Runtime在運行時進行,我們可以通過類名/方法名反射得到相應(yīng)的類和方法。理論上你可以在運行時通過類名/方法名調(diào)用任何OC方法,替換任何類的實現(xiàn)以及新增任意類,所以JSPatch的基本原理是:JS傳遞字符串給OC,OC通過Runtime接口調(diào)用和替換OC方法。這是最基礎(chǔ)的原理。
在JSPatch實現(xiàn)方法替換上,通過Selector調(diào)用方法時,會從methodList鏈表里找到對應(yīng)的Method進行調(diào)用,這個methodList上的元素是可以動態(tài)替換的,可以把某個Selector對應(yīng)的函數(shù)指針I(yè)MP替換成新的,也可以拿到已有的某個Selector對應(yīng)的函數(shù)指針I(yè)MP,讓另一個Selector跟它對應(yīng),Runtime提供了相應(yīng)的方法實現(xiàn)這些。
參考文章
Runtime常用的幾個應(yīng)用場景
iOS開發(fā)-- Runtime的幾個小例子
iOS開發(fā)之Runtime常用示例總結(jié)