iOS-Runtime-原理篇

Runtime

號外 : 一些關于runtime的小demo在我的下一篇文章iOS-Runtime-實踐篇

我們都知道Objective-C是一門動態語言, 動態之處體現在它將許多靜態語言編譯鏈接時要做的事通通放到運行時去做, 這大大增加了我們編程的靈活性.

毫不過分地說, Runtime就是OC的靈魂.

接下來我就要撥開OC最外層的外衣, 帶大家看看OC的真面目(C/C++).

目錄

  1. 類和對象
  2. 消息發送和轉發
  3. KVO原理

類和對象

@interface Person : NSObject {
    NSString *_name;
    int _age;
}

- (void)study;
+ (void)study;

@end

@implementation Person

- (void)study
{
    NSLog(@"instance - study");
}

+ (void)study
{
    NSLog(@"class - study");
}

@end

為了更好地說明類在底層的表現形式是怎樣, 我們將上面代碼利用clang -rewrite-objc Person.m指令將其用C/C++重寫, 一窺究竟.

把不必要的刪除, 整理后為下面

struct _class_t { 
    struct _class_t *isa; // isa指針
    struct _class_t *superclass; // 父類
    void *cache;
    void *vtable;
    struct _class_ro_t *ro; // class的其他信息
};
// class包含的信息
struct _class_ro_t { 
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    const unsigned char *ivarLayout;
    const char *name; // 類名
    const struct _method_list_t *baseMethods; // 方法列表
    const struct _objc_protocol_list *baseProtocols; // 協議列表
    const struct _ivar_list_t *ivars; // ivar列表
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties; // 屬性列表
};

// Person(class)
struct _class_t OBJC_CLASS_$_Person  = {
    .isa = &OBJC_METACLASS_$_Person, // 指向Person-metaclass
    .superclass = &OBJC_CLASS_$_NSObject, // 指向NSObject-class
    .cache = &_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_Person, // 包含了實例方法, ivar信息等
};

// Person(metaclass)
struct _class_t OBJC_METACLASS_$_Person = {
    .isa = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
    .superclass = &OBJC_METACLASS_$_NSObject, // 指向NSObject-metaclass
    .cache = &_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_METACLASS_RO_$_Person, // 包含了類方法
};

原來(顯然), 我們的類其實就是一個結構體!!! 類跟我們的對象一樣, 都有一個isa指針, 所以類其實也是對象的一種.

isa指針

isa指針非常重要, 對象需要通過isa指針找到它的類, 類需要通過isa找到它的元類. 這在調用實例方法和類方法的時候起到重要的作用.


isa指針

實例對象在調用方法時, 首先通過isa指針找到它所屬的類, 然后在類的緩存(cache)里找該方法的IMP, 如果沒有, 則去類的方法列表中查找, 然后找到則調用該方法, 找不到則報錯.

類對象調用方法則如出一轍, 通過isa指針找到元類, 然后就跟上述一致了. 這里涉及的發送消息機制下面會詳細講..

下面展示一些運行時動態獲取對象和類的屬性的C語言方法

類和類名 :

// 返回對象的類
Class object_getClass ( id obj );
// 設置對象的類
Class object_setClass ( id obj, Class cls );
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 創建一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 在應用中注冊由objc_allocateClassPair創建的類
void objc_registerClassPair ( Class cls );
// 銷毀一個類及其相關聯的類
void objc_disposeClassPair ( Class cls );
// 獲取類的類名
const char * class_getName ( Class cls );
// 返回給定對象的類名
const char * object_getClassName ( id obj );

ivar和屬性 :

// 添加成員變量
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
// 添加屬性
BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
// 返回類的某一ivar
Ivar class_getInstanceVariable(__unsafe_unretained Class cls, const char *name)
// 返回對象中實例變量的值
id object_getIvar ( id obj, Ivar ivar );
// 設置對象中實例變量的值
void object_setIvar ( id obj, Ivar ivar, id value );
// 獲取整個成員變量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
// 獲取屬性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );

方法 :

// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 交換兩個方法的實現(Method Swizzling)
void method_exchangeImplementations(Method m1, Method m2);

這里說個注意點 : addIvar并不能為一個已經存在的類添加成員變量, 只能為那些運行時動態添加的類, 并且只能在objc_allocateClassPairobjc_registerClassPair這兩個方法之間才能添加Ivar.

消息發送和轉發機制

在OC中, 如果向某對象發送消息, 那就會使用動態綁定機制來決定需要調用的方法. OC的方法在底層都是普通的C語言函數, 所以對象收到消息后究竟要調用什么函數完全由運行時決定, 甚至可以在運行時改變執行的方法.

[person read:book];
會被編譯成
objc_msgSend(person, @selector(read:), book);

objc_msgSend的具體流程如下

1. 通過isa指針找到所屬類
2. 查找類的cache列表, 如果沒有則下一步
3. 查找類的"方法列表"
4. 如果能找到與選擇子名稱相符的方法, 就跳至其實現代碼
5. 找不到, 就沿著繼承體系繼續向上查找
6. 如果能找到與選擇子名稱相符的方法, 就跳至其實現代碼
7. 找不到, 執行"消息轉發".
方法查找

消息轉發

