最近在面試的過程中才發現太多沒有注意的細節,每一個問題問到最后都是在懷疑人生中度過...
正好趁著工作敲定了之后將performSelector相關的細節總結一番。
基礎用法
performSelecor響應了OC語言的動態性:延遲到運行時才綁定方法。當我們在使用以下方法時:
[obj performSelector:@selector(play)];
[obj performSelector:@selector(play:) withObject:@"李周"];
[obj performSelector:@selector(play:with:) withObject:@"李周" withObject:@"謝華華"];
編譯階段并不會去檢查方法是否有效存在,只會給出警告:
Undeclared selector ''
如果要執行的方法名也是動態不確定的一個參數:
[obj performSelector:selector];
編譯器也只會提示說因為當前方法名未知可能會引起內存泄露相關問題:
PerformSelector may cause a leak because its selector is unknown
所以在實際開發中,為了避免運行時突然報錯找不到方法等問題,少使用performSelector方法。
二 延遲執行
[obj performSelector:@selector(play) withObject:@"李周" afterDelay:4.f];
該方法將延遲4秒后再執行play方法。其實說到對時間方面的處理在項目中經常用到的是NSTimer:當一個NSTimer注冊到Runloop后,Runloop會重復的在相應的時間點注冊事件,當然Runloop為了節省資源并不會在準確的時間點觸發事件。
而performSelector:withObject:afterDelay:其實就是在內部創建了一個NSTimer,然后會添加到當前線程的Runloop中。所以當該方法添加到子線程中時,需要格外的注意兩個地方:
① 在子線程中執行會不會調用test方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self performSelector:@selector(test) withObject:nil afterDelay:2];
});
會發現test方法并沒有被調用,因為子線程中的runloop默認是沒有啟動的狀態。使用run方法開啟當前線程的runloop,但是一定要注意run方法和執行該延遲方法的順序。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(test) withObject:nil afterDelay:2];
});
會發現即便添加了run方法,但是test方法還是沒有被調用,在最后打印當前線程的runloop,會發現:
timers = <CFArray 0x6000002a8100 [0x109f67bb0]>{type = mutable-small, count = 1, values = (
0 : <CFRunLoopTimer 0x6000001711c0 [0x109f67bb0]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 544280547 (1.98647892 @ 3795501066754), callout = (Delayed Perform) lZLearningFromInterviewController test (0x105ea0d9c / 0x104b2e2c0) (), context = <CFRunLoopTimer context 0x600000470080>}
子線程的runloop中確實添加了一個CFRunLoopTimer的事件,但是到最后都不會被執行。
將run方法和performSelector延遲方法調換順序后運行:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self performSelector:@selector(test) withObject:nil afterDelay:2];
[[NSRunLoop currentRunLoop] run];
});
此時test方法會被調用,分別打印執行完performSelecor和run方法之后,發現在執行完performSelector方法后該timer事件會被添加到子線程的runloop中:
timers = <CFArray 0x6000000b3c80 [0x112956bb0]>{type = mutable-small, count = 1, values = (
0 : <CFRunLoopTimer 0x60000016fc00 [0x112956bb0]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 544280800 (1.98171604 @ 4048676578329), callout = (Delayed Perform) lZLearningFromInterviewController test (0x10e88fd9c / 0x1
但是當執行完run方法之后,runloop中的timer事件已經是執行完的狀態:
timers = <CFArray 0x6000000b3c80 [0x112956bb0]>{type = mutable-small, count = 0, values = ()},
所以在子線程中兩者的順序必須是先執行performSelector延遲方法之后再執行run方法。因為run方法只是嘗試想要開啟當前線程中的runloop,但是如果該線程中并沒有任何事件(source、timer、observer)的話,并不會成功的開啟。
② test方法中執行的線程
[self performSelector:@selector(test) withObject:nil afterDelay:2];
如果在子線程中調用該performSelector延遲方法,會發現調用該延遲方法的子線程和test方法中執行的子線程是同一個,也就是說:
對于該performSelector延遲方法而言,如果在主線程中調用,那么test方法也是在主線程中執行;如果是在子線程中調用,那么test也會在該子線程中執行。
在回答完延遲方法之后,會將該方法和performSelector:withObject:作對比,那么performSelector:withObject:在不添加到子線程的Runloop中時是否能執行?
我當時想的是,performSelector:withObject:方法和延遲方法類似,只不過是馬上執行而已,所以也需要添加到子線程的RunLoop中。
這么想是錯的,performSelector:withObject:只是一個單純的消息發送,和時間沒有一點關系。所以不需要添加到子線程的Runloop中也能執行。
三 異步執行
有時候面試關于多線程的問題時,會提問說:
如何在不使用GCD和NSOperation的情況下,實現異步線程?
反正我第一反應就是:幸虧,把NSThread給我留下了!
所以能直接使用NSThread的三個方法:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[NSThread detachNewThreadSelector:@selector(test) toTarget:self withObject:nil];
[NSThread detachNewThreadWithBlock:^{
NSLog(@"block中的線程 ---- %@",[NSThread currentThread]);
}];
但是一般面試還會接著往下問:
如果也不使用NSThread已有的方法呢?
這個時候已經沒有時間吐槽了只能接著想了...后來的后來我在perSelector的相關方法中找到了解答:
① performSelectorInBackground 后臺執行
[self performSelectorInBackground:@selector(test) withObject:nil];
該方法一目了然,開啟新的線程在后臺執行test方法
②performSelector:onThread:在指定線程執行
[self performSelector:@selector(test) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];
這個方法有一個thread參數是指定執行的線程,但是很奇怪當我使用自己創建的線程 [[NSThread alloc] init];時,并不會執行test方法,只有當使用[NSThread currentThread]時才會執行:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(tests) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
});
還需要再考證考證這個方法的使用。
四 多參傳遞
一般在聊完這么多和performSelector相關的方法后,不要放松警惕,又一個懷疑人生的問題來了.
performSelector如何進行多值傳輸?
問題一聽馬上就能回答使用NSArray或者NSDictionary或者自定義Model的形式,但是我查到了一個很妙的方法:
因為在OC中調用一個方法實際上就是發送消息objc_msgSend:
{
NSNumber *age = [NSNumber numberWithInt:20];
NSString *name = @"李周";
NSString *gender = @"女";
NSArray *friends = @[@"謝華華",@"亞呼呼"];
SEL selector = NSSelectorFromString(@"getAge:name:gender:friends:");
NSArray *array = @[age,name,gender,friends];
((void(*)(id,SEL,NSNumber*,NSString*,NSString*,NSArray*)) objc_msgSend)(self,selector,age,name,gender,friends);
}
- (void)getAge:(NSNumber *)age name:(NSString *)name gender:(NSString *)gender friends:(NSArray *)friends
{
NSLog(@"%d----%@---%@---%@",[age intValue],name,gender,friends[0]);
}
導入#import <objc/message.h>即可。但是這種方式并不是oc封裝的方法所以使用十分的不方便。
網上的第二種方法其實也是以NSArray的形式傳值,然后創建NSInvocation的方式,將參數一一綁定。
-(id)performSelector:(SEL)aSelector withObject:(NSArray *)object
{
//獲得方法簽名
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSelector];
if (signature == nil) {
return nil;
}
//使用NSInvocation進行參數的封裝
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = aSelector;
//減去 self _cmd
NSInteger paramtersCount = signature.numberOfArguments - 2;
paramtersCount = MIN(object.count, paramtersCount);
for (int i = 0; i < paramtersCount; i++) {
id obj = object[i];
if ([obj isKindOfClass:[NSNull class]]) continue;
[invocation setArgument:&obj atIndex:i+2];
}
[invocation invoke];
id returnValue = nil;
if (signature.methodReturnLength > 0) { //如果有返回值的話,才需要去獲得返回值
[invocation getReturnValue:&returnValue];
}
return returnValue;
}
NSNumber *age = [NSNumber numberWithInt:20];
NSString *name = @"李周";
NSString *gender = @"女";
NSArray *friends = @[@"謝華華",@"亞呼呼"];
SEL selector = NSSelectorFromString(@"getAge:name:gender:friends:");
NSArray *array = @[age,name,gender,friends];
[self performSelector:selector withObject:array];
NSInvocation我是在消息轉發機制中認識的,所以這種方法類似于消息轉發機制中的最后一層,多了創建NSInvocation對象的開銷。而且本質上還是就NSArray進行轉發。
面試其實就是在對細節的深究,好在最后面到了喜歡的公司,希望加入新的環境能學習到更多深刻的知識。