iOS開發:不會runtime,還怎麼找工作?

iOS開發:不會runtime,還怎麼找工作?

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 對象都有相同的結構,如圖:

iOS開發:不會runtime,還怎麼找工作?

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)。那什麼是元類呢?元類是類對象的類。也可以換一種容易理解的說法:

  1. 當你給對象發送消息時,runtime處理時是在這個對象的類的方法列表中尋找;
  2. 當你給類發消息時,runtime處理時是在這個類的元類的方法列表中尋找。

3 對象,類,元類

我們來看一個很經典的圖來加深理解:(isa:虛線箭頭, super class:實線箭頭)

iOS開發:不會runtime,還怎麼找工作?

可以總結為下:

  1. 每一個Instance(實例對象)都有一個isa指針指向它的類Class;
  2. 每一個Class都有一個isa指針指向它的Meta Class;
  3. 每一個Meta Class的isa指針都指向最上層的Meta Class,這個Meta Class是NSObject的Meta Class。(包括NSObject的Meta Class的isa指針也是指向的NSObject的Meta Class,也就是自己,這裡形成了個閉環);
  4. 每一個Meta Class的super class指針指向它原本Class的 Super Class的Meta Class (這裡最上層的NSObject的Meta Class的super class指針還是指向NSObject自己);
  5. 最上層的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);
}

方法:類方法和實例方法的區別

類方法:

  1. 類方法是屬於類對象的, 類方法列表存儲在元類中。
  2. 類方法只能通過類對象調用
  3. 類方法中的self是類對象
  4. 類方法可以調用其他的類方法
  5. 類方法中不能訪問成員變量
  6. 類方法中不能直接調用對象方法

實例方法:

  1. 實例方法是屬於實例對象的,實例方法列表存儲在類中。
  2. 實例方法只能通過實例對象調用
  3. 實例方法中的self是實例對象
  4. 實例方法中可以訪問成員變量
  5. 實例方法中直接調用實例方法
  6. 實例方法中也可以調用類方法(通過類名)

屬性

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方法由用戶自己實現,不自動生成。

注意:

  1. 如果使用@property定義了某個屬性XX,而沒有寫@synthesize和@dynamic,是無法同時重寫該屬性的seter和getter方法的。
  2. 協議(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:,默認的實現是拋出異常。

消息轉發流程如下圖:

iOS開發:不會runtime,還怎麼找工作?

注意:

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];
 }
}

好了,今天的學習就到這裡,大家記得幫忙轉發哦~


分享到:


相關文章: