Objective-C Runtime

轉載  Objective-C Runtime

文章目錄
  1. 1. 引言
  2. 2. 簡介
  3. 3. 與 Runtime 交互
    1. 3.1. Objective-C 源代碼
    2. 3.2. NSObject 的方法
    3. 3.3. Runtime 的函數
  4. 4. Runtime 基礎數據結構
    1. 4.1. SEL
    2. 4.2. id
    3. 4.3. Class
      1. 4.3.1. cache_t
      2. 4.3.2. class_data_bits_t
      3. 4.3.3. class_ro_t
      4. 4.3.4. class_rw_t
      5. 4.3.5. realizeClass
    4. 4.4. Category
    5. 4.5. Method
    6. 4.6. Ivar
    7. 4.7. objc_property_t
    8. 4.8. protocol_t
    9. 4.9. IMP
  5. 5. 消息
    1. 5.1. objc_msgSend 函數
    2. 5.2. 方法中的隱藏參數
    3. 5.3. 獲取方法地址
  6. 6. 動態方法解析
  7. 7. 消息轉發
    1. 7.1. 重定向
    2. 7.2. 轉發
    3. 7.3. 轉發和多繼承
    4. 7.4. 替代者對象(Surrogate Objects)
    5. 7.5. 轉發與繼承
  8. 8. 健壯的實例變量 (Non Fragile ivars)
  9. 9. Objective-C Associated Objects
  10. 10. Method Swizzling
  11. 11. 總結

本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具有了靈活的動態特性,使這門古老的語言煥發生機。主要內容以下:html

引言

曾經以爲Objc特別方便上手,面對着 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把 [receiver message] 當成簡單的方法調用,而無視了「發送消息」這句話的深入含義。其實 [receiver message] 會被編譯器轉化爲:node

1
objc_msgSend(receiver, selector)

若是消息含有參數,則爲:ios

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

若是消息的接收者可以找到對應的 selector,那麼就至關於直接執行了接收者這個對象的特定方法;不然,消息要麼被轉發,或是臨時向接收者動態添加這個 selector 對應的實現內容,要麼就乾脆玩完崩潰掉。git

如今能夠看出 [receiver message] 真的不是一個簡簡單單的方法調用。由於這只是在編譯階段肯定了要向接收者發送 message 這條消息,而 receive 將要如何響應這條消息,那就要看運行時發生的狀況來決定了。程序員

Objective-C 的 Runtime 鑄就了它動態語言的特性,這些深層次的知識雖然平時寫代碼用的少一些,可是倒是每一個 Objc 程序員須要瞭解的。github

簡介

由於Objc是一門動態語言,因此它老是想辦法把一些決定工做從編譯鏈接推遲到運行時。也就是說只有編譯器是不夠的,還須要一個運行時系統 (runtime system) 來執行編譯後的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個 Objc 運行框架的一塊基石。objective-c

Runtime其實有兩個版本: 「modern」 和 「legacy」。咱們如今用的 Objective-C 2.0 採用的是現行 (Modern) 版的 Runtime 系統,只能運行在 iOS 和 macOS 10.5 以後的 64 位程序中。而 maxOS 較老的32位程序仍採用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在於當你更改一個類的實例變量的佈局時,在早期版本中你須要從新編譯它的子類,而現行版就不須要。算法

Runtime 基本是用 C 和彙編寫的,可見蘋果爲了動態系統的高效而做出的努力。你能夠在這裏下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。編程

與 Runtime 交互

Objc 從三種不一樣的層級上與 Runtime 系統進行交互,分別是經過 Objective-C 源代碼,經過 Foundation 框架的NSObject類定義的方法,經過對 runtime 函數的直接調用。數組

Objective-C 源代碼

大部分狀況下你就只管寫你的Objc代碼就行,runtime 系統自動在幕後辛勤勞做着。
還記得引言中舉的例子吧,消息的執行會使用到一些編譯器爲實現動態語言特性而建立的數據結構和函數,Objc中的類、方法和協議等在 runtime 中都由一些數據結構來定義,這些內容在後面會講到。(好比 objc_msgSend 函數及其參數列表中的 id 和 SEL 都是啥)

NSObject 的方法

Cocoa 中大多數類都繼承於 NSObject 類,也就天然繼承了它的方法。最特殊的例外是 NSProxy,它是個抽象超類,它實現了一些消息轉發有關的方法,能夠經過繼承它來實現一個其餘類的替身類或是虛擬出一個不存在的類,說白了就是領導把本身展示給你們風光無限,可是把活兒都交給幕後小弟去幹。

有的NSObject中的方法起到了抽象接口的做用,好比description方法須要你重載它併爲你定義的類提供描述內容。NSObject還有些方法能在運行時得到類的信息,並檢查一些特性,好比class返回對象的類;isKindOfClass:isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象可否響應指定的消息;conformsToProtocol:檢查對象是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。

Runtime 的函數

Runtime 系統是一個由一系列函數和數據結構組成,具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下。許多函數容許你用純C代碼來重複實現 Objc 中一樣的功能。雖然有一些方法構成了NSObject類的基礎,可是你在寫 Objc 代碼時通常不會直接用到這些函數的,除非是寫一些 Objc 與其餘語言的橋接或是底層的debug工做。在 Objective-C Runtime Reference 中有對 Runtime 函數的詳細文檔。

