技 術 文 章 / 超 人
Runtime(運行時機制)概念
Object-C 是面向對象的語言,C是面向結構也就是面向過程的語言。Object-C是基于C基礎上的語言。而Runtime為Object-C添加了面向對象的特性,將靜態語言編譯期和鏈接期做的事情放到了運行時來處理。所以Runtime是每一個IOS開發人員都應該了解的。Runtime是一套純C的API。
Objective-C不僅需要一個編譯器,還需要一個運行時系統來執行編譯的代碼。對于Objective-C來說,這個運行時系統就像一個操作系統一樣:它讓所有的工作可以正常的運行。這個運行時系統即Objc Runtime。Objc Runtime其實是一個Runtime庫,它基本上是用C和匯編寫的,這個庫使得C語言有了面向對象的能力。
Runtime庫主要做下面幾件事:
封裝
:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝后,我們就可以在程序運行時創建,檢查,修改類、對象和它們的方法了。
找出方法的最終執行代碼
:當程序執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。這將在后面詳細介紹。
對于C語言而言函數的調用在編譯時就會決定調用那一個函數。而OC在編譯時并不能決定調用那一個函數,只有在運行時才能確定調用那一個函數,舉個簡單的例子:在日常開發時,在.h中聲明了某個方法,而.m中卻沒有實現該方法,但編譯項目卻不會報錯,因為OC編譯時并沒有確定.h中聲明但方法具體調用.m的那一個函數,當然也就不知道該函數有沒有實現了,而在運行時調用該函數時就會報錯,因為運行時才會確定具體調用那一個函數,函數沒有實現那么就會報錯。OC是動態調用,正因為動態性,所以在運行時可以修改調用方法的指針。
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 版本,這兩個版本之間都在努力的保持一致。
基礎知識補充
-
Class
:定義Objective-C類 -
Ivar
:定義對象的實例變量,包括類型和名字。 -
Protocol
:定義正式協議。 -
objc_property_t
:定義屬性。叫這個名字可能是為了防止和Objective-C 1.0中的用戶類型沖突,那時候還沒有屬性。 -
Method
:定義對象方法或類方法。這個類型提供了方法的名字(就是選擇器)、參數數量和類型,以及返回值(這些信息合起來稱為方法的簽名),還有一個指向代碼的函數指針(也就是方法的實現)。 -
SEL
:定義選擇器。選擇器是方法名的唯一標識符。 -
IMP
:定義方法實現。這只是一個指向某個函數的指針,該函數接受一個對象、一個選擇器和一個可變長參數列表(varargs),返回一個對象 -
unsigned
signed和unsigned不會改變類型長度,僅表示最高位是否為符號位,其中unsigned表示大于等于0的正數
受限于內存分配的機制,一個NSObject對象會分配16byte的內存空間,但在64位系統下只使用了8byte,在32位系統下只使用了4byte.
獲取Obj-C指針所指向的內存的大小
malloc_size((__brige const void *)obj);
一個NSObject實例對象成員變量所占的大小是8byte。
獲取Object對象大小方法
Class_getInstanceSize([NSObject class])
//其本質是
size_t class_getInstanceSize(Class cls)
{
if(!cls)return 0;
return cls->alignedInstanceSize();
}
看到這里。你應該對Runtime有一個初步的了解了。建議先看看Runtime的方法,在看下面的內容。這樣才會容易明白實例代碼的用意。請查看Runtime方法說明
使用Runtime
1.導入頭文件 #import <objc/message.h>
2.OC 解決消息機制方法提示步驟[查找build setting-> 搜索msg->objc_msgSend(YES --> NO]
Runtime最主要是消息機制,OC在編譯時會轉化為C,轉化為: objc_msgSend(receiver, selector),何以證明?新建一個類 MyClass,其.m文件如下:
#import "MyClass.h"
@implementation MyClass
-(instancetype)init{
if (self = [super init]) {
[self showUserName];
}
return self;
}
-(void)showUserName{
NSLog(@"Dave Ping");
}
然后使用 clang 重寫命令:
$ clang -rewrite-objc MyClass.m
然后在同一目錄下會多出一個 MyClass.cpp 文件,雙擊打開,可以看到 init 方法已經被編譯器轉化為下面這樣:
static instancetype _I_MyClass_init(MyClass * self, SEL _cmd) {
if (self = ((MyClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyClass"))}, sel_registerName("init"))) {
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"));
}
return self;
}
我們要找的就是它:
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("showUserName"))
objc_msgSend 函數被定義在 objc/message.h 目錄下,其函數原型是醬紫滴:
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
該函數有兩個參數,一個 id 類型,一個 SEL 類型。
SEL
SEL 被定義在 objc/objc.h 目錄下:
typedef struct objc_selector *SEL;
其實它就是個映射到方法的C字符串,你可以用 Objective-C 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來獲得一個 SEL 類型的方法選擇器。簡單來說就是方法名稱
id
與 SEL 一樣,id 也被定義在 objc/objc.h 目錄下:
typedef struct objc_object *id;
id 是一個結構體指針類型,它可以指向 Objective-C 中的任何對象。objc_object 結構體定義如下:
struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};
我們通常所說的對象,就長這個樣子,這個結構體只有一個成員變量 isa,對象可以通過 isa 指針找到其所屬的類。isa 是一個 Class 類型的成員變量,那么 Class 又是什么呢?
isa 等價于 isKindOf方法
- 實例對象 isa 指向類對象
- 類對象指 isa 向元類對象
- 元類對象的 isa 指向元類的基類
isa 有兩種類型
- 純指針:指向內存地址
- NON_POINTER_ISA:除了內存地址,還存有一些其他信息
Class
Class 也是一個結構體指針類型:
typedef struct objc_class *Class;
objc_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;
我們通常說的類就長這樣子:
-
Class
: Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針 -
super_class
:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。 -
name
:是類名。 -
version
:我們可以使用這個字段來提供類的版本信息。這對于對象的序列化非常有用,它可以讓我們識別出不同類定義版本中實例變量布局的改變。 -
info
:是類的詳情。 -
instance_size
:是該類的實例對象的大小。 -
ivars
:指向該類的成員變量列表。 -
methodLists
:指向該類的實例方法列表,它將方法選擇器和方法實現地址聯系起來。methodLists 是指向 ·objc_method_list 指針的指針,也就是說可以動態修改 *methodLists 的值來添加成員方法,這也是 Category 實現的原理,同樣解釋了 Category 不能添加屬性的原因。 -
cache
:用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法后,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對于那些經常用到的方法的調用,但提高了調用的效率。 -
protocols
:指向該類的協議列表。
objc_object與id
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
可以看到,這個結構體只有一個字體,即指向其類的isa指針。這樣,當我們向一個Objective-C對象發送消息時,運行時庫會根據實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector指向的方法。找到后即運行這個方法。
當創建一個特定類的實例對象時,分配的內存包含一個objc_object數據結構,然后是類的實例變量的數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object數據結構。
另外還有我們常見的id,它是一個objc_object結構類型的指針。它的存在可以讓我們實現類似于C++中泛型的一些操作。該類型的對象可以轉換為任何一種對象,有點類似于C語言中void *指針類型的作用。
objc_cache
上面提到了objc_class結構體中的cache字段,它用于緩存調用過的方法。這個字段是一個指向objc_cache結構體的指針,其定義如下:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
mask
:一個整數,指定分配的緩存bucket的總數。在方法查找過程中,Objective-C runtime使用這個字段來確定開始線性查找數組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash散列算法。occupied
:一個整數,指定實際占用的緩存bucket的總數。buckets
:指向Method數據結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用,另外被占用的bucket可能是不連續的。這個數組可能會隨著時間而增長
元類(Meta Class)
例:
NSArray *array = [NSArray array];
這個例子中,+array消息發送給了NSArray類,而這個NSArray也是一個對象。既然是對象,那么它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那么這些就有一個問題了,這個isa指針指向什么呢?為了調用+array方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念:
meta-class是一個類對象的類。
當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。
meta-class之所以重要,是因為它存儲著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
再深入一下,meta-class也是一個類,也可以向它發送一個消息,那么它的isa又是指向什么呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。這樣就形成了一個完美的閉環。
IMP
IMP就是Implementation的縮寫,顧名思義,它是指向一個方法實現的指針,每一個方法都有一個對應的IMP,所以,我們可以直接調用方法的IMP指針,來避免方法調用死循環的問題。
IMP的定義如下:
if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
else
typedef id (*IMP)(id, SEL, ...);
endif
實際上直接調用一個方法的IMP指針的效率是高于調用方法本身的,所以,如果你有一個合適的時機獲取到方法的IMP的話,你可以試著調用它。
objc_msgSend說明
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
首先,Runtime 系統會把方法調用轉化為消息發送,即 objc_msgSend,并且把方法的調用者,和方法選擇器,當做參數傳遞過去.
此時,方法的調用者會通過 isa 指針來找到其所屬的類,然后在 cache 或者 methodLists 中查找該方法,找得到就跳到對應的方法去執行。
如果在類中沒有找到該方法,則通過 super_class 往上一級超類查找(如果一直找到 NSObject 都沒有找到該方法的話,這種情況,我們放到后面消息轉發的時候再說)。
前面我們說 methodLists 指向該類的實例方法列表,實例方法即-方法,那么類方法(+方法)存儲在哪兒呢?類方法被存儲在元類中,Class 通過 isa 指針即可找到其所屬的元類。
上圖實線是 super_class 指針,虛線是 isa 指針。根元類的超類是NSObject,而 isa 指向了自己。NSObject 的超類為 nil,也就是它沒有超類。
前面我們使用 clang 重寫命令,看到 Runtime 是如何將方法調用轉化為消息發送的。我們也可以依樣畫葫蘆,來學習使用一下 objc_msgSend。新建一個類 TestClass,添加如下方法:
-(void)showAge{
NSLog(@"24");
}
-(void)showName:(NSString *)aName{
NSLog(@"name is %@",aName);
}
-(void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
}
-(float)getHeight{
return 187.5f;
}
-(NSString *)getInfo{
return @"Hi, my name is Dave Ping, I'm twenty-four years old in the year, I like apple, nice to meet you.";
}
我們可以像下面這樣,使用 objc_msgSend 依次調用這些方法:
Class *objct = NSClassFromStrin[@"TestClass"];
((void (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("showAge"));
((void (*) (id, SEL, NSString *)) objc_msgSend) (objct, sel_registerName("showName:"), @"Dave Ping");
((void (*) (id, SEL, float, float)) objc_msgSend) (objct, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
float f = ((float (*) (id, SEL)) objc_msgSend_fpret) (objct, sel_registerName("getHeight"));
NSLog(@"height is %.2f",f);
NSString *info = ((NSString* (*) (id, SEL)) objc_msgSend) (objct, sel_registerName("getInfo"));
NSLog(@"%@",info);
也許你已經注意到,objc_msgSend 在使用時都被強制轉換了一下,這是因為 objc_msgSend 函數可以hold住各種不同的返回值以及多個參數,但默認情況下是沒有參數和返回值的。如果我們把調用 showAge 方法改成這樣:
objc_msgSend(objct, sel_registerName("showAge"));
Xcode 就會報錯:
Too many arguments to function call, expected 0, have 2.
objc_msgSendSuper
編譯器會根據情況在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret 或 objc_msgSend_fpret 五個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用 objc_msgSendSuper 方法,如果消息返回值是數據結構,就會調用 objc_msgSendSuper_stret 方法,如果返回值是浮點數,則調用 objc_msgSend_fpret 方法。
這里我們重點說一下 objc_msgSendSuper,objc_msgSendSuper 函數原型如下:
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
當我們調用 [super selector] 時,Runtime 會調用 objc_msgSendSuper 方法,objc_msgSendSuper 方法有兩個參數,super 和 op,Runtime 會把 selector 方法選擇器賦值給 op。而 super 是一個 objc_super 結構體指針,objc_super 結構體定義如下:
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
Runtime 會創建一個 objc_spuer 結構體變量,將其地址作為參數(super)傳遞給 objc_msgSendSuper,并且將 self 賦值給 receiver:super—>receiver=self.
舉個栗子,問下面的代碼輸出什么:
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案是全部輸出 Son。
使用 clang 重寫命令,發現上述代碼被轉化為:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));
當調用 [super class] 時,會轉換成 objc_msgSendSuper 函數:
- 第一步先構造 objc_super 結構體,結構體第一個成員就是 self。第二個成員是 (id)class_getSuperclass(objc_getClass(“Son”)).
第二步是去 Father 這個類里去找 - (Class)class,沒有,然后去 NSObject 類去找,找到了。最后內部是使用 objc_msgSend(objc_super->receiver, @selector(class)) 去調用,此時已經和 [self class] 調用相同了,所以兩個輸出結果都是 Son。
Associated對象關聯
對象關聯允許開發者對已經存在的類在 Category 中添加自定義的屬性:
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
object 是源對象
value 是被關聯的對象
key 是關聯的鍵,objc_getAssociatedObject 方法通過不同的 key 即可取出對應的被關聯對象
policy 是一個枚舉值,表示關聯對象的行為,從命名就能看出各個枚舉值的含義:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /** 指定對關聯對象的弱引用. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** 指定對關聯對象的強引.不是atomically */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /** 指定復制的關聯對象.* 不是 atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /** 指定對關聯對象的強引用.* 是 atomically. */
OBJC_ASSOCIATION_COPY = 01403 /** 指定復制的關聯對象. * 是 atomically. */
};
要取出被關聯的對象使用 objc_getAssociatedObject 方法即可,要刪除一個被關聯的對象,使用 objc_setAssociatedObject 方法將對應的 key 設置成 nil 即可:
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_removeAssociatedObjects 方法將會移除源對象中所有的關聯對象.
舉個栗子,假如我們要給 UIButton 添加一個監聽單擊事件的 block 屬性,新建 UIButton 的 Category,其.m文件如下:
#import "UIButton+ClickBlock.h"
static const void *associatedKey = "associatedKey";
@implementation UIButton (ClickBlock)
//Category中的屬性,只會生成setter和getter方法,不會生成成員變量
-(void)setClick:(clickBlock)click{
/* 建立click的關聯,暗號為associatedKey */
objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
/* 先移除監聽,防止重復監聽 */
[self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
/* 判斷是否實現了click,沒有實現就不用注冊 */
if (click) {
/* 注冊監聽 */
[self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}
}
-(clickBlock)click{
/* 當使用click屬性時,直接獲取關聯對象當click,相當于button.click使用時獲取當對象就關聯對象 */
return objc_getAssociatedObject(self, associatedKey);
}
-(void)buttonClick{
/* 收到監聽消息后回調到click對象中 */
if (self.click) {
self.click();
}
}
@end
然后在代碼中,就可以使用 UIButton 的屬性來監聽單擊事件了:
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = self.view.bounds;
[self.view addSubview:button];
button.click = ^{
NSLog(@"buttonClicked");
};
Runtime案例使用
自動歸檔
一般到歸檔寫法時這樣的:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.ID forKey:@"ID"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.ID = [aDecoder decodeObjectForKey:@"ID"];
self.name = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
那么問題來了,如果當前 Model 有100個屬性的話,就需要寫100行這種代碼:
[aCoder encodeObject:self.name forKey:@"name"];
想想都頭疼,通過 Runtime 我們就可以輕松解決這個問題:
1.使用 class_copyIvarList 方法獲取當前 Model 的所有成員變量.
2.使用 ivar_getName 方法獲取成員變量的名稱.
3.通過 KVC 來讀取 Model 的屬性值(encodeWithCoder:),以及給 Model 的屬性賦值(initWithCoder:).
舉個栗子,新建一個 Model 類,其.m文件如下
#import "TestModel.h"
#import #import @implementation TestModel
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
// 注意kvc的特性是,如果能找到key這個屬性的setter方法,則調用setter方法
// 如果找不到setter方法,則查找成員變量key或者成員變量_key,并且為其賦值
// 所以這里不需要再另外處理成員變量名稱的“_”前綴
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
return self;
}
@end
Ivar 定義對象的實例變量,包括類型和名字
是objc_ivar的指針,包含變量名,變量類型等成員。ivars是一個數組,數組中每個元素是指向Ivar(變量信息)的指針
- Ivar定義:
typedef objc_ivar * Ivar;
- Ivar結構體:
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
#ifdef __LP64__
int space;
#endif
}
- Ivar的相關操作
//獲取Ivar的名稱
const char *ivar_getName(Ivar v);
//獲取Ivar的類型編碼,
const char *ivar_getTypeEncoding(Ivar v)
//通過變量名稱獲取類中的實例成員變量
Ivar class_getInstanceVariable(Class cls, const char *name)
//通過變量名稱獲取類中的類成員變量
Ivar class_getClassVariable(Class cls, const char *name)
//獲取指定類的Ivar列表及Ivar個數
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
//獲取實例對象中Ivar的值
id object_getIvar(id obj, Ivar ivar)
//設置實例對象中Ivar的值
void object_setIvar(id obj, Ivar ivar, id value)
字典轉模型
一般的字典轉模型是這樣滴:
-(instancetype)initWithDictionary:(NSDictionary *)dict{
if (self = [super init]) {
self.age = dict[@"age"];
self.name = dict[@"name"];
}
return self;
}
可想而知,遇到的問題跟歸檔時候一樣(后來使用MJExtension),這里我們稍微來學習一下其中原理,字典轉模型的時候:
1.根據字典的 key 生成 setter 方法
2.使用 objc_msgSend 調用 setter 方法為 Model 的屬性賦值(或者 KVC)
模型轉字典的時候:
1.調用 class_copyPropertyList 方法獲取當前 Model 的所有屬性
2.調用 property_getName 獲取屬性名稱
3.根據屬性名稱生成 getter 方法
4.使用 objc_msgSend 調用 getter 方法獲取屬性值(或者 KVC)
代碼如下:
#import "NSObject+KeyValues.h"
#import @implementation NSObject (KeyValues)
//字典轉模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
/* 創建model */
id objc = [[self alloc] init];
/* 遍歷字典里所有的key */
for (NSString *key in aDictionary.allKeys) {
/* 取出key里的值 */
id value = aDictionary[key];
/*根據key在self中獲取該key的屬性指針property,判斷當前屬性是不是Model的*/
objc_property_t property = class_getProperty(self, key.UTF8String);
/* outCount存放你定義屬性的個數 */
unsigned int outCount = 0;
/* 根據指針動態取出對象所有屬性 */
objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
/* 取出對象首個屬性 */
objc_property_attribute_t attribute = attributeList[0];
/* 取出屬性值 */
NSString *typeString = [NSString stringWithUTF8String:attribute.value];
/* 判斷取出的屬性類型是否是當前這個model類 */
if ([typeString isEqualToString:[NSString stringWithFormat:@"@\"%@\"",self.class]]) {
/* 如果model中有一個屬性也是model,即嵌套model。就遍歷一次屬性model */
value = [self objectWithKeyValues:value];
}
/**********************/
//生成setter方法,并用objc_msgSend調用
NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
/* 獲取方法名 */
SEL setter = sel_registerName(methodName.UTF8String);
/* 判斷是否有該方法名 */
if ([objc respondsToSelector:setter]) {
/* 如果有就發送消息到該方法 */
((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
}
}
return objc;
}
//模型轉字典
-(NSDictionary *)keyValuesWithObject{
/* outCount存放你定義屬性的個數 */
unsigned int outCount = 0;
/* 取出模型中所有到屬性指針,并獲取屬性個數 */
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
/* 創建一個字典 */
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
/* 根據屬性數量遍歷 */
for (int i = 0; i < outCount; i ++) {
/* 取出每個屬性指針 */
objc_property_t property = propertyList[i];
//生成getter方法,并用objc_msgSend調用
const char *propertyName = property_getName(property);
/* 獲取方法名 */
SEL getter = sel_registerName(propertyName);
/* 判斷是否存在該方法 */
if ([self respondsToSelector:getter]) {
/* 如果存在就從該方法獲取值 */
id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
/*判斷當前屬性是不是Model*/
if ([value isKindOfClass:[self class]] && value) {
/* 如果是就在遍歷一次屬性model */
value = [value keyValuesWithObject];
}
/**********************/
/* 判斷是否有值 */
if (value) {
/* 獲取屬性名稱到字符串 */
NSString *key = [NSString stringWithUTF8String:propertyName];
/* 存值 */
[dict setObject:value forKey:key];
}
}
}
return dict;
}
@end
字典轉模型中的知識點補充
-
objc_property_t :屬性指針
類型:
typedef struct property_t *objc_property_t;
@property
標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property 結構體的指針.可以通過 class_copyPropertyList 和 protocol_copyPropertyList 方法來獲取類和協議中的屬性
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回類型為指向指針的指針,哈哈,因為屬性列表是個數組,每個元素內容都是一個 objc_property_t 指針,而這兩個函數返回的值是指向這個數組的指針。
可以用 property_getName 函數來查找屬性名稱:
const char *property_getName(objc_property_t property)
可以用class_getProperty 和 protocol_getProperty通過給出的名稱來在類和協議中獲取屬性的引用:
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類型字符串:
const char *property_getAttributes(objc_property_t property)
對比下 class_copyIvarList 函數,使用 class_copyPropertyList 函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。
-
objc_property_attribute_t:屬性指針對象的列表
objc_property_attribute_t的value和name
常用attribute | name | value |
---|---|---|
nonatomic | "N" | "" |
strong/retain | "&" | "" |
weak | "W" | "" |
屬性的類型type | "T" | "@TypeName", eg"@"NSString"" |
屬性對應的實例變量Ivar | "V" | "Ivar_name", eg "_name" |
readonly | "R" | "" |
getter | "G" | "GetterName", eg"isRight" |
setter | "S" | "SetterName", eg"setName" |
assign/atomic | 默認即為assign和retain |
動態方法解析
如果某個對象調用了不存在的方法時會怎么樣,一般情況下程序會crash,錯誤信息類似下面這樣:
unrecognized selector sent to instance 0x7fd0a141afd0
但是在程序crash之前,Runtime 會給我們動態方法解析的機會,消息發送的步驟大致如下:
1.檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函數了
2.檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執行任何一個方法不會 Crash,因為會被忽略掉
3.如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應的函數去執行,如果 cache 找不到就找一下方法分發表
-
4.如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止
如果還找不到就要開始進入消息轉發了,消息轉發的大致過程如圖:
消息轉發
- 1.進入
resolveInstanceMethod:
方法,指定是否動態添加方法。若返回NO,則進入下一步,若返回YES,則通過class_addMethod
函數動態地添加方法,消息得到處理,此流程完畢。- 2.
resolveInstanceMethod:
方法返回 NO 時,就會進入forwardingTargetForSelector:
方法,這是 Runtime 給我們的第二次機會,用于指定哪個對象響應這個 selector。返回nil,進入下一步,返回某個對象,則會調用該對象的方法。- 3.若
forwardingTargetForSelector:
返回的是nil,則我們首先要通過methodSignatureForSelector:
來指定方法簽名,返回nil,表示不處理,若返回方法簽名,則會進入下一步。- 4.當第
methodSignatureForSelector:
方法返回方法簽名后,就會調用forwardInvocation:
方法,我們可以通過 anInvocation 對象做很多處理,比如修改實現方法,修改響應對象等。如果到最后,消息還是沒有得到響應,程序就會crash
未完待續...