本文是笔者自己自学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: ,那么这个计时器就会成为一个被已定时的计时器,将会启动你要求的事件。一个已被定时了的计时器是被添加了一个运行循环的。为了使计时器可以唤起目标事件,我们必须按照一个运行循环为计时器进行定时。这一点会在之后的例子中进一步说明。
- 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;
}