今天來總結(jié)運行時(Runtime)的相關(guān)知識珍手。有的人總是說runtime我沒用過啊汤锨,那為什么面試的時候經(jīng)常要問双抽。其實在項目里你肯定用到了,只是你沒注意闲礼‰剐冢看了好多博客文章,都是先說的原理柬泽,但是原理好多人一看一大堆直接就不想看了慎菲,今天我們先來看看runtime的有什么用。它對我們的開發(fā)又有哪些幫助呢锨并。怎么用它
- 關(guān)聯(lián)對象(Objective-C Associated Objects)給分類增加屬性
- Method Swizzling方法添加和替換 KVO實現(xiàn)
- 消息轉(zhuǎn)發(fā)(熱更新)解決Bug
- 實現(xiàn)NSCoding的自動歸檔和自動解檔
- 實現(xiàn)字典和模形的自動轉(zhuǎn)換
一露该、Runtime的使用
關(guān)聯(lián)對象添加屬性
//關(guān)聯(lián)對象
//objc 被關(guān)聯(lián)的對象
//key 關(guān)聯(lián)的key 要求唯一
//value關(guān)聯(lián)的對象
//policy 內(nèi)存管理的策略
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取關(guān)聯(lián)的對象
id objc_getAssociatedObject(id object, const void *key)
//移除關(guān)聯(lián)的對象
void objc_removeAssociatedObjects(id object)
上面說了方法和參數(shù),下面來使用
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (DefaultColor)
@property (nonatomic, strong) UIColor *defaultColor;
@end
NS_ASSUME_NONNULL_END
@implementation UIView (DefaultColor)
//@dynamic defaultColor;
static char kDefaultColorKey;
-(void)setDefaultColor:(UIColor *)defaultColor{
objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(UIColor *)defaultColor{
return objc_getAssociatedObject(self, &kDefaultColorKey);
}
@end
在set方法中關(guān)聯(lián)屬性琳疏,get方法中獲取關(guān)聯(lián)的對象有决。為了驗證是否添加成功,下來調(diào)用一下
UIView *firstView=[UIView new];
firstView.defaultColor=[UIColor redColor];
NSLog(@"默認顏色是:%@",firstView.defaultColor);
RuntimeDemo[33582:12690582] 默認顏色是:UIExtendedSRGBColorSpace 1 0 0 1
成功的在分類上添加了一個屬性空盼,通過關(guān)聯(lián)對象實現(xiàn)的內(nèi)存管理是由ARC管理的书幕,所以只需要內(nèi)定合適的內(nèi)存策略,就不用擔心對象的釋放揽趾。
二 動態(tài)方法交換
//cls 獲取方法的類
//name 方法的名稱SEL
//獲取類方法的Mthod
Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//獲取實例對象方法的Mthod
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交換兩個方法的實現(xiàn)
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
1.動態(tài)方法交換
下面來看看具體的實現(xiàn)過程
[self testA];
[self testB];
}
-(void)testA{
NSLog(@"測試A");
}
-(void)testB{
NSLog(@"測試B");
}
-(IBAction)btnclick:(UIButton *)sender{
Method test1=class_getInstanceMethod([self class], @selector(testA));
Method test2=class_getInstanceMethod([self class], @selector(testB));
method_exchangeImplementations(test1, test2);
[self testA];
[self testB];
2021-03-15 15:01:31.348613+0800 RuntimeDemo[33614:12699375] 測試A
2021-03-15 15:01:31.348648+0800 RuntimeDemo[33614:12699375] 測試B
2021-03-15 15:01:46.393346+0800 RuntimeDemo[33614:12699375] 測試B
2021-03-15 15:01:46.393598+0800 RuntimeDemo[33614:12699375] 測試A
在最初調(diào)用的順序來看台汇,是測試A--測試B,之后點擊按鈕完成方法的交換篱瞎,最后打印出測試B--測試A苟呐,交換成功。
上面是一個簡單的方法交換俐筋,那么對于系統(tǒng)的方法又是怎么去替換的呢牵素?
攔截并替換系統(tǒng)方法
#import "UIFont+Test.h"
#import <objc/runtime.h>
@implementation UIFont (Test)
+(UIFont *)test_systemFontOfSize:(CGFloat)fontSize{
//獲取設(shè)備屏幕寬度,并計算出比例scale
CGFloat width = [[UIScreen mainScreen] bounds].size.width;
CGFloat scale = width/375.0;
//注意:由于方法交換澄者,系統(tǒng)的方法名已變成了自定義的方法名笆呆,所以這里使用了
//自定義的方法名來獲取UIFont
return [UIFont test_systemFontOfSize:fontSize * scale];
}
+(void)load{
Method method1=class_getClassMethod([UIFont class], @selector(systemFontOfSize:));
Method method2=class_getClassMethod([UIFont class], @selector(test_systemFontOfSize:));
method_exchangeImplementations(method1, method2);
}
創(chuàng)建了UIFont的一個分類请琳,并且攔截替換了系統(tǒng)的systemFontOfSize方法。當在調(diào)用systemFontOfSize時赠幕,可以看到效果是test_systemFontOfSize俄精。
當然也可以攔截替換viewDidLoad。
kvo的實現(xiàn)我們放到后面仔細的說一說原理
實現(xiàn)NSCoding的自動歸檔和解檔
歸檔過程中model中有超級多的屬性是時一個一個處理起來很麻煩榕堰,這個時候就可以使用Runtim來改進他們
//歸檔
-(void)encodeWithCoder:(NSCoder *)coder{
unsigned int count=0;
Ivar *varlist=class_copyIvarList([self class], &count);
for (NSInteger i=0; i<count; i++) {
Ivar ivar=varlist[i];
NSString *key=[NSString stringWithUTF8String:ivar_getName(ivar)];
id value=[self valueForKey:key];
[coder encodeObject:value forKey:key];
}
free(varlist);
}
-(instancetype)initWithCoder:(NSCoder *)coder{
self=[super init];
if (self) {
unsigned int count=0;
Ivar *varlist=class_copyIvarList([self class], &count);
for (NSInteger i=0; i<count; i++) {
Ivar ivar=varlist[i];
const char *name = ivar_getName(ivar);
NSString *key=[NSString stringWithUTF8String:name];
id value = [coder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(varlist);
}
return self;
}
通過Runtime我們拿到類的屬性列表竖慧,遍歷列表來歸檔和解檔。
StudetModel *stModel=[[StudetModel alloc]init];
stModel.name=@"小李子";
stModel.age=@"18";
stModel.number=@"9527";
stModel.score=@"598";
NSString *temp=NSTemporaryDirectory();
NSString *file=[temp stringByAppendingString:@"student.data"];
[NSKeyedArchiver archiveRootObject:stModel toFile:file];
StudetModel *model=[NSKeyedUnarchiver unarchiveObjectWithFile:file];
NSLog(@"person-name:%@逆屡,person-age:%@",model.name,model.age);
打印出:2021-03-15 16:43:38.446580+0800 RuntimeDemo[33700:12732235] person-name:小李子圾旨,person-age:18
實現(xiàn)字典和模型的自動轉(zhuǎn)換(MJExtension)
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
//(1)獲取類的屬性及屬性對應的類型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[i];
//通過property_getName函數(shù)獲得屬性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通過property_getAttributes函數(shù)可以獲得屬性的名字和@encode編碼
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
//立即釋放properties指向的內(nèi)存
free(properties);
//(2)根據(jù)類型給屬性賦值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
以上就是runtime的一些簡單應用場景
Runtime消息傳遞原理
先來看看
PeopleOBJC *people=[[PeopleOBJC alloc]init];
[people testRuntime:@"888"];
上面這段代碼它的實現(xiàn)原理是什么呢?
個人把它分為了3個階段
定位方法
在這之前先要明白2個概念
1.Class
class被定義為指向objc_class的指針
struct objc_class {
Class _Nonnull isa //指向元類
#if !__OBJC2__
Class _Nullable super_class //父類
const char * _Nonnull name //類名
long version //類的版本信息
long info //類信息
long instance_size //該類的大小
struct objc_ivar_list * _Nullable ivars //類的成員變量鏈表
struct objc_method_list * _Nullable * _Nullable methodLists //方法定義鏈表
struct objc_cache * _Nonnull cache //方法緩存
struct objc_protocol_list * _Nullable protocols //協(xié)議鏈表
#endif
} OBJC2_UNAVAILABLE;
isa指向元類康二,后面在詳細的說碳胳。可以看到沫勿,一個勒種保存了所有的成員變量(ivar)、所有的方法(method_list)味混、所有實現(xiàn)的協(xié)議(protocol_list).cache 我們后面在說产雹。下面來看對象的定義
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
這里id被定義指向了objc_object的指針,說明objc_object就是我們常用對象的定義翁锡。一個對象唯一保存的信息是它的class的地址蔓挖。當我們調(diào)用一個對象的方法時,它會通過對象的isa找到對應的objc_class馆衔。
2.元類
在上面我們看了objc_class瘟判,我們把成員變量 方法列表統(tǒng)統(tǒng)去掉,發(fā)現(xiàn)沒角溃,是不是和objc_object拷获。這說明objc_class不僅是個類,它同時也是一個對象
那么問題來了减细,它指向那個類了?
這時候就引出了Meta Class(元類)匆瓜。
每一個類都有對應的元類,而在元類的methodLists 中未蝌,保存了類的方法列表驮吱。isa指針指向?qū)脑悺?br>
那么方法的定位就可以變成
1.對象的isa找到對應的類
2.然后通過類的isa找到了對應的元類
3.在元類的methodLists 中,找到對應的方法萧吠。
元類也是objc_class左冬,那它應該也對應一個對象的啊,按這么弄下去纸型,無限循環(huán)沒個頭啊拇砰,所以元類的isa指向的是基類的元類九昧。而基類的元類的 isa 指向自己。這樣就形成了一個完美的閉環(huán)毕匀。
然后下面這個比較常見的圖片也就能理解了
方法的實現(xiàn)
既然找到了方法铸鹰,我們就來看看方法是怎么實現(xiàn)的。先來看看幾個定義
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
Method定義了一個objc_method指針皂岔,而在objc_method中定義了SEL和imp蹋笼。那么問題來了,這兩又是什么玩意躁垛?
其實在我們平常的項目中絕對見過SEL
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 輸出:viewDidLoad
打印出來的是viewDidLoad剖毯,這說明SEL它只是保存了一個方法名的字符串。
這是不是也就解釋了為什么在同一個類中教馆,不能取相同的名字逊谋。即使它們的參數(shù)類型不同,也不能行土铺。
2.imp
// IMP
typedef id (*IMP)(id, SEL, ...);
可以看到它是一個函數(shù)指針胶滋,它就是函數(shù)的地址。imp中有兩個參數(shù)悲敷,第一個參數(shù)id就是當前對象的地址究恤。第二個參數(shù)SEL 就是方法名
這么一看,Method的機構(gòu)就很明了了后德。它建立了SEL和IMP的關(guān)聯(lián)部宿,當對一個對象發(fā)送消息是,會通過給出的SEL去找到IMP瓢湃,然后執(zhí)行
上面調(diào)用方法的過程是不是也就清晰了理张。
1.當想一個對象發(fā)送消息時,通過對象isa找到它鎖對應的類
2.類的isa指向的元類绵患,在緩存列表中查找方法雾叭。
3.若在緩存中找到,調(diào)用方法藏雏,若沒找到拷况,則去MethodList列表中查找。
4.如果在當前類的方法列表中還沒找到掘殴,則需要到他的父類去查找赚瘦。
5找到方法以后,通過SEL找到IMP奏寨,然后調(diào)用方法起意。
攔截調(diào)用
既然是消息的傳遞肯定會有找不到方法的時候,那找不到方法時病瞳,又是怎么處理的呢揽咕。先來看看這張圖
從上圖看到悲酷,運行時會調(diào)用+resolveInstanceMethod:或者 +resolveClassMethod:,可以提供一個函數(shù)實現(xiàn)亲善。如果添加了函數(shù)设易,并且返回yes,運行時系統(tǒng)就會重新啟動一次消息發(fā)送過程
[self performSelector:@selector(testbtnclick:)];
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel==@selector(testbtnclick:)) {
class_addMethod([self class], sel, (IMP)testbtnclickMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void testbtnclickMethod(id obj,SEL _cmd){
NSLog(@"測試動態(tài)解析");
}
輸出: RuntimeDemo[35397:13336395] 測試動態(tài)解析
平常沒有實現(xiàn)testbtnclick方法的時候蛹头,是不是直接就奔潰了顿肺,但是現(xiàn)在我們通過class_addMethod動態(tài)的添加testbtnclickMethod函數(shù),并執(zhí)行這個函數(shù)的IMP渣蜗,打印出結(jié)果屠尊。
那我不想在這個方法里設(shè)置,還有沒有其他的方式呢耕拷?當然是有的
如果resolve方法返回 YES 讼昆,運行時就會移到下一步:forwardingTargetForSelector。
如果目標對象實現(xiàn)了forwardingTargetForSelector:骚烧,runtime就會調(diào)用這個方法浸赫,就有把這個消息轉(zhuǎn)發(fā)給其他對象的機會。上代碼
#import "PeopleOBJC.h"
@implementation PeopleOBJC
-(void)testRuntime{
NSLog(@"測試runtime數(shù)據(jù)");
}
@end
[self performSelector:@selector(testRuntime)];
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
return YES;
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector==@selector(testRuntime)) {
return [PeopleOBJC new];
}
return [super forwardingTargetForSelector:aSelector];
}
打印出:RuntimeDemo[35441:13341243] 測試runtime數(shù)據(jù)
把當前的testRuntime方法轉(zhuǎn)到了PeopleOBJC實現(xiàn)止潘。
零零總總的幾天掺炭,總結(jié)Runtime,方便自己系統(tǒng)化的理解凭戴。要是真的一行一行的去看runtime的源碼,我肯定看不下來炕矮。只能看別人的博客么夫,加上自己的理解,寫出來肤视。