Runtime 基礎數據結構

還記得引言中的objc_msgSend:方法吧,它的真身是這樣的:

1
id objc_msgSend ( id self, SEL op, ... );

下面將會逐漸展開介紹一些術語,其實它們都對應着數據結構。熟悉 Objective-C 類的內存模型或看過相關源碼的能夠直接跳過。

SEL

objc_msgSend函數第二個參數類型爲SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,能夠理解爲區分方法的 ID,而這個 ID 的數據結構是SEL:

1
typedef struct objc_selector *SEL;

其實它就是個映射到方法的C字符串,你能夠用 Objc 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來得到一個 SEL 類型的方法選擇器。

不一樣類中相同名字的方法所對應的方法選擇器是相同的,即便方法名字相同而變量類型不一樣也會致使它們具備相同的方法選擇器,因而 Objc 中方法命名有時會帶上參數類型(NSNumber 一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。

id

objc_msgSend 第一個參數類型爲id,你們對它都不陌生,它是一個指向類實例的指針:

1
typedef struct objc_object *id;

objc_object又是啥呢,參考 objc-private.h 文件部分源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();
... 此處省略其餘方法聲明
}

objc_object 結構體包含一個 isa 指針,類型爲 isa_t 聯合體。根據 isa 就能夠順藤摸瓜找到對象所屬的類。isa 這裏還涉及到 tagged pointer 等概念。由於 isa_t 使用 union 實現,因此可能表示多種形態,既能夠當成是指針,也能夠存儲標誌位。有關 isa_t 聯合體的更多內容能夠查看 Objective-C 引用計數原理

PS: isa 指針不老是指向實例對象所屬的類,不能依靠它來肯定類型,而是應該用 class 方法來肯定實例對象的類。由於KVO的實現機理就是將被觀察對象的 isa 指針指向一箇中間類而不是真實的類,這是一種叫作 isa-swizzling 的技術,詳見官方文檔

Class

Class 實際上是一個指向 objc_class 結構體的指針:

1
typedef struct objc_class *Class;

而 objc_class 包含不少方法,主要都爲圍繞它的幾個成員作文章:

1
2
3
4
5
6
7
8
9
10
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
... 省略其餘方法
}

objc_class 繼承於 objc_object,也就是說一個 ObjC 類自己同時也是一個對象,爲了處理類和對象的關係,runtime 庫建立了一種叫作元類 (Meta Class) 的東西,類對象所屬類型就叫作元類,它用來表述類對象自己所具有的元數據。類方法就定義於此處,由於這些方法能夠理解成類對象的實例方法。每一個類僅有一個類對象,而每一個類對象僅有一個與之相關的元類。當你發出一個相似 [NSObject alloc] 的消息時,你事實上是把這個消息發給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。全部的元類最終都指向根元類爲其超類。全部的元類的方法列表都有可以響應消息的類方法。因此當 [NSObject alloc] 這條消息發給類對象的時候,objc_msgSend() 會去它的元類裏面去查找可以響應消息的方法,若是找到了,而後對這個類對象執行方法調用。

上圖實線是 superclass 指針,虛線是isa指針。 有趣的是根元類的超類是 NSObject,而 isa 指向了本身,而 NSObject 的超類爲 nil,也就是它沒有超類。

能夠看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。

cache_t

1
2
3
4
5
6
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
... 省略其餘方法
}

_buckets 存儲 IMP_mask 和 _occupied 對應 vtable

cache 爲方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找可以響應消息的方法,由於這樣效率過低了,而是優先在 cache 中查找。Runtime 系統會把被調用的方法存到 cache 中(理論上講一個方法若是被調用,那麼它有可能從此還會被調用),下次查找的時候效率更高。

bucket_t 中存儲了指針與 IMP 的鍵值對:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;

public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }

void set(cache_key_t newKey, IMP newImp);
};

有關緩存的實現細節,能夠查看 objc-cache.mm 文件。

class_data_bits_t

objc_class 中最複雜的是 bitsclass_data_bits_t 結構體所包含的信息太多了,主要包含 class_rw_tretain/release/autorelease/retainCount 和 alloc 等信息,不少存取方法也是圍繞它展開。查看 objc-runtime-new.h 源碼以下:

1
2
3
4
5
6
7
8
9
struct class_data_bits_t {

// Values are the FAST_ flags above.
uintptr_t bits;
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
... 省略其餘方法
}

注意 objc_class 的 data 方法直接將 class_data_bits_t 的data 方法返回,最終是返回 class_rw_t,保了好幾層。

能夠看到 class_data_bits_t 裏又包了一個 bits,這個指針跟不一樣的 FAST_ 前綴的 flag 掩碼作按位與操做,就能夠獲取不一樣的數據。bits 在內存中每一個位的含義有三種排列順序:

32 位:

0 1 2 - 31
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_DATA_MASK

64 位兼容版:

0 1 2 3 - 46 47 - 63
FAST_IS_SWIFT FAST_HAS_DEFAULT_RR FAST_REQUIRES_RAW_ISA FAST_DATA_MASK 空閒

64 位不兼容版:

