IOS并发编程

ios的多线程管理有3种方式

  1. NSThread & Run Loop
  2. NSOperation
  3. GCD

Run Loop

首先我们说一下线程的起用和退出的问题,当我们自己创建一个线程并分配给它活干的时候,它会立刻开始给我们干活,一旦活干完了,它又没有马上找到新活,那么就会立刻退出,这个线程就结束了。注意,这里是它一旦发现自己没活可干,就会马上消失,片刻都不会停留。

这样我们就遇到一个问题,如果我们打算让这个线程做一个延时的任务,或者想让它接受其它的回调命令,或者等待一个点击等非即时性命令,而这个线程是不知道等的,因为它一发现自己没活干,就消失了。这显然不是我们希望的。可能有人会问,主线程不会这样啊,原因就是我们要讨论的主题,主线程默认开启了RunLoop,而我们创建的线程默认是没有开启的。

因此,如果我们要在非主线程执行一些非即时性的事情,就必须手动开启RunLoop,它一旦开启,线程就会开启监听状态,这样线程便不会退出,而是转入休息状态,RunLoop负责把风,一旦发现活来了,就通知线程开始干活。如果我们确认这个线程再也不需要在处理任何非即时性事件时,可以停止RunLoop,这时候线程就再看看手头有没有现活,有继续做,没有就立刻退出。

输入源传递异步消息给相应的处理例程,并调用runUntilDate:方法来退出(在线程里面相关的NSRunLoop对象调用)。定时源则直接传递消息给处理例程,但并不会退出run loop。

(未完待续,这块还是个半吊子。。。。。。)

NSThread

NSThread 比其他两种方式轻量级,但需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁也会有一定的系统开销。

使用NSThread来创建线程有多个方法:

  1. 使用 + (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)anArgument 类方法来生成一个新的线程。
  2. 使用 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 创建一个新的 NSThread 对象,并调用它的 start 方法。
  3. 调用NSObject的+performSelectorInBackground:withObject:方法生成子线程。
  4. 创建一个NSThread子类,然后调用子类实例的start方法。

编写你线程的主体入口点的额外步骤:

  1. ARC这篇文章里面提到了,有3种情况应该自己创建autorelease池,其中一种情况就是创建了子线程。
  2. 子线程应该自行管理好抛出异常,如果不在子线程内部设置异常处理函数,会导致程序直接Crash。
  3. 如果子线程不止一次操作,需要循环处理的话,设置一个Run Loop。

每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。你使用NSThread的threadDictionary方法来检索一个NSMutableDictionary对象,你可以在它里面添加任何线程需要的键。

你创建的任何线程默认的优先级是和你本身线程相同。你可以使用NSThread的 setThreadPriority:类方法来设置当前运行线程的优先级。

退出一个线程推荐的方法是让它从主题入口点正常退出。对于直接杀死线程,这是完全不鼓励的,这样无法保证线程当前使用资源被清理干净了。如果需要在操作中间中断一个线程,建议使用Run Loop的输入源来接收取消消息。

NSOperation

NSOperation本身是抽象基类,我们必须实现子类。Foundation framework提供NSInvocationOperation和NSBlockOperation两个具体子类,你可以直接使用。

所有 operation objects 都支持以下关键特性:

  1. 支持建立基于图的operation objects依赖。可以阻止某个operation 运行,直到它依赖的所有 operation 都已经完成。
  2. 支持可选的 completion block,在 operation 的主任务完成后调用。
  3. 支持应用使用 KVO 通知来监控 operation 的执行状态。
  4. 支持 operation 优先级,从而影响相对的执行顺序。
  5. 支持取消,允许你中止正在执行的任务。

创建一个 NSInvocationOperation 对象并发执行

@implementation MyCustomClass

- (NSOperation*)taskWithData:(id)data {
    NSInvocationOperation* theOp = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(myTaskMethod:) object:data];
    return theOp;
}

// This is the method that does the actual work of the task.
- (void)myTaskMethod:(id)data {
    // Perform the task.
}
@end

创建一个NSBlockOperation 对象

NSBlockOperation* theOp = [NSBlockOperation blockOperationWithBlock: ^{
    NSLog(@"Beginning operation.\n");
    // Do some work.
}];

使用addExecutionBlock:可以添加更多block到这个block operation对象。如果需要顺序地执行 block,你必须直接提交到所需的dispatch queue。

