1. 程式人生 > >iOS之Runtime原理解讀

iOS之Runtime原理解讀

Runtime簡介

做過Android開發的同學都知道,早期的Android系統採用的是Dalvik機制,應用每次執行的時候,位元組碼都需要通過即時編譯器轉換為機器碼,大大的降低了app的執行效率。在Android 5.0系統之後,系統採用了ART機制,應用在第一次安裝的時候,位元組碼就會預先編譯成機器碼,以後每次執行速度大大的提高了。

OC是一門動態語言,所以它總是想辦法把一些決定工作從編譯推遲到執行時,也就是說在iOS的編譯系統裡,光有編譯器是不夠的,還需要一個執行時系統 (runtime system) 來執行編譯後的工作。

iOS系統採用的就是Runtime機制。對於C語言,函式的呼叫在編譯的時候會決定呼叫哪個函式。對於OC函式來說,在編譯的時候並不能決定真正呼叫哪個函式,只有在真正執行的時候才會根據函式的名稱找到對應的函式來呼叫。

基本結構

要談Runtime機制,必然要先了解OC的物件以及類的結構。首先我們看一下和Runtime相關的標頭檔案。
這裡寫圖片描述
和執行時相關的標頭檔案,其中主要使用的函式定義在message.h和runtime.h這兩個檔案中。在message.h中主要包含了一些向物件傳送訊息的函式,這是OC物件方法呼叫的底層實現。使用時只需要匯入標頭檔案即可。

#import <objc/message.h>
#import <objc/runtime.h>

runtime.h是執行時最重要的檔案,其中包含了對執行時進行操作的方法。 主要包括:

操作物件的型別的定義

/// An opaque type that represents a method in a class definition. 一個型別,代表著類定義中的一個方法
typedef struct objc_method *Method; /// An opaque type that represents an instance variable.代表例項(物件)的變數 typedef struct objc_ivar *Ivar; /// An opaque type that represents a category.代表一個分類 typedef struct objc_category *Category; /// An opaque type that represents an Objective-C declared property.代表OC宣告的屬性
typedef struct objc_property *objc_property_t; // Class代表一個類,它在objc.h中這樣定義的 typedef struct objc_class *Class; 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;

這些型別的定義,對一個類進行了完全的分解,將類定義或者物件的每一個部分都抽象為一個型別type,對操作一個類屬性和方法非常方便。OBJC2_UNAVAILABLE標記的屬性是Ojective-C 2.0不支援的,但實際上可以用響應的函式獲取這些屬性。

對於上面的原始碼,有幾個欄位需要說明:
isa:這裡的isa指標同樣是一個指向objc_class的指標,表明該Class的型別,這裡的isa指標指向的就是我們常說的meta-class了。不難看出,類本身也是一個物件。
super_class:這個指標就是指向該class的super class,即指向父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
cache:用於快取最近使用的方法。一個接收者物件接收到一個訊息時,它會根據isa指標去查詢能夠響應這個訊息的物件。在實際使用中,這個物件只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次訊息來時,我們都是methodLists中遍歷一遍,效能勢必很差。這時,cache就派上用場了。在我們每次呼叫過一個方法後,這個方法就會被快取到cache列表中,下次呼叫的時候runtime就會優先去cache中查詢,如果cache沒有,才去methodLists中查詢方法。這樣,對於那些經常用到的方法的呼叫,但提高了呼叫的效率。
version:我們可以使用這個欄位來提供類的版本資訊。這對於物件的序列化非常有用,它可是讓我們識別出不同類定義版本中例項變數佈局的改變。
objc_method_list: 方法連結串列中存放的是該類的成員方法(-方法),類方法(+方法)存在meta-class的objc_method_list連結串列中。

通過圖來描述相應的繼承關係如下:
這裡寫圖片描述
說明:
所有metaclass中isa指標都指向跟metaclass,而跟metaclass則指向自身。
Root metaclass是通過繼承Root class產生的,與root class結構體成員一致,也就是前面提到的結構。
不同的是Root metaclass的isa指標指向自身。
root class的super class 指向的是nil。

函式的定義

函式的定義規則如下:

  • 對物件進行操作的方法一般以object_開頭
  • 對類進行操作的方法一般以class_開頭
  • 對類或物件的方法進行操作的方法一般以method_開頭
  • 對成員變數進行操作的方法一般以ivar_開頭
  • 對屬性進行操作的方法一般以property_開頭開頭
  • 對協議進行操作的方法一般以protocol_開頭

