標籤:data- bool http style retain 快速 建立 內建 window
訊息轉寄是一種功能強大的技術,可以大大增加Objective-C的表現力。什麼是訊息轉寄?簡而言之,它允許未知的訊息被困住並作出反應。換句話說,無論何時發送未知訊息,它??都會以一個很好的包發送到您的代碼中,此時您可以隨心所欲地執行任何操作。
為什麼它被稱為 “轉寄”? 當某個對象沒有任何響應某個 訊息 的操作就 “轉寄” 該 訊息。原因是這種技術主要是為了讓對象讓其他對象為他們處理 訊息,從而 “轉寄”。
1. 類,對象,方法
在我們開始使用訊息機制之前,我們可以約定我們的術語。例如,很多人不清楚“方法”與“訊息”是什麼,但這對於理解訊息傳遞系統如何在低層級工作至關重要。
- 方法:與一個類相關的一段實際代碼,並給出一個特定的名字。例:
- (int)meaning { return 42; }
- 訊息:發送給對象的名稱和一組參數。樣本:向0x12345678對象發送
meaning
並且沒有參數。
- 選取器:表示訊息或方法名稱的一種特殊方式,表示為類型SEL。選取器本質上就是不透明的字串,它們被管理,因此可以使用簡單的指標相等來比較它們,從而提高速度。(實現可能會有所不同,但這基本上是他們在外部看起來的樣子。)例如:
@selector(meaning)
。
- 訊息發送:接收資訊並尋找和執行適當方法的過程。
1.1 OC的方法與C的函數
Objective-C方法最終被產生為C函數,並帶有一些額外的參數。Objective-C中的方法預設被隱藏了兩個參數:self
和_cmd
。你可能知道self
是作為一個隱式參數傳遞的,它最終成為一個明確的參數。鮮為人知的隱式參數_cmd
(它儲存了正在發送的訊息的選取器)是第二個這樣的隱式參數。總之,self
指向對象本身,_cmd
指向方法本身。舉兩個例子來說明:
例1:- (NSString *)name
這個方法實際上有兩個參數:self
和_cmd
。
例2:- (void)setValue:(int)val
這個方法實際上有三個參數:self
,_cmd
和 val
。
在編譯時間你寫的 Objective-C 函數調用的文法都會被翻譯成一個 C 的函數調用 objc_msgSend()
。比如,下面兩行代碼就是等價的:
[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
1.2 類,對象,方法的C表達
在 Objective-C 中,類、對象和方法都是一個 C 的結構體,從 objc/runtime.h 以及 objc/objc.h標頭檔中,我們可以找到他們的定義:
struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;#endif} OBJC2_UNAVAILABLE;/* Use `Class` instead of `struct objc_class *` */
/// Represents an instance of a class.struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY;};
struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE;} OBJC2_UNAVAILABLE;
struct objc_method_list { struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE;#ifdef __LP64__ int space OBJC2_UNAVAILABLE;#endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE;}
1.3 訊息發送
在C語言函數中發生了什麼事情?編譯器是如何找到這個方法的呢?訊息發送的主要步驟如下:
- 首先檢查這個selector是不是要忽略。比如Mac OS X開發,有了記憶體回收就不會理會retain,release這些函數。
- 檢測這個selector的target是不是nil,OC允許我們對一個nil對象執行任何方法不會Crash,因為運行時會被忽略掉。
- 如果上面兩步都通過了,就開始尋找這個類的實現IMP,先從cache裡尋找,如果找到了就運行對應的函數去執行相應的代碼。
- 如果cache中沒有找到就找類的方法列表中是否有對應的方法。
- 如果類的方法列表中找不到就到父類的方法列表中尋找,一直找到NSObject類為止。
- 如果還是沒找到就要開始進入動態方法解析,後面會說
2. 動態特性:方法解析和訊息轉寄
沒有方法的實現,程式會在運行時掛掉並拋出 unrecognized selector sent to …
的異常。但在異常拋出前,Objective-C 的運行時會給你三次拯救程式的機會:
- Method resolution
- Fast forwarding
- Normal forwarding
2.1 動態方法解析: Method Resolution
首先,Objective-C 運行時會調用 + (BOOL)resolveInstanceMethod:
或者 + (BOOL)resolveClassMethod:
,讓你有機會提供一個函數實現。如果你添加了函數並返回 YES, 那運行時系統就會重新啟動一次訊息發送的過程。還是以 foo 為例,你可以這麼實現:
void fooMethod(id obj, SEL _cmd) { NSLog(@"Doing foo");}+ (BOOL)resolveInstanceMethod:(SEL)aSEL{ if(aSEL == @selector(foo:)){ class_addMethod([self class], aSEL, (IMP)fooMethod, "[email protected]:"); return YES; } return [super resolveInstanceMethod];}
這裡第一字元v
代表函數傳回型別void
,第二個字元@
代表self的類型id
,第三個字元:
代表_cmd的類型SEL
。這些符號可在Xcode中的開發人員文檔中搜尋Type Encodings就可看到符號對應的含義,更詳細的官方文檔傳送門 在這裡,此處不再列舉了。
2.2 快速轉寄: Fast Rorwarding
訊息轉寄機制執行前,runtime系統允許我們替換訊息的接收者為其他對象。通過- (id)forwardingTargetForSelector:(SEL)aSelector
方法。如果此方法返回的是nil 或者self,則會進入訊息轉寄機制(- (void)forwardInvocation:(NSInvocation *)invocation
),否則將會向返回的對象重新發送訊息。
- (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(foo:)){ return [[BackupClass alloc] init]; } return [super forwardingTargetForSelector:aSelector];}
2.3 訊息轉寄: Normal Forwarding
- (void)forwardInvocation:(NSInvocation *)invocation { SEL sel = invocation.selector; if([alternateObject respondsToSelector:sel]) { [invocation invokeWithTarget:alternateObject]; } else { [self doesNotRecognizeSelector:sel]; }}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; if (!methodSignature) { methodSignature = [NSMethodSignature signatureWithObjCTypes:"[email protected]:*"]; } return methodSignature;}
forwardInvocation:
方法就是一個不能識別訊息的分發中心,將這些不能識別的訊息轉寄給不同的訊息對象,或者轉寄給同一個對象,再或者將訊息翻譯成另外的訊息,亦或者簡單的“吃掉”某些訊息,因此沒有響應也不會報錯。例如:我們可以為了避免直接閃退,可以當訊息沒法處理時在這個方法中給使用者一個提示,也不失為一種友好的使用者體驗。
其中,參數invocation
是從哪來的?在forwardInvocation:
訊息發送前,runtime系統會向對象發送methodSignatureForSelector:
訊息,並取到返回的方法簽名用於產生NSInvocation對象。所以重寫forwardInvocation:
的同時也要重寫methodSignatureForSelector:
方法,否則會拋出異常。當一個對象由於沒有相應的方法實現而無法響應某個訊息時,運行時系統將通過forwardInvocation:
訊息通知該對象。每個對象都繼承了forwardInvocation:
方法,我們可以將訊息轉寄給其它的對象。
3. 應用實戰:訊息轉寄3.1 特定奔潰預防處理
下面有一段因為沒有實現方法而會導致奔潰的代碼:
- (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:[UIColor whiteColor]]; self.title = @"Test2ViewController"; //執行個體化一個button,未實現其方法 UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = CGRectMake(50, 100, 200, 100); button.backgroundColor = [UIColor blueColor]; [button setTitle:@"訊息轉寄" forState:UIControlStateNormal]; [button addTarget:self action:@selector(doSomething) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button];}
為解決這個問題,可以專門建立一個處理這種問題的分類:
#import "NSObject+CrashLogHandle.h"@implementation NSObject (CrashLogHandle)- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { //方法簽名 return [NSMethodSignature signatureWithObjCTypes:"[email protected]:@"];}- (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"NSObject+CrashLogHandle---在類:%@中 未實現該方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));}@end
因為在category中複寫了父類的方法,會出現下面的警告:
解決辦法就是在Xcode的Build Phases中的資源檔裡,在對應的檔案後面 -w ,忽略所有警告。
3.2 蘋果系統API迭代造成的奔潰處理3.2.1 相容系統API迭代的傳統方案
隨著每年iOS系統與硬體的更新迭代,部分效能更優異或者可讀性更高的API將有可能對原有API進行廢棄與更替。與此同時我們也需要對現有APP中的老舊API進行版本相容,當然進行版本相容的方法也有很多種,下面筆者會列舉常用的幾種:
if ([object respondsToSelector: @selector(selectorName)]) { //using new API} else { //using deprecated API}
if (NSClassFromString(@"ClassName")) { //using new API}else { //using deprecated API}
#define isOperatingSystemAtLeastVersion(majorVersion, minorVersion, patchVersion)[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: (NSOperatingSystemVersion) { majorVersion, minorVersion, patchVersion}]if (isOperatingSystemAtLeastVersion(11, 0, 0)) { //using new API} else { //using deprecated API}
3.2.2 相容系統API迭代的新方案
**需求:**假設現在有一個過去寫好的類,如下所示,其中有一行因為系統API過時導致奔潰的代碼:
- (void)viewDidLoad { [super viewDidLoad]; [self.view setBackgroundColor:[UIColor whiteColor]]; self.title = @"Test3ViewController"; UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 375, 600) style:UITableViewStylePlain]; tableView.delegate = self; tableView.dataSource = self; tableView.backgroundColor = [UIColor orangeColor]; // May Crash Line tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"]; [self.view addSubview:tableView];}
其中有一行會發出警告,Xcode也給出了推薦解決方案,如果你點擊Fix它會自動添加檢查系統版本的代碼,如所示:
**方案1:**手動加入版本判斷邏輯
以前的適配處理,可根據作業系統版本進行判斷
if (isOperatingSystemAtLeastVersion(11, 0, 0)) { scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;} else { viewController.automaticallyAdjustsScrollViewInsets = NO;}
**方案2:**訊息轉寄
在iOS11 Base SDK直接採取最新的API並且配合Runtime的訊息轉寄機制就能實現一行代碼在不同版本作業系統下採取不同的訊息調用方式
- UIScrollView+Forwarding.m
#import "UIScrollView+Forwarding.h"#import "NSObject+AdapterViewController.h"@implementation UIScrollView (Forwarding)- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1 NSMethodSignature *signature = nil; if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) { signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]; }else { signature = [super methodSignatureForSelector:aSelector]; } return signature;}- (void)forwardInvocation:(NSInvocation *)anInvocation { // 2 BOOL automaticallyAdjustsScrollViewInsets = NO; UIViewController *topmostViewController = [self cm_topmostViewController]; NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3 [viewControllerInvocation setTarget:topmostViewController]; [viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]; [viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4 [viewControllerInvocation invokeWithTarget:topmostViewController]; // 5}@end
- NSObject+AdapterViewController.m
#import "NSObject+AdapterViewController.h"@implementation NSObject (AdapterViewController)- (UIViewController *)cm_topmostViewController { UIViewController *resultVC; resultVC = [self cm_topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]]; while (resultVC.presentedViewController) { resultVC = [self cm_topViewController:resultVC.presentedViewController]; } return resultVC;}- (UIViewController *)cm_topViewController:(UIViewController *)vc { if ([vc isKindOfClass:[UINavigationController class]]) { return [self cm_topViewController:[(UINavigationController *)vc topViewController]]; } else if ([vc isKindOfClass:[UITabBarController class]]) { return [self cm_topViewController:[(UITabBarController *)vc selectedViewController]]; } else { return vc; }}@end
當我們在iOS10調用新API時,由於沒有具體對應API實現,我們將其原有的訊息轉寄至當前棧頂UIViewController去調用低版本API。
關於[self cm_topmostViewController];
,執行之後得到的結果可以查看如下:
方案2的整體流程:
為即將轉寄的訊息返回一個對應的方法簽名(該簽名後面用於對轉寄訊息對象(NSInvocation *)anInvocation進行編碼用)
開始訊息轉寄((NSInvocation *)anInvocation封裝了原有訊息的調用,包括了方法名,方法參數等)
由於轉寄調用的API與原始調用的API不同,這裡我們建立一個用於訊息調用的NSInvocation對象viewControllerInvocation並配置好對應的target與selector
配置所需參數:由於每個方法實際是預設內建兩個參數的:self和_cmd,所以我們要配置其他參數時是從第三個參數開始配置
訊息轉寄
3.2.3 驗證對比新方案
注意測試的時候,選擇iOS10系統的模擬器進行驗證(沒有的話可以先Download Simulators),安裝完後如下如選擇:
- 不注釋並匯入UIScrollView+Forwarding類
- 注釋掉UIScrollView+Forwarding的功能代碼
會如所示奔潰:
4. 總結4.1 類比多繼承
面試挖坑:OC是否支援多繼承?好,你說不支援多繼承,那你有沒有類比多繼承特性的辦法?
轉寄和繼承相似,可用於為OC編程添加一些多繼承的效果,一個對象把訊息轉寄出去,就好像他把另一個對象中放法接過來或者“繼承”一樣。訊息轉寄彌補了objc不支援多繼承的性質,也避免了因為多繼承導致單個類變得臃腫複雜。
雖然轉寄可以實現繼承功能,但是NSObject還是必須表面上很嚴謹,像respondsToSelector:
和isKindOfClass:
這類方法只會考慮繼承體系,不會考慮轉寄鏈。
4.2 訊息機制總結
Objective-C 中給一個對象發送訊息會經過以下幾個步驟:
在對象類的 dispatch table 中嘗試找到該訊息。如果找到了,跳到相應的函數IMP去執行實現代碼;
如果沒有找到,Runtime 會發送 +resolveInstanceMethod:
或者 +resolveClassMethod:
嘗試去 resolve 這個訊息;
如果 resolve 方法返回 NO,Runtime 就發送 -forwardingTargetForSelector:
允許你把這個訊息轉寄給另一個對象;
如果沒有新的目標對象返回, Runtime 就會發送-methodSignatureForSelector:
和 -forwardInvocation:
訊息。你可以發送 -invokeWithTarget:
訊息來手動轉寄訊息或者發送 -doesNotRecognizeSelector:
拋出異常。
iOS訊息轉寄