0 1 2 3 - 46 47
FAST_IS_SWIFT FAST_REQUIRES_RAW_ISA FAST_HAS_CXX_DTOR FAST_DATA_MASK FAST_HAS_CXX_CTOR
48 49 50 51 52 - 63
FAST_HAS_DEFAULT_AWZ FAST_HAS_DEFAULT_RR FAST_ALLOC FAST_SHIFTED_SIZE_SHIFT 空閒

其中 64 位不兼容版每一個宏對應的含義以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// class is a Swift class
#define FAST_IS_SWIFT (1UL<<0)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA (1UL<<1)
// class or superclass has .cxx_destruct implementation
// This bit is aligned with isa_t->hasCxxDtor to save an instruction.
#define FAST_HAS_CXX_DTOR (1UL<<2)
// data pointer
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// class or superclass has .cxx_construct implementation
#define FAST_HAS_CXX_CTOR (1UL<<47)
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_HAS_DEFAULT_AWZ (1UL<<48)
// class or superclass has default retain/release/autorelease/retainCount/
// _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR (1UL<<49)
// summary bit for fast alloc path: !hasCxxCtor and
// !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC (1UL<<50)
// instance size in units of 16 bytes
// or 0 if the instance size is too big in this field
// This field must be LAST
#define FAST_SHIFTED_SIZE_SHIFT 51

這裏面除了 FAST_DATA_MASK 是用一段空間存儲數據外,其餘宏都是隻用 1 bit 存儲 bool 值。class_data_bits_t 提供了三個方法用於位操做:getBit,setBits 和 clearBits,對應到存儲 bool 值的掩碼也有封裝函數,好比:

1
2
3
4
5
6
7
bool isSwift() {
return getBit(FAST_IS_SWIFT);
}

void setIsSwift() {
setBits(FAST_IS_SWIFT);
}

重頭戲在於最大的那塊存儲區域–FAST_DATA_MASK,它其實就存儲了指向 class_rw_t 的指針:

1
2
3
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

對這片內存讀寫處於併發環境,但並不須要加鎖,由於會經過對一些狀態(realization or construction)判斷來決定是否可讀寫。

class_data_bits_t 甚至還包含了一些對 class_rw_t 中 flags 成員存取的封裝函數。

class_ro_t

objc_class 包含了 class_data_bits_tclass_data_bits_t 存儲了 class_rw_t 的指針,而 class_rw_t 結構體又包含 class_ro_t 的指針。

class_ro_t 中的 method_list_tivar_list_tproperty_list_t 結構體都繼承自 entsize_list_tt<Element, List, FlagMask>。結構爲 xxx_list_t 的列表元素結構爲 xxx_t,命名很工整。protocol_list_t 與前三個不一樣,它存儲的是 protocol_t * 指針列表,實現比較簡單。

entsize_list_tt 實現了 non-fragile 特性的數組結構。假如蘋果在新版本的 SDK 中向 NSObject 類增長了一些內容,NSObject 的佔據的內存區域會擴大,開發者之前編譯出的二進制中的子類就會與新的 NSObject 內存有重疊部分。因而在編譯期會給 instanceStart 和 instanceSize 賦值,肯定好編譯時每一個類的所佔內存區域起始偏移量和大小,這樣只需將子類與基類的這兩個變量做對比便可知道子類是否與基類有重疊,若是有,也可知道子類須要挪多少偏移量。更多細節能夠參考後面的章節 Non Fragile ivars。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

class_ro_t->flags 存儲了不少在編譯時期就肯定的類的信息,也是 ABI 的一部分。下面這些 RO_ 前綴的宏標記了 flags 一些位置的含義。其中後三個並不須要被編譯器賦值,是預留給運行時加載和初始化類的標誌位,涉及到與 class_rw_t 的類型強轉。運行時會用到它作判斷,後面會講解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define RO_META (1<<0) // class is a metaclass
#define RO_ROOT (1<<1) // class is a root class
#define RO_HAS_CXX_STRUCTORS (1<<2) // class has .cxx_construct/destruct implementations
// #define RO_HAS_LOAD_METHOD (1<<3) // class has +load implementation
#define RO_HIDDEN (1<<4) // class has visibility=hidden set
#define RO_EXCEPTION (1<<5) // class has attribute(objc_exception): OBJC_EHTYPE_$_ThisClass is non-weak
// #define RO_REUSE_ME (1<<6) // this bit is available for reassignment
#define RO_IS_ARC (1<<7) // class compiled with ARC
#define RO_HAS_CXX_DTOR_ONLY (1<<8) // class has .cxx_destruct but no .cxx_construct (with RO_HAS_CXX_STRUCTORS)
#define RO_HAS_WEAK_WITHOUT_ARC (1<<9) // class is not ARC but has ARC-style weak ivar layout

#define RO_FROM_BUNDLE (1<<29) // class is in an unloadable bundle - must never be set by compiler
#define RO_FUTURE (1<<30) // class is unrealized future class - must never be set by compiler
#define RO_REALIZED (1<<31) // class is realized - must never be set by compiler

class_rw_t

class_rw_t 提供了運行時對類拓展的能力,而 class_ro_t 存儲的大可能是類在編譯時就已經肯定的信息。兩者都存有類的方法、屬性(成員變量)、協議等信息,不過存儲它們的列表實現方式不一樣。

class_rw_t 中使用的 method_array_tproperty_array_tprotocol_array_t 都繼承自 list_array_tt<Element, List>, 它能夠不斷擴張,由於它能夠存儲 list 指針,內容有三種:

  1. 一個 entsize_list_tt 指針
  2. entsize_list_tt 指針數組

