之前去XXXX公司面試被問到“怎樣使用performSelector傳入3個以上參數,其中一個為結構體?”當時年少無知,學藝不精,現在開始總結吧。
Selector
對于要討論的Selector來說,可以做這樣簡短的定義
Selector就是用字符串表示某個類的某個方法
看不懂吧,我也很煩這種概念類的說法,看似高上大,看得人一頭霧水。
以下是更加專業的說法:
Selector就是OC的虛擬表(virtual table)中指向實際執行的函數指針(function pointer)的一個C字符串
那問題來了,z到底是干什么用的?
因為method可以用字符串表示,因此,某個method就可以變成用來傳遞的參數。
要想進一步了解概念:
Selector 官方解釋
Objective-C 類和對象是什么?
其他的文章和書都說,Objective-C 是用C語言做父集,在C語言的基礎上,加上了一層類做導向,而Cocoa Framework 的Cocoa這個名字就是來自于C加上OO。也是因為這個Objective-C 可以直接調用C的API,而如果將.m重命名.mm, 程序里面還可以混合C++的語法,從而就變成了Objective-C ++。
Objective-C 的程序在編譯時,編譯器會編譯成C然后再繼續編譯。所有的Objective-C 類會變成C的結構體,所有的方法(以及block)會被編譯成C function,接下來,在執行的時候,Objective-C的runtime才會建立某個C Structure與C function的關聯,也就是說,一個類到底有哪些方法可以調用,是在運行時候(runtime)才決定的。
為什么
Objective-C的對象會被編譯成結構體(Structure)?
例如,我們現在寫一個最簡單的類,里面只有 int i這個成員變量:
@interface SelectorClass : NSObject {
int i;
}
@end”
會被編譯為:
typedef struct {
int a;
} SelectorClass;
因為Objective-C的對象本質就是C的結構體,所以當我們建立一個Objective-C的對象之后,我們就可以把這個對象當做調用C的結構體的調用:
SelectorClass *obj = [[SelectorClass alloc] init];
obj->a = 10;
如何向類中加入方法(method)?
在執行的時候,runtime會為每一個類準備好一個虛擬表(專業術語叫virtual table),虛擬表里面會以每一個字符串當做key,每一個key對應到C function的指定的位置。Runtime里面,把實現C function定義成IMP(具體的方法地址)這個類型(type);至于拿來當做key的字符串,就叫做selector,類型(type)定義成SEL(方法名稱的描述),然后我們就可以使用@selector關鍵字建立selector。
其實SEL就是C語言字符串,我們可以來寫一個簡單的程序驗證一下:
NSLog(@"%s", (char *)(@selector(doSomething)));
每次對一個對象調用某個方法,runtime在做的事情,就是把方法的名稱作為字符串,尋找與字符串符合的C function實現,然后執行。也就是說,以下三件事是一樣的:
我們可以直接要求某個對象執行某個方法:
[myObject doSomthing];
或者是通過performSelector:
調用。performSelector:
是NSObject的方法,而Cocoa Framework中所有的對象都繼承自NSObject,所以每一個對象都可以調用這個方法。
[myObject performSelector:@selector(doSomething)];
我們可以把以上這個程序看做一句話【我們正在吃飯】,使用performSelector:
就像是【我們正在進行一個吃飯的動作】。
而其實,底層執行的卻是objc_msgSend
。
objc_msgSend(myObject, @selector(doSomething), NULL);
看其他的書或者文章,常常看到這句話【要求某個類執行某個方法】、【要求某個類執行某個selector】,其實都是一樣的事情,我也常見過另外一種寫法,叫做【對接受者(receiver)傳遞消息(message)】。因為一個類有些方法,是在runtime一個一個加入的;所以我們就有機會在程序已經執行的時候加入,繼續對某個類加入新方法,一個類已經存在某個方法,也可以在runtime(運行時)用別的實現換掉,一般來說,我們會用Category做這件事。
重要的是:在Objective-C中,一個類會有哪些方法,并不是固定的如果我們在程序中對某個對象調用目前還不存在的方法,編譯的時候,編譯器并不會當做編譯錯誤,只是會發出警告而已,而彈出警告的條件,也就只有是否會引入的頭文件(header)中到底有沒有這個方法而已,所以我們一不小心,就很可能調用沒有實現的方法(或者換種說法,我們要求執行的selector并沒有對應的實現)。如果我們是使用performSelector:
調用,更是完全不會警告。直到實際執行的時候,才會發生unrecognized selector sent to instance
錯誤而導致的應用程序的crash。之所以是警告,而不是當做編譯錯誤,就是因為某個方法有可能之后才會被加入。蘋果認為你會寫出調用到沒有實現的selector,必定是因為你接下來在某個時候、某個地方,就會加入這種方法的實現。
由于Objective-C語言中,類有哪些方法可以在runtime改變,所以我們也會將Objective-C 列入像是“ Perl、Python、Ruby等所謂的動態語言(Dynamic Language)之類。而在這種動態類導向的語言,一個類到底有哪些方法可以調用,往往會比這個對象到底屬于哪些類更加重要。
如果我們不想用category,而想要自己動手寫點程序,手動將這些方法加入到某個類中,我們就可以這么寫。首先先聲明一個C function,至少要有兩個參數,第一個參數是執行method的對象,第二個參數是selector,像是這樣:
void myMethodIMP(id self, SEL _cmd) {
doSomething();
}
接下來可以調用class_addMethod
加入selector與實現的封裝。
“#import <objc/runtime.h>
// 此處省略。。。。。
class_addMethod([MyClass class],
@selector(myMethod), (IMP)myMethodIMP, "v@:");
接下來就可以調用了:
MyClass *myObject = [[MyClass alloc] init];
[myObject myMethod];
那么:
Selector 有何用處呢?
1.Target/Action 模式
Selector的主要用途,就是實現target/action。在Xcode新建一個工程,在xib中建立一個UIButton對象,然后將按鈕連線到controller中聲明IBAction的方法上,這時候,Controller就是Button的target,而要求Controller執行的方法就叫做action。
如果我們要設計一個程序里沒有的UI控件,第一步就是要了解怎么實現target/action。
在UIKit中的Target/Action 稍微復雜一點,因為同一個按鈕可以一次連線好多個target和action。如果想要產生一個按鈕或者其他的自定義控件,我們會繼承自UIView,然后建立兩個成員變量:target與action,action是一個selector。
@interface MyButton : UIView
{
id target;
SEL action;
}
@property (assign) IBOutlet id target;
@property (assign) SEL action;
@end
@implementation MyButton
- (void)mouseDown:(UIEvent *)e
{
[super mouseDown:e];
[target performSelector:action withObject:self];
}
@synthesize target, action;
@end
這里將target的類型特別設置為id,代表的是任意Objective-C對象的指標,如同:Controller到底是什么類,這里來說并不重要,而且這里也不該將target的Class寫死,因為如此一來,就變成某個Controller才可以使用這些按鈕。
接著在mouseDown:
中,要求target執行之前傳入action,由于selector是字符串,是可以傳遞參數,所以也就可以成為按鈕的成員變量。
接下來就可以使用代碼連接target與action,在Controller的程序中,按照以下寫法即可:
[(MyButton *)button setTarget:self];
[(MyButton *)button setAction:@selector(clickAction:)];
把要做什么事情當做參數傳遞,每個語言都有不一樣的做法。Objective-C用的是拿字符串來尋找對應的實現函數的指針,在C語言中就會直接傳遞指針,一些更高級的語言或者就會把一段代碼當做字符串傳遞,要使用的時候再去評估(evaluate)這段代碼字符c串,或者是一段代碼本來就是一個對象,所以可以把代碼當做一個對象傳遞,這就是所謂的【匿名函數】(Anonymous Function),Objective-C的匿名函數當然是block了。
檢查方法是否存在?
有時可能調用到并不存在的方法,如果這樣做就會產生錯誤。但是很多時候遇到的問題是:我們并不很確定某些方法到底有沒有實現,如果有,就實現,如果沒有,就略過或者使用其他的方法。
最常見的問題就是顧忌向下兼容的問題:比如不同的iOS版本間,高版本使用的方法,低版本可能沒有。這樣的話可能就要做檢查。
檢查某個對象是否運行了某個方法,只要調用respondsToSelector:
即可:
比如:
BOOL scale = 1.0;
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
scale = [UIScreen mainScreen].scale;
}
在其他語言中,也需要這樣檢查method是否存在嗎?在Ruby語言中,類似 的respond_to?
語法,至于Python,我們可以用dir
這個funciton檢查某個對象的全部attribute 中是否存在對應到某個方法中的key,但是更常見的做法就是使用try…catch 語法,如果遇到某個方法可能不存在,就包在 try…catch的block中,像是:
try:
myObject.doSomething()
except Exception, e:
print "The method does not exist.
在Objective-C中,同樣也有try…catch 語法,在很多語言中,善用try…catch也可以將程序寫的條理清晰,但是Objective-C語言中不鼓勵使用。原因是與Objective-C的內存管理機制有關系,如果大量使用 try…catch,會導致內存泄漏
Objective-C是沒有垃圾回收機制(Garbage Collection,簡稱GC)的語言,在iOS 5的時候蘋果放棄使用runtime管理內存,而是推出ARC(Automatic Reference Counter),在編譯時決定什么時候釋放內存。
由于傳統的 Objective-C內存管理大量使用一套叫做auto-release的機制,雖然說是auto(自動),其實也沒有多自動,頂多算是半自動--將一些應該釋放的對象延遲釋放,在這一輪的runloop中先不釋放,而是到了下一輪的runloop開始時才釋放這些內存。如果使用try…catch 捕捉錯誤,就會跳出原本的runloop,而導致該釋放的內存沒有被釋放。
Timer是什么?
NSObject 除了performSelector:
這個方法之外,同樣可以performSelector
開頭的,還有好幾組API可以調用,例如:
performSelector:withObject:afterDelay:
就可以讓我們在一定的秒數之內之后,才要求某個方法執行。
[self performSelector:@selector(doSomething)
withObject:nil afterDelay:2.0];
如果時間還不到已經預定要執行的時間,方法還沒執行,我們也可以取消剛才預定要執行的方法,只要調用cancelPreviousPerformRequestsWithTarget:
即可,如以下代碼:
[NSObject cancelPreviousPerformRequestsWithTarget:self];
performSelector:withObject:afterDelay:的效果相當于新建NSTimer對象,當我們想要延長調用某個方法的時候,或者是要某件事情重復執行的,都可通過建立NSTimer對象實現,要使用Timer,也就必須使用selector語法。
首先要告訴一個timer要做的事情:
- (void)doSomething:(NSTimer *)timer
{
// Do something
}
然后通過```doSomething:``的selector 建立timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:someObject
selector:@selector(doSomething:)
userInfo:nil
repeats:YES];
除了可以通過指定的 target與selector 除外,還可以通過指定NSInvocation
來調用新建的NSTimer
對象;NSInvocation其實就是將 target/action以及整個 action 中的要傳遞給target的參數這三者,再包裝成一個類。調用的方法是scheduledTimerWithTimeInterval:invocation:repeats:
.
通過建立NSInvocation
對象建立timer的方式如下。
NSMethodSignature *sig = [MyClass
instanceMethodSignatureForSelector: @selector(doSomething:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:someObject];
[invocation setSelector:@selector(doSomething:)];
[invocation setArgument:&anArgument atIndex:2];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
invocation:invocation
repeats:YES];
注意,在調用NSInvocation
的setArgument:atIndex
的時候,我們要傳遞的參數,要從2開始,因為在這想象成:這是給````objc_msgSend```調用參數,在0的參數是對象的self,位置在1的是selector。
接收 NSNotification?
接下來的面試套路再寫
如果我們要接收NSNotification,我們也要在開始的時候注冊通知,指定由哪一個 selector 處理通知。
如何在某個線程中執行方法?
除了-performSelector:withObject:afterDelay:
之外,NSObject還有好幾個方法,是讓指定的selector丟到某個線程中執行,包括:
-(void)performSelectorOnMainThread:
(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
-(void)performSelectorOnMainThread:
(SEL)aSelector withObject:(id)arg waitUntilDone:
(BOOL)wait modes:(NSArray<NSString *> *)array
-(void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait
-(void)performSelector:(SEL)aSelector
onThread:(NSThread *)thr withObject:(id)arg
waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array
-(void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
假如有一件事情,暫且稱為doSomething,它會執行的時間有點長,這時候我們就可以把這件事情丟到Background執行,也就是另外新建一條線層執行:
[self performSelectorInBackground:
@selector(doSomething) withObject:nil];
注意,在Background執行執行的時候,這個方法的內部要建立自己的自動釋放池(AutoRelease Pool)。
執行完成后,可以通過-performSelectorOnMainThread:withObjectwaitUntilDone:
,通知主線程,事情已經做完了,就好像,如果要轉換一個比較大的文件,就可以在Background實現轉換,轉換完之后,再告訴主線程,在UI上跳出提示視窗,提示使用者已經轉換完畢。
- (void)doSomthing
{
@autoreleasepool {
// Do something here.
[self performSelectorOnMainThread:@selector(doAnotherThing)
withObject:nil
waitUntilDone:NO];
}
}
Array排序
要做數組排序,就得先告訴這個數組里面每一個元素的大小,所以我們要把怎么比較大小這個事件傳遞到array上, Cocoa Framework 提供了三種方式排序數組,我們可以吧如何比較大小寫成C Function然后傳遞給C Function的指針,現在可以傳遞給Block,而如果是數組里面的對象有負責比較大小的方法,也可以通過selector 指定用哪一個方法排序。
NSString、NSDate、NSNumber 以及 NSIndexPath,都提供 compare:這些方法,假如有一個數組里面都是字符串的話,就可以用compare:排序,NSString用來比較大小順序的方法與選項(像是是否忽略大小寫,字符串中如果出現數字,是否要以數字的大小排列而不是只照字母順序。。。等等),其中最常用的:localizedCompare:
,這個方法會參考目前使用者所在的系統語言而決定排列方式,像是簡體中文下的用拼音排序,繁體中文會用筆畫排序。。。等等。
在使用sortedArrayUsingSelector:
產生重新排序的新的數組,如果是NSMutableArray(可變數組),則可以調用sortUsingSelector:
NSArray *sortedArray = [anArray sortedArrayUsingSelector:
@selector(localizedCompare:)];
也可以通過傳遞selector,要求數組里面的每一個對象都執行一次指定的method。
[anArray makeObjectsPerformSelector:@selector(doSomething)];
代替if...else與switch…case
因為 selector 其實就是C的字符串,除了可以作為參數傳遞之外,也可以放在array或者是dictionary里面。有的時候,如果你覺得寫一堆的if...else與switch…case太長太丑太low,例如,原來我們這樣寫:
switch(condition) {
case 0:
[object doSomething];
break;
case 1:
[object doAnotherThing];
break;
default:
break;
}
可以換成:
[object performSelector:NSSelectorFromString(@[@"doSomething",
@"doAnotherThing"][condition])];
我們可以使用NSStringFromSelector
,將selector轉換成NSString,反之,也可以使用NSSelectorFromString
將NSString換成selector。
....呼叫私有API
其實吧,Objective-C 里面其實沒有真正所謂的私有方法,一個對象實現哪些方法,及時沒有import對應的頭文件,我們也可以調用的到。系統里面有許多原來就內建的class,有一些頭文件并沒有聲明方法,但是從一些相關的網站或者其他途徑,我們就是知道這些方法,我們想調用看看的時候,這時候我們往往會用performSelector
調用:因為我們沒有頭文件。
但是蘋果一般不建議這么做,今天一個方法沒有放到頭文件里面,就代表在做系統升級的時候,系統可能吧整個底層的實現替換掉,這個方法可能就此消失了,而造成了系統升級之后,系統調用不存在的方法而造成的程序crash。而且在上架的時候,蘋果的審查會拒絕使用私有API的軟件。
**以上都是說明selector是啥,下面開始寫面試問的performSelector: **
呼叫 performSelector:
需要注意的地方
1.對super呼叫performSelector:
對于一個對象調用某個方法,或者是通過performSelector:
調用,意思是一樣的,但是對于super的調用,卻有不一樣的結果。如果是:
[super doSomthing];
代表的是調用super的doSomthing實現。如果是:
[super performSelector:@selector(doSomething)];
調用的是super的performSelector:
,最后結果仍然等同于[self doSomething]
.
Selector是 Objective-C 中所有的所有的開始
Objective-C 對象有哪些方法,就是這個對象的類中virtual table(虛擬表)中,有多少selection/C function pointer的pair,
這個特性造就成了在Objective-C 中可以做很多神奇的事情,同時也造成了Objective-C 這門語言的限制。
Objective-C 的神奇之處,就在于,既然對象有哪些方法可以在runtime決定,因此每個對象也都可以在運行時(runtime)改變。我們可以在不用繼承對象的情況下,就新增加新的方法,通常最常見的方法就是category,我們也可以隨時把既有的selector指到不同的 C function pointer 上,像是把兩個selector所指定的function pointer交換,這種交換selector 實現的做法叫做method swizzling(方法交叉)。
由于一個selector只會指向一個實現,因此,Objective-C不會有C++、 Java、C#等語言當中的overloading(重載)。所謂的overloading,就是可以允許有很多名稱相同,但是參數類型不同的function或者method存在,在調用的時候,如果傳入指定類型的參數,就會調用該參數類型的那一組function 或 method。在Objective-C 當中,同一個名稱的方法,就會以最后在runtime載入的哪一組,代替之前的實現。
Objective-C 的virtual table像一個dictionary,而C++、Java 等語言的virtual table則是一個array。在Objective-C中,我們調用[someObject doSomthing]
時候,我們其實就是在表格中尋找符合“doSomthing”這個字符串的方法,在 C++ 或 Java 中,我們調用someObject.doSomething()
時候,在做的事情大概的要求就是【執行virtual table 中第八個方法】。
由于沒做一次方法的調用,都是在做一次 selector/function pointer 的查表。與其他的靜態語言相比較,這種查表的動作比較耗時,因此執行率也比不上C++等程序語言。但是Objective-C 其實有一個其他的優點:我們不需要連接特定的版本的runtime與libraries,就算是libraries中export出來的 function換了位置,只要selector 不變,還是可以找到應該要執行的 C function,所以舊的版本的App在新版本的操作系統上執行時,新版本操作系統并不需要保留舊版的Libraries,而避免C++等語言等語言中的所謂的DLL Hell問題。
2014年,蘋果推出Swift,說Swift是比 Objective-C更快、執行效率更高的語言,Swift之所以效率比Objective-C更高,是因為swift又改變了virtual table的實現,看起來更像是 C++、Java 等語言的virtual table 設計。因為Swift也有必須鏈接指定版本的runtime問題,所以在每一個Swift App中的App bundle中,其實都包含一份Swift runtime。