iOS7 Programming Cookbook-Chapter 7-Concurrency(Timer and Thread部分)

本文是笔者自己自学iOS7开发的过程中,练习翻译的《iOS7 Programming Cookbook》,兼做个人的笔记和代码汇总。

由于第七章篇幅过长,拆分成四部分,这是其中的Timer和Thread部分。

7.14 Creating Timer

问题:你希望按照某一特定频率重复执行一个特殊任务,比如你希望在应用运行时每秒钟都更新你的屏幕

解决方法:使用Timer

#import "AppDelegate.h"

@interface AppDelegate ()
@property (nonatomic, strong) NSTimer *paintingTimer;
@end

@implementation AppDelegate

- (void) paint:(NSTimer *)paramTimer{
    /* Do something here */
    NSLog(@"Painting");
}
-(void)startPainting{
    self.paintingTimer=[NSTimer
                        scheduledTimerWithTimeInterval:1.0
                        target:self
                        selector:@selector(paint:)
                        userInfo:nil repeats:YES];
}


-(void)stopPainting{
    if(self.paintingTimer!=nil){
        [self.paintingTimer invalidate];
    }
}

- (void)applicationWillResignActive:(UIApplication *)application{
    [self stopPainting];
}
- (void)applicationDidBecomeActive:(UIApplication *)application{
    [self startPainting];
}
invalidate方法也将释放timer,这样我们就不用手动释放了。

讨论:

timer是一种以特定时间间隔执行一个事件的对象。timer必须按照一个运行循环进行定时。定义一个NSTimer对象会创建一个未被定时的计时器,这个计时器暂时不会执行任何行为,但是当你希望定时时,它是可用的。一旦你安排了一次调用,比如scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:那么这个计时器就会成为一个被已定时的计时器,将会启动你要求的事件。一个已被定时了的计时器是被添加了一个运行循环的。为了使计时器可以唤起目标事件,我们必须按照一个运行循环为计时器进行定时。这一点会在之后的例子中进一步说明。

一旦计时器被创建且加入了一个运行循环。它将或隐式或显式地每n秒执行一次目标方法。因为n是浮点数,你可以制定一个1秒以下的数值。
有多种创建,初始化以及为定时一个计时器的方法。其中最简单的一种是通过NSTimer的类方法 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:   这个方法有几个不同的参数:
  • scheduledTimerWithTimeInterval:唤起事件之前需要等待的时间,秒数。比如你希望每秒调用目标方法两次,那么你就需要将这个参数设为0.5。
  • target:接收事件的目标对象。
  • selector:会接受到事件的目标对象的方法签名。
  • userInfo:这个实体会被保持,为了之后的引用。
  • repeats:指定该计时器是否重复调用目标方法(参数值设为YES),还是只调用一次(参数值设为NO)。

还有其他的创建计时器的方法,其中一个就是NSTimer的类方法scheduledTimerWithTimeInterval:invocation:repeats:  

@interface AppDelegate ()
@property (nonatomic, strong) NSTimer *paintingTimer;
@end

- (void) stopPainting{
    if (self.paintingTimer != nil){
        [self.paintingTimer invalidate];
    }
}

- (void) startPainting{
    /* Here is the selector that we want to call */
    SEL selectorToCall = @selector(paint:);
    /* Here we compose a method signature out of the selector. We
     know that the selector is in the current class so it is easy
     to construct the method signature */
    NSMethodSignature *methodSignature =    [[self class] instanceMethodSignatureForSelector:selectorToCall];
    /* Now base our invocation on the method signature. We need this
     invocation to schedule a timer */
    NSInvocation *invocation =
    [NSInvocation invocationWithMethodSignature:methodSignature];
    [invocation setTarget:self];
    [invocation setSelector:selectorToCall];
    self.paintingTimer = [NSTimer timerWithTimeInterval:1.0
                                             invocation:invocation
                                                repeats:YES];;
    /* Do your processing here and whenever you are ready,
     use the addTimer:forMode instance method of the NSRunLoop class
     in order to schedule the timer on that run loop */
    [[NSRunLoop currentRunLoop] addTimer:self.paintingTimer
                                 forMode:NSDefaultRunLoopMode];

}

-(void) paint:(NSTimer *)paramTimer{
    /* Do something here */
    NSLog(@"Painting");
}

