iOS底層知識(shí) -- runtime(運(yùn)行時(shí))詳解

1.什么是運(yùn)行時(shí)?

1>運(yùn)行時(shí)是一套 純C(C和匯編寫(xiě)的) 的API。而 OC 就是運(yùn)行時(shí)機(jī)制,也就是在運(yùn)行時(shí)候的一些機(jī)制,其中最主要的是消息機(jī)制。
2>編譯器最終都會(huì)講OC代碼轉(zhuǎn)換為運(yùn)行時(shí)代碼

我們先來(lái)看看官方函數(shù)objc_msgSend的聲明:

/* Basic Messaging Primitives
 *
 * On some architectures, use objc_msgSend_stret for some struct return types.
 * On some architectures, use objc_msgSend_fpret for some float return types.
 * On some architectures, use objc_msgSend_fp2ret for some float return types.
 *
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */
void objc_msgSend(void /* id self, SEL op, ... */ )

從這個(gè)函數(shù)的注釋可以看出來(lái)了,這是個(gè)最基本的用于發(fā)送消息的函數(shù)。另外,這個(gè)函數(shù)并不能發(fā)送所有類(lèi)型的消息,只能發(fā)送基本的消息。比如,在一些處理器上,我們必須使用objc_msgSend_stret來(lái)發(fā)送返回值類(lèi)型為結(jié)構(gòu)體的消息,使用objc_msgSend_fpret來(lái)發(fā)送返回值類(lèi)型為浮點(diǎn)類(lèi)型的消息,而又在一些處理器上,還得使用objc_msgSend_fp2ret來(lái)發(fā)送返回值類(lèi)型為浮點(diǎn)類(lèi)型的消息。

最關(guān)鍵的一點(diǎn):無(wú)論何時(shí),要調(diào)用objc_msgSend函數(shù),必須要將函數(shù)強(qiáng)制轉(zhuǎn)換成合適的函數(shù)指針類(lèi)型才能調(diào)用。

從objc_msgSend函數(shù)的聲明來(lái)看,它應(yīng)該是不帶返回值的,但是我們?cè)谑褂弥袇s可以強(qiáng)制轉(zhuǎn)換類(lèi)型,以便接收返回值。另外,它的參數(shù)列表是可以任意多個(gè)的,前提也是要強(qiáng)制函數(shù)指針類(lèi)型。

例如:

    [[NSObject alloc] init];
    等于是給 NSObject 送一個(gè)消息 objc_msgSend()  如alloc方法 objc_msgSend(NSObject, @selector(alloc));
     objc_msgSend();
     每一句OC代碼到最底層都是轉(zhuǎn)換成 runtime 運(yùn)行時(shí)代碼

objc_msgSend的原型是:

 id objc_msgSend(id theReceiver, SELtheSelector, ...)
參數(shù)分別是消息接收對(duì)象,消息對(duì)應(yīng)的方法的標(biāo)識(shí)SEL,以及參數(shù)。
在執(zhí)行objc_msgSend方法時(shí),主要完成了以下幾個(gè)工作:
The messaging function does everything necessary for dynamic binding:
(1),It first finds the procedure (method implementation) that the selector refers to. Since the same method can be implemented differently by separate classes, the precise procedure that it finds depends on the class of the receiver.
它首先找到 SEL 對(duì)應(yīng)的方法實(shí)現(xiàn) IMP。因?yàn)椴煌念?lèi)對(duì)同一方法可能會(huì)有不同的實(shí)現(xiàn),所以找到的方法實(shí)現(xiàn)依賴(lài)于消息接收者的類(lèi)型。
(2),It then calls the procedure, passing it the receiving object (a pointer to its data), along with any arguments that were specified for the method.
然后將消息接收者對(duì)象(指向消息接收者對(duì)象的指針)以及方法中指定的參數(shù)傳遞給方法實(shí)現(xiàn) IMP。
(3),Finally, it passes on the return value of the procedure as its own return value.
最后,將方法實(shí)現(xiàn)的返回值作為該函數(shù)的返回值返回。

OC代碼轉(zhuǎn)換為C語(yǔ)言代碼驗(yàn)證:

clang -rewrite-objc 的作用是把oc代碼轉(zhuǎn)寫(xiě)成c/c++代碼,我們常用它來(lái)窺探OC的底層實(shí)現(xiàn)。
打開(kāi)終端,來(lái)到main.m所在目錄,執(zhí)行如下命令:
執(zhí)行之后,目錄下多出一個(gè)main.cpp文件,然后就可以打開(kāi)看看看具體實(shí)現(xiàn)

可以看到main函數(shù)的具體實(shí)現(xiàn)為


image.png
 可以知道這句代碼  [[NSObject alloc] init] 底層C語(yǔ)言實(shí)現(xiàn)為
 ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
3>運(yùn)行時(shí)可以做很多底層操作比如:
(1)動(dòng)態(tài)添加對(duì)象的成員變量和成員方法
  • runtime動(dòng)態(tài)添加屬性

應(yīng)用場(chǎng)景
在分類(lèi)中,所寫(xiě)的@property (nonatomic, strong) NSString *name;都僅僅是生成了get和set方法,并沒(méi)有生成對(duì)應(yīng)的_name屬性,但是有時(shí)候我們會(huì)有一種需求,想要讓分類(lèi)中保存一下新的屬性值,因?yàn)閟et和get方法只能是對(duì)已經(jīng)有的東西做操作,比如說(shuō)最常用的UIView的分類(lèi)我們對(duì)frame中的x,y,width,height做操作。

解決
用runtime動(dòng)態(tài)的給分類(lèi)添加屬性,并且另他產(chǎn)生關(guān)聯(lián),使用如下

.h文件實(shí)現(xiàn)
@property (nonatomic, strong) NSString *name;
.m文件實(shí)現(xiàn)
#import <objc/runtime.h>
// 動(dòng)態(tài)添加屬性
- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
    return objc_getAssociatedObject(self, @"name");
    
}
  • runtime實(shí)現(xiàn)動(dòng)態(tài)添加方法

    由于OC是動(dòng)態(tài)語(yǔ)言,所以只要聲明了一個(gè)方法,那么這個(gè)對(duì)象就是可以調(diào)用這個(gè)方法的,無(wú)論這個(gè)方法是否實(shí)現(xiàn)。當(dāng)執(zhí)行這個(gè)方式時(shí),發(fā)現(xiàn)沒(méi)有被實(shí)現(xiàn)那么就會(huì)報(bào)錯(cuò),通過(guò)可以使用runtime動(dòng)態(tài)添加方法,來(lái)解決這個(gè)問(wèn)題。其次,使用performSelector方法也可以給對(duì)象方法消息。
    一般情況,只要是聲明的方法一定要實(shí)現(xiàn),但是這樣做有定義的弊端就是無(wú)論這些方法是否要用,都會(huì)被實(shí)現(xiàn),那么就會(huì)添加到相應(yīng)的“方法編號(hào)區(qū)”、“方法列表區(qū)”、“方法區(qū)”這樣就會(huì)消耗內(nèi)存,其實(shí)可以使用runtime的動(dòng)態(tài)添加方法來(lái)解決這一狀況。

具體實(shí)現(xiàn)為:

.m文件
#import <objc/runtime.h>
// 動(dòng)態(tài)添加方法
void addtest(id self, SEL sel) {
    NSLog(@"動(dòng)態(tài)添加方法%@", self);
}
//有未實(shí)現(xiàn)的 ‘對(duì)象方法’的時(shí)候就會(huì)調(diào)用這個(gè)方法,在這個(gè)方法中進(jìn)行動(dòng)態(tài)添加方法的處理
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == NSSelectorFromString(@"addtest")) {
        //class: 給那個(gè)類(lèi)添加方法
        //SEL:添加那個(gè)方法
        //IMP:方法實(shí)現(xiàn) 函數(shù) 函數(shù)入口 函數(shù)名
        // type: 包含方法的參數(shù)
        class_addMethod(self, sel, (IMP)addtest, "v@:");
        return YES;
    } return [super resolveInstanceMethod:sel]; 
}
(2)獲得某個(gè)類(lèi)的所有成員方法、所有成員變量

unsigned int count;

  • 獲取屬性列表
 objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);
    }
  • 獲取方法列表
Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Method method = methodList[i];
        NSLog(@"method---->%@",
        NSStringFromSelector(method_getName(method)));
    }
  • 獲取成員變量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i; i<count; i++) {
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"Ivar---->%@",
        [NSString stringWithUTF8String:ivarName]);
    }
(3)動(dòng)態(tài)交換兩個(gè)方法的實(shí)現(xiàn)(特別是系統(tǒng)自帶方法)

適用場(chǎng)景:iOS6(擬物化)-->iOS7(扁平化),從iOS7開(kāi)始使用扁平化的圖標(biāo)風(fēng)格,這代表著要把之前所有設(shè)置圖片的地方重新設(shè)置一遍,工作比較繁瑣,還適用于重大的促銷(xiāo)節(jié)日,不同的節(jié)日顯示不同的圖標(biāo),等等

  • 為UIImage添加拓展UIImage+Extension
    具體實(shí)現(xiàn)方法:
#import "UIImage+Extension.h"
#import <objc/runtime.h>

@implementation UIImage (Extension)

// 當(dāng)某個(gè)類(lèi)或分類(lèi)加載進(jìn)內(nèi)存時(shí),會(huì)調(diào)用一次
+ (void)load{
    // 得到類(lèi)方法
//    class_getClassMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>)
    // 得到對(duì)象方法
//    class_getInstanceMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>)
    Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
    Method m2 = class_getClassMethod([UIImage class], @selector(runtime_imageNamed:));
    method_exchangeImplementations(m1, m2);
}

// 自定義新方法覆蓋系統(tǒng)方法
+ (UIImage *)runtime_imageNamed:(NSString *)name{
    double version = [[UIDevice currentDevice].systemVersion doubleValue];
    if (version >= 7.0) {  // 如果系統(tǒng)版本大于7.0 就加載另一套圖片,  這里也可以加日期判斷,如果是節(jié)日促銷(xiāo)就顯示不同的圖標(biāo)
        name = [name stringByAppendingString:@"_os7"];
    }
    return [UIImage imageNamed:name];
}

2.如何利用運(yùn)行時(shí)

1>將某些OC代碼轉(zhuǎn)為運(yùn)行時(shí)代碼,探究底層,比如block實(shí)現(xiàn)原理
2>攔截系統(tǒng)自帶的方法調(diào)用,比如攔截imageNamed: viewDidLoad alloc(ObjC黑科技 - Method Swizzle)
3>實(shí)現(xiàn)字典和模型的自動(dòng)轉(zhuǎn)換
4>實(shí)現(xiàn)NSCoding屬性的自動(dòng)歸檔和自動(dòng)解檔

歸檔接檔可以避免以下這樣的代碼


image.png

具體做法為

.h文件
#import <Foundation/Foundation.h>

@interface Model : NSObject<NSCoding>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double weight;
@property (copy, nonatomic) NSString *name;
@end

.m文件
#import "Model.h"
#import <objc/runtime.h>

@implementation Model
/*
 從文件中讀取對(duì)象時(shí)會(huì)調(diào)用這個(gè)方法(開(kāi)發(fā)者需要在此方法中說(shuō)明那些屬性需要提取出來(lái))
 */