自定义operation需要实现的方法:

  1. 自定义initialization方法:初始化,将operation对象设置为已知状态(必须实现)
  2. 自定义main方法:执行你的任务(必须实现)
  3. main方法中需要调用的其它自定义方法
  4. Accessor方法:设置和访问operation对象的数据
  5. NSCoding协议的方法:允许operation对象archive和unarchive

operation响应取消,调用isCancelled:方法,如果返回YES(表示已取消),则立即退出执行。以下地方可能需要调用 isCancelled:g

  • 在执行任何实际的工作之前
  • 在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次
  • 代码中相对比较容易中止操作的任何地方

Operation对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行。但是如果你希望异步执行操作,你就必须定义operation对象为并发操作来实现。

  • start (必须)所有并发操作都必须覆盖这个方法,以自定义的实现替换默认行为。手动执行一个操作时,你会调用start方法。因此你对这个方法的实现是操作的起点,设置一个线程或其它执行环境,来执行你的任务。你的实现在任何时候都绝对不能调用super
  • main (可选)这个方法通常用来实现operation对象相关联的任务。尽管你可以在start方法中执行任务,使用main来实现任务可以让你的代码更加清晰地分离设置和任务代码。
  • isExecuting isFinished (必须)并发操作负责设置自己的执行环境,并向外部 client 报告执行环境的状态。因此并发操作必须维护某些状态信息,以知道是否正在执行任务,是否已经完成任务。使用这两个方法报告自己的状态。这两个方法的实现必须能够在其它多个线程中同时调用。另外这些方法报告的状态变化时,还需要为相应的key path产生适当的KVO通知。
  • isConcurrent (必须)标识一个操作是否并发 operation,覆盖这个方法并返回 YES。

使用 NSOperation 的addDependency:方法在两个operation对象之间建立依赖关系。表示当前operation 对象将依赖于参数指定的目标 operation 对象。如果你自定义了 operation 对象的行为,就必须在自定义代码中生成适当的 KVO 通知,以确保依赖能够正确地执行。

operation 可以在主任务完成之后执行一个 completion block。你可以使用这个 completion block 来执行任何不属于主任务的工作。

执行Operations有2种方法:

  1. 执行 Operations 最简单的方法是添加到 operation queue
  2. 手动执行 Operation,要求 Operation 已经准备好,isReady返回 YES,此时你才能调用start方法来执行

NSOperation的官方例程

GCD

GCD dispatch queues 是执行任务的强大工具,允许你同步或异步地执行任意代码block。

目前GCD中有三种类型的Dispatch Queue:

  1. Main Queue:关联到主线程的队列,可以使用函数dispatch_get_main_queue()获得,加到这个队列中的工作都会分发到主线程运行。主线程只有一个,因此很明显这个是串行队列,每次运行一个工作。
  2. Global Queue:全局队列是并发队列,又根据优先级细分为高优先级、默认优先级和低优先级三种。通过 dispatch_get_global_queue 加上优先级参数获得这个全局队列,例如 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
  3. 自定义Queue:自己创建一个队列,通过函数 dispatch_queue_create 创建,例如 dispatch_queue_create("com.kiloapp.test", NULL) 。第一个参数是队列的名字,Apple建议使用反DNS型的名字命名,防止重名;第二个参数是创建的queue的类型,iOS 4.3以前只支持串行,即DISPATCH_QUEUE_SERIAL(就是NULL),iOS4.3以后也开始支持并行队列,即参数DISPATCH_QUEUE_CONCURRENT。

添加单个任务到Queue:

  • dispatch_asyncdispatch_async_f 函数异步添加
  • dispatch_syncdispatch_sync_f 函数同步添加
  • dispatch_after 同步延迟添加
ios

Blocks

Blocks是C语言的新拓展,一句话来概括的话,可以理解为“anonymous functions together with automatic (local) variables.” Blocks有点像函数,但是它可以在其它函数或方法中进行声明和定义,同时它还是匿名的(匿名函数),并可以捕获其所在作用域中的变量(闭包特性)。

使用^操作符来来声明一个block变量和指示block文本的开始。Block本身的主体被{}包含着,如下面的例子那样

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