- (void)applicationWillResignActive:(UIApplication *)application
{
   [self stopPainting];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self startPainting];
}

为一个计时器定时可以比作启动一辆汽车的引擎。已定时的计时器是一个正在运行的汽车引擎,一个未定时的计时器是一个准备启动但尚未运行的汽车引擎,我们可以在应用中在我们希望的任何时候启动或停止计时器,就想我们需要根据情况启动和停止汽车引擎一样。如果你想在应用中手动为一个计时器定时,你可以使用NSTimer的类方法scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:,你准备好了之后就可以将计时器加入你选择的运行循环之中了:
- (void) startPainting{
    self.paintingTimer = [NSTimer timerWithTimeInterval:1.0
                                                 target:self
                                               selector:@selector(paint:) userInfo:nil
                                                repeats:YES];
    /* Do your processing here and whenever you are ready,
     use the addTimer:forMode instance method of the NSRunLoop class
     in order to schedule the timer on that run loop */
    [[NSRunLoop currentRunLoop] addTimer:self.paintingTimer
                                 forMode:NSDefaultRunLoopMode];
}

就像你可以使用 scheduledTimerWithTimeInterval:invocation:repeats: 通过invocation创建定时的计时器。你也可以使用NSTimer的类方法 timerWithTimeInterval:invocation:repeats:来使用invocation创建一个为定时的计时器:

- (void) paint:(NSTimer *)paramTimer{
    /* Do something here */
    NSLog(@"Painting");
}
- (void) startPainting{
    /* Here is the selector that we want to call */
    SEL selectorToCall = @selector(paint:);
    /* Here we compose a method signature out of the selector. We
     know that the selector is in the current class so it is easy
     to construct the method signature */
    NSMethodSignature *methodSignature =
    [[self class] instanceMethodSignatureForSelector:selectorToCall];
    /* Now base our invocation on the method signature. We need this
     invocation to schedule a timer */
    NSInvocation *invocation =
    [NSInvocation invocationWithMethodSignature:methodSignature];
    [invocation setTarget:self];
    [invocation setSelector:selectorToCall];
    self.paintingTimer = [NSTimer timerWithTimeInterval:1.0
                                             invocation:invocation
                                                repeats:YES];;
    /* Do your processing here and whenever you are ready,
     use the addTimer:forMode instance method of the NSRunLoop class
     in order to schedule the timer on that run loop */
    [[NSRunLoop currentRunLoop] addTimer:self.paintingTimer
                                 forMode:NSDefaultRunLoopMode];
}

- (void) stopPainting{
    if (self.paintingTimer != nil){
        [self.paintingTimer invalidate];
    }
}
- (void)applicationWillResignActive:(UIApplication *)application{
    [self stopPainting];
}
- (void)applicationDidBecomeActive:(UIApplication *)application{
    [self startPainting];
}

timer的目标方法接收到调用它的那个计时器实例作为参数。比如这个paint:方法证明了计时器如何被传入目标方法,并默认作为目标方法唯一的参数:

- (void) paint:(NSTimer *)paramTimer{
    /* Do something here */
    NSLog(@"Painting");
}

这个参数提供给你一个调用该方法的计时器的引用。比如你可以使用invalidate方法防止计时器重复运行,如果需要的话。你也可以调用NSTimer实例的userInfo方法来恢复计时器持有的对象。这个对象被传入NSTimer的初始化方法,直接被传入计时器中以备以后的引用。


7.15 Creating Concurrency with Threads

问题:你希望对于在应用中分离任务有最大限度的控制。比如你希望在UI仍然可以运行并与用户交互的条件下,运行一个复杂运算。

解决方案:

在你的应用中使用线程,像这样:

-(void)downloadNewFile:(id)paramObject{
    
    @autoreleasepool {
        
        NSString* fileURL=(NSString*)paramObject;
        NSURL* url=[NSURL URLWithString:fileURL];
        NSURLRequest* request=[NSURLRequest requestWithURL:url];
        NSURLResponse* response=nil;
        NSError* error=nil;
        NSData* downloadedData=[NSURLConnection sendSynchronousRequest:request
                                                     returningResponse:&response
                                                                 error:&error];
        if([downloadedData length]>0){
            
        }else{
            
        }
        
    }
}