- (instancetype)initWithCoder:(NSCoder *)decoder{
    if (self = [super init]) {
        // 用來(lái)存儲(chǔ)成員變量的數(shù)量
        unsigned int outCount = 0;
        // 獲得當(dāng)前類(lèi)下所有的成員變量
        Ivar *ivars =class_copyIvarList([self class], &outCount);
        // 遍歷所有的成員變量
        for (int i = 0; i < outCount; i ++) {
            // 取出i位置所有對(duì)應(yīng)的成員變量
            Ivar ivar = ivars[i];
//            C語(yǔ)言代碼轉(zhuǎn)變?yōu)镺C代碼
//            + (nullable instancetype)stringWithUTF8String:(const char *)nullTerminatedCString;
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            // 獲得key對(duì)應(yīng)的值
            id value = [decoder decodeObjectForKey:key];
            // 設(shè)置到成員變量上
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    
    return self;
}
/*
 將對(duì)象寫(xiě)入文件的時(shí)候會(huì)調(diào)用這個(gè)方法(開(kāi)發(fā)者需要在此方法中說(shuō)明要存儲(chǔ)那些屬性)
 */
- (void)encodeWithCoder:(NSCoder *)encoder{
    // 用來(lái)存儲(chǔ)成員變量的數(shù)量
    unsigned int outCount = 0;
    // 獲得當(dāng)前類(lèi)下所有的成員變量
    Ivar *ivars =class_copyIvarList([self class], &outCount);
    // 遍歷所有的成員變量
    for (int i = 0; i < outCount; i ++) {
        // 取出i位置所有對(duì)應(yīng)的成員變量
        Ivar ivar = ivars[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 獲得key對(duì)應(yīng)的值
        id value = [self valueForKey:key];
        
        [encoder encodeObject:value forKey:key];
    }
    free(ivars);
}

調(diào)用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    Model *model = [[Model alloc] init];
    model.age = 25;
    model.name = @"Bob";
    model.weight = 60.3;
    [NSKeyedArchiver archiveRootObject:model toFile:@"/Users/bobzhou/Desktop/model.data"];
    
    Model *model1 = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/bobzhou/Desktop/model.data"];
    NSLog(@"%zd---%@---%lf", model1.age, model1.name, model1.weight);
}
5>實(shí)現(xiàn)分類(lèi)增加屬性(實(shí)現(xiàn)分類(lèi)屬性互不干擾)

在分類(lèi)中聲明一個(gè)@property,只會(huì)生成get和set方法的聲明,并不會(huì)實(shí)現(xiàn)
場(chǎng)景:為所有的對(duì)象都增加一個(gè)name屬性,任何對(duì)象都可以賦值調(diào)用該屬性
為所有的對(duì)象增加屬性,我們可以考慮為NSObject寫(xiě)一個(gè)拓展

.h文件實(shí)現(xiàn)
#import <Foundation/Foundation.h>

@interface NSObject (Extension)
/*
 相當(dāng)于是是有聲明,沒(méi)有方法的實(shí)現(xiàn)
 - (void)setName:(NSString *)name
 - (NSString *)name
 */
@property (nonatomic, copy) NSString *name;

@end

.m文件實(shí)現(xiàn)
/** 為所有OC 對(duì)象都添加一個(gè)屬性 NSString *name*/
#import "NSObject+Extension.h"
#import <objc/runtime.h>

@implementation NSObject (Extension)

char NameKey;
- (void)setName:(NSString *)name{
    // 將某個(gè)值 跟 某個(gè)對(duì)象關(guān)聯(lián)起來(lái) (將某個(gè)值 存儲(chǔ)到 某個(gè)對(duì)象中)
    /*
     id  _Nonnull object:表示關(guān)聯(lián)者,是一個(gè)對(duì)象,變量名理所當(dāng)然也是object
     const void * _Nonnull key:獲取被關(guān)聯(lián)者的索引key
     id  _Nullable value:被關(guān)聯(lián)者,這里是一個(gè)block
     objc_AssociationPolicy policy: 關(guān)聯(lián)時(shí)采用的協(xié)議,有assign,retain,copy等協(xié)議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
     */
    
    objc_setAssociatedObject(self, &NameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}
- (NSString *)name{
    // 利用參數(shù)key可以將對(duì)象object中存儲(chǔ)的對(duì)應(yīng)值取出來(lái)
    return objc_getAssociatedObject(self, &NameKey);
}

調(diào)用如下:
 /*
     為所有的對(duì)象增加一個(gè)屬相name
     */
    NSObject *obj = [[NSObject alloc] init];
    obj.name = @"rumtime-obj";
    
    UITableView *tableview = [[UITableView alloc] init];
    tableview.name = @"runtime-tableview";
    
    NSArray *array = [NSArray array];
    array.name = @"runtime-array";
    
    NSLog(@"%@--%@--%@", obj.name, tableview.name, array.name);
打印結(jié)果如下: rumtime-obj+++++++runtime-tableview+++++++runtime-array

3.運(yùn)行時(shí)常用的函數(shù)

  • 得到類(lèi)方法<objc/runtime.h>
 class_getClassMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>) 
  • 得到對(duì)象方法<objc/runtime.h>
class_getInstanceMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>)

  • 交換兩個(gè)方法的實(shí)現(xiàn)<objc/runtime.h>
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
  • 給某個(gè)對(duì)象發(fā)消息<objc/message.h>
objc_msgSend()
  • 將某個(gè)值 跟 某個(gè)對(duì)象關(guān)聯(lián)起來(lái) (將某個(gè)值 存儲(chǔ)到 某個(gè)對(duì)象中)
    /*
     id  _Nonnull object:表示關(guān)聯(lián)者,是一個(gè)對(duì)象,變量名理所當(dāng)然也是object
     const void * _Nonnull key:獲取被關(guān)聯(lián)者的索引key
     id  _Nullable value:被關(guān)聯(lián)者,這里是一個(gè)block
     objc_AssociationPolicy policy: 關(guān)聯(lián)時(shí)采用的協(xié)議,有assign,retain,copy等協(xié)議,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
     */
  objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
  • 利用參數(shù)key可以將對(duì)象object中存儲(chǔ)的對(duì)應(yīng)值取出來(lái)
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
  • 獲得當(dāng)前類(lèi)下所有的成員變量(outCount返回成員變量總數(shù))
Ivar *ivars = class_copyIvarList([self class], &outCount);
  • 獲得成員變量名稱(chēng)
ivar_getName(Ivar _Nonnull v) 
  • 獲得成員變量類(lèi)型
ivar_getTypeEncoding(<#Ivar  _Nonnull v#>)
  • 釋放內(nèi)存(當(dāng)C語(yǔ)言中包含copy,create,retain,new等詞語(yǔ),那需要在最后面釋放資源)
free(ivars);

Demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容