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設置值
[self setValue:value forKey:key];
}
}
return self;
}
- (void)func1
{
NSLog(@"執(zhí)行了func1方法");
}
- (void)func2
{
NSLog(@"執(zhí)行了func2方法");
}
// 測試消息轉發(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結構體指針五嫂,包含了變量名、變量類型等信息肯尺∥衷担可以看到私有屬性_name instanceName都能夠訪問到了。在有些項目中则吟,為了對某些私有屬性進行隱藏槐臀,某些.h文件中沒有出現(xiàn)相應的顯式創(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結構體指針祠够,表示對類中的某個方法的描述压汪。
在api中的定義typedef struct objc_method *Method;
而objc_method結構體如下:
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,本質上是一個函數(shù)指針朝氓。
SEL:數(shù)據(jù)類型魔市,表示方法選擇器,可以理解為對方法的一種包裝赵哲。在每個方法都有一個與之對應的SEL類型的數(shù)據(jù)待德,根據(jù)一個SEL數(shù)據(jù)"@selector(方法名)"就可以找到對應的方法地址,進而調用方法枫夺。
因此可以通過:獲取Method結構體->得到SEL選擇器的名稱->得到對應的方法名将宪,這樣的方式認識OC中關于方法的定義。
*/
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轉為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先轉為NSNumber類型;
第四個參數(shù)是使用策略劝萤,是一個枚舉值渊涝,類似@property屬性創(chuàng)建時設置的關鍵字,可從命名看出各枚舉的意義
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()方法椭住,本質上只是為對象person添加了對height的屬性關聯(lián)崇渗,但是達到了新屬性的作用;
使用場景:假設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ù)表示待調用的方法名稱金拒,第三個參數(shù)IMP表示一個函數(shù)指針兽肤,這里表示指定具體的實現(xiàn)。
- (IBAction)addMethod:(id)sender {
/* 動態(tài)添加方法:
第一個參數(shù)表示 Class cls 類型绪抛;
第二個參數(shù)表示待調用的方法名稱资铡;
第三個參數(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)(方法的內部都默認包含兩個參數(shù)Class和SEL方法症副,被稱為隱式參數(shù)店雅。)
int myAddingFunction(id self, SEL _cmd) {
NSLog(@"已新增方法:NewMethod");
return 1;
}
6.交換兩種方法之后(功能對調)
Method政基、class_getInstanceMethod、method_exchangeImplementation
要使用dispatch_once執(zhí)行方法交換闹啦,方法交換要求線程安全沮明,而且保證在任何情況下只能交換一次。
回顧下NSObject類的+load和+initialize這兩個方法吧窍奋。在程序啟動時荐健,Runtime會去加載所有的類。在這一時期琳袄,如果類或者類的分類實現(xiàn)了+load方法江场,則會去調用這個方法。而+initialize方法是在類或子類第一次接收消息之前會被調用窖逗,這包括類的實例對象或者類對象扛稽。如果類一直沒有被用到,則這個方法不會被調用滑负≡谡牛基于這兩個方法的特殊性,我們可以將類使用時所需要的一些前置條件在這兩個方法中處理矮慕。不過帮匾,如果可能,應該盡量放在+initialize中痴鳄。因為+load方法是在程序啟動時調用瘟斜,勢必會影響到程序的啟動時間。而+initialize方法可以說是懶加載調用痪寻,只有用到才會去執(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. 序列化相關库继,歸檔&解檔
這里實際上還是獲取到所有屬性值,然后使用KVC來設置值窜醉。注意是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設置值
[self setValue:value forKey:key];
}
}
return self;
}
9. 實現(xiàn)字典和模型的自動轉換
優(yōu)秀的JSON轉模型第三方庫JSONModel拜英、YYModel等都利用runtime對屬性進行獲取,賦值操作琅催,要比KVC進行模型轉換更強大居凶,更有效率。在YYModel的源碼可以看出藤抡,YY對NSObject的內容進行了又一次封裝侠碧,添加了許多描述內容。其中YYClassInfo是對Class進行了再次封裝缠黍,而YYClassIvarInfo弄兜、YYClassMethodInfo、YYCIPropertyInfo分別是對Class的Ivar瓷式、Method和property進行了封裝和描述替饿。在提取Class的相關信息時都運用了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);
}
以下是一個簡陋版的字典轉模型方案
@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進行遞歸,轉為模型
if (![typeString hasPrefix:@"NS"]) {
Class modelClass = NSClassFromString(typeString);
value = [modelClass setModelWithDict:value];
}
}
//將字典中的值設置給模型
[Model setValue:value forKeyPath:key];
}
free(property_t);
return Model;
}
@end
10. 實現(xià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];
}
}];
// 跳轉到對應的控制器
[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];
// 屬性名轉為字符串
NSString *propertyName = [[NSString alloc] initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
// 判斷該屬性是否存在
if ([propertyName isEqualToString:verifyPropertyName]) {
free(properties);
return YES;
}
}
free(properties);
return NO;
}
11. 消息轉發(fā)(resolveInstanceMethod:)
一個方法的聲明必定會有與之對應的實現(xiàn),如果調用了只有聲明沒有實現(xiàn)的方法會導致程序crash饥漫,而實現(xiàn)并非只有中規(guī)中矩的在.m里寫上相同的方法名再在內部寫實現(xiàn)代碼榨呆。
當調用[receiver message]時,會觸發(fā)id objc_msgSend(id self庸队, SEL op积蜻, ...)這個函數(shù)闯割。
receiver通過isa指針找到當前對象的class,并在class中尋找op竿拆,如果找到宙拉,調用op,如果沒找到丙笋,到super_class中繼續(xù)尋找谢澈,如此循環(huán)直到NSObject(引自引文)。 如果NSObject中仍然沒找到御板,程序并不會立即crash锥忿,而是按照優(yōu)先級執(zhí)行下列三個方法(下列方法優(yōu)先級依次遞減,高優(yōu)先級方法消息轉發(fā)成功不會再執(zhí)行低優(yōu)先級方法):
- 1 + resolveInstanceMethod:(SEL)sel // 對應實例方法
- resolveClassMethod:(SEL)sel // 對應類方法
- 2 - (id)forwardingTargetForSelector:(SEL)aSelector
- 3 - (void)forwardInvocation:(NSInvocation *)anInvocation
// 注意sayHello方法并沒有實現(xiàn)哦怠肋,如果直接調用的話是會崩潰的
- (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. 消息轉發(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. 消息轉發(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調用任何OC的原生接口,替換任意的OC原生方法笙各。目前主要用于下發(fā)JS腳本替換原生OC代碼钉答,實時修復線上bug,更多詳情可以閱讀JSPatch技術文檔杈抢。JSPatch能做到通過JS調用和改寫OC方法最根本的原因是OC是動態(tài)語言数尿,OC上所有方法的調用和類的生成都通過OC Runtime在運行時進行,我們可以通過類名/方法名反射得到相應的類和方法春感。理論上你可以在運行時通過類名/方法名調用任何OC方法砌创,替換任何類的實現(xiàn)以及新增任意類,所以JSPatch的基本原理是:JS傳遞字符串給OC鲫懒,OC通過Runtime接口調用和替換OC方法嫩实。這是最基礎的原理。
在JSPatch實現(xiàn)方法替換上窥岩,通過Selector調用方法時甲献,會從methodList鏈表里找到對應的Method進行調用,這個methodList上的元素是可以動態(tài)替換的颂翼,可以把某個Selector對應的函數(shù)指針I(yè)MP替換成新的晃洒,也可以拿到已有的某個Selector對應的函數(shù)指針I(yè)MP,讓另一個Selector跟它對應朦乏,Runtime提供了相應的方法實現(xiàn)這些球及。
參考文章
Runtime常用的幾個應用場景
iOS開發(fā)-- Runtime的幾個小例子
iOS開發(fā)之Runtime常用示例總結