iOS套路面試題之Selector

之前去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。

Selector示意圖

其實SEL就是C語言字符串,我們可以來寫一個簡單的程序驗證一下:

NSLog(@"%s", (char *)(@selector(doSomething)));

順利印出「doSomething」這個 C 字串

每次對一個對象調用某個方法,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];

注意,在調用NSInvocationsetArgument: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。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,195評論 30 471
  • 面試筆試都是必考語法知識的。請認真復習和深入研究OC。 Objective-C 方法和選擇器有何不同?(Diffe...
    b485c88ab697閱讀 5,719評論 0 35
  • 基礎部分 1、設計模式是什么? 你知道哪些設計模式,并簡要敘述? 設計模式是一種編碼經驗,就是用比較成熟的邏輯去處...
    luzsyn閱讀 525評論 0 2
  • [TOC] 1 APR介紹 太專業的術語就不說了(其實太專業的術語我也不會說……)以下都是個人理解,專業介紹看官網...
    hylexus閱讀 1,122評論 0 0