iOS 6 开发进阶与实践----第14章 保持界面响应

来源:互联网 发布:js 正则匹配 编辑:程序博客网 时间:2024/06/06 17:08

14保持界面响应

Grand Central DispatchGCD)是一种技术,它允许应用程序充分使用现代计算机所具有的多处理核心甚至多处理器。如果你在GCD发布之前使用了操作队列,那么当你现在重新再iOS下进行编译时,程序将自动获得GCD的功能。如果你使用另外一种形式的并发,比如线程,而非操作队列,那么应用程序就无法自动使用GCD功能了。


14.1 探索并发问题

每一个应用至少都有一个用于执行的线程,并且应用程序的主运行循环在此进行。

TextField可以通过设置键盘类型,来限制用户只能输入数字等。


14.3

    Timer并不保证精确地按照指定的时间间隔执行。由于运行循环函数的工作原理,没有办法保证Timer能够在精确地时刻运行。Timer在执行一次之后,指定的时间过去之前,是不会再次执行的。这意味着他不会比预计的时间间隔早,而只有可能会晚。通常情况下,实际的时间间隔只比预期玩若干秒,但也不能完全依赖于此。如果主循环上有长时间运行的方法,那么运行循环无法在预计的时刻启动Timer,直到主循环处理完毕。


NSTimer测试代码:

- (void)viewDidLoad {

    [super viewDidLoad];

    [self timerTest];

}


- (void)timerTest

{

    //self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];

    [self.timer fire];

    for (int i=0; i<10000; i++) {

        NSLog(@"%f",sqrt(i));

    }

}


- (void)test

{

    NSLog(@"---------------------------------------------------test");

}



2016-06-27 16:00:41.269 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:41.270 GCD[1056:47916] 0.000000

2016-06-27 16:00:41.270 GCD[1056:47916] 1.000000

2016-06-27 16:00:41.270 GCD[1056:47916] 1.414214

2016-06-27 16:00:41.270 GCD[1056:47916] 1.732051

———

2016-06-27 16:00:44.697 GCD[1056:47916] 99.974997

2016-06-27 16:00:44.698 GCD[1056:47916] 99.979998

2016-06-27 16:00:44.698 GCD[1056:47916] 99.984999

2016-06-27 16:00:44.698 GCD[1056:47916] 99.989999

2016-06-27 16:00:44.698 GCD[1056:47916] 99.995000

2016-06-27 16:00:44.704 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:45.592 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:46.592 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:47.592 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:48.591 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:49.591 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:50.591 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:51.592 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:52.591 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:53.591 GCD[1056:47916] ---------------------------------------------------test

2016-06-27 16:00:54.591 GCD[1056:47916] ---------------------------------------------------test


啥话不用说,显然主线程的阻塞,影响了NSTimer的正常运行。


示例2

- (void)viewDidLoad {

    [super viewDidLoad];

    [self timerTest];

}


- (void)timerTest

{

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];

    [self.timer fire];

    for (int i=0; i<10000; i++) {

        //NSLog(@"%f",sqrt(i));

    }

}


- (void)test

{

    NSLog(@"---------------------------------------------------test");

}


- (void)applicationWillResignActive:(NSNotification*)noti

{

    NSLog(@"%@",[NSThread currentThread]);

}


2016-06-27 16:09:25.908 GCD[1146:52531] ---------------------------------------------------test

test只执行了了一次,显然有问题。


示例3

- (void)timerTest

{

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];

    NSRunLoop *loop = [NSRunLoop mainRunLoop];

    [loop addTimer:self.timer forMode:NSDefaultRunLoopMode];

    [self.timer fire];

    for (int i=0; i<10000; i++) {

        //NSLog(@"%f",sqrt(i));

    }

}


此时才能正常运行。



示例4

- (void)timerTest

{

    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];

    NSRunLoop *loop = [NSRunLoop mainRunLoop];

    [loop addTimer:self.timer forMode:NSDefaultRunLoopMode];

    [self.timer fire];

    for (int i=0; i<10000; i++) {

        NSLog(@"%f",sqrt(i));

    }

}


此时会和示例1相同。


如果使用

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

