• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • 淺談Runtime運行時

    runtime

    runtime又叫運行時,是一套底層的c語言api,其為iOS內部核心之一。OC是動態運行時語言,它會將一些工作放在代碼運行時去處理,而非編譯時,比如動態的遍歷屬性和方法,動態的添加屬性和方法,動態的修改屬性和方法等。

    了解runtime,首先要先了解它的核心--消息傳遞。

    消息傳遞

    消息直到運行時才會與方法實踐綁定起來。
    一個實例對象調用實例方法,像這樣[obj doSomething];,編譯器轉成消息發送objc_msgSend(obj, @selector(doSomething),,);

    OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
    
    

    runtime時的運行流程如下:

    1. 首先通過調用對象的isa找到class;
    2. 在class的method_list里面找該方法,這里如果是實例對象,則去實例對象的類的方法列表中找,如果是類對象調用類方法,則去元類的方法列表中找,具體下面解釋;
    3. 如果class里沒找到,繼續往它的superClass里找;
    4. 一旦找到doSomething這個函數,就去執行它的實現IMP;

    下面介紹一下對象(object),類(class),方法(method)的結構體:

    //對象
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    //類
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    } OBJC2_UNAVAILABLE;
    
    //方法列表
    struct objc_method_list {
        struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
        int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    //方法
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }
    
    

    類對象(objc_class)

    OC中類是Class來表示,實際上是一個指向objc_class結構體的指針。

    //對象
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    //類
    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    } OBJC2_UNAVAILABLE;
    //方法列表
    struct objc_method_list {
        struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
        int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }       
    
    

    觀察一下對象的結構體和類對象的結構體,可以看到里面都有一個isa指針,對象的isa指針指向類,類的isa指針指向元類(metaClass),元類也是類,元類的isa指針最終指向根元類(rootMetaClass),根元類的isa指針指向自己,最終形成一個閉環。

    image

    可以看到類結構體中有一個methodLists,也就解釋了上文提到的成員方法記錄在class method-list中,類方法記錄在metaClass中。即Instance-object的信息記錄在class-object中,而class-object的信息記錄在meta-class中。

    結構體中有一個ivars指針指向objc_ivar_list結構體,是該類的屬性列表,因為編譯器編譯順序是父類,子類,分類,所以這也就是為什么分類category不能添加屬性,因為類在編譯的時候已經注冊在runtime中了,屬性列表objc_ivar_list和instance_size內存大小都已經確定了,同時runtime會調用class_setIvarLayout和class_setWeakIvarLayout來處理strong和weak引用。可以通過runtime的關聯屬性來給分類添加屬性(原因是category結構體中有一個instanceProperties,下文會講到)。因為編譯順序是父類,子類,分類,所以消息遍歷的順序是分類,子類,父類,先進后出。

    objc_cache結構體,是一個很有用的方法緩存,把經常調用的方法緩存下來,提高遍歷效率。將方法的method_name作為key,method_imp作為value保存下來。

    Method(objc_method)

    結構體如下:

    //方法
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }
    
    

    可以看到里面有一個SEL和IMP,這里講一下兩者的區別。

    SEL是selector的OC表示,數據結構為:typedef struct objc_selector *SEL;是個映射到方法的c字符串;不同于函數指針,函數指針直接保存了方法地址,SEL只是一個編號;也是objc_cache中的key。

    ps.這也帶來了一個弊端,函數重載不適用,因為函數重載是方法名相同,參數名不同,但是SEL只記了方法名,沒有參數,所以沒法區分不同的method。

    ps.在不同的類中,相同的方法名,方法選擇器也是相同的。

    IMP是函數指針,數據結構為typedef id (IMP)(id,SEL,**);保存了方法地址,由編譯器綁定生成,最終方法執行哪段代碼由IMP決定。IMP指向了方法的實現,一組id和SEL可以確定唯一的實現。

    有了SEL這個中間過程,我們可以對一個編號和方法實現做些中間操作,也就是說我們一個SEL可以指向不同的函數指針,這樣就可以完成一個方法名在不同的時候執行不同的函數體。另外可以將SEL作為參數傳遞給不同的類執行,也就是我們某些業務只知道方法名但需要根據不同的情況讓不同的類執行。個人理解,消息轉發就是利用了這個中間過程。

    runtime是如何通過selector找到對應的IMP的?
    上文講了類對象中有實例方法的列表,元類對象中有類方法的列表,列表中記錄著方法的名稱,參數和實現。而selector本質就是方法名稱也就是SEL,通過方法名稱可以在列表中找到方法實現。

    在尋找IMP的時候,runtime提供了兩種方法:

    1. IMP class_getMethodImplementation(Class cls, SEL name);
    2. IMP method_getImplementation(Method m);

    對于第一種方法來說,實例方法和類方法都是調用這個方法來找到IMP,不同的是第一個參數,實例方法傳的參數是[obj class];,而類方法傳的參數是objc_getMetaClass("obj");
    對于第二種方法來說,傳入的參數只有Method,區分類方法和實例方法在于封裝Method的函數,類方法:Method class_getClassMethod(Class cls, SEL name);實例方法:Method class_getInstanceMethod(Class cls, SEL name);

    Category(objc_category)

    category是表示指向分類的一個結構體指針,結構體如下:

    struct category_t { 
        const char *name; 
        classref_t cls; 
        struct method_list_t *instanceMethods; 
        struct method_list_t *classMethods;
        struct protocol_list_t *protocols;
        struct property_list_t *instanceProperties;
    };
    
    
    name:是指 class_name 而不是 category_name。
    cls:要擴展的類對象,編譯期間是不會定義的,而是在Runtime階段通過name對應到對應的類對象。
    instanceMethods:category中所有給類添加的實例方法的列表。
    classMethods:category中所有添加的類方法的列表。
    protocols:category實現的所有協議的列表。
    instanceProperties:表示Category里所有的properties,這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加實例變量的原因,不過這個和一般的實例變量是不一樣的。
    
    

    從上面的結構體可以看出,分類category可以添加實例方法,類方法,協議,以及通過關聯對象添加屬性,不可以添加成員變量。

    runtime消息轉發

    前文講到,到一個方法被執行,也就是發送消息,會去相關的方法列表中尋找對應的方法實現IMP,如果一直到根類都沒找到就會進入到消息轉發階段,下面介紹一下消息轉發的最后三個集會。

    • 動態方法解析
    • 備用接收者
    • 完整消息轉發

    動態方法解析

    首先,當消息傳遞到根類都找不到方法實現時,運行時runtime會調用+resolveInstanceMethod:或者+resolveClassMethod:,讓你有機會提供一個函數實現。如果你添加了函數,并返回了yes,那運行時就會重新走一步消息發送的過程。

    實現一個動態方法解析的例子如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //執行foo函數
        [self performSelector:@selector(foo:)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(foo:)) {//如果是執行foo函數,就動態解析,指定新的IMP
            class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    void fooMethod(id obj, SEL _cmd) {
        NSLog(@"Doing foo");//新的foo函數
    }
    
    

    可以看到雖然沒有實現foo這個函數,但是我們通過class_addMethod動態的添加了一個新的函數實現fooMethod,并返回了yes。

    如果返回no,就會進入下一步,- forwardingTargetForSelector:。

    備用接收者

    實現的例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface Person: NSObject
    
    @end
    
    @implementation Person
    
    - (void)foo {
        NSLog(@"Doing foo");//Person的foo函數
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //執行foo函數
        [self performSelector:@selector(foo)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return NO;//返回NO,進入下一步轉發
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(foo)) {
            return [Person new];//返回Person對象,讓Person對象接收這個消息
        }
    
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    
    

    可以看到我們通過-forwardingTargetForSelector:方法將當前viewController的foo函數轉發給了Person的foo函數去執行了。

    如果在這一步還不能處理未知的消息,則進入下一步完整消息轉發。

    完整消息轉發

    首先會發送-methodSignatureForSelector:消息獲得函數的參數和返回值類型。如果-methodSignatureForSelector:返回nil,runtime會發出-doseNotRecognizeSelector消息,程序會掛掉;如果返回一個函數標簽,runtime就會創建一個NSInvocation對象,并發送-forwardInvocation:消息給目標對象。

    實現例子如下:

    #import "ViewController.h"
    #import "objc/runtime.h"
    
    @interface Person: NSObject
    
    @end
    
    @implementation Person
    
    - (void)foo {
        NSLog(@"Doing foo");//Person的foo函數
    }
    
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        //執行foo函數
        [self performSelector:@selector(foo)];
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return NO;//返回NO,進入下一步轉發
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return nil;//返回nil,進入下一步轉發
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];//簽名,進入forwardInvocation
        }
    
        return [super methodSignatureForSelector:aSelector];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        SEL sel = anInvocation.selector;
    
        Person *p = [Person new];
        if([p respondsToSelector:sel]) {
            [anInvocation invokeWithTarget:p];
        }
        else {
            [self doesNotRecognizeSelector:sel];
        }
    
    }
    
    @end
    
    

    通過簽名,runtime生成了一個anInvocation對象,發送給了forwardInvocation:,我們再forwardInvocation:里面讓Person對象去執行了foo函數。

    以上就是runtime的三次函數轉發流程。

    版權聲明:本文為weixin_48504920原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/weixin_48504920/article/details/106662721

    智能推薦

    (轉)自定義注解之運行時注解(RetentionPolicy.RUNTIME),獲取注解

    對注解概念不了解的可以先看這個:Java注解基礎概念總結 前面有提到注解按生命周期來劃分可分為3類: 1、RetentionPolicy.SOURCE:注解只保留在源文件,當Java文件編譯成class文件的時候,注解被遺棄; 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加載class文件時候被遺棄,這是默認的生命周期; 3、RetentionPolicy....

    JVM-012-運行時常量池(Runtime Constant Pool)

    運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。 Java虛擬機對Class文件每一部分(自然也包括常量池)的格式都有嚴格規定,每一個字節用于存儲哪種...

    Objective-C Runtime 運行時:方法中 SEL, IMP, Method 的定義與關系

    SEL 代表方法的名稱。僅以名字來識別。 不論兩個類是否存在依存關系,只要他們擁有相同的方法名,那么他們的SEL都是相同的。比如,有n個viewcontroller頁面,每個頁面都有一個viewdidload,每個頁面的載入,肯定都是不盡相同的。但是我們可以通過打印,觀察發現,這些viewdidload的SEL都是同一個: 因此類方法定義時,盡量不要用相同的名字,就算是變量類型不同也不行。否則會引...

    【Android】【學習筆記】運行時資源替換-Runtime Resource Overlay 學習實踐

    初入職場的新的軟件攻城獅,每天任務除了基礎的技能學習,包括C++、Java、Andorid、算法等,就是跟著工作導師布置的任務,學習一些實用的小技能。工作導師作為資深程序猿,悶騷是一大屬性,除了自己要能主動勾搭,還有就是要學會提高自覺性了。這天工作導師冷不丁發來一個鏈接,主要內容就是本次需要詳細記錄的Android運行時資源替換:http://blog.csdn.net/azhengye/arti...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    猜你喜歡

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!

    Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...

    requests實現全自動PPT模板

    http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    精品国产乱码久久久久久蜜桃不卡