class_rw_t 的內容是能夠在運行時被動態修改的,能夠說運行時對類的拓展大都是存儲在這裏的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
... 省略操做 flags 的相關方法
}

class_rw_t->flags 存儲的值並非編輯器設置的,其中有些值可能未來會做爲 ABI 的一部分。下面這些 RW_ 前綴的宏標記了 flags 一些位置的含義。這些 bool 值標記了類的一些狀態,涉及到聲明週期和內存管理。有些位目前甚至還空着。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define RW_REALIZED (1<<31) // class_t->data is class_rw_t, not class_ro_t
#define RW_FUTURE (1<<30) // class is unresolved future class
#define RW_INITIALIZED (1<<29) // class is initialized
#define RW_INITIALIZING (1<<28) // class is initializing
#define RW_COPIED_RO (1<<27) // class_rw_t->ro is heap copy of class_ro_t
#define RW_CONSTRUCTING (1<<26) // class allocated but not yet registered
#define RW_CONSTRUCTED (1<<25) // class allocated and registered
// #define RW_24 (1<<24) // available for use; was RW_FINALIZE_ON_MAIN_THREAD
#define RW_LOADED (1<<23) // class +load has been called
#if !SUPPORT_NONPOINTER_ISA
#define RW_INSTANCES_HAVE_ASSOCIATED_OBJECTS (1<<22) // class instances may have associative references
#endif
#define RW_HAS_INSTANCE_SPECIFIC_LAYOUT (1 << 21) // class has instance-specific GC layout
// #define RW_20 (1<<20) // available for use
#define RW_REALIZING (1<<19) // class has started realizing but not yet completed it
#define RW_HAS_CXX_CTOR (1<<18) // class or superclass has .cxx_construct implementation
#define RW_HAS_CXX_DTOR (1<<17) // class or superclass has .cxx_destruct implementation
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ (1<<16)
#if SUPPORT_NONPOINTER_ISA
#define RW_REQUIRES_RAW_ISA (1<<15) // class's instances requires raw isa
#endif

demangledName 是計算機語言用於解決實體名稱惟一性的一種方法,作法是向名稱中添加一些類型信息,用於從編譯器中向連接器傳遞更多語義信息。

realizeClass

在某個類初始化以前,objc_class->data() 返回的指針指向的實際上是個 class_ro_t 結構體。等到 static Class realizeClass(Class cls) 靜態方法在類第一次初始化時被調用,它會開闢 class_rw_t 的空間,並將 class_ro_t 指針賦值給 class_rw_t->ro。這種偷天換日的行爲是靠 RO_FUTURE 標誌位來記錄的:

1
2
3
4
5
6
7
8
9
10
11
12
13
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}

注意以前 RO 和 RW flags 宏標記的一個細節:

1
2
3
4
5
#define RO_FUTURE (1<<30)
#define RO_REALIZED (1<<31)

#define RW_REALIZED (1<<31)
#define RW_FUTURE (1<<30)

也就是說 ro = (const class_ro_t *)cls->data(); 這種強轉對於接下來的 ro->flags & RO_FUTURE 操做徹底是 OK 的,兩種結構體第一個成員都是 flagsRO_FUTURE 與 RW_FUTURE 值同樣的。

通過 realizeClass 函數處理的類纔是『真正的』類,調用它時不能對類作寫操做。

Category

Category 爲現有的類提供了拓展性,它是 category_t 結構體的指針。

1
typedef struct category_t *Category;

category_t 存儲了類別中能夠拓展的實例方法、類方法、協議、實例屬性和類屬性。類屬性是 Objective-C 2016 年新增的特性,沾 Swift 的光。因此 category_t 中有些成員變量是爲了兼容 Swift 的特性,Objective-C 暫沒提供接口,僅作了底層數據結構上的兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;

method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}

property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

在 App 啓動加載鏡像文件時,會在 _read_images 函數間接調用到 attachCategories 函數,完成向類中添加 Category 的工做。原理就是向 class_rw_t 中的 method_array_tproperty_array_tprotocol_array_t 數組中分別添加 method_list_tproperty_list_tprotocol_list_t 指針。以前講過 xxx_array_t 能夠存儲對應 xxx_list_t 的指針數組。

在調用 attachCategories 函數以前,會先使用 unattachedCategoriesForClass 函數獲取類中還未添加的類別列表。這個列表類型爲 locstamped_category_list_t,它封裝了 category_t 以及對應的 header_infoheader_info 存儲了實體在鏡像中的加載和初始化狀態,以及一些偏移量,在加載 Mach-O 文件相關函數中常常用到。

1
2
3
4
5
6
7
8
9
10
11
12
struct locstamped_category_t {
category_t *cat;
struct header_info *hi;
};

struct locstamped_category_list_t {
uint32_t count;
#if __LP64__
uint32_t reserved;
#endif
locstamped_category_t list[0];
};

因此更具體來講 attachCategories 作的就是將 locstamped_category_list_t.list 列表中每一個 locstamped_category_t.cat中的那方法、協議和屬性分別添加到類的 class_rw_t 對應列表中。header_info 中的信息決定了是不是元類,從而選擇應該是添加實例方法仍是類方法、實例屬性仍是類屬性等。源碼在 objc-runtime-new.mm 文件中,很好理解。