创建一个Timer并准备好启动时,需要获取一个运行循环的引用,然后向其添加Timer

    NSRunLoop *loop = [NSRunLoop mainRunLoop];

    [loop addTimer:self.timer forMode:NSDefaultRunLoopMode];

    当把Timer放入调度器后,运行循环将一直持有这个Timer知道停止它。

    如果你想在创建Timer的同时将其放入运行循环,从而省去上面两行代码,那么可以使用工厂方法:+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

此时运行循环会持有Timer,不用担心他会被释放。


14.3.2 停止Timer

当你不再需要某个Timer时,可以通过调用实例的invalidate方法将其从运行循环中移除。调用invalidate后,Timer将停止它的下一个启动,并将其从运行循环中删除,这将释放这个Timer和其所占有的内存空间(deallocated)。


14.4 使用Timer修改Stalled应用程序

下面有两个测试代码


- (void)viewDidAppear:(BOOL)animated

{

    [super viewDidAppear:animated];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        for (float i=0; i<10000; i++) {

            NSLog(@"%f",sqrt(i));

            dispatch_async(dispatch_get_main_queue(), ^{

                self.processView.progress = i/10000.0;

            });

        }

    });

}


上面的代码,进度条会按照预期一点点前进,但下面的代码会在计算完成后直接由0%显示到100%


- (void)viewDidAppear:(BOOL)animated

{

    [super viewDidAppear:animated];

 

    for (float i=0; i<10000; i++) {

        NSLog(@"%f",sqrt(i));

        self.processView.progress = i/10000.0;

    }

}

下面的一样不好用,viewDidAppear已经在主线程了

- (void)viewDidAppear:(BOOL)animated

{

    [super viewDidAppear:animated];

    

    for (float i=0; i<10000; i++) {

        NSLog(@"%f",sqrt(i));

        dispatch_async(dispatch_get_main_queue(), ^{

            self.processView.progress = i/10000.0;

        });

    }

}


14.5 操作队列和并发

14.5.1 线程

主线程,CocoaTouch应用程序中,主线程包含应用程序的主运行循环,他负责处理输入和更新用户界面。


每一个线程都具有对应用程序内存相同的访问性。正式这种特性,使得多个线程共同享用相同的内存,并且由于没有办法预测它们各自的执行调度情况使得处理并发时会遇到一系列问题。

1.资源竞争

    使用操作队列并不能消除资源竞争问题,可以考虑使用互斥锁(MutexMutualExclusion Lock)解决问题。

2.互斥锁与@synchronized


常见的互斥体有两种:

l  非递归互斥体(nonrecursive mutex——如果当前拥有互斥体的线程在没有首先释放它的情况下,试图再次获得它,就会导致死锁或失败;


l  递归互斥体(recursive mutex——拥有互斥体的线程可以多次获得它而不会产生自死锁,只要这个线程最终以相同次数释放这个互斥体即可。



void thread_fuc(void*)

{

  mutex.Lock();

  {

     mutex.Lock();

     {

     //....

     }

     mutex.Unlock();

  }

  mutex.Unlock();

}


NSRecursiveLock



2. 互斥锁与@synchronized

@synchronize(self){

    xxxxxx……

}

注意@synchronized关键词后面的括号内有一个值—self。这个参数被称为互斥信号,或者直接称为Mutex。它使用Mutex来检查是否有其他线程正在使用。


@synchronized是一种被称为递归互斥的机制,也就是说同步的代码段可以安全的调用另一个同步的代码段,只要两个代码段公用同一个Mutex

但是,如果他们没有使用相同的Mutex,就不要从一个同步代码段调用另一个同步代码段。因为这样会有死锁的风险。



3.原子性和线程安全

nonatomic下的getter

-NSMutableString *) foo

{

    return foo;

}


atomic下的版本

- (NSMutableString*)foo

{

    NSString *ret;

    @synchronized(self)

    {

        ret = foo;

    }

    return ret;

}


线程安全和原子性是两个不同的感念,对属性使用atomic并不意味着线程安全。在某些简单的情况下,使用atomic确实能够实现线程安全,单线程安全并不是对象级别的特性。


4. 死锁

尽量不要从一个同步代码段调用另一个使用不同Mutex的同步代码段,这样有可能造成死锁。


5.休眠

[NSThread sleepForTimeInterval:2.5];

或者

[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.5]];

