歡迎訪問我的博客原文
Runtime 是指將數(shù)據(jù)類型的確定由編譯時(shí)推遲到了運(yùn)行時(shí)。它是一套底層的純 C 語(yǔ)言 API找御,我們平時(shí)編寫的 Objective-C 代碼冗尤,最終都會(huì)轉(zhuǎn)換成 runtime 的 C 語(yǔ)言代碼琢唾。
不過楞件,runtime API 的實(shí)現(xiàn)是用 C++ 開發(fā)的(源碼中的實(shí)現(xiàn)文件都是 .mm
文件)。
為了更全面地理解 runtime 機(jī)制视粮,我們結(jié)合最新的objc4 源碼來進(jìn)行解讀。
消息傳遞
我們知道 Objective-C 是面向?qū)ο箝_發(fā)的橙凳,而 C 語(yǔ)言則是面向過程開發(fā)蕾殴,這就需要將面向?qū)ο蟮念愞D(zhuǎn)變成面向過程的結(jié)構(gòu)體。
在 Objective-C 中岛啸,所有的消息傳遞中的“消息”都會(huì)被編譯器轉(zhuǎn)化為:
id objc_msgSend ( id self, SEL op, ... );
比如執(zhí)行一個(gè)對(duì)象的方法:[obj foo];
钓觉,底層運(yùn)行時(shí)會(huì)被編譯器轉(zhuǎn)化為:objc_msgSend(obj, @selector(foo));
。
那么方法內(nèi)部的執(zhí)行流程究竟是怎么樣的呢坚踩?我先來了解一些概念荡灾。
概念
objc_object
Objective-C 對(duì)象是由 id
類型表示的,它本質(zhì)上是一個(gè)指向 objc_object
結(jié)構(gòu)體的指針瞬铸。
typedef struct objc_object *id;
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
struct objc_object {
private:
isa_t isa;
// public & private method...
}
我們看到 objc_object
的結(jié)構(gòu)體中只有一個(gè)對(duì)象批幌,就是指向其類的 isa
指針。
當(dāng)向一個(gè)對(duì)象發(fā)送消息時(shí)嗓节,runtime 會(huì)根據(jù)實(shí)例對(duì)象的 isa
指針找到其所屬的類荧缘。
objc_class
Objective-C 的類是由 Class
類型來表示的,它實(shí)際上是一個(gè)指向 objc_class
結(jié)構(gòu)體的指針拦宣。
typedef struct objc_class *Class;
objc_class
結(jié)構(gòu)體中定義了很多變量:
struct objc_class : objc_object {
// 指向類的指針(位于 objc_object)
// Class ISA;
// 指向父類的指針
Class superclass;
// 用于緩存指針和 vtable截粗,加速方法的調(diào)用
cache_t cache; // formerly cache pointer and vtable
// 存儲(chǔ)類的方法、屬性鸵隧、遵循的協(xié)議等信息的地方
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
// class_data_bits_t 結(jié)構(gòu)體的方法绸罗,用于返回class_rw_t 指針()
class_rw_t *data() {
return bits.data();
}
// other methods...
}
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
// other methods
}
objc_class
繼承自 objc_object
,因此它也擁有了 isa
指針豆瘫。除此之外珊蟀,它的結(jié)構(gòu)體中還保存了指向父類的指針、緩存靡羡、實(shí)例變量列表系洛、方法列表、遵守的協(xié)議等略步。
元類
元類(metaclass)是類對(duì)象的類描扯,它的結(jié)構(gòu)體和 objc_class
是一樣的。
由于所有的類自身也是一個(gè)對(duì)象趟薄,我們可以向這個(gè)對(duì)象發(fā)送消息绽诚,比如調(diào)用類方法。那么為了調(diào)用類方法,這個(gè)類的 isa
指針必須指向一個(gè)包含類方法的一個(gè) objc_class
結(jié)構(gòu)體恩够。而類對(duì)象中只存儲(chǔ)了實(shí)例方法卒落,卻沒有類方法,這就引出了元類的概念蜂桶,元類中保存了創(chuàng)建類對(duì)象以及類方法所需的所有信息儡毕。
為了更方便理解,舉個(gè)例子:
- (void)eat; // 一個(gè)實(shí)例方法
+ (void)sleep; // 一個(gè)類方法
// 那么實(shí)例方法需要由類對(duì)象來調(diào)用:
[person eat];
// 而類方法需要由元類來調(diào)用:
[Person sleep];
假如 person
對(duì)象也能調(diào)用 sleep
方法扑媚,那我們就無法區(qū)分它調(diào)用的就究竟是 + (void)sleep;
還是 - (void)sleep;
腰湾。
類對(duì)象是元類的實(shí)例,類對(duì)象的 isa
指針指向了元類疆股。
這個(gè)說法可能有點(diǎn)繞费坊,借助這張經(jīng)典的圖來理解:
當(dāng)向?qū)ο蟀l(fā)消息,runtime 會(huì)在這個(gè)對(duì)象所屬類方法列表中查找發(fā)送消息對(duì)應(yīng)的方法旬痹,但當(dāng)向類發(fā)送消息時(shí)附井,runtime 就會(huì)在這個(gè)類的 meta class 方法列表里查找。所有的 meta class两残,包括 Root class永毅,Superclass,Subclass 的 isa 都指向 Root class 的 meta class磕昼,這樣能夠形成一個(gè)閉環(huán)卷雕。
Method(method_t)
Method 是一個(gè)指向 method_t
結(jié)構(gòu)體的指針,我們?cè)?objc-private.h
和 objc-runtime-new.h
中找到關(guān)于它的定義:
typedef struct method_t *Method;
struct method_t {
// 方法選擇器
SEL name;
// 類型編碼
const char *types;
// 方法實(shí)現(xiàn)的指針
MethodListIMP imp;
}
所以 Method 和 SEL票从、IMP 的關(guān)系就是 Method = SEL + IMP + types漫雕。
關(guān)于 types 的寫法,參考 Type Encodings峰鄙。
SEL(objc_selector)
SEL 又稱方法選擇器浸间,是一個(gè)指向 objc_selector
結(jié)構(gòu)體的指針,也是 objc_msgSend
函數(shù)的第二個(gè)參數(shù)類型吟榴。
typedef struct objc_selector *SEL;
方法的 selector
用于表示運(yùn)行時(shí)方法的名稱魁蒜。代碼編譯時(shí),會(huì)根據(jù)方法的名字(不包括參數(shù))生成一個(gè)唯一的整型標(biāo)識(shí)( Int 類型的地址)吩翻,即 SEL兜看。
一個(gè)類的方法列表中不能存在兩個(gè)相同的 SEL,這也是 Objective-C 不支持重載的原因狭瞎。
不同類之間可以存在相同的 SEL细移,因?yàn)椴煌惖膶?shí)例對(duì)象執(zhí)行相同的 selector
時(shí),會(huì)在各自的方法列表中去尋找自己對(duì)應(yīng)的 IMP熊锭。
獲取 SEL 的方式有三種:
-
sel_registerName
函數(shù) - Objective-C 編譯器提供的
@selector()
方法 -
NSSeletorFromString()
方法
IMP
IMP 本質(zhì)上就是一個(gè)函數(shù)指針弧轧,指向方法實(shí)現(xiàn)的地址雪侥。
typedef void (*IMP)(void /* id, SEL, ... */ );
參數(shù)說明:
- id:指向 self 的指針(如果是實(shí)例方法,則是類實(shí)例的內(nèi)存地址精绎;如果是類方法速缨,則是指向元類的指針)
- SEL:方法選擇器
- ...:方法的參數(shù)列表
SEL 與 IMP 的關(guān)系類似于哈希表中 key 與 value 的關(guān)系。采用這種哈希映射的方式可以加快方法的查找速度代乃。
cache_t
cache_t
表示類緩存旬牲,是 object_class 的結(jié)構(gòu)體變量之一。
struct cache_t {
// 存放方法的數(shù)組
struct bucket_t *_buckets;
// 能存儲(chǔ)的最多數(shù)量
mask_t _mask;
// 當(dāng)前已存儲(chǔ)的方法數(shù)量
mask_t _occupied;
// ...
}
為了加速消息分發(fā)搁吓,系統(tǒng)會(huì)對(duì)方法和對(duì)應(yīng)的地址進(jìn)行緩存引谜,就放在 cache_t
中。
實(shí)際運(yùn)行中擎浴,大部分常用的方法都是會(huì)被緩存起來的,runtime 系統(tǒng)實(shí)際上非扯窘В快贮预,接近直接執(zhí)行內(nèi)存地址的程序速度。
category_t
category_t
表示一個(gè)指向分類的結(jié)構(gòu)體的指針契讲。
struct category_t {
// 是指類名仿吞,而不是分類名
const char *name;
// 要擴(kuò)展的類對(duì)象,編譯期間是不會(huì)定義的捡偏,而是在運(yùn)行時(shí)階段通過name對(duì)應(yīng)到相應(yīng)的類對(duì)象
classref_t cls;
// 實(shí)例方法列表
struct method_list_t *instanceMethods;
// 類方法列表
struct method_list_t *classMethods;
// 協(xié)議列表
struct protocol_list_t *protocols;
// 實(shí)例屬性
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);
};
這里涉及到一個(gè)經(jīng)典問題:
分類中可以添加實(shí)例變量/成員變量/屬性嗎唤冈?
首先,分類中無法直接添加實(shí)例變量和成員變量银伟。
實(shí)踐一下你虹,我們就會(huì)發(fā)現(xiàn),在分類中添加實(shí)例變量/成員變量彤避,在編譯階段傅物,就會(huì)報(bào)錯(cuò),但添加屬性是允許的琉预。
這是因?yàn)?strong>在分類的結(jié)構(gòu)體當(dāng)中董饰,沒有“實(shí)例變量/成員變量”的結(jié)構(gòu),但是有“屬性”的結(jié)構(gòu)圆米。
那么分類中就可以直接添加屬性嗎卒暂?
其實(shí)也不然,雖然分類的 .h
中沒有報(bào)錯(cuò)信息娄帖,.m
中卻報(bào)出了如下的警告也祠,且運(yùn)行時(shí)會(huì)報(bào)錯(cuò)。
警告提示上表明有兩種解決方法:
第一種:用 @dynamic修飾
块茁。但實(shí)際上齿坷,@dynamic
修飾只是告訴編譯器桂肌,屬性的 setter 和 getter 方法會(huì)由用戶自行實(shí)現(xiàn)。但這樣做只能消除警告永淌,無法解決問題崎场,運(yùn)行時(shí)依然會(huì)崩潰。
第二種:給分類手動(dòng)添加 setter 和 getter 方法遂蛀,這是一種有效的方案谭跨。
我們知道 @property = ivar + setter + getter
。
可以通過 objc_setAssociatedObject
和 objc_getAssociatedObject
向分類中動(dòng)態(tài)添加屬性李滴,具體實(shí)現(xiàn)見下文中的“關(guān)聯(lián)對(duì)象給分類增加屬性”螃宙。
流程
消息傳遞的完整過程為:
也就是查找 IMP 的過程:
- 先從當(dāng)前 class 的 cache 方法列表里去查找。
- 如果找到了所坯,如果找到了就返回對(duì)應(yīng)的 IMP 實(shí)現(xiàn)谆扎,并把當(dāng)前的 class 中的 selector 緩存到 cache 里面。
- 如果類的方法列表中找不到芹助,就到父類的方法列表中查找堂湖,一直找到 NSObject 類為止。
- 最后再找不到状土,就會(huì)進(jìn)入動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)的機(jī)制无蜂。
消息轉(zhuǎn)發(fā)
如果消息傳遞后仍無法找到 IMP,就進(jìn)入了消息轉(zhuǎn)發(fā)流程蒙谓。
- 通過運(yùn)行期的動(dòng)態(tài)方法解析功能斥季,我們可以在需要用到某個(gè)方法時(shí)再將其加入類中。
- 對(duì)象可以把其無法解讀的某些選擇子轉(zhuǎn)交給備用接受者來處理累驮。
- 經(jīng)過上述兩步之后酣倾,如果還是沒有辦法處理選擇子,那就啟動(dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制慰照。
動(dòng)態(tài)方法解析
動(dòng)態(tài)方法解析的兩個(gè)方法:
// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// 添加實(shí)例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
我們?cè)倏纯催@兩個(gè)方法在源碼中的調(diào)用:
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
// 判斷是不是元類
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
// 調(diào)用類的 resolveInstanceMethod 方法灶挟,動(dòng)態(tài)添加實(shí)例方法
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
// 調(diào)用元類的 resolveClassMethod 方法,動(dòng)態(tài)添加類方法
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
下面看一個(gè)動(dòng)態(tài)方法解析的例子毒租。
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo)) {
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");
}
可以看到雖然沒有實(shí)現(xiàn) foo
這個(gè)函數(shù)稚铣,但是我們通過 class_addMethod
動(dòng)態(tài)添加 fooMethod
函數(shù),并執(zhí)行 fooMethod
這個(gè)函數(shù)的IMP墅垮。
如果 resolveInstanceMethod:
方法返回 NO 惕医,運(yùn)行時(shí)就會(huì)移到下一步:forwardingTargetForSelector:
。
備用接收者
如果目標(biāo)對(duì)象實(shí)現(xiàn)了 forwardingTargetForSelector:
方法算色,runtime 就會(huì)調(diào)用這個(gè)方法抬伺,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他接受者的機(jī)會(huì)。
實(shí)現(xiàn)一個(gè)備用接收者的例子如下:
#import "ViewController.h"
#import <objc/runtime.h>
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函數(shù)
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 返回 NO灾梦,進(jìn)入下一步轉(zhuǎn)發(fā)峡钓。
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
//返回 Person對(duì)象妓笙,讓 Person 對(duì)象接收這個(gè)消息
return [Person new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
上面的實(shí)現(xiàn)就是利用 forwardingTargetForSelector
把當(dāng)前 ViewController
類的方法 foo
轉(zhuǎn)發(fā)給了備用接受者 Person
類去執(zhí)行了。
完整的消息轉(zhuǎn)發(fā)
如果在上一步還無法處理未知消息能岩,唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制寞宫。
主要涉及到兩個(gè)方法:
- 發(fā)送
methodSignatureForSelector
進(jìn)行方法簽名,這可以將函數(shù)的參數(shù)類型和返回值封裝拉鹃。如果返回 nil辈赋,runtime 會(huì)發(fā)出doesNotRecognizeSelector
消息,程序同時(shí)崩潰膏燕。 - 如果返回了一個(gè)函數(shù)簽名钥屈,runtime 就會(huì)創(chuàng)建一個(gè)
NSInvocation
對(duì)象并發(fā)送forwardInvocation
消息給目標(biāo)對(duì)象。
實(shí)現(xiàn)一個(gè)完整轉(zhuǎn)發(fā)的例子如下:
#import "ViewController.h"
#import <objc/runtime.h>
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 返回 NO坝辫,進(jìn)入下一步轉(zhuǎn)發(fā)篷就。
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 返回 nil,進(jìn)入下一步轉(zhuǎn)發(fā)近忙。
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 簽名腻脏,進(jìn)入 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 生成了一個(gè)對(duì)象 anInvocation
银锻,發(fā)送給方法 forwardInvocation
,我們?cè)诜椒ㄖ凶?Person
對(duì)象執(zhí)行 foo
函數(shù)做鹰。
以上就是 runtime 的三次轉(zhuǎn)發(fā)流程击纬,下面列舉一下 runtime 的實(shí)際應(yīng)用。
應(yīng)用
<span id="add-prop-to-category-with-associated-objects">關(guān)聯(lián)對(duì)象給分類增加屬性</span>
關(guān)聯(lián)對(duì)象(Associated Objects) 是 Objective-C 運(yùn)行時(shí)的特性钾麸,允許開發(fā)者向已經(jīng)存在的類在擴(kuò)展中添加自定義屬性更振。
關(guān)聯(lián)對(duì)象 runtime 提供了3個(gè) API 接口:
// 獲取關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key);
// 設(shè)置關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 移除關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object);
參數(shù)說明:
-
object
:被關(guān)聯(lián)的對(duì)象 -
key
:關(guān)聯(lián)對(duì)象的唯一標(biāo)識(shí) -
value
: 關(guān)聯(lián)的對(duì)象 -
policy
:內(nèi)存管理的策略
關(guān)于內(nèi)存管理的策略,源碼中 runtime.h
這樣描述:
/* Associative References */
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
我們看看內(nèi)存策略對(duì)應(yīng)的屬性修飾饭尝。
內(nèi)存策略 | 屬性修飾 | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一個(gè)關(guān)聯(lián)對(duì)象的弱引用肯腕。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 指定一個(gè)關(guān)聯(lián)對(duì)象的強(qiáng)引用,不能被原子化使用钥平。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一個(gè)關(guān)聯(lián)對(duì)象的 copy 引用实撒,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一個(gè)關(guān)聯(lián)對(duì)象的強(qiáng)引用涉瘾,能被原子化使用知态。 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一個(gè)關(guān)聯(lián)對(duì)象的 copy 引用,能被原子化使用立叛。 |
下面利用關(guān)聯(lián)對(duì)象實(shí)現(xiàn)一個(gè)“在分類中增加一個(gè)用 copy
修飾的非原子性屬性 prop
的功能负敏。
上文中,我們已經(jīng)知道分類中不能直接添加屬性秘蛇,需要手動(dòng)添加存取方法:
// NSObject+AssociatedObject.h
#import <Foundation/Foundation.h>
@interface NSObject (AssociatedObject)
@property (nonatomic, copy) NSString *prop;
@end
// NSObject+AssociatedObject.m
#import "NSObject+AssociatedObject.h"
#import <objc/runtime.h>
// key 有三種常見寫法:
//
// 1. static void *propKey = &propKey;
// 2. static NSString *propKey = @"propKey";
// 3. static char propKey;
static NSString *propKey = @"propKey";
@implementation NSObject (AssociatedObject)
- (void)setProp:(NSString *)prop {
objc_setAssociatedObject(self, &propKey, prop, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)prop {
return objc_getAssociatedObject(self, &propKey);
}
@end
黑魔法添加和替換方法
黑魔法是方法交換(method swizzling)其做,也就是交換方法的 IMP 實(shí)現(xiàn)顶考。
一般是在 + (void)load;
中執(zhí)行方法交換。因?yàn)樗募虞d時(shí)機(jī)較早妖泄,基本能確保方法已交換驹沿。
方法添加
在動(dòng)態(tài)方法解析中已經(jīng)提到了“方法添加”。
//class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
參數(shù)說明:
-
cls
:被添加方法的類 -
name
:添加的方法的名稱的 SEL -
imp
:方法的實(shí)現(xiàn)浮庐。該函數(shù)必須至少要有兩個(gè)參數(shù)甚负,self,_cmd -
types
:類型編碼
方法替換
方法替換就是改變類的選擇子映射表。
如果要互換兩個(gè)已經(jīng)寫好的方法實(shí)現(xiàn)审残,可以用下面的函數(shù)
void method_exchangeImplementations(Method m1, Method m2);
方法實(shí)現(xiàn)可以通過下面的函數(shù)獲得:
void class_getInstanceMethod(Class aClass, SEL aSelector);
下面實(shí)現(xiàn)一個(gè)替換 ViewController
中 viewDidLoad
方法的例子梭域。
@implementation ViewController
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(msviewDidLoad);
Method originalMethod = class_getInstanceMethod(class,originalSelector);
Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
// 判斷 original 的方法是否已經(jīng)實(shí)現(xiàn),如果未實(shí)現(xiàn)搅轿,將 swizzledMethod 的實(shí)現(xiàn)和類型添加進(jìn) originalSelector 中
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 將 originalMethod 的實(shí)現(xiàn)和類型替換到 swizzledSelector 中
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
// 交換 originalMethod 和 swizzledMethod
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)msviewDidLoad {
NSLog(@"msviewDidLoad");
[self msviewDidLoad];
}
- (void)viewDidLoad {
NSLog(@"viewDidLoad");
[super viewDidLoad];
}
@end
KVO 實(shí)現(xiàn)
KVO 全稱是 Key-value observing病涨,也就是鍵值觀察者模式,它提供了一種當(dāng)其它對(duì)象屬性被修改的時(shí)候能通知到當(dāng)前對(duì)象的機(jī)制璧坟。
KVO 的實(shí)現(xiàn)也是依賴于 runtime 中的 isa-swizzling
既穆。
當(dāng)觀察某對(duì)象 A 時(shí),KVO 機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)新的名為:NSKVONotifying_A
的新類雀鹃,該類繼承自對(duì)象 A 的本類幻工,且 KVO 為 NSKVONotifying_A
重寫觀察屬性的 setter 方法,setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后黎茎,通知所有觀察對(duì)象屬性值的更改情況囊颅。
舉個(gè)例子:
#import "ViewController.h"
#import <objc/runtime.h>
#import "A.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
A *a = [A new];
NSLog(@"Before KVO: [a class] = %@, a -> isa = %@", [a class], object_getClass(a));
[a addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"After KVO: [a class] = %@, a -> isa = %@", [a class], object_getClass(a));
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
}
@end
程序運(yùn)行的結(jié)果為:
Before KVO: [a class] = A, a -> isa = A
After KVO: [a class] = A, a -> isa = NSKVONotifying_A
可以看到當(dāng)對(duì) a 進(jìn)行觀察后,雖然對(duì)象 a
的 class
還是 A
傅瞻,isa 實(shí)際指向了它的子類 NSKVONotifying_A
踢代,來實(shí)現(xiàn)當(dāng)前類屬性值改變的監(jiān)聽;
所以當(dāng)我們從應(yīng)用層面上看來嗅骄,完全沒有意識(shí)到有新的類出現(xiàn)胳挎,這是系統(tǒng)“隱瞞”了對(duì) KVO 的底層實(shí)現(xiàn)過程,讓我們誤以為還是原來的類溺森。但是此時(shí)如果我們創(chuàng)建一個(gè)新的名為 NSKVONotifying_A
的類慕爬,就會(huì)發(fā)現(xiàn)系統(tǒng)運(yùn)行到注冊(cè) KVO 的那段代碼時(shí)程序就崩潰,因?yàn)橄到y(tǒng)在注冊(cè)監(jiān)聽的時(shí)候動(dòng)態(tài)創(chuàng)建了名為 NSKVONotifying_A
的中間類屏积,并指向這個(gè)中間類了澡罚。
那么子類 NSKVONotifying_A
的 setter 方法里具體實(shí)現(xiàn)了什么?
KVO 的鍵值觀察通知依賴于 NSObject 的兩個(gè)方法:
-willChangeValueForKey:
:被觀察屬性發(fā)生改變之前肾请,該方法被調(diào)用留搔,通知系統(tǒng)該 keyPath 的屬性值即將變更;-didChangeValueForKey:
:被觀察屬性發(fā)生改變之后铛铁,該方法被調(diào)用隔显,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更却妨。方法observeValueForKey:ofObject:change:context:
也會(huì)被調(diào)用。且重寫觀察屬性的 setter 方法這種繼承方式的注入是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的括眠。
因此彪标,KVO 為子類的觀察者屬性重寫調(diào)用存取方法的工作原理在代碼中相當(dāng)于:
- (void)setName:(NSString *)name {
// KVO 在調(diào)用存取方法之前總調(diào)用
[self willChangeValueForKey:@"name"];
// 調(diào)用父類的存取方法
[super setValue:newName forKey:@"name"];
// KVO 在調(diào)用存取方法之后總調(diào)用
[self didChangeValueForKey:@"name"];
}
實(shí)現(xiàn)字典和模型之間的轉(zhuǎn)換(MJExtension)
原理:
通過在 NSObject
的分類中添加方法 -initWithDict:
。
具體實(shí)現(xiàn)為:用 runtime 提供的函數(shù) class_copyPropertyList
獲取屬性列表掷豺,再遍歷 Model
自身所有屬性(通過 property_getName
函數(shù)獲得屬性的名字捞烟,通過 property_getAttributes
函數(shù)獲得屬性的類型)。如果屬性在 json
中有對(duì)應(yīng)的值当船,則將其賦值题画。
源碼:
- (instancetype)initWithDict:(NSDictionary *)dict {
if (self = [self init]) {
// 1、獲取類的屬性及屬性對(duì)應(yīng)的類型
NSMutableArray * keys = [NSMutableArray array];
NSMutableArray * attributes = [NSMutableArray array];
/*
* 例子
* name = value3 attribute = T@"NSString",C,N,V_value3
* name = value4 attribute = T^i,N,V_value4
*/
unsigned int outCount;
objc_property_t * properties = class_copyPropertyList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
objc_property_t property = properties[I];
// 通過 property_getName 函數(shù)獲得屬性的名字
NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
[keys addObject:propertyName];
//通過 property_getAttributes 函數(shù)獲得屬性類型
NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
[attributes addObject:propertyAttribute];
}
// 立即釋放properties指向的內(nèi)存
free(properties);
// 2德频、根據(jù)類型給屬性賦值
for (NSString * key in keys) {
if ([dict valueForKey:key] == nil) continue;
[self setValue:[dict valueForKey:key] forKey:key];
}
}
return self;
}
實(shí)現(xiàn) NSCoding 的自動(dòng)歸檔和解檔
原理:
在 Model
的基類中重寫方法:-initWithCoder:
和 -encodeWithCoder:
苍息。
具體實(shí)現(xiàn)為:用 runtime 提供的函數(shù) class_copyIvarList
獲取實(shí)例變量列表,再遍歷 Model
自身所有屬性壹置,并對(duì)屬性進(jìn)行 encode
和 decode
操作竞思。
源碼:
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[I];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[I];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
JSPatch
JSPatch 是一款 iOS 動(dòng)態(tài)更新框架,只需要在項(xiàng)目中引入引擎钞护,就可以使用 JavaScript 調(diào)用所有 Objective-C 原生接口盖喷,從而實(shí)現(xiàn)熱更新。
它通過完整的消息轉(zhuǎn)發(fā)實(shí)現(xiàn)了獲取參數(shù)的問題难咕。
原理:
當(dāng)調(diào)用一個(gè) NSObject 對(duì)象不存在的方法時(shí)传蹈,并不會(huì)馬上拋出異常,而是會(huì)經(jīng)過多層轉(zhuǎn)發(fā)步藕,層層調(diào)用對(duì)象的 -resolveInstanceMethod:
、-forwardingTargetForSelector:
挑格、-methodSignatureForSelector:
咙冗、-forwardInvocation:
等方法,其中 -forwardInvocation:
里的 NSInvocation
對(duì)象會(huì)保存了這個(gè)方法調(diào)用的所有信息漂彤,包括方法名雾消、參數(shù)和返回值類型等。所以只需要讓被 JS 替換的方法最后都調(diào)用到 -forwardInvocation:
挫望,就可以解決無法拿到參數(shù)值的問題了立润。