Method

Method是一種表明類中的某個方法的類型。

1
typedef struct method_t *Method;

而 objc_method 在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct method_t {
SEL name;
const char *types;
IMP imp;

struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
  • 方法名類型爲 SEL,前面提到過相同名字的方法即便在不一樣類中定義,它們的方法選擇器也相同。
  • 方法類型 types 是個char指針,其實存儲着方法的參數類型和返回值類型。
  • imp 指向了方法的實現,本質上是一個函數指針,後面會詳細講到。

Ivar

Ivar 是一種表明類中實例變量的類型。

1
typedef struct ivar_t *Ivar;

而 ivar_t 在上面的成員變量列表中也提到過:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;

uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};

能夠根據實例查找其在類中的名字,也就是「反射」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}

class_copyIvarList 函數獲取的不只有實例變量,還有屬性。但會在本來的屬性名前加上一個下劃線。

objc_property_t

@property 標記了類中的屬性,這個沒必要多說你們都很熟悉,它是一個指向objc_property 結構體的指針:

1
typedef struct property_t *objc_property_t;

能夠經過 class_copyPropertyList 和 protocol_copyPropertyList 方法來獲取類和協議中的屬性:

1
2
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回類型爲指向指針的指針,哈哈,由於屬性列表是個數組,每一個元素內容都是一個 objc_property_t 指針,而這兩個函數返回的值是指向這個數組的指針。

舉個栗子,先聲明一個類:

1
2
3
4
5
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end

你能夠用下面的代碼獲取屬性列表:

1
2
3
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你能夠用 property_getName 函數來查找屬性名稱:

1
const char *property_getName(objc_property_t property)

你能夠用class_getProperty 和 protocol_getProperty經過給出的名稱來在類和協議中獲取屬性的引用:

1
2
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你能夠用property_getAttributes函數來發掘屬性的名稱和@encode類型字符串:

1
const char *property_getAttributes(objc_property_t property)

把上面的代碼放一塊兒,你就能從一個類中獲取它的屬性啦:

1
2
3
4
5
6
7
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

對比下 class_copyIvarList 函數,使用 class_copyPropertyList 函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。

protocol_t

雖然 Objective-C 的 Category 和 protocol 拓展能力有限,但也得爲了將就 Swift 的感覺,充個胖子。

flags 32 位指針最後兩位是給加載 Mach-O 的 fix-up 階段使用的,前 16 位預留給 Swift 用的。

protocol 主要內容實際上是(可選)方法,其次就是繼承其餘 protocol。Swift 還支持 protocol 多繼承,因此須要 protocols 數組來作兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
... 省略一些封裝的便捷 get 方法
}

IMP

IMPobjc.h中的定義是:

1
typedef void (*IMP)(void /* id, SEL, ... */ );

它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息以後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。既然獲得了執行某個實例某個方法的入口,咱們就能夠繞開消息傳遞階段,直接執行方法,這在後面會提到。

你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每一個方法名都對應一個 SEL 類型的方法選擇器,而每一個實例對象中的 SEL 對應的方法實現確定是惟一的,經過一組 id 和 SEL 參數就能肯定惟一的方法實現地址;反之亦然。

消息

前面作了這麼多鋪墊,如今終於說到了消息了。Objc 中發送消息是用中括號([])把接收者和消息括起來,而直到運行時纔會把消息與方法實現綁定。

有關消息發送和消息轉發機制的原理,能夠查看這篇文章

objc_msgSend 函數

在引言中已經對objc_msgSend進行了一點介紹,看起來像是objc_msgSend返回了數據,其實objc_msgSend從不返回數據而是你的方法被調用後返回了數據。下面詳細敘述下消息發送步驟:

  1. 檢測這個 selector 是否是要忽略的。好比 Mac OS X 開發,有了垃圾回收就不理會 retainrelease 這些函數了。
  2. 檢測這個 target 是否是 nil 對象。ObjC 的特性是容許對一個 nil 對象執行任何一個方法不會 Crash,由於會被忽略掉。
  3. 若是上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 裏面找,完了找獲得就跳到對應的函數去執行。
  4. 若是 cache 找不到就找一下方法分發表。
  5. 若是分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類爲止。
  6. 若是還找不到就要開始進入動態方法解析了,後面會提到。

PS:這裏說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯繫起來。

其實編譯器會根據狀況在objc_msgSendobjc_msgSend_stretobjc_msgSendSuper, 或 objc_msgSendSuper_stret四個方法中選擇一個來調用。若是消息是傳遞給超類,那麼會調用名字帶有」Super」的函數;若是消息返回值是數據結構而不是簡單值時,那麼會調用名字帶有」stret」的函數。排列組合正好四個方法。

值得一提的是在 i386 平臺處理返回類型爲浮點數的消息時,須要用到objc_msgSend_fpret函數來進行處理,這是由於返回類型爲浮點數的函數對應的 ABI(Application Binary Interface) 與返回整型的函數的 ABI 不兼容。此時objc_msgSend再也不適用,因而objc_msgSend_fpret被派上用場,它會對浮點數寄存器作特殊處理。不過在 PPC 或 PPC64 平臺是不須要麻煩它的。