- (void)viewDidLoad{
    [super viewDidLoad];
    
    NSString *fileToDownload = @"http://www.OReilly.com";
    [NSThread detachNewThreadSelector:@selector(downloadNewFile:) toTarget:self
                           withObject:fileToDownload];
}

讨论:

任何一个iOS应用都由一个或多个线程组成。在iOS中,一个通常有一个视图控制器的应用,会由系统类库创建4或5个线程分配给它。无论你是否使用多线程,都至少会有一个线程创建给你的应用。这个线程就被叫做“主UI线程”。

为了理解线程的价值,我们来做一个实验,假设我们有三个循环:

- (void) firstCounter{
        NSUInteger counter = 0;
        for (counter = 0;counter < 1000;counter++){
            NSLog(@"First Counter = %lu", (unsigned long)counter);
        }
}
- (void) secondCounter{
        NSUInteger counter = 0;
        for (counter = 0;counter < 1000;counter++){
            NSLog(@"Second Counter = %lu", (unsigned long)counter);
        }
}
- (void) thirdCounter{
    NSUInteger counter = 0;
    for (counter = 0;counter < 1000;counter++){
        NSLog(@"Third Counter = %lu", (unsigned long)counter);
    }
}


- (void)viewDidLoad
{
    [super viewDidLoad];
    [self firstCounter];
    [self secondCounter];
    [self thirdCounter];
}

如果我们希望所有的循环计数同时运行呢?当然,我们应该为每个循环分配一个进程。但是我们已经知道了应用载入时都会为我们创建进程,无论我们写了什么代码在应用中,都会在线程中执行。所以我们只需要为其中两个循环计数创建进程,把第三个循环计数留给主线程执行:

- (void) firstCounter{
    
    @autoreleasepool {
   
        NSUInteger counter = 0;
        for (counter = 0;counter < 1000;counter++){
            NSLog(@"First Counter = %lu", (unsigned long)counter);
        }
    }
}
- (void) secondCounter{
    @autoreleasepool {
        NSUInteger counter = 0;
        for (counter = 0;counter < 1000;counter++){
            NSLog(@"Second Counter = %lu", (unsigned long)counter);
        }
    }
}
- (void) thirdCounter{
    NSUInteger counter = 0;
    for (counter = 0;counter < 1000;counter++){
        NSLog(@"Third Counter = %lu", (unsigned long)counter);
    }
}


- (void)viewDidLoad
{
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(firstCounter) toTarget:self
                           withObject:nil];
    [NSThread detachNewThreadSelector:@selector(secondCounter) toTarget:self
                           withObject:nil]; /* Run this on the main thread */
    [self thirdCounter];
}

每个线程都需要一个自动释放池作为线程中创建的第一个对象,如果你不这么做的话,当线程退出时,你在线程中分配的所有对象都会导致内存泄露。为了更好地理解这点,让我们来看下面的代码:

- (void) autoreleaseThread:(id)paramSender{
    NSBundle *mainBundle = [NSBundle mainBundle];
    NSString *filePath = [mainBundle pathForResource:@"MacBookAir"
                                              ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    /* Do something with the image */
    NSLog(@"Image = %@", image);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(autoreleaseThread:)
                             toTarget:self
                           withObject:self];
}
如果你运行这段代码,就会得到如下输出:
 *** __NSAutoreleaseNoPool(): Object 0x5b2c990 of
    class NSCFString autoreleased with no pool in place - just leaking
    *** __NSAutoreleaseNoPool(): Object 0x5b2ca30 of
    class NSPathStore2 autoreleased with no pool in place - just leaking
    *** __NSAutoreleaseNoPool(): Object 0x5b205c0 of
    class NSPathStore2 autoreleased with no pool in place - just leaking
    *** __NSAutoreleaseNoPool(): Object 0x5b2d650 of
    class UIImage autoreleased with no pool in place - just leaking
