第一篇:摘抄自 Objective-C Runtime的那點事兒一消息機制
RunTime簡稱運行時。就是系統(tǒng)在運行的時候的一些機制,其中最主要的是消息機制。對于C語言,函數(shù)的調(diào)用在編譯的時候會決定調(diào)用哪個函數(shù)。編譯完成之后直接順序執(zhí)行,無任何二義性。OC的函數(shù)調(diào)用成為消息發(fā)送。屬于動態(tài)調(diào)用過程。在編譯的時候并不能決定真正調(diào)用哪個函數(shù)(事實證明,在編譯階段,OC可以調(diào)用任何函數(shù),即使這個函數(shù)并未實現(xiàn),只要申明過就不會報錯。而C語言在編譯階段就會報錯)。只有在真正運行的時候才會根據(jù)函數(shù)的名稱找到對應(yīng)的函數(shù)來調(diào)用。
那OC是怎么實現(xiàn)動態(tài)調(diào)用的呢?下面我們來看看OC通過發(fā)送消息來達到動態(tài)調(diào)用的秘密。假如在OC中寫了這樣的一個代碼:
[obj makeText];
其中obj是一個對象,makeText是一個函數(shù)名稱。對于這樣一個簡單的調(diào)用。在編譯時RunTime會將上述代碼轉(zhuǎn)化成
objc_msgSend(obj,@selector(makeText));
obj解釋
首先看obj這個對象,iOS中的obj都繼承于NSObject。我們看到在NSObject中存在一個Class的isa指針:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
isa的類型為結(jié)構(gòu)體
//An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
-->
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //指向metaclass
#if !__OBJC2__
Class super_class; //指向其父類
const char *name; //類名
long version; //類的版本信息,初始化默認為0,可以通過runtime函數(shù)class_setVersion 和 class_getVersion 進行修改、讀取
long info; //一些標(biāo)識信息,如CLS_CLASS(0x1L)表示該類為普通class,其中包含對象方法和成員變量;CLS_META(0x2L)表示該類為 metaclass,其中包含類方法
long instance_size; // 該類的實例變量大小(包括從父類繼承下來的實例變量)
struct objc_ivar_list *ivars; //用于存儲每個成員變量的地址
struct objc_method_list **methodLists; //與 info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲對象方法,如CLS_META (0x2L),則存儲類方法
struct objc_cache *cache; // 指向最近使用的方法的指針,用于提升效率;
struct objc_protocol_list *protocols; // 存儲該類遵守的協(xié)議
#endif
} OBJC2_UNAVAILABLE;
解釋:
Class isa : 指向metaclass,也就是靜態(tài)的Class。
一般<u>Object對象中的isa指向普通的Class</u>,這個Class中存儲普通成員變量和對象方法(- 開頭的方法),<u>普通Class中的isa指針指向靜態(tài)Class</u>,靜態(tài)Class中存儲static類型成員變量和類方法(+ 開頭的方法)。
Class super_class: 指向父類,如果這個類是根類,則為NULL。
注意:
所有metaclass中isa指針都指向根metaclass,而根metaclass則指向自身。
if !__OBJC2__ endif標(biāo)記的屬性是Ojective-C 2.0不支持的,但實際上可以用響應(yīng)的函數(shù)獲取這些屬性,例如:如果想要獲取Class的name屬性,可以按如下方法獲取:
Class classPerson = Person.class;
// printf("%s\n", classPerson->name); //用這種方法已經(jīng)不能獲取name了 因為OBJC2_UNAVAILABLE
const char *cname = class_getName(classPerson);
printf("%s", cname); // 輸出:Person
@selector(makeText) :
這是一個SEL方法選擇器。SEL主要作用是快速地通過方法名字(makeText)查找對應(yīng)方法的函數(shù)指針,然后調(diào)用其函數(shù)。
SEL本身是一個Int類型的一個地址,地址中存放著方法的名字。在一個類中,每個方法對應(yīng)著一個SEL,所以不能存在多個名稱相同的方法,即使參數(shù)類型不同。因為SEL是根據(jù)方法名字生成的,相同的方法名稱只能對應(yīng)一個SEL。
那么消息發(fā)送之后是怎么來動態(tài)查找對應(yīng)的方法的?
-->首先編譯器會將 [obj makeText]; 轉(zhuǎn)化為 objc_msgSend(obj, @selector(makeText));。
然后在 objc_msgSend函數(shù)中,首先通過obj的isa指針找到obj對應(yīng)的class。在Class中先去cache中通過SEL查找對應(yīng)函數(shù)method(猜測cache中的method列表是以SEL為key通過hash表來存儲的,這樣能提高函數(shù)查找速度),若cache中沒有找到,則到methodLists中查找,若找不到,則取superClass中查找。若找到了,則將method加入到cache中,以便下次查找,并通過method中的函數(shù)指針跳轉(zhuǎn)到對應(yīng)的函數(shù)中去執(zhí)行。
第二篇:iOS~runtime理解
我們寫的代碼在程序運行過程中都會被轉(zhuǎn)化成runtime的C代碼執(zhí)行,例如[target doSomething];會被轉(zhuǎn)化成objc_msgSend(target, @selector(doSomething));。
相關(guān)的定義
/// 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.
// 類別 Category
typedef struct objc_category *Category;
/// An opaque type that represents an Objective-C declared property.
// 類中聲明的屬性
typedef struct objc_property *objc_property_t;
獲取各種列表的方法:
//獲取列表
- (void)getList{
unsigned int count;
//獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([Person class], &count);
for (unsigned int i=0; i < count; i++) {
objc_property_t property = propertyList[i];
const char *propertyName = property_getName(property);
NSLog(@"property ----> %@",[NSString stringWithUTF8String:propertyName]);
}
//獲取方法列表
Method *methodList = class_copyMethodList([Person class], &count);
for (unsigned int i=0; i < count; i++) {
Method method = methodList[i];
NSLog(@"method----> %@",NSStringFromSelector(method_getName(method)));
}
//獲取成員變量列表
Ivar *ivars = class_copyIvarList([Person class], &count);
for (unsigned int i=0; i < count; i++) {
Ivar ivar = ivars[i];
const char * ivarName = ivar_getName(ivar);
NSLog(@"Ivar ----> %@",[NSString stringWithUTF8String:ivarName]);
}
//獲取協(xié)議列表
__unsafe_unretained Protocol ** protocolList = class_copyProtocolList([Person class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myProtocal = protocolList[i];
const char *protocolName = protocol_getName(myProtocal);
NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]);
}
}
方法調(diào)用過程
如果用實例對象調(diào)用實例方法,會到實例的isa指針指向的對象(也就是類對象)操作。
如果調(diào)用的是類方法,就會到類對象的isa指針指向的對象(也就是元類對象)中操作。
--> 1.首先,在相應(yīng)操作的對象中的緩存方法列表中找調(diào)用的方法,如果找到,轉(zhuǎn)向相應(yīng)實現(xiàn)并執(zhí)行。
--> 2.如果沒找到,在相應(yīng)操作的對象中的方法列表中找調(diào)用的方法,如果找到,轉(zhuǎn)向相應(yīng)實現(xiàn)并執(zhí)行。
--> 3.如果沒找到,去父類指針?biāo)赶虻膶ο笾袌?zhí)行1,2。
--> 4.依次類推,如果一直到根類還沒找到,轉(zhuǎn)向攔截調(diào)用。
--> 5.如果沒有重寫攔截調(diào)用的方法,程序報錯。
以上的過程給我?guī)淼膯l(fā):
- 重寫父類的方法,并沒有覆蓋掉父類的方法,只是在當(dāng)前類對象中找到了這個方法后就不會再去父類中找了。
- 如果想調(diào)用已經(jīng)重寫過的方法的父類的實現(xiàn),只需使用super這個編譯器標(biāo)識,它會在運行時跳過在當(dāng)前的類對象中尋找方法的過程。
-->攔截調(diào)用
攔截調(diào)用就是,在找不到調(diào)用的方法程序崩潰之前,你有機會通過重寫NSObject的四個方法來處理。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后兩個方法需要轉(zhuǎn)發(fā)到其他的類處理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- 第一個方法是當(dāng)你調(diào)用一個不存在的類方法的時候,會調(diào)用這個方法。默認返回NO,可以加上自己的處理后返回YES。
- 第二個方法和第一個方法相似,只不過處理的是實例方法。
- 第三個方法是將你調(diào)用的不存在的方法重定向到一個其他聲明了這個方法的類,只需要你返回一個有這個方法的target。
- 第四個方法是將你調(diào)用的不存在的方法打包成
NSInvocation傳給你。做完你自己的處理后,調(diào)用invokeWithTarget :方法讓某個target觸發(fā)這個方法。
動態(tài)添加方法
重寫攔截調(diào)用的方法并且返回YES -> 我們是怎么實現(xiàn)的呢?
<u>有一個辦法是根據(jù)傳進來的SEL類型的selector動態(tài)添加一個方法:</u>
//隱式調(diào)用方法
[target performSelector:@selector(resolveAdd:) withObject:@"test"];
然后,在target對象內(nèi)部重寫攔截調(diào)用的方法,動態(tài)添加方法。
void runAddMethod(id self, SEL _cmd, NSString *string){
NSLog(@"add C IMP %@",string);
}
+ (BOOL)resolveInstanceMethod:(SEL) sel{
//給本類動態(tài)添加一個方法
if([NSStringFromSelector(sel) isEqualToString:@"resolveAdd:"]){
class_addMethod(self, sel, (IMP)runAddMethod, "v@:*");
}
}
其中 class_addMethod 的四個參數(shù)分別是:
1、Class cls 給哪個類添加方法,本例中是self
2、SEL name添加的方法,本例中是重寫的攔截調(diào)用傳進來的selector。
3、IMP imp 方法的實現(xiàn),C方法的方法實現(xiàn)可以直接獲得。如果是OC方法,可以用 +(IMP)instanceMethodForSelector:(SEL)aSelector;獲得方法的實現(xiàn)。
4、"v@:*"方法的簽名,代表有一個參數(shù)的方法。
關(guān)聯(lián)對象
現(xiàn)在你準(zhǔn)備用一個系統(tǒng)的類,但是系統(tǒng)的類并不能滿足你的需求,你需要額外添加一個屬性。
這種情況的一般解決辦法就是繼承。
但是,只增加一個屬性,就去繼承一個類,總是覺得太麻煩類。
這個時候,runtime的關(guān)聯(lián)屬性就發(fā)揮它的作用了。
//首先定義一個全局變量,用它的地址作為關(guān)聯(lián)對象的key
static char associatedObjectKey;
//設(shè)置關(guān)聯(lián)對象
objc_setAssociatedObject(target, $associatedObjectKey, @"添加的字符串屬性", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//獲取關(guān)聯(lián)對象
NSString *string = objc_getAssociatedObject(target, & associatedObjectKey);
NSLog(@"AssociatedObject = %@", string);
objc_setAssociatedObject的四個參數(shù):
-
id object給誰設(shè)置關(guān)聯(lián)對象。
2.const void *key關(guān)聯(lián)對象唯一的key,獲取時會用到。
3.id value關(guān)聯(lián)對象。
4.objc_AssociationPolicy關(guān)聯(lián)策略,有以下幾種策略:
如果你熟悉OC,看名字應(yīng)該知道這幾種策略的意思了吧。
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
objc_getAssociatedObject的兩個參數(shù)
1.id object 獲取誰的關(guān)聯(lián)對象。
2.const void *key 根據(jù)這個唯一的key獲取關(guān)聯(lián)對象。
其實,你還可以把添加和獲取關(guān)聯(lián)對象的方法寫在你需要用到這個功能的類的類別中,方便使用。
//添加關(guān)聯(lián)對象
- (void)addAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//關(guān)聯(lián)策略依情況而定
}
//獲取關(guān)聯(lián)對象
- (id)getAssociatedObject{
return objc_getAssociatedObject(self, _cmd);
}
注意:這里面我們把getAssociatedObject方法的地址作為唯一的key,
_cmd代表當(dāng)前調(diào)用方法的地址。
方法交換 (詳見原文)
方法交換,顧名思義,就是將兩個方法的實現(xiàn)交換。例如,將A方法和B方法交換,調(diào)用A方法的時候,就會執(zhí)行B方法中的代碼,反之亦然。
話不多說,這是參考Mattt大神在NSHipster上的文章自己寫的代碼。
方法交換對于我來說更像是實現(xiàn)一種思想的最佳技術(shù):AOP面向切面編程。
既然是切面,就一定不要忘記,交換完再調(diào)回自己。
一定要保證只交換一次,否則就會很亂。
最后,據(jù)說這個技術(shù)很危險,謹慎使用。