PS:有木有發現這些函數的命名規律哦?帶「Super」的是消息傳遞給超類;「stret」可分爲「st」+「ret」兩部分,分別表明「struct」和「return」;「fpret」就是「fp」+「ret」,分別表明「floating-point」和「return」。

方法中的隱藏參數

咱們常常在方法中使用self關鍵字來引用實例自己,但從沒有想過爲何self就能取到調用當前方法的對象吧。其實self的內容是在方法運行時被偷偷的動態傳入的。

objc_msgSend找到方法對應的實現時,它將直接調用該方法實現,並將消息中全部的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:

  • 接收消息的對象(也就是self指向的內容)
  • 方法選擇器(_cmd指向的內容)

之因此說它們是隱藏的是由於在源代碼方法的定義中並無聲明這兩個參數。它們是在代碼被編譯時被插入實現中的。儘管這些參數沒有被明確聲明,在源代碼中咱們仍然能夠引用它們。在下面的例子中,self引用了接收者對象,而_cmd引用了方法自己的選擇器:

1
2
3
4
5
6
7
8
9
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();

if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

在這兩個參數中,self 更有用。實際上,它是在方法實現中訪問消息接收者對象的實例變量的途徑。

而當方法中的super關鍵字接收到消息時,編譯器會建立一個objc_super結構體:

1
struct objc_super { id receiver; Class class; };

這個結構體指明瞭消息應該被傳遞給特定超類的定義。但receiver仍然是self自己,這點須要注意,由於當咱們想經過[super class]獲取超類時,編譯器只是將指向selfid指針和class的SEL傳遞給了objc_msgSendSuper函數,由於只有在NSObject類才能找到class方法,而後class方法調用object_getClass(),接着調用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數是指向selfid指針,與調用[self class]相同,因此咱們獲得的永遠都是self的類型。

獲取方法地址

IMP那節提到過能夠避開消息綁定而直接獲取方法的地址並調用方法。這種作法不多用,除非是須要持續大量重複調用某方法的極端狀況,避開消息發送氾濫而直接調用該方法會更高效。

NSObject類中有個methodForSelector:實例方法,你能夠用它來獲取某個方法選擇器對應的IMP,舉個栗子:

1
2
3
4
5
6
7
void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);

當方法被當作函數調用時,上節提到的兩個隱藏參數就須要咱們明確給出了。上面的例子調用了1000次函數,你能夠試試直接給target發送1000次setFilled:消息會花多久。

PS:methodForSelector:方法是由 Cocoa 的 Runtime 系統提供的,而不是 Objc 自身的特性。

動態方法解析

你能夠動態地提供一個方法的實現。例如咱們能夠用@dynamic關鍵字在類的實現文件中修飾一個屬性:

1
@dynamic propertyName;

這代表咱們會爲這個屬性動態提供存取方法,也就是說編譯器不會再默認爲咱們生成setPropertyName:propertyName方法,而須要咱們動態提供。咱們能夠經過分別重載resolveInstanceMethod:resolveClassMethod:方法分別添加實例方法實現和類方法實現。由於當 Runtime 系統在Cache和方法分發表中(包括超類)找不到要執行的方法時,Runtime會調用resolveInstanceMethod:resolveClassMethod:來給程序員一次動態添加方法實現的機會。咱們須要用class_addMethod函數完成向特定類添加特定方法實現的操做:

1
2
3
4
5
6
7
8
9
10
11
12
13
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子爲resolveThisMethodDynamically方法添加了實現內容,也就是dynamicMethodIMP方法中的代碼。其中 「v@:」 表示返回值和參數,這個符號涉及 Type Encoding

PS:動態方法解析會在消息轉發機制浸入前執行。若是 respondsToSelector: 或 instancesRespondToSelector:方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。若是你想讓該方法選擇器被傳送到轉發機制,那麼就讓resolveInstanceMethod:返回NO

評論區有人問如何用 resolveClassMethod: 解析類方法,我將他貼出有問題的代碼作了糾正和優化後以下,能夠順便將實例方法和類方法的動態方法解析對比下:

頭文件:

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

m 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#import "Student.h"
#import <objc/runtime.h>

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end

須要深入理解 [self class] 與 object_getClass(self) 甚至 object_getClass([self class]) 的關係,其實並不難,重點在於 self 的類型:

  1. 當 self 爲實例對象時,[self class] 與 object_getClass(self) 等價,由於前者會調用後者。object_getClass([self class]) 獲得元類。
  2. 當 self 爲類對象時,[self class] 返回值爲自身,仍是 selfobject_getClass(self) 與 object_getClass([self class]) 等價。

凡是涉及到類方法時,必定要弄清楚元類、selector、IMP 等概念,這樣才能作到觸類旁通,隨機應變。

消息轉發

重定向

在消息轉發機制執行前,Runtime 系統會再給咱們一次偷樑換柱的機會,即經過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者爲其餘對象:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

畢竟消息轉發要耗費更多時間,抓住此次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回self,由於那樣會死循環。 若是此方法返回nil或self,則會進入消息轉發機制(forwardInvocation:);不然將向返回的對象從新發送消息。

若是想替換類方法的接受者,須要覆寫 + (id)forwardingTargetForSelector:(SEL)aSelector 方法,並返回類對象

1
2
3
4
5
6
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}

轉發

