一、什麼是多態
1.概念:相同接口,不同的實現
來自不同類可以定義共享相同名稱的方法。
動態類型能使程序直到執行時才確定對象所屬類型
動態類型綁定能使程序直到執行時才確定要對對象調用的實際方法
2.Objective-C不同於傳統程序設計語言,它可以再運行時加入新的數據類型和新的程序模塊:動態類型識別,動態綁定,動態加載
3.id類型:通用指針類型,弱類型,編譯時不進行類型檢查
多態:不同對象以自己的方式響應相同的消息的能力叫做多態。
由於每個類都屬於該類的名字空間,這使得多態稱爲可能。類定義中的名字和類定義外的名字並不會衝突。類的實例變量和類方法有如下特點:
-
和C語言中結構體中的數據成員一樣,類的實例變量也位於該類獨有的名字空間。
-
類方法也同樣位於該類獨有的名字空間。與C語言中的方法名不同,類的方法名並不是一個全局符號。一個類中的方法名不會和其他類中同樣的方法名衝突。兩個完全不同的類可以實現同一個方法。
方法名是對象接口的一部分。對象收到的消息的名字就是調用的方法的名字。因爲不同的對象可以有同名的方法,所以對象必須能理解消息的含義。同樣的消息發給不同的對象,導致的操作並不相同。
多態的主要好處就是簡化了編程接口。它容許在類和類之間重用一些習慣性的命名,而不用爲每一個新加的函數命名一個新名字。這樣,編程接口就是一些抽象的行爲的集合,從而和實現接口的類的區分開來。
Objective-C支持方法名的多態,但不支持參數和操作符的多態。
動態特性基礎
1、動態類型
即運行時再決定對象的類型。這類動態特性在日常應用中非常常見,簡單說就是id類型。id類型即通用的對象類,任何對象都可以被id指針所指,而在實際使用中,往往使用introspection來確定該對象的實際所屬類:
id obj = someInstance;
if ([obj isKindOfClass:someClass])
{
someClass *classSpecifiedInstance = (someClass *)obj;
// Do Something to classSpecifiedInstance which now is an instance of someClass
//...
}
-isMemberOfClass:
是 NSObject
的方法,用以確定某個 NSObject
對象是否是某個類的成員。與之相似的爲 -isKindOfClass:
,可以用以確定某個對象是否是某個類或其子類的成員。這兩個方法爲典型的introspection方法。在確定對象爲某類成員後,可以安全地進行強制轉換,繼續之後的工作。
2、動態綁定
基於動態類型,在某個實例對象被確定後,其類型便被確定了。該對象對應的屬性和響應的消息也被完全確定,這就是動態綁定。在繼續之前,需要明確Objective-C中消息的概念。由於OC的動態特性,在OC中其實很少提及“函數”的概念,傳統的函數一般在編譯時就已經把參數信息和函數實現打包到編譯後的源碼中了,而在OC中最常使用的是消息機制。調用一個實例的方法,所做的是向該實例的指針發送消息,實例在收到消息後,從自身的實現中尋找響應這條消息的方法。
動態綁定所做的,即是在實例所屬類確定後,將某些屬性和相應的方法綁定到實例上。這裏所指的屬性和方法當然包括了原來沒有在類中實現的,而是在運行時才需要的新加入的實現。在Cocoa層,我們一般向一個NSObject對象發送-respondsToSelector:或者-instancesRespondToSelector:等來確定對象是否可以對某個SEL做出響應,而在OC消息轉發機制被觸發之前,對應的類的+resolveClassMethod:和+resolveInstanceMethod:將會被調用,在此時有機會動態地向類或者實例添加新的方法,也即類的實現是可以動態綁定的。一個例子:
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
//該方法在OC消息轉發生效前被調用
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
//向[self class]中新加入返回爲void的實現,SEL名字爲aSEL,實現的具體內容爲dynamicMethodIMP class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, “v@:”);
return YES;
}
return [super resolveInstanceMethod:aSel];
}
當然也可以在任意需要的地方調用class_addMethod
或者method_setImplementation
(前者添加實現,後者替換實現),來完成動態綁定的需求。
3、動態加載
根據需求加載所需要的資源,這點很容易理解,對於iOS開發來說,基本就是根據不同的機型做適配。最經典的例子就是在Retina設備上加載@2x的圖片,而在老一些的普通屏設備上加載原圖。隨着Retina iPad的推出,和之後可能的Retina Mac的出現,這個特性相信會被越來越多地使用。
三、在Objective-C中如何實現多態
在Objective-C中是通過一個叫做selector的選取器實現的。它也指那個在Objective-C中,selector有兩個意思, 當用在給對象的源碼消息時,用來指方法的名字。在源碼編譯後代替方法名的唯一的標識符。 編譯後的選擇器的類型是SEL有同樣名字的方法、也有同樣的選擇器。你可以使用選擇器來調用一個對象的方法。
選取器有以下特點:
* 所有同名的方法擁有同樣的選取器
* 所有的選取器都是不一樣的
(1) SEL和@selector
選擇器的類型是 SEL。@selector指示符用來引用選擇器,返回類型是SEL。
例如:
SEL responseSEL;
responseSEL = @selector(loadDataForTableView:);
可以通過字符串來得到選取器,例如:
responseSEL = NSSelectorFromString(@"loadDataForTableView:");
也可以通過反向轉換,得到方法名,例如:
NSString *methodName = NSStringFromSelector(responseSEL);
(2) 方法和選取器
選取器確定的是方法名,而不是方法實現。這是多態性和動態綁定的基礎,它使得向不同類對象發送相同的消息成爲現實;否則,發送消息和標準C中調用方法就沒有區別,也就不可能支持多態性和動態綁定。
另外,同一個類的同名類方法和實例方法擁有相同的選取器。
(3) 方法返回值和@參數類型
消息機制通過選取器找到方法的返回值類型和參數類型,因此,動態綁定(例:向id定義的對象發送消息)需要同名方法的實現擁有相同返回值類型和相同的參數類型;否則,運行時可能出現找不到對應方法的錯誤。
有一個例外,雖然同名竈方法和實例方法擁有相同的選取器,但是它們可以有不同的參數類型和返回值類型。
下面是Object類確認對象類型的基礎API:
方法 | 功能 |
---|---|
-(BOOL)isKindOfClass:class-object | 判斷對象是否是某一種類(包括其父類)的實例 |
-(BOOL)isMemberOfClass:class-object | 判斷對象是否是某一種類(不包括其父類)的實例 |
-(BOOL)respondsToSelector:selector | 判斷對象是否有實現某個方法 |
+(BOOL)instancesRepondToSelector:selector | 判斷類的實例是否有實現某個方法 |
+(BOOL)isSubclassOfClass:class-object | 判斷一個類是否是某個類的子類 |
-(id)performSelector:selector | 讓對象調用指定的方法 |
-(id)performSelector:selector withObject: object | 讓對象調用指定的方法並且帶一個參數 |
-(id)performSelector:selector withObject: object1 withObject: object2 | 讓對象調用指定的方法並且帶兩個參數 |
上面的表格中提到了常用的api方法,那麼在程序中我們可以靈活使用這些api來方便地實現各種動態編程語言特性,使得Obj-C擁有了很多動態腳本語言纔有地特性
四、深入運行時特性
基本的動態特性在常規的Cocoa開發中非常常用,特別是動態類型和動態綁定。由於Cocoa程序大量地使用Protocol-Delegate的設計模式,因此絕大部分的delegate指針類型必須是id,以滿足運行時delegate的動態替換(在Java裏這個設計模式被叫做Strategy?不是很懂Java,求糾正)。而Objective-C還有一些高級或者說更底層的運行時特性,在一般的Cocoa開發中較爲少見,基本被運用與編寫OC和其他語言的接口上。但是如果有所瞭解並使用得當的話,在Cocoa開發中往往可以輕易解決一些棘手問題。
這類運行時特性大多由/usr/lib/libobjc.A.dylib
這個動態庫提供,裏面包括了對於類、實例成員、成員方法和消息發送的很多API,包括獲取類實例變量列表,替換類中的方法,爲類成員添加變量,動態改變方法實現等,十分強大。完整的API列表和手冊可以在這裏找到。雖然文檔開頭表明是對於Mac
OS X Objective-C 2.0適用,但是由於這些是OC的底層方法,因此對於iOS開發來說也是完全相同的。
一個簡單的例子,比如在開發Universal應用或者遊戲時,如果使用IB構建了大量的自定義的UI,那麼在由iPhone版轉向iPad版的過程中所面臨的一個重要問題就是如何從不同的nib中加載界面。在iOS5之前,所有的UIViewController
在使用默認的界面加載時(init
或者initWithNibName:bundle:
),都會走-loadNibNamed:owner:options:
。而因爲我們無法拿到-loadNibNamed:owner:options
的實現,因此對其重載是比較困難而且存在風險的。因此在做iPad版本的nib時,一個簡單的辦法是將所有的nib的命名方式統一,然後使用自己實現的新的類似-loadNibNamed:owner:options
的方法將原方法替換掉,同時保證非iPad的設備還走原來的loadNibNamed:owner:options
方法。使用OC運行時特性可以較簡單地完成這一任務。
代碼如下,在程序運行時調用+swizze
,交換自己實現的loadPadNibNamed:owner:options
和系統的loadNibNamed:owner:options
,之後所有的loadNibNamed:owner:options
消息都將會發爲loadPadNibNamed:owner:options
,由自己的代碼進行處理。
+(BOOL)swizze {
Method oldMethod = class_getInstanceMethod(self, @selector(loadNibNamed:owner:options:));
if (!oldMethod) {
return NO;
}
Method newMethod = class_getInstanceMethod(self, @selector(loadPadNibNamed:owner:options:));
if (!newMethod) {
return NO;
}
method_exchangeImplementations(oldMethod, newMethod);
return YES;
}
loadPadNibNamed:owner:options
的實現如下,注意在其中的loadPadNibNamed:owner:options
由於之前已經進行了交換,因此實際會發送爲系統的loadNibNamed:owner:options
。以此完成了對不同資源的加載。
-(NSArray *)loadPadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary *)options {
NSString *newName = [name stringByReplacingOccurrencesOfString:@"@pad" withString:@""];
newName = [newName stringByAppendingFormat:@"@pad"];
//判斷是否存在
NSFileManager *fm = [NSFileManager defaultManager];
NSString* filepath = [[NSBundle mainBundle] pathForResource:newName ofType:@”nib”];
//這裏調用的loadPadNibNamed:owner:options:實際爲爲交換後的方法,即loadNibNamed:owner:options:
if ([fm fileExistsAtPath:filepath]) {
return [self loadPadNibNamed:newName owner:owner options:options];
} else {
return [self loadPadNibNamed:name owner:owner options:options];
}
}
當然這只是一個簡單的例子,而且這個功能也可以通過別的方法來實現。比如添加UIViewController的類別來重載init,但是這樣的重載會比較危險,因爲你UIViewController的實現你無法完全知道,因此可能會由於重載導致某些本來應有的init代碼沒有覆蓋,從而出現不可預測的錯誤。當然在上面這個例子中重載VC的init的話沒有什麼問題(因爲對於VC,init的方法會被自動轉發爲loadNibNamed:owner:options,因此init方法並沒有做其他更復雜的事情,因此只要初始化VC時使用的都是init的話就沒有問題)。但是並不能保證這樣的重載對於其他類也是可行的。因此對於實現未知的系統方法,使用上面的運行時交換方法會是一個不錯的選擇~