如果你没有显式的给block表达式声明一个返回值,它会自动的从block内容推断出来。如果返回值是推断的,而且参数列表也是void,那么你同样可以省略参数列表的void。如果或者当出现多个返回状态的时候,它们必须是完全匹配的(如果有必要可以使用强制转换)。

block有点像函数,事实上它确实可以像函数一样使用

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

printf("%d", myBlock(3));
// prints "21"

block也可以作为函数的参数

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };

qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
    char *left = *(char **)l;
    char *right = *(char **)r;
    return strncmp(left, right, 1);
});

block有闭包的特性,所以可以捕获所在作用域的变量

void testBlock() {
    int a = 1;
    int b = 2;
    int (^aBlock)(void) = ^ { return a + b; };
    printf("%d\n", aBlock());   // 输出 3
    a = 0;
    printf("%d\n", aBlock());   // 还是输出 3
}

需要注意的是,两次输出的值都为3,即使在第二次输出前我们已经将a的值赋为0。这是因为在定义aBlock时编译器已经对a和b的值作了一个const拷贝(你不能在aBlock中修改a的值)并保存,导致后续外部对a的修改没有影响到aBlock的执行结果。如果想在aBlock中通过引用访问a或者修改a的值,你需要在a的声明前加上一个限定词__block

void testBlock() {
    __block int a = 1;
    int b = 2;
    int (^aBlock)(void) = ^ { return a + b; };
    a = 0;
    printf("%d\n", aBlock());   // 输出 2
}

这样,使用该限定词的变量会通过引用的方式传入Block,使得它的值可以在Block执行后被修改。这样的变量通常是保存在栈中的,但是如果引用该变量的Block被拷贝,它也会随之被拷贝到堆中。使用__block的变量有两个限制,它们不能是可变长的数组,并且它们不能是包含有C99可变长度的数组变量的数据结构.

Blocks 通常代表一个很小、自包的代码片段。因此它们作为封装的工作单元在并 发执行,或在一个集合项上,或当其他操作完成时的回调的时候非常实用。

在引用计数的环境里面,默认情况下当你在block里面引用一个Objective-C对象的时候,该对象会被retain。当你简单的引用了一个对象的实例变量时,它同样被retain。但是被 __block 存储类型修饰符标记的对象变量不会被 retain。在垃圾回收机制里面,如果你同时使用 __weak__block 来标识一个变量,那么该block将不会保证它是一直是有效的。

如果你在实现方法的时候使用了 block,对象的内存管理规则更微妙:

  • 如果你通过引用来访问一个实例变量,self 会被 retain。
  • 如果你通过值来访问一个实例变量,那么变量会被 retain。
ios

ARC

ARC以前的生活

reference counting

在ARC以前,是手动内存管理,但不论是自动的,还是手动的内存管理,reference counting始终是内存管理的一个核心内容。
用办公室的照明管理来类比说明一下reference counting的原理。

按以下规则来安排照明灯的管理

  1. 当某人进入办公室时,办公室是空,则他负责打开灯
  2. 之后有人进入办公室,照明灯继续使用
  3. 当某人离开办公室,他就不再需要照明灯了
  4. 当最后一个人离开了,他关掉照明灯

我们量化的来管理照明灯系统,使用counter来计数

  1. 当某人进入办公室时,办公室是空,counter +1,它由0变成了1,所以打开灯
  2. 当另外的人进来,counter +1,它由1变成2
  3. 当某人离开,counter -1,它由2变成1
  4. 当最后一个人离开,counter变成了0,则关掉灯

Drawing

reference counting的原理跟照明灯的管理是一样的

  1. 打开灯 == 创建(alloc/new/copy/mutableCopy group)一个Objective-C对象并使用它 reference counting = 1
  2. 又进来一个人使用灯 == 取得(retain)Objective-C对象所有权 reference counting = 2
  3. 有一个人离开,不再使用灯 == 放弃(release)Objective-C对象所有权 reference counting = 1
  4. 最后一个人离开,关掉灯 == 丢弃(dealloc)Objective-C对象 reference counting = 0

对于新创建的对象,方法名以alloc/new/copy/mutableCopy group开头的,才拥有这个对象,才有责任负责释放它。

实现对象的 dealloc

NSObject 类定义了一个名为 dealloc 的方法。这个方法在对象无主(没有所有者)的情况下, 当内存回收的时候会由系统自动调用。dealloc 方法的作用就是释放对象的内存,并弃掉它持有的任何资源—-以及它对其他对象的所有权。