例如:使用runtime對當前的應用中載入的類進行列印操作。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    unsigned int count = 0;
    Class *classes = objc_copyClassList(&count);
    for (int i = 0; i < count; i++) {
        const char *cname = class_getName(classes[i]);
        printf("%s\n", cname);
    }
}

Runtime應用

那麼Runtime在我們實際開發中會起到說明作用呢?主要有以下幾點:
1. 動態的新增物件的成員變數和方法,修改屬性值和方法
2. 動態交換兩個方法的實現
3. 實現分類也可以新增屬性
4. 實現NSCoding的自動歸檔和解檔
5. 實現字典轉模型的自動轉換
6. 動態建立一個類(比如KVO的底層實現)

OC的方法呼叫在Runtime

1.OC程式碼呼叫方法

Receiver *receiver = [[Receiver alloc] init];
 [receiver  message];

2.在編譯時RunTime會將上述程式碼轉化成[傳送訊息]

objc_msgSend(receiver,@selector(message));

下面我們通過一個簡單的例子來講解Runtime的常見應用。
建立Student類

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Student : NSObject
@property(nonatomic,copy)NSString *name;
- (void)eat;
- (void)sleep;
@end

Student.m檔案

#import "Student.h"
@implementation Student
- (void)eat{
    NSLog(@"%@吃飯了",self.name);
}
- (void)sleep{
    NSLog(@"%@睡覺了",self.name);
}
@end

1. 動態變數控制

- (void)changeVariable {
    Student *student = [Student new];
    student.name = @"庫克";
    NSLog(@"%@",student.name);

    unsigned int count;
    Ivar *ivar = class_copyIvarList([student class], &count);
    for (int i = 0; i< count; i++) {
        Ivar var = ivar[i];
        const char *varName = ivar_getName(var);
        NSString *name = [NSString stringWithCString:varName encoding:NSUTF8StringEncoding];
        if ([name isEqualToString:@"_name"]) {
            object_setIvar(student, var, @"Steve Jobs");
            break;
        }
    }
    NSLog(@"%@",student.name);   
}

輸出結果:

2017-05-22 11:06:00.153 Day2017-05-22[9296:1003059] 庫克
2017-05-22 11:06:03.155 Day2017-05-22[9296:1003059] Steve Jobs

2.動態新增方法

void happyNewYear(id self, SEL _cmd){
    NSLog(@"你好庫克");
}

注意:
1.void的前面沒有+、-號,因為只是C的程式碼。
2.必須有兩個指定引數(id self,SEL _cmd)

- (void)addMethod
{
    Student *student = [Student new];
    student.name = @"庫克";

    class_addMethod([student class], @selector(join), (IMP)happyNewYear, "[email protected]:");
    [student performSelector:@selector(join)];
}

輸出結果:

2017-05-22 11:10:06.379 Day2017-05-22[9296:1003059] 你好庫克

3. 動態為Category擴充套件加屬性

XCode執行你在Category的.h檔案申明@Property,編譯通過,但執行時如果沒有Runtime處理,進行賦值取值,就馬上報錯。
首先新增分類Student+Category
標頭檔案:

#import "Student.h"
@interface Student (Category)
@property(nonatomic,copy)NSString *firstName;
@end

.m檔案:

#import "Student+Category.h"
#import <objc/runtime.h>
@implementation Student (Category)
const char name;
- (void)setFirstName:(NSString *)firstName  {
    objc_setAssociatedObject(self, &name, firstName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)firstName {
   return  objc_getAssociatedObject(self, &name);
}
@end

呼叫:

- (void)addExtentionProperty
{
    Student *student = [Student new];
    student.firstName = @"Steve";
    NSLog(@"新增屬性firstName結果:%@ ",student.firstName);
}

4.動態交換方法實現

- (void)exchangeMethod
{
    Student *student = [Student new];
    student.name = @"庫克";
    [student eat];
    [student sleep];

    NSLog(@"----------交換方法實現-----------");
    Method m1 = class_getInstanceMethod([student class], @selector(eat));
    Method m2 = class_getInstanceMethod([student class], @selector(sleep));
    method_exchangeImplementations(m1, m2);

    [student eat];
    [student sleep];
}