注意,你永远不应该在主线程上使用这两个休眠方法。因为主线程是进行事件处理并更新用户界面的唯一线程。如果你让主线程休眠,那么界面肯定会终止响应。


14.5.2 操作

操作是操作队列所管理的包含一系列指令的对象。

不同的线程不能共享相同的AutoreleasePool。操作需要运行在不同的线程上,所以他们无法使用Main线程的AutoreleasePool,就需要分配一个新的。

下面将展示如何实现一个NSOperation子类。

第一要将代码包含在@try结构中,这样可以捕获任何异常。操作的main方法不抛出任何异常是非常重要的,不然会造成崩溃,

下面是NSOperation子类的main方法框架。

- (void)main{

    @try{

        @autoreleasepool{

            // Do work here…

        }

        @catch(NSException *e){

            // Important that we don’t re-throw exception here

            NSLog(@“Exception:%@”,e);

        }

    }

}

1.操作依赖性

如:

MyOperation *firstOperation = [[MyOperation alloc] init];

MyOperation *secondOperation = [[MyOperation alloc] init];

[secondOperation addDependency: firstOperation];

……

如上示例,即使firstOperationsecondOperation同时被添加进了队列中,他们也不会同时执行,及时队列拥有足够的线程资源。因为firstOperationsecondOperation的依赖,所以secondOperationfirstOperation执行完之前是不会执行的。


2.操作优先级

每一个操作都有优先级,可以使用setQueuePriority:方法设置。

NSOperationQueuePriorityVeryLow/Low/Normal/Hight/VeryHigh

虽然高优先级的操作会在低优先级的操作之前执行,但如果操作还没有准备好,则不会执行。在所有已经准备好的操作中,具有高优先级的操作将被选中首先执行。


3.其他操作状态

isCancelled属性,确定一个操作是否被取消。操作的main方法中的代码应该周期性地检查isCancelled属性,以判断操作是否已经被取消。如果是,那么main方法应该立即停止运行并返回,这样操作将被终止。

isExecuting,如果操作的main方法当前正在被执行,那么isExecuting属性将返回YES。如果返回NO,则意味着由于某种原因操作还没有启动。

isFinished,当操作的main方法返回后,将使isFinished属性变为YES,这样它将从队列中被移除。


4.取消操作

你可以通过调用cancel方法取消操作,如下所示。

[firstOperation cancel];

这样,操作的isCanceled会被设为YES。剩下的就需要操作的main方法来处理了。调用cancel只会修改isCancelled的值,而操作被强制终止的原因在于main方法对此进行了处理,当他检查到这个属性的值发生变化后,将会返回并终止执行。

操作的取消是在操作级别本身被处理而非操作队列这一点,在开始的时候会导致一些看起来错误的操作。如果队列中的操作还没有开始执行就被取消了,那么操作会列在队列中。因为调用挂起操作的cancel方法并不会是操作从队列中移除,操作队列也没有提供任何一种机制来移除操作。取消后的操作直到完成执行之前都不会被移除,他会一直等待直到在执行的时候发现被取消后才会返回,从而真正的从队列中移除。


14.5.3 操作队列

1.将操作添加到队列

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

[queue addOperation:newOperation];

无序添加任何其他代码,就可以直接将操作放入队列中。当操作被添加到队列以后,只要有可用的线程并且准备好执行后会立即开始执行。甚至当你仍然在添加其它操作是会开始执行。操作队列会基于可用的硬件资源来设置默认的线程数量。

2.设置最大并发操作数

[queue setMaxConcurrentOperationCount:X];

也可以设置基于硬件资源重置操作的最大数量

[queue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];

3.挂起队列

操作队列可以被暂停(或者叫做挂起)。这样,他将停止执行新的操作。除非取消,不然正在执行的操作将会继续运行,不过一旦队列被挂起,那么新的操作将不会开始执行。


14.6 使用操作队列优化Stalled应用程序



0 0
原创粉丝点击