Runtime簡介以及常見的使用場景
? ?Runtime簡稱運行時,是一套比較底層的純C語言的API,作為OC的核心,運行時是一種面向?qū)ο蟮木幊陶Z言的運行環(huán)境,其中最主要的是消息機制,Objective-C就是基于運行時的。
? ?所謂運行時,是指盡可能地把決定從編譯期推遲到運行期,就是盡可能地做到動態(tài).只是在運行的時候才會去確定對象的類型和方法的.因此利用Runtime機制可以在程序運行時動態(tài)地修改類和對象中的所有屬性和方法。
? ?對于C語言,函數(shù)的調(diào)用在編譯的時候會決定調(diào)用哪個函數(shù)。對于OC的函數(shù),屬于動態(tài)調(diào)用過程,在編譯的時候并不能決定真正調(diào)用哪個函數(shù),只有在真正運行的時候才會根據(jù)函數(shù)的名稱找到對應的函數(shù)來調(diào)用。
Objective-C從三種不同的層級上與Runtime系統(tǒng)進行交互,分別是
①通過Objective-C源代碼;
②通過Foundation框架的NSObject類定義的方法;
③通過對Runtime函數(shù)的直接調(diào)用(需要導入#import );
? ?大部分情況下只管寫OC代碼就行,因為OC底層默認實現(xiàn)Runtime,每一個OC的方法,底層必然有一個與之對應的Runtime方法。。
以下是Runtime的一些使用場景:
1.發(fā)送消息
方法調(diào)用的本質(zhì),就是讓對象發(fā)送消息。objc_msgSend,只有對象才能發(fā)送消息
舉個簡單的例子:如下
//(1).調(diào)用對象方法:
//創(chuàng)建Dog對象
Dog *dog = [[Dog alloc] init];
//調(diào)用對象方法
[dog run];
//調(diào)用對象本質(zhì):讓對象發(fā)送消息
objc_msgSend(dog, @selector(run));
//(2).調(diào)用類方法方式:
//有兩種
//第一種通過類名調(diào)用
[Dog run];
//第二種通過類對象調(diào)用
[[Dog class] run];
//用類名調(diào)用類方法,底層會自動把類名轉(zhuǎn)換成類對象調(diào)用
//調(diào)用類方法本質(zhì):讓類對象發(fā)送消息
objc_msgSend([Dog class],@selector(run));
消息機制原理:對象根據(jù)方法編號SEL去映射表查找對應的方法實現(xiàn)
2.動態(tài)添加方法
開發(fā)使用場景:如果一個類方法非常多,加載類到內(nèi)存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態(tài)給某個類,添加方法解決。經(jīng)典面試題:有沒有使用performSelector,其實主要想問你有沒有動態(tài)添加過方法。簡單使用
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
//默認person,沒有實現(xiàn)eat方法,可以通過performSelector調(diào)用,但是會報錯。
//動態(tài)添加方法就不會報錯
[p performSelector:@selector(eat)];
}
@end
@implementation Person
//void(*)()
//默認方法都有兩個隱式參數(shù),
void eat(id self,SEL sel)
{
NSLog(@"%@%@",self,NSStringFromSelector(sel));
}
//當一個對象調(diào)用未實現(xiàn)的方法,會調(diào)用這個方法處理,并且會把對應的方法列表傳過來.
//剛好可以用來判斷,未實現(xiàn)的方法是不是我們想要動態(tài)添加的方法
+(BOOL)resolveInstanceMethod:(SEL)sel
{ ?if (sel == @selector(eat)) {
//動態(tài)添加eat方法
/**第一個參數(shù):給哪個類添加方法,第二個參數(shù):添加方法的方法編號,第三個參數(shù):添加方法的函數(shù)實現(xiàn)(函數(shù)地址),第四個參數(shù):函數(shù)的類型,(返回值+參數(shù)類型) v:void @:對象->self :表示SEL->_cmd*/
class_addMethod(self, @selector(eat),eat, "v@:"); ?}
return [super resolveInstanceMethod:sel];
}
@end
注意:
? ?+ (BOOL)resolveInstanceMethod:(SEL)aSEL這個函數(shù)與forwardingTargetForSelector類似,都會在對象不能接受某個selector時觸發(fā),執(zhí)行起來略有差別。前者的目的主要在于給用戶一個機會來向該對象添加所需的selector,后者的目的在于允許用戶將selector轉(zhuǎn)發(fā)給另一個對象。另外觸發(fā)時機也不完全一樣,該函數(shù)是個類函數(shù),在程序剛啟動,界面尚未顯示出時,就會被調(diào)用。
? ?在類不能處理某個selector的情況下,如果類重載了該函數(shù),并使用class_addMethod添加了相應的selector,并返回YES,那么后面forwardingTargetForSelector就不會被調(diào)用,如果在該函數(shù)中沒有添加相應的selector,那么不管返回什么,后面都會繼續(xù)調(diào)用forwardingTargetForSelector,如果在forwardingTargetForSelector并未返回能接受該selector的對象,那么resolveInstanceMethod會再次被觸發(fā),這一次,如果仍然不添加selector,程序就會報異常
3.運行時關(guān)聯(lián)對象提高效率,給分類添加屬性。
使用的時候與懶加載的特點相似,從`關(guān)聯(lián)對象`中獲取對象屬性,如果有,直接返回。
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loadingthe view, typically from a nib.
//給系統(tǒng)NSObject類動態(tài)添加屬性name
NSObject *objc = [[NSObject alloc] init];
objc.name = @"旺財";
NSLog(@"%@",objc.name);
}
@end
//定義關(guān)聯(lián)的key
staticconst char *key = "name";
@implementation NSObject (Property)
-(NSString *)name
{
//根據(jù)關(guān)聯(lián)的key,獲取關(guān)聯(lián)的值。
NSString*name =objc_getAssociatedObject(self, key);
if(name! = nil) {
return name;
}}
-(void)setName:(NSString *)name
{//第一個參數(shù):給哪個對象添加關(guān)聯(lián)
//第二個參數(shù):關(guān)聯(lián)的key,通過這個key獲取
//第三個參數(shù):關(guān)聯(lián)的value
//第四個參數(shù):關(guān)聯(lián)的策略
objc_setAssociatedObject(self, key, name,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
4.使用運行時字典轉(zhuǎn)模型
大體思路:利用運行時,遍歷模型中所有屬性,根據(jù)模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性賦值。步驟:提供一個NSObject分類,專門字典轉(zhuǎn)模型,以后所有模型都可以通過這個分類轉(zhuǎn)。(所有字典轉(zhuǎn)模型框架的核心算法)
創(chuàng)建NSObject的分類Runtime:
在.h中的類方法如下:
#import
@interface NSObject (Runtime)
///給定一個字典,創(chuàng)建self類對應的對象
///@param dict字典
///@return對象
+ (instancetype)hd_objWithDict:(NSDictionary*)dict;
///獲取類的屬性列表數(shù)組
///@return類的屬性列表數(shù)組
+ (NSArray*)hd_objProperties;
@end
在.m中的類方法如下:
//所有字典轉(zhuǎn)模型框架的核心算法
+ (instancetype)hd_objWithDict:(NSDictionary*)dict {
//實例化對象
id object = [[self alloc]init];
//使用字典,設置對象信息
1>獲得self的屬性列表
NSArray *proList = [self hd_objProperties];
2>遍歷字典
[dictenumerateKeysAndObjectsUsingBlock:^(id_Nonnullkey,id_Nonnullobj,BOOL*_Nonnullstop) {
NSLog(@"key %@ --- value %@", key,obj);
//
3>判斷key是否在proList中
if([proListcontainsObject:key]) {
//說明屬性存在,可以使用`KVC`設置數(shù)值
[objectsetValue:objforKey:key];
}}];
return object;
}
constchar* kPropertiesListKey ="CZPropertiesListKey";
+ (NSArray*)hd_objProperties{
//從`關(guān)聯(lián)對象`中獲取對象屬性,如果有,直接返回!
//獲取關(guān)聯(lián)對象-動態(tài)添加的屬性
NSArray*ptyList =objc_getAssociatedObject(self,kPropertiesListKey);
if(ptyList != nil) {
return ptyList;
}
//調(diào)用運行時方法,取得類的屬性列表
//Ivar成員變量
//Method方法
//Property屬性
//Protocol協(xié)議
//所有屬性的`數(shù)組`,C語言中,數(shù)組的名字,就是指向第一個元素的地址
//retain/create/copy需要release,最好option + click
unsigned int count =0;
objc_property_t *proList =class_copyPropertyList([self class], &count);
NSLog(@"屬性的數(shù)量%d", count);
//創(chuàng)建數(shù)組
NSMutableArray *arrayM = [NSMutableArrayarray];
//遍歷所有的屬性
for(unsignedinti = 0; i < count; i++) {
// 1.從數(shù)組中取得屬性
//C語言的結(jié)構(gòu)體指針,通常不需要`*`
objc_property_t ?pty = proList[i];
// 2.從pty中獲得屬性的名稱
const char *cName =property_getName(pty);
NSString *name = [NSStringstringWithCString:cNameencoding:NSUTF8StringEncoding];
//NSLog(@"%@",name);
// 3.屬性名稱添加到數(shù)組
[arrayM addObject:name];
}
//釋放數(shù)組
free(proList);
// ---
2.對象的屬性數(shù)組已經(jīng)獲取完畢,利用關(guān)聯(lián)對象,動態(tài)添加屬性
/**
參數(shù)
1.對象self [OC中class也是一個特殊的對象]
2.動態(tài)添加屬性的key,獲取值的時候使用
3.動態(tài)添加的屬性值
4.對象的引用關(guān)系
*/
objc_setAssociatedObject(self,kPropertiesListKey, arrayM.copy,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return arrayM.copy;
}
注意:必須保證,模型中的屬性和字典中的key一一對應。如果不一致,就會調(diào)用[ setValue:forUndefinedKey:],報key找不到的錯。
分析:模型中的屬性和字典的key不一一對應,系統(tǒng)就會調(diào)用setValue:forUndefinedKey:報錯。
解決:重寫對象的setValue:forUndefinedKey:,把系統(tǒng)的方法覆蓋,
就能繼續(xù)使用KVC,字典轉(zhuǎn)模型了。
-(void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}
? ?通過運行時字典轉(zhuǎn)模型的好處在于寫在NSObject的分類中,和類的關(guān)聯(lián)性不強對類解耦,以后再做字典轉(zhuǎn)模型的時候只需要把這個分類往任何一個程序中一拖,程序中的對象就都具備了這個字典轉(zhuǎn)模型的方法。
5.交叉方法(黑魔法)
? ?開發(fā)使用場景:系統(tǒng)自帶的方法功能不夠,給系統(tǒng)自帶的方法擴展一些功能,并且保持原有的功能。方式一:繼承系統(tǒng)的類,重寫方法.方式二:使用runtime,交換方法.
? ?Runtime在AFN中的使用細節(jié):在AFN的NSURLSessionMangerM方法里面第363行寫了一個靜態(tài)的內(nèi)聯(lián)函數(shù),做了一個交叉方法,交叉的是af_resume和resume方法,這樣的話,可以在發(fā)送網(wǎng)絡之前發(fā)起一個通知,能接受到任何一個網(wǎng)絡請求的事件的變化。
@implementation ViewController
-(void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loadingthe view, typically from a nib.
//需求:給imageNamed方法提供功能,每次加載圖片就判斷下圖片是否加載成功。
//步驟一:先搞個分類,定義一個能加載圖片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
//步驟二:交換imageNamed和imageWithName的實現(xiàn),就能調(diào)用imageWithName,間接調(diào)用imageWithName的實現(xiàn)。
UIImage *image = [UIImageimageNamed:@"123"];
}
@end
@implementation UIImage (Image)
//加載分類到內(nèi)存的時候調(diào)用
+(void)load
{
//交換方法
//獲取imageWithName方法地址
Method imageWithName =class_getClassMethod(self, @selector(imageWithName:));
//獲取imageWithName方法地址
Method imageName =class_getClassMethod(self, @selector(imageNamed:));
//交換方法地址,相當于交換實現(xiàn)方式
method_exchangeImplementations(imageWithName,imageName);
}
//既能加載圖片又能打印
+(instancetype)imageWithName:(NSString *)name
{//這里調(diào)用imageWithName,相當于調(diào)用imageName
UIImage *image = [self imageWithName:name];
if (image == nil) {
NSLog(@"加載空的圖片");}
return image;
}
@end