上面我們提到, 如果到最后都找不到, 就會來到消息轉發

  • 動態方法解析 : 先問接收者所屬的類, 你看能不能動態添加個方法來處理這個"未知的選擇子"? 如果能, 則消息轉發結束.
  • 備胎(后備接收者) : 請接收者看看有沒有其他對象能處理這條消息? 如果有, 則把消息轉給那個對象, 消息轉發結束.
  • 消息簽名 : 這里會要求你返回一個消息簽名, 如果返回nil, 則消息轉發結束.
  • 完整的消息轉發 : 備胎都搞不定了, 那就只能把該消息相關的所有細節都封裝到一個NSInvocation對象, 再問接收者一次, 快想辦法把這個搞定了. 到了這個地步如果還無法處理, 消息轉發機制也無能為力了.
動態方法解析 :

對象在收到無法解讀的消息后, 首先調用其所屬類的這個類方法 :

+ (BOOL)resolveInstanceMethod:(SEL)selector 
// selector : 那個未知的選擇子
// 返回YES則結束消息轉發
// 返回NO則進入備胎

假如尚未實現的方法不是實例方法而是類方法, 則會調用另一個方法resolveClassMethod:

備胎 :

動態方法解析失敗, 則調用這個方法

- (id)forwardingTargetForSelector:(SEL)selector
// selector : 那個未知的選擇子
// 返回一個能響應該未知選擇子的備胎對象

通過備胎這個方法, 可以用"組合"來模擬出"多重繼承".

消息簽名 :

備胎搞不定, 這個方法就準備要被包裝成一個NSInvocation對象, 在這里要先返回一個方法簽名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// NSMethodSignature : 該selector對應的方法簽名
完整的消息轉發 :

給接收者最后一次機會把這個方法處理了, 搞不定就直接程序崩潰!

- (void)forwardInvocation:(NSInvocation *)invocation
// invocation : 封裝了與那條尚未處理的消息相關的所有細節的對象

在這里能做的比較現實的事就是 : 在觸發消息前, 先以某種方式改變消息內容, 比如追加另外一個參數, 或是改變選擇子等等. 實現此方法時, 如果發現某調用操作不應該由本類處理, 可以調用超類的同名方法. 則繼承體系中的每個類都有機會處理該請求, 直到NSObject. 如果NSObject搞不定, 則還會調用doesNotRecognizeSelector:來拋出異常, 此時你就會在控制臺看到那熟悉的unrecognized selector sent to instance..

消息轉發流程

上面這4個方法均是模板方法,開發者可以override,由runtime來調用。最常見的實現消息轉發,就是重寫方法3和4,忽略這個消息或者代理給其他對象.

Method Swizzling

被稱為黑魔法的一個方法, 可以把兩個方法的實現互換.
如上文所述, 類的方法列表會把選擇子的名稱映射到相關的方法實現上, 使得"動態消息派發系統"能夠據此找到應該調用的方法. 這些方法均以函數指針的形式來表示, 這種指針叫做IMP,
<pre> id (*IMP)(id, SEL, ...)</pre>

NSString類的選擇子映射表

OC運行時系統提供了幾個方法能夠用來操作這張表, 動態增加, 刪除, 改變選擇子對應的方法實現, 甚至交換兩個選擇子所映射到的指針. 如,

經過一些操作后的NSString選擇子映射表

如何交換兩個已經寫好的方法實現?

// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交換實現
void method_exchangeImplementations(Method m1, Method m2)

通過Method Swizzling可以為一些完全不知道其具體實現的黑盒方法增加日志記錄功能, 利于我們調試程序. 并且我們可以將某些系統類的具體實現換成我們自己寫的方法, 以達到某些目的. (例如, 修改主題, 修改字體等等)

KVO原理

KVO的實現也依賴Runtime. Apple文檔曾簡單提到過KVO的實現原理 :

Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...

Apple的文檔提得不多, 但是大神Mike Ash在很早很早以前就已經做過研究, 摘下了KVO神秘的面紗了, 有興趣的可以去查下, 這里不多深究, 只是簡單闡述下原理.

原來當你對一個對象進行觀察時, 系統會自動新建一個類繼承自原類, 然后重寫被觀察屬性的setter方法. 然后重寫的setter方法會負責在調用原setter方法前后通知觀察者. 然后把原對象的isa指針指向這個新類, 我們知道, 對象是通過isa指針去查找自己是屬于哪個類, 并去所在類的方法列表中查找方法的, 所以這個時候這個對象就自然地變成了新類的實例對象.

不僅如此, Apple還重寫了原類的- class方法, 視圖欺騙我們, 這個類沒有變, 還是原來的那個類. 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.


后記

這只是我的Runtime文章的第一篇, 之后還會有Runtime實踐篇以及利用Runtime解決實際問題的幾個demo, 感興趣的話還請大家關注關注_

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,789評論 0 9
  • 一、Runtime簡介 Runtime簡稱運行時。OC就是運行時機制,也就是在運行時候的一些機制,其中最主要的是消...
    林安530閱讀 1,080評論 0 2
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,166評論 0 9
  • 如果想了解Runtime的實際應用請看Runtime全面剖析之簡單使用 一:Runtime簡介二: Runtime...
    iYeso閱讀 822評論 0 2
  • 目錄 Objective-C Runtime到底是什么 Objective-C的元素認知 Runtime詳解 應用...
    Ryan___閱讀 1,962評論 1 3