- (void)dealloc{
    [_firstName release];
    [super dealloc];
}

手动管理内存的限定符

  • assign 对于NSInteger、CGPoint、C数据类型等,这些直接在栈上开辟的内存,无需管理他们内存计数器,使用assign比较合适
  • retain 对于在堆中开辟的内存,我们需要维护内存的计数器。
  • copy 如果指针A和指针B不想相互牵扯,A管理A的内存,B管理B的内存,copy正是为这个而生。

Autorelease

Autorelease从名字来看,你也许会认为它类似于ARC,但其实不是这样的,它更类似于C语言的局部变量。对于Autorelease,你可以像局部变量一样使用对象,这意味着当执行离开代码块,对象将自动调用release方法。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];

在以上代码的最后,[pool drain]将执行[obj release]。 Cocoa 希望程序中长期存在一个 autorelease 池。如果池不存在,autorelease 的对象就无从 release 了,从而造成内存泄露。当程序中没有 autorelease 池,你的程序还给对象发送 autorelease 消息,这是 Cocoa 会发出一个错误日志。AppKit 和 UIKit 框架自动在每个消息循环的开始都创建一个池(比如鼠标按下事件、触摸事件)并在结尾处销毁这个池。正因为如此,你实际上不需要创建 autorelease 池,甚至不需要知道创建 autorelease 池的代码如何写。下面三种情形下,你却应该使用你自己的 autorelease 池:

  1. 如果你写的程序,不是基于 UI Framwork。例如你写的是一个基于命令行的程序。
  2. 如果你程序中的一个循环,在循环体中创建了大量的临时对象。你可以在循环体内部新建一个 autorelease 池,并在一次循环结束时销毁这些临时对象。这样可以减少你的程序对内存的占用峰值。
  3. 如果你发起了一个 secondary 线程(main 线程之外的线程)。这时你“必须”在线程的最初执行代码中创建 autorelease 池,否则你的程序就内存泄露了。

Autorelease 池常常被称为“嵌套”的,但实际上也可以把这些嵌套的池理解为在一个栈中。当一个对象收到了 autorelease 消息时,又或者这个对象 作为 addObject:方法的参数传递的时候,这个对象被放到了当时这个栈中最顶端的那个池中。

ARC

在工程中使用ARC非常简单:只需要像往常那样编写代码,只不过永远不写retain,releaseautorelease三个关键字就好。 ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做的只不过是在代码编译时为你自动在合适的位置插入releaseautorelease,就如同之前手动内存管理时你所做的那样。因此,至少在效率上ARC机制是不会比手动内存管理弱的,而因为可以在最合适的地方完成reference counting的维护,以及部分优化,使用ARC甚至能比MRC取得更高的运行效率。

ARC下有以下4个限定符

  • strong 跟retain一样,在ARC下,如果对象没有任何strong指针指向,那么就将被销毁
  • weak weak指针指向的对象reference counting不会增加,对象无strong指针指向时,对象会被销毁,在ARC机制作用下,所有指向这个对象的weak指针将被置为nil。delegate、outlet多使用weak
  • unsafe_unretained 这就是原来的assign。当需要支持iOS4时需要用到这个关键字
  • autoreleasing 这个就等于手动内存管理的Autorelease

上文Autorelease池的代码在ARC下就转换为:

@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

桥接

ARC只适用于NSObject,底层对象依旧需要进行手动内存管理。在ARC中,编译器需要知道这些底层对象指针应该由谁来负责释放,这时就要进行桥接处理。

  • __bridge 只做类型转换,不改变对象所有权,是我们最常用的转换符。
  • __bridge_retained 将Objective-C对象转换为Core Foundation对象,把对象所有权桥接给Core Foundation对象,同时剥夺ARC的管理权,后续需要开发者使用CFRelease或者相关方法手动来释放对象。
  • __bridge_transfer 将非Objective-C对象转换为Objective-C对象,同时将对象的管理权交给ARC,开发者无需手动管理内存。

从OC转CF,ARC管理内存

NSString *aNSString = [[NSString alloc]initWithFormat:@"test"];
CFStringRef aCFString = (__bridge CFStringRef)aNSString;
(void)aNSString

从CF转OC,需要开发者手动释放,不归ARC管(void* id的转换)