这段表示我们创建的实例导致了内存泄露,这是因为我们忘记在在线程中创建自动释放池了。下面是正确的代码:
- (void) autoreleaseThread:(id)paramSender{
    @autoreleasepool {
        
    NSBundle *mainBundle = [NSBundle mainBundle];
    NSString *filePath = [mainBundle pathForResource:@"MacBookAir"
                                              ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    /* Do something with the image */
    NSLog(@"Image = %@", image);
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(autoreleaseThread:)
                             toTarget:self
                           withObject:self];
}


7.16 Invoking Background Methods

问题:你希望不用和线程打交道就可以创建线程的方法

解决方法:

使用NSObject的实例方法performSelectorInBackground:withObject即可

- (void) firstCounter{
    
    @autoreleasepool {
        
        NSUInteger counter = 0;
        for (counter = 0;counter < 1000;counter++){
            NSLog(@"First Counter = %lu", (unsigned long)counter);
        }
    }
}
- (void) secondCounter{
    @autoreleasepool {
        NSUInteger counter = 0;
        for (counter = 0;counter < 1000;counter++){
            NSLog(@"Second Counter = %lu", (unsigned long)counter);
        }
    }
}
- (void) thirdCounter{
    NSUInteger counter = 0;
    for (counter = 0;counter < 1000;counter++){
        NSLog(@"Third Counter = %lu", (unsigned long)counter);
    }
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [self performSelectorInBackground:@selector(firstCounter)
                           withObject:nil];
    [self performSelectorInBackground:@selector(secondCounter)
                           withObject:nil];
    [self performSelectorInBackground:@selector(thirdCounter)
                           withObject:nil];
    self.window = [[UIWindow alloc] initWithFrame:
                   [[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible];
    return YES;
}

讨论:performSelectorInBackground:withObject方法在后台为我们创建了一个新的进程,等同于为选择器创建了一个新的进程。我们需要注意,自定义的selector需要有一个自动释放池就像计数器内存环境里的其他线程一样。

7.17 Exiting Thread and Timers

问题:你希望停止一个线程或者计时器,或者避免其重新开始

解决方法:

对于计时器,使用NSTimer的实例方法invalidate即可。对于线程,则使用cancel方法。要避免使用线程的exit方法,因为这之后没有机会对该线程进行清理,你的应用会因此导致内存泄露:

NSThread* thread=/*Get the reference to your thread here*/;
[thread cancel];
NSTimer* timer=/*Get the reference to your timer here*/;
[timer invalidate];

讨论:

计时器的退出十分直接,调用invalidate方法即可。你可以简单地调用计时器的invalidate实例方法。在你调用该方法之后,计时器不会再唤起其他的目标事件了。

而线程的退出稍微复杂一些,当线程睡眠时,调用取消(cancel)方法,线程的循环会将任务执行完成后才会退出。

来看下面这个例子:

- (void) threadEntryPoint{
    @autoreleasepool {
        NSLog(@"Thread Entry Point");
        while ([[NSThread currentThread] isCancelled] == NO){
            [NSThread sleepForTimeInterval:4];
            NSLog(@"Thread Loop");
        }
        NSLog(@"Thread Finished");
    }
}
- (void) stopThread{
    NSLog(@"Cancelling the Thread");
    [self.myThread cancel];
    NSLog(@"Releasing the thread");
    self.myThread = nil;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.myThread = [[NSThread alloc]
                     initWithTarget:self
                     selector:@selector(threadEntryPoint) object:nil];
    
    [self performSelector:@selector(stopThread) withObject:nil
               afterDelay:3.0f];
    
    [self.myThread start];

    self.window = [[UIWindow alloc] initWithFrame:
                   [[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible];
    return YES;
}

输出结果为:

 Thread Entry Point
    Cancelling the Thread
    Releasing the thread
    Thread Loop
    Thread Finished

于是我们就清楚地看到线程在退出前仍要将当前循环执行完成。这种情况可以通过在循环内判断线程是否被取消来避免,改进代码如下:

- (void) threadEntryPoint{
    @autoreleasepool {
        NSLog(@"Thread Entry Point");
        while ([[NSThread currentThread] isCancelled] == NO){
            [NSThread sleepForTimeInterval:4];
            if ([[NSThread currentThread] isCancelled] == NO){
                NSLog(@"Thread Loop");
            }
        }
        NSLog(@"Thread Finished");
    }
}
- (void) stopThread{
    NSLog(@"Cancelling the Thread");
    [self.myThread cancel];
    NSLog(@"Releasing the thread");
    self.myThread = nil;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.myThread = [[NSThread alloc]
                     initWithTarget:self
                     selector:@selector(threadEntryPoint) object:nil];
    
    [self performSelector:@selector(stopThread) withObject:nil
               afterDelay:3.0f];
    
    [self.myThread start];

    self.window = [[UIWindow alloc] initWithFrame:
                   [[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible];
    return YES;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值