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];
}