CFStringRef aCFString = CFStringCreateWithCString(NULL, "test", kCFStringEncodingASCII);  
NSString *aNSString = (__bridge NSString *)aCFString;    
(void)aNSString;    
CFRelease(aCFString); 
ios

ViewController生命周期

编程实现ViewController初始化时,创建一个自定义的初始化方法,调用父类的init方法,然后进行类的特定初始化。一般情况下,初始化方法不写过于复杂的。

ViewController加载View对象的过程:

  1. ViewController调用loadView方法,loadView的默认实现执行以下两个方法之一
    • 如果ViewController与storyboard关联,它会从storyboard加载
    • 如果ViewController不与storyboard关联,则会创建一个空UIView对象分配给View属性
  2. ViewController调用viewDidLoad方法,执行子类的额外加载任务

Drawing

编程创建View,而不使用storyboard,应该重载loadView方法。在方法中实现以下步骤:

  1. 创建root view对象
    • root view应该包含ViewController关联的所有其他view。一般情况下,root view的大小应该充满整个屏幕,但也可以根据需要调整。“Resizing the View Controller’s Views.”
    • 你可以使用通用的UIView对象,也可以使用自定义的UIView,以及任何view的扩展来填充屏幕
  2. 创建subviews添加到root view
    • 对于每个subview都应该创建并初始化
    • 添加到父view使用addSubview:方法
  3. 如果你使用auto layout,对于你创建的每个view设置足够的约束,否则,实现viewWillLayoutSubviewsviewDidLayoutSubviews来调整subview的大小
  4. 分配root view给ViewController的view属性

编程创建view示例

- (void)loadView
{
    CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];
    UIView *contentView = [[UIView alloc] initWithFrame:applicationFrame];
    contentView.backgroundColor = [UIColor blackColor];
    self.view = contentView;

    levelView = [[LevelView alloc] initWithFrame:applicationFrame       viewController:self];
    [self.view addSubview:levelView];
}

有效的内存管理:

  • Initialization methods 为ViewController分配关键的数据结构
  • loadView 创建你的view对象,如果使用storyboard,则不需要重载此方法
  • Custom properties and methods 创建自定义对象
  • viewDidLoad 分配或者加载view所需数据
  • didReceiveMemoryWarning 使用这个方法来释放所有ViewController相关联的非必要对象,ios6以后,也可以在这个方法释放view
  • dealloc 重载这个方法只执行ViewController的最后清理,实例变量和属性对象被自动释放,不需要显式地释放它们

对于ios6以后的版本,按需要释放ViewController的view对象。对于释放view对象来获取内存空间的必要性,应该由开发者来判断。

释放ViewController不显示的view

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Add code to clean up any of your own resources that are no longer necessary.
    if ([self.view window] == nil)
    {
        // Add code to preserve data stored in the views that might be
        // needed later.

        // Add code to clean up other strong references to the view in
        // the view hierarchy.
        self.view = nil;
    }
}

整个ViewController的生命周期中,其余方法调用如图所示

ios

应用生命周期

ios应用有5种状态:

  • Not Running(非运行状态)应用未运行
  • Inactive(前台非活动状态)应用正在进入前台,此时不接受事件处理
  • Active(前台活动状态)前台正常运行状态
  • Background(后台状态)不存在后台run loop,则进入Suspended状态
  • Suspended(挂起状态)不执行代码,内存不够时,应用将终止

整个应用的生命周期如图所示

Drawing

总结:

  • didFinishLaunching整个生命周期只会调用一次。
  • 应用能进行后台运行,首先SDK必须在4.0以上的版本,其次得在info.plist中不禁用后台。
  • 内存不足情况下,以及用户自行关闭应用的情况下,不会执行applicationWillTerminate:,所以必须要在applicationWillResignActive事件里保存数据。
  • becomeActiveresignActive配对操作进行UI数据等的恢复。
  • enterBackgroundenterForeground配对操作进行用户数据等的恢复。
  • 在以前,当app被按home键退出后,app仅有最多5秒钟的时候做一些保存或清理资源的工作。但是应用可以调用UIApplication的beginBackgroundTaskWithExpirationHandler方法,让app最多有10分钟的时间在后台长久运行。这个时间可以用来做清理本地缓存,发送统计数据等工作。
ios