當動態方法解析不做處理返回NO時,消息轉發機制會被觸發。在這時forwardInvocation:方法會被執行,咱們能夠重寫這個方法來定義咱們的轉發邏輯:

1
2
3
4
5
6
7
8
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}

該消息的惟一參數是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數。咱們能夠實現forwardInvocation:方法來對不能處理的消息作一些默認的處理,也能夠將消息轉發給其餘對象來處理,而不拋出錯誤。

這裏須要注意的是參數anInvocation是從哪的來的呢?其實在forwardInvocation:消息發送前,Runtime系統會向對象發送methodSignatureForSelector:消息,並取到返回的方法簽名用於生成NSInvocation對象。因此咱們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,不然會拋異常。

當一個對象因爲沒有相應的方法實現而沒法響應某消息時,運行時系統將經過forwardInvocation:消息通知該對象。每一個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現只是簡單地調用了doesNotRecognizeSelector:。經過實現咱們本身的forwardInvocation:方法,咱們能夠在該方法實現中將消息轉發給其它對象。

forwardInvocation:方法就像一個不能識別的消息的分發中心,將這些消息轉發給不一樣接收對象。或者它也能夠象一個運輸站將全部的消息都發送給同一個接收對象。它能夠將一個消息翻譯成另一個消息,或者簡單的」吃掉「某些消息,所以沒有響應也沒有錯誤。forwardInvocation:方法也能夠對不一樣的消息提供一樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不一樣的對象連接到消息鏈的能力。

注意: forwardInvocation:方法只有在消息接收對象中沒法正常響應消息時纔會被調用。 因此,若是咱們但願一個對象將negotiate消息轉發給其它對象,則這個對象不能有negotiate方法。不然,forwardInvocation:將不可能會被調用。

轉發和多繼承

轉發和繼承類似,能夠用於爲Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好似它把另外一個對象中的方法借過來或是「繼承」過來同樣。

這使得不一樣繼承體系分支下的兩個類能夠「繼承」對方的方法,在上圖中WarriorDiplomat沒有繼承關係,可是Warriornegotiate消息轉發給了Diplomat後,就好似DiplomatWarrior的超類同樣。

消息轉發彌補了 Objc 不支持多繼承的性質,也避免了由於多繼承致使單個類變得臃腫複雜。它將問題分解得很細,只針對想要借鑑的方法才轉發,並且轉發機制是透明的。

替代者對象(Surrogate Objects)

轉發不只能模擬多繼承,也能使輕量級對象表明重量級對象。弱小的女人背後是強大的男人,畢竟女人遇到難題都把它們轉發給男人來作了。這裏有一些適用案例,能夠參看官方文檔

轉發與繼承

儘管轉發很像繼承,可是NSObject類不會將二者混淆。像respondsToSelector: 和 isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發鏈。好比上圖中一個Warrior對象若是被問到是否能響應negotiate消息:

1
2
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

結果是NO,儘管它可以接受negotiate消息而不報錯,由於它靠轉發消息給Diplomat類來響應消息。

若是你爲了某些意圖偏要「弄虛做假」讓別人覺得Warrior繼承到了Diplomatnegotiate方法,你得從新實現 respondsToSelector: 和 isKindOfClass:來加入你的轉發算法:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

除了respondsToSelector: 和 isKindOfClass:以外,instancesRespondToSelector:中也應該寫一份轉發算法。若是使用了協議,conformsToProtocol:一樣也要加入到這一行列中。相似地,若是一個對象轉發它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發的消息。好比一個對象能給它的替代者對象轉發消息,它須要像下面這樣實現methodSignatureForSelector:

1
2
3
4
5
6
7
8
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

健壯的實例變量 (Non Fragile ivars)

在 Runtime 的現行版本中,最大的特色就是健壯的實例變量。當一個類被編譯時,實例變量的佈局也就造成了,它代表訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據本身所佔空間而產生位移:

上圖左邊是NSObject類的實例變量佈局,右邊是咱們寫的類的佈局,也就是在超類後面加上咱們本身類的實例變量,看起來不錯。但試想若是哪天蘋果更新了NSObject類,發佈新版本的系統的話,那就悲劇了:

咱們自定義的類被劃了兩道線,那是由於那塊區域跟超類重疊了。惟有蘋果將超類改成之前的佈局才能拯救咱們,但這樣也致使它們不能再拓展它們的框架了,由於成員變量佈局被死死地固定了。在脆弱的實例變量(Fragile ivars) 環境下咱們須要從新編譯繼承自 Apple 的類來恢復兼容性。那麼在健壯的實例變量下會發生什麼呢?

在健壯的實例變量下編譯器生成的實例變量佈局跟之前同樣,可是當 runtime 系統檢測到與超類有部分重疊時它會調整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。

須要注意的是在健壯的實例變量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))來代替。

優化 App 的啓動時間 講過加載 Mach-O 文件時有個步驟是經過 fix-up 修改偏移量來解決 fragile base class。

Objective-C Associated Objects

在 OS X 10.6 以後,Runtime系統讓Objc支持向對象動態添加變量。涉及到的函數有如下三個:

1
2
3
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );

這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

這些常量對應着引用關聯值的政策,也就是 Objc 內存管理的引用計數機制。有關 Objective-C 引用計數機制的原理,能夠查看這篇文章

Method Swizzling

