iOS開發發展至今,也有10個年頭了,面試要求也越來越高。現在的iOS面試管你是1年還是3年或者5年,都是問各種runtime底層,objc源碼,各類框架源碼,架構設計。有句話說的好,面試造航母,入職擰螺絲。 好了,開始我們今天的主題,runtime基礎學習。
1.對象
在OC語言中,所有對象都是C結構體。
首先它有一個指針指向類定義,然後是以結構體屬性的形式出現的每個父類的 ivar(instance variable,實例變量),接著是對象所屬的類的 ivar。
這個結構體叫做 objc_object,指向它的指針叫 id,定義如下:
typedef struct objc_object { Class isa; } *id;
每個 Objective-C 對象都有相同的結構,如圖:
2. 類
類和對象的定義:
typedef struct objc_class *Class; typedef struct objc_object *id;
Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。並且繼承了 struct objc_object 。
Class結構體包含一個元類指針(很快就會詳細解釋)、一個父類指針和關於此類的數據。特別要注意的數據有類名、ivar、方法、屬性和協議。它的定義如下:
typedef struct objc_class *Class; struct objc_class { Class isa OBJC_ISA_AVAILABILITY; ///< 指向metaClass(元類) #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; ///< 父類 const char *name OBJC2_UNAVAILABLE; ///< 類名 long version OBJC2_UNAVAILABLE; ///< 類的版本信息,默認為0 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;
這裡的isa指針指向的是另外一個類叫做元類(metaClass)。那什麼是元類呢?元類是類對象的類。也可以換一種容易理解的說法:
- 當你給對象發送消息時,runtime處理時是在這個對象的類的方法列表中尋找;
- 當你給類發消息時,runtime處理時是在這個類的元類的方法列表中尋找。
3 對象,類,元類
我們來看一個很經典的圖來加深理解:(isa:虛線箭頭, super class:實線箭頭)
可以總結為下:
- 每一個Instance(實例對象)都有一個isa指針指向它的類Class;
- 每一個Class都有一個isa指針指向它的Meta Class;
- 每一個Meta Class的isa指針都指向最上層的Meta Class,這個Meta Class是NSObject的Meta Class。(包括NSObject的Meta Class的isa指針也是指向的NSObject的Meta Class,也就是自己,這裡形成了個閉環);
- 每一個Meta Class的super class指針指向它原本Class的 Super Class的Meta Class (這裡最上層的NSObject的Meta Class的super class指針還是指向NSObject自己);
- 最上層的NSObject Class的super class指向 nil;
4 方法和屬性
Objective-C 運行時定義了幾種重要的類型:
Class 定義Objective-C類。
Ivar 定義對象的實例變量,包括類型和名字。
Protocol 定義正式協議。
objc_property_t 定義屬性。叫這個名字可能是為了防止和Objective-C1.0中的用戶類型衝突,那時候還沒有屬性。
Method 定義對象方法或類方法。這個類型提供了方法的名字(就是選擇器)、參數數量和類型,以及返回值(這些信息合起來稱為方法的簽名),還有一個指向代碼的函數指針(也就
是方法的實現)。
SEL 定義選擇器。選擇器是方法名的唯一標識符。
IMP 定義方法實現。這只是一個指向某個函數的指針,該函數接受一個對象、一個選擇器和一個可變長參數列表(varargs),返回一個對象:
typedef id (*IMP)(id, SEL, …);
運行時方法都以操作目標的名字開頭,一般那也是它們的第一個參數。例如:
void PrintObjectMethods() { unsigned int count = 0; Method *methods = class_copyMethodList([NSObject class],&count); for (unsigned int i = 0; i < count; ++i) { SEL sel = method_getName(methods[i]); const char *name = sel_getName(sel); printf("%s\n", name); } free(methods); }
方法:類方法和實例方法的區別
類方法:
- 類方法是屬於類對象的, 類方法列表存儲在元類中。
- 類方法只能通過類對象調用
- 類方法中的self是類對象
- 類方法可以調用其他的類方法
- 類方法中不能訪問成員變量
- 類方法中不能直接調用對象方法
實例方法:
- 實例方法是屬於實例對象的,實例方法列表存儲在類中。
- 實例方法只能通過實例對象調用
- 實例方法中的self是實例對象
- 實例方法中可以訪問成員變量
- 實例方法中直接調用實例方法
- 實例方法中也可以調用類方法(通過類名)
屬性
1)@property:
@property有兩個對應的詞,一個是 @synthesize,一個是 @dynamic。如果 @synthesize和 @dynamic都沒寫,那麼默認的就是@syntheszie var = _var; @property的本質是:
@property = ivar(實例變量) + getter/setter(存取方法);
2)@synthesize:
@synthesize表示如果屬性沒有手動實現setter和getter方法,編譯器會自動加上這兩個方法。
3)@dynamic:
@dynamic 告訴編譯器:屬性的 setter與getter方法由用戶自己實現,不自動生成。
注意:
- 如果使用@property定義了某個屬性XX,而沒有寫@synthesize和@dynamic,是無法同時重寫該屬性的seter和getter方法的。
- 協議(Protocol)和類別中定義的屬性都只是聲明getter/setter方法而已。因此需要在.m文件中實現對應的getter/setter方法, 具體如下:
- (void)setCellHeight:(NSNumber *)cellHeight { [self willChangeValueForKey:@"cellHeight"]; objc_setAssociatedObject(self, _cmd, cellHeight, OBJC_ASSOCIATION_COPY_NONATOMIC); [self didChangeValueForKey:@"cellHeight"]; } - (NSNumber *)cellHeight { return objc_getAssociatedObject(self, @selector(setCellHeight:)); }
在ARC(即自動內存管理)模式下,如果定義屬性時不指定任何屬性關鍵字,則:
基本數據類型默認關鍵字是: atomic, readwrite, assign
普通的 Objective-C 對象:atomic, readwrite, strong
5. KVC & KVO
無論是Swift還是Objective-C,KVC的定義都是對NSObject的擴展來實現的(Objective-C中有個顯式的NSKeyValueCoding類別名,而Swift沒有,也不需要)。所以對於所有繼承了NSObject的類型,也就是幾乎所有的Objective-C對象都能使用KVC(一些純Swift類和結構體是不支持KVC的),下面是KVC最為重要的四個方法:
//直接通過Key來取值 - (nullable id)valueForKey:(NSString *)key; //通過Key來設值 - (void)setValue:(nullable id)value forKey:(NSString *)key; //通過KeyPath來取值 - (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來設值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
設置
當調用setValue:屬性值 forKey:@”name“的代碼時,底層的執行機制如下:
1)程序優先調用setName方法,代碼通過setter方法完成設置。注意,這裡的是指成員變量名,首字母大小寫要符合KVC的命名規則,下同。
2)如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那麼在這一步KVC會執行setValue:forUndefinedKey:方法,拋出異常,不過一般開發者不會這麼做。
3)KVC機制繼續搜索該類裡面有沒有名為_的成員變量,無論該變量是在類接口處定義,還是在類實現處定義,也無論用了什麼樣的訪問修飾符,只要存在以_命名的變量,KVC都可以對該成員變量賦值。
4)如果該類即沒有set:方法,也沒有_成員變量,KVC會繼續搜索_is的成員變量,再給它們賦值。
5)和上面一樣,如果該類即沒有set:方法,也沒有和is成員變量,KVC機制再會繼續搜索和is的成員變量。再給它們賦值。 如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
如果開發者想讓這個類禁用KVC裡,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set:屬性名時,會直接用setValue:forUndefinedKey:方法。
總結如下,以Name為例:
setName方法---> 檢查+ (BOOL)accessInstanceVariablesDirectly方法 返回值 ---> _Name成員變量 ---> _isName成員變量 ---> Name成員變量 ---> isName成員變量 ---> setValue:forUndefinedKey 拋出異常。
取值
當調用valueForKey:@”name“的代碼時,KVC對key的搜索方式不同於setValue:屬性值 forKey:@”name“,其搜索方式如下:
1)查找Getter方法:
首先按get, , is的順序查找getter方法,找到的話會直接調用。如果是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
2)數組處理:
如果上面的getter沒有找到,KVC則會查找countOf, objectInAtIndex或AtIndexes格式的方法。 如果countOf方法和另外兩個方法中的一個被找到,那麼就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬於NSArray的方法,就會以countOf,objectInAtIndex或AtIndexes這幾個方法組合的形式調用。還有一個可選的get:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
3)集合處理:
如果上面的方法沒有找到,那麼會同時查找countOf,enumeratorOf,memberOf格式的方法。如果這三個方法都找到,那麼就返回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發NSSet的消息,就會以countOf,enumeratorOf,memberOf組合的形式調用。
4)檢查accessInstanceVariablesDirectly方法並查找成員變量: 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),那麼和先前的設值一樣,會按_, _is,,is的順序搜索成員變量名,這裡不推薦這麼做,因為這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那麼會直接調用valueForUndefinedKey:
5)還沒有找到的話,調用valueForUndefinedKey: 拋出異常。
valueForKey:和 setValue:forKey的特殊處理,在下面兩個方法中,我們可以對於不存在key做一些處理。
- (id)valueForUndefinedKey:(NSString *)key { id value = nil; if ([key isEqualToString:@"age"]) { value = @(10); } else { value = [super valueForUndefinedKey:key]; } return value; } - (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key { if ([key isEqualToString:@"age"]) { NSLog(@"hehe, set age"); } else { [super setValue:value forUndefinedKey:key]; } }
6. 消息傳遞和消息轉發
動態綁定:動態綁定是指在執行期間(非編譯期)判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。程序運行過程中,把函數(或過程)調用與響應調用所需要的代碼相結合的過程稱為動態綁定。
在 Objective-C 中調用方法,稱之為消息傳遞。
消息傳遞是把選擇器(SEL)映射為函數指針,並調用被引用的函數,以及傳遞給它一 個對象指針、一個選擇器和一組函數參數。
Objective-C 運行時的核心就在於消息分派器:objc_msgSend
具體流程如下:
1) 檢查接受對象是否為nil,如果是,調用nil處理程序。默認的行為是什麼都不做。
2) 在垃圾收集環境中(iOS 不支持,這一塊是為了內容完整才給出的),檢查有沒有短路選擇器 (retain、release、autorelease、retainCount),如果有,返回 self。是的,這意味著在垃圾收集環境中 retainCount 會返回 self,不過你應該用不到。
3) 檢查類緩存中是不是已經有方法實現了,有的話,直接調用。
4) 比較請求的選擇器和類中定義的選擇器,如果找到了,調用方法實現。
5) 比較請求的選擇器和父類中定義的選擇器,然後是父類的父類,以此類推。如果找到選擇器, 調用方法實現。
//調用_objc_msgForward進入消息轉發流程:
6) 動態轉發:resolveInstanceMethod:(或resolveClassMethod:)
在這裡我們可以調用class_addMethod為該Class動態添加實現。
如果它返回YES,那麼重新開始。這一次對象會響應這個選擇器,一般是因為它已經調用過 class_addMethod。 例如:
//Car.h @interface Car : NSObject - (void)runTo:(NSString *)place; @end //Car.m @implementation Car + (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(runTo:)) { class_addMethod(self, sel, (IMP)dynamicMethodIMPRunTo, "v@:@"); return YES; } return [super resolveInstanceMethod:sel]; } //動態添加的@selector(runTo:) 對應的實現 static void dynamicMethodIMPRunTo(id self, SEL _cmd,id place){ NSLog(@"dynamicMethodIMPRunTo %@",place); } @end
7) 快速轉發:forwardingTargetForSelector:
嘗試找到一個能響應該消息的對象。如果獲取到,則直接轉發給它。如果返回了nil,繼續下面的動作。這裡不要返回 self,否則會形成死循環。
//Person.h @interface Person : NSObject - (void)runTo:(NSString *)place; @end //Person.m @implementation Person - (void)runTo:(NSString *)place;{ NSLog(@"person runTo %@",place); } @end //Car.h @interface Car : NSObject - (void)runTo:(NSString *)place; @end //Car.m @implementation Car - (id)forwardingTargetForSelector:(SEL)aSelector{ //將消息轉發給Person的實例 if (aSelector == @selector(runTo:)) { return [[Person alloc]init]; } return [super forwardingTargetForSelector:aSelector]; } @end
8) 普通轉發(比前面講到的機制要慢上幾十到幾百倍):forwardInvocation: + methodSignatureForSelector:
調用methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果返回非 nil,創建一個 NSInvocation 並傳給forwardInvocation:。如果返回nil,則直接調用doesNotRecognizeSelector拋出異常;
調用forwardInvocation:方法,將methodSignatureForSelector方法獲取到的方法簽名包裝成Invocation傳入,如何處理就在這裡面了。
@implementation Car - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{ //判斷selector是否為需要轉發的,如果是則手動創建生成方法簽名並返回。 if (aSelector == @selector(runTo:)){ return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; } return [super forwardingTargetForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { //判斷是否為需要轉發的SEL if ([someOtherObject respondsToSelector:[anInvocation selector]]) { [anInvocation invokeWithTarget:someOtherObject]; } else { [super forwardInvocation:anInvocation]; } } @end
9) 調用 doesNotRecognizeSelector:,默認的實現是拋出異常。
消息轉發流程如下圖:
注意:
1.我們可以使用methodForSelector手動獲得IMP來直接調用。
用 methodForSelector 獲取IMP時,會嘗試forward機制, 所以在沒有對應方法時,返回的是 _objc_msgForward ,不會返回NULL。
2.使用respondsToSelector 判斷對象是否能響應消息時, 會避開forward機制 ,但是該方法會嘗試一次resolveInstanceMethod。
3.如果某個方法已實現,需要強制進行消息轉發(即手動觸發) 實現方法如下:
利用 method swizzling 將selector的實現改變為_objc_msgForward或者_objc_msgForward_stret,在調selector時就會進行消息轉發。如果轉發的消息的返回值是struct類型,就使用_objc_msgForward_stret,否則使用_objc_msgForward。代碼如下:
@interface Car : NSObject - (void)printAA; - (void)printAAAA; @end //對 runTo: 進行消息轉發 @implementation Car - (void)printAA { NSLog(@“hello AA”); } + (void)load { //強制轉發 [self forceMessageForward]; } #pragma mark - 強制消息轉發 + (void)forceMessageForward { SEL selector = @selector(printAA); Method targetMethod = class_getInstanceMethod(self.class, @selector(selector)); const char *typeEncoding = method_getTypeEncoding(targetMethod); //利用 method swizzling 將selector的實現改變為_objc_msgForward或者_objc_msgForward_stret, 強制方法進行消息轉發。 //在調selector時就會進行消息轉發。如果轉發的消息的返回值是struct類型,就使用_objc_msgForward_stret,否則使用_objc_msgForward。 IMP targetMethodIMP = _objc_msgForward; class_replaceMethod(self.class, selector, targetMethodIMP, typeEncoding); } - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *sig = nil; //假設有方法簽名為"@@:@" //第一個@表示返回值類型為id, //第二個@表示的是函數的調用者類型, //第三個:表示 SEL //第四個@表示需要一個id類型的參數 //判斷selector是否為需要轉發的,如果是則手動創建生成方法簽名並返回。 if (aSelector == @selector(printAA)){ sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; } else if (aSelector == @selector(printAAAA)){ sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; } else { sig = [super forwardingTargetForSelector:aSelector]; } return sig; }
//消息轉發,調用這個方法。anInvocation中保存著調用方法時傳遞的參數信息。
//對於未實現的方法要走forwardInvocation消息轉發的話,必須重寫methodSignatureForSelector,並返回對應的方法簽名。
//對於這種通過替換IMP為_objc_msgForward強制消息轉發的SEL,如果沒有其他未實現的方法的話,可不寫methodSignatureForSelector。
- (void)forwardInvocation:(NSInvocation *)anInvocation { if (anInvocation.selector == @selector(printAA)){ NSLog(@"AA打印"); } else if (anInvocation.selector == @selector(printAAAA)) { NSLog(@"AAAA打印"); } else { [super forwardInvocation:anInvocation]; } }
好了,今天的學習就到這裡,大家記得幫忙轉發哦~