(三)iOS--Runtime原理、應用淺談

今天來總結(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)毕匀。
然后下面這個比較常見的圖片也就能理解了

截屏2021-03-16 下午5.33.59.png

方法的實現(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)用
既然是消息的傳遞肯定會有找不到方法的時候,那找不到方法時病瞳,又是怎么處理的呢揽咕。先來看看這張圖

截屏2021-03-17 上午10.49.14.png

從上圖看到悲酷,運行時會調(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的源碼,我肯定看不下來炕矮。只能看別人的博客么夫,加上自己的理解,寫出來肤视。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末档痪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子邢滑,更是在濱河造成了極大的恐慌腐螟,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件困后,死亡現(xiàn)場離奇詭異乐纸,居然都是意外死亡,警方通過查閱死者的電腦和手機摇予,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門汽绢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人侧戴,你說我怎么就攤上這事宁昭〉穑” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵积仗,是天一觀的道長疆拘。 經(jīng)常有香客問我,道長寂曹,這世上最難降的妖魔是什么哎迄? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮稀颁,結(jié)果婚禮上芬失,老公的妹妹穿的比我還像新娘。我一直安慰自己匾灶,他們只是感情好棱烂,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阶女,像睡著了一般颊糜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上秃踩,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天衬鱼,我揣著相機與錄音,去河邊找鬼憔杨。 笑死鸟赫,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的消别。 我是一名探鬼主播抛蚤,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寻狂!你這毒婦竟也來了岁经?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蛇券,失蹤者是張志新(化名)和其女友劉穎缀壤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纠亚,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡塘慕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了菜枷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苍糠。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖啤誊,靈堂內(nèi)的尸體忽然破棺而出岳瞭,到底是詐尸還是另有隱情拥娄,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布瞳筏,位于F島的核電站稚瘾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏姚炕。R本人自食惡果不足惜摊欠,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柱宦。 院中可真熱鬧些椒,春花似錦、人聲如沸掸刊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽忧侧。三九已至石窑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蚓炬,已是汗流浹背松逊。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肯夏,地道東北人经宏。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像驯击,于是被迫代替她去往敵國和親烛恤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容