以前所說的消息轉發雖然功能強大,但須要咱們瞭解而且能更改對應類的源代碼,由於咱們須要實現本身的轉發邏輯。當咱們沒法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現時,該怎麼辦呢?可能繼承類並重寫方法是一種想法,可是有時沒法達到目的。這裏介紹的是 Method Swizzling ,它經過從新映射方法對應的實現來達到「偷天換日」的目的。跟消息轉發相比,Method Swizzling 的作法更爲隱蔽,甚至有些冒險,也增大了debug的難度。

PS: 對於熟練使用 Method Swizzling 的開發者,能夠跳過此章節,看看我另外一篇『稍微深刻』一點的文章 Objective-C Method Swizzling

這裏摘抄一個 NSHipster 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import <objc/runtime.h> 

@implementation UIViewController (Tracking)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}

@end

上面的代碼經過添加一個Tracking類別到UIViewController類中,將UIViewController類的viewWillAppear:方法和Tracking類別中xxx_viewWillAppear:方法的實現相互調換。Swizzling 應該在+load方法中實現,由於+load是在一個類最開始加載時調用。dispatch_once是GCD中的一個方法,它保證了代碼塊只執行一次,並讓其爲一個原子操做,線程安全是很重要的。

若是類中不存在要替換的方法,那就先用class_addMethodclass_replaceMethod函數添加和替換兩個方法的實現;若是類中已經有了想要替換的方法,那麼就調用method_exchangeImplementations函數交換了兩個方法的 IMP,這是蘋果提供給咱們用於實現 Method Swizzling 的便捷方法。

可能有人注意到了這行:

1
2
3
4
5
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self) 與 [self class] 返回的結果類型都是 Class,但前者爲元類,後者爲其自己,由於此時 self 爲 Class 而不是實例.注意 [NSObject class] 與 [object class] 的區別:

1
2
3
4
5
6
7
+ (Class)class {
return self;
}

- (Class)class {
return object_getClass(self);
}

PS:若是類中沒有想被替換實現的原方法時,class_replaceMethod至關於直接調用class_addMethod向類中添加該方法的實現;不然調用method_setImplementation方法,types參數會被忽略。method_exchangeImplementations方法作的事情與以下的原子操做等價:

1
2
3
4
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

最後xxx_viewWillAppear:方法的定義看似是遞歸調用引起死循環,其實不會的。由於[self xxx_viewWillAppear:animated]消息會動態找到xxx_viewWillAppear:方法的實現,而它的實現已經被咱們與viewWillAppear:方法實現進行了互換,因此這段代碼不只不會死循環,若是你把[self xxx_viewWillAppear:animated]換成[self viewWillAppear:animated]反而會引起死循環。

看到有人說+load方法自己就是線程安全的,由於它在程序剛開始就被調用,不多會碰到併發問題,因而 stackoverflow 上也有大神給出了另外一個 Method Swizzling 的實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
NSLog(@"arg1 is %@", arg1);
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}

上面的代碼一樣要添加在某個類的類別中,相比第一個種實現,只是去掉了dispatch_once 部分。

Method Swizzling 的確是一個值得深刻研究的話題,找了幾篇不錯的資源推薦給你們:

在用 SpriteKit 寫遊戲的時候,由於 API 自己有一些缺陷(增刪節點時不考慮父節點是否存在啊,很容易崩潰啊有木有!),我在 Swift 上使用 Method Swizzling彌補這個缺陷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
extension SKNode {

class func yxy_swizzleAddChild() {
let cls = SKNode.self
let originalSelector = #selector(SKNode.addChild(_:))
let swizzledSelector = #selector(SKNode.yxy_addChild(_:))
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}

class func yxy_swizzleRemoveFromParent() {
let cls = SKNode.self
let originalSelector = #selector(SKNode.removeFromParent)
let swizzledSelector = #selector(SKNode.yxy_removeFromParent)
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}

@objc func yxy_addChild(_ node: SKNode) {
if node.parent == nil {
self.yxy_addChild(node)
}
else {
print("This node has already a parent!\(String(describing: node.name))")
}
}

@objc func yxy_removeFromParent() {
if parent != nil {
DispatchQueue.main.async(execute: { () -> Void in
self.yxy_removeFromParent()
})
}
else {
print("This node has no parent!\(String(describing: name))")
}
}

}

而後其餘地方調用那兩個類方法:

1
2
SKNode.yxy_swizzleAddChild()
SKNode.yxy_swizzleRemoveFromParent()

由於 Swift 中的 extension 的特殊性,最好在某個類的load() 方法中調用上面的兩個方法.我是在AppDelegate 中調用的,因而保證了應用啓動時可以執行上面兩個方法.

總結

咱們之因此讓本身的類繼承 NSObject 不只僅由於蘋果幫咱們完成了複雜的內存分配問題,更是由於這使得咱們可以用上 Runtime 系統帶來的便利。可能咱們平時寫代碼時可能不多會考慮一句簡單的 [receiver message] 背後發生了什麼,而只是當作方法或函數調用。深刻理解 Runtime 系統的細節更有利於咱們利用消息機制寫出功能更強大的代碼,好比 Method Swizzling 等。

Update 20170820: 使用 objc4-709 源碼重寫部分章節,更新至 Swift 4 代碼示例。

參考連接:

相關文章
相關標籤/搜索