小程故事


  • 首页

  • 归档

iOS 多线程 GCD (四)

发表于 2018-08-20

GCD信号量:dispatch_semaphore

信号量函数:

1
2
3
4
5
6
7
8
//创建信号量,设置了信号量的处置,如果小于0会返回NULL
dispatch_semaphore_create(信号量值);

//等待降低信号量
dispatch_semaphore_wait(信号量,等待时间);

//提高信号量
dispatch_semaphore_signal(信号量);

Dispatch Semaphore 在实际的开发中主要用于:保证线程同步,将异步执行任务,转换为同步执行任务;保证线程安全,为线程加锁。

iOS 多线程 GCD (三)

发表于 2018-08-20

GCD 队列组:dispatch_group

有些时候需要分别异步执行多个耗时的操作,等到耗时的操作全部完成之后,再回到主线程执行一些任务,这个时候就会用到 GCD 队列组。

dispatch_group_notify

监听 group 中任务的完成状态,当所有的任务都执行完毕后,追加任务到 group 中,并且执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"任务一完成");
});

dispatch_group_async(group, queue, ^{
NSLog(@"任务二完成");
});

dispatch_group_async(group, queue, ^{
NSLog(@"任务三完成");
});
//在分组的所有任务完成后触发
dispatch_group_notify(group, queue, ^{
NSLog(@"所有任务完成");
});

但是网络请求是步的耗时的,上述的方法并不适用于多个网络请求的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"请求一");
});
});

dispatch_group_async(group, queue, ^{
NSLog(@"任务二完成");
});

dispatch_group_async(group, queue, ^{
NSLog(@"任务三完成");
});
//在分组的所有任务完成后触发
dispatch_group_notify(group, queue, ^{
NSLog(@"所有任务完成");
});

此时输出”所有任务完成”之后,才会输出”请求一”。关于多个网络请求可以使用 dispatch_group_enter 和 dispatch_group_leave

dispatch_group_enter和dispatch_group_leave

dispatch_group_enter 表示一个任务追加到 group,执行一次,相当于 group 中未执行完毕的任务数+1;

dispatch_group_leave 表示一个任务离开了 group,执行一次,相当于 group 中未执行完毕的任务数-1;

当group中未执行完毕的任务数为0时,执行追加到 dispatch_group_notify 中的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"任务1");
dispatch_group_leave(group);
});
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"任务2");
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
NSLog(@"任务3");
dispatch_group_leave(group);
});

dispatch_group_notify(group, queue, ^{
NSLog(@"完成");
});

dispatch_group_wait

dispatch_group_wait 同 dispatch_group_notify 的功能类似,在 group 上的任务完成前,dispatch_group_wait 会阻塞当前线程,所以不能放在主线程中调用。当 group 上的任务完成,或者等待的时间超过了设置的超时时间就会结束等待。

1
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

iOS 多线程 GCD (二)

发表于 2018-08-20

GCD 的一些其他方法

GCD 栅栏方法: dispatch_barrier_async

dispatch_barrier_async 起到的是一个栅栏的作用,它等待所有位 于dispatch_barrier_async 函数之前的操作执行完毕之后才执行,在dispatch_barrier_async 函数执行完毕之后,dispatch_barrier_async 之后的操作才会得到执行。dispatch_barrier_async 函数同 dispatch_queue_create 函数的并发队列配合使用。

GCD 延时执行方法: dispatch_after

dispatch_after 是用来延迟执行的GCD方法,这个函数并不是在指定时间后执行处理,而是在指定的时间将处理追加到 dispatch_queue。

1
2
3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

});

GCD 只执行一次:dispatch_once

在创建单例或者有整个程序运行过程中只执行一次的代码时,就需要使用 GCD 的 dispatch_once 函数。dispatch_once 保证在程序运行过程中,代码只被执行一次,即使是在多线程的环境下,也可以保证线程安全。

1
2
3
4
5
6
-(void)once{
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
//只执行一次的代码
});
}

单例的创建:

1
2
3
4
5
6
7
8
9
+(instancetype)sharedInstance{
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
sharedInstance = [[self alloc] init];
});

return sharedInstance;
}

GCD 快速迭代:dispatch_apply

dispatch_apply 函数按照指定的次数将指定的 block 追加到指定的 dispatch_queue 中,并且等待全部的处理执行结束。

1
2
3
4
5
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_apply(10, queue, ^(size_t index){
NSLog(@"%zu", index);
});
NSLog(@"Done");

由于是在 Global Dispatch Queue 中执行,所以各个处理的执行时间不定,但是 done 一定会在最后的位置输出,因为 dispatch_apply 函数会等待所有的处理结束。

iOS 多线程 GCD (一)

发表于 2018-08-16

GCD

GCD(Grand Central Dispatch) 是 Apple 开发的一个多核变编程的比较新的解决方法。是一个在线程池模式的基础上执行的并发任务。可用于多核的并行计算;可以自动利用更多的 CPU 内核;可以自动管理线程的生命周期,比如创建线程、调度任务、销毁线程,程序员只要告诉 GCD 要执行什么任务,不需要操心线程的管理。

任务和队列

在 GCD 中有两个概念:任务和队列。

任务

任务就是要在线程中执行的那段代码,在 GCD 中主要讲这段任务代码放到block中。
任务的执行主要有两种方式:同步执行 (sync) 和异步执行 (async)。这两者的主要区别是:是否需要等待队列中的任务执行结束,时候有开启新的线程的能力。

同步执行(sync):

同步添加任务到指定的队列,要等待队列中的其他任务执行完毕,才能够执行。只能在当前的线程中执行任务,不具备开启新的线程的能力。

异步执行(async):

异步添加任务到指定的队列,不用等待,就可以执行任务。可以在新的线程中执行任务,具备开启新的线程的能力。

需要注意的是:异步执行虽然具备开启新的线程的能力,但是不一定会开启新的线程。这还跟任务添加到的指定的队列的类型有关系。

队列

这里的队列是指用来存放任务的队列,新的任务总是被插入到队列的末尾,读取任务的时候总是从队列的头部开始。每次读取一个任务,就会从队列中释放掉一个任务。

在 GCD 中有两种队列:串行队列和并发队列。它们都遵循先进先出的原则。主要区别是执行顺序不同,开启的线程数不同。

串行队列

串行队列 (Serial Dispatch Queue) 每次只执行一个任务,任务一个接着一个的执行。只开启一个线程,一个任务执行完毕后,再开始执行下一个任务。

并发队列

并发队列 (Concurrent Dispatch Queue) 可以让多个任务并发(同时)执行。可以开启多个线程,同时执行任务。
并发队列的并发功能只在异步函数 (dispatch_async) 下才是有效的。

GCD 的使用

GCD 的使用主要分为两个步骤:

  1. 创建队列(串行队列或者是并发队列);
  2. 将任务追加到队列中,系统根据任务类型执行任务(同步执行或者异步执行)

队列的创建及获取

可以使用 dispatch_queue_create 来创建队列,需要传入两个参数,第一个参数是表示队列的唯一标识,可以为空;第二个参数用来标识是串行队列还是并发队列:

1
2
3
4
5
6

//串行队列
dispatch_queue_t queue = dispatch_queue_create("queueSerial", DISPATCH_QUEUE_SERIAL);

//并行队列
dispatch_queue_t queue = dispatch_queue_create("queueConcurrent", DISPATCH_QUEUE_CONCURRENT);

主队列(Main Dispatch Queue)

GCD提供了一种特殊的串行队列:主队列 (Main Dispatch Queue)。所有在主队列中的任务都会在主线程中执行。

1
2
//获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();

全局并发队列(Global Dispatch Queue)

GCD默认提供了全局并发队列 (Glonal Dispatch Queue)。dispatch_get_global_queue 可以获取全局并发队列,需要传入两个参数。第一个参数标识队列的优先级,一般可用 DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时还没有实际的用途,可用0.

1
2
//获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

任务的创建

1
2
3
4
5
6
7
8
9
//创建同步执行任务
dispatch_sync(queue, ^{
//任务代码
});

//创建异步执行任务
dispatch_async(queue, ^{
//任务代码
});

任务有同步执行和异步执行,队列也有串行队列和并发队列,在串行队列中又有比较特殊的主队列,这样就会出现6种不同的组合方式:

同步执行 + 并发队列

没有开启新的线程,串行执行任务。
虽然并发队列可以开启多个线程,同时执行多个任务。但是同步执行的任务本身不能创建新的线程,只有当前的一个线程,所以也就不存在并发了。而且当前的线程只有等待当前的队列中正在执行的任务执行完毕之后才能继续顺序的执行接下来的任务。

同步执行 + 串行队列

没有开启新的线程,串行执行任务。

同步执行 + 主队列

没有开启新的线程,串行执行任务

异步执行 + 并发队列

有开启新的线程,并发执行任务。

异步执行 + 串行队列

有开启新的线程(1条),串行执行任务

异步执行 + 主队列

没有开启新的线程,串行执行任务

GCD 线程间的通信

在 iOS 开发中,会将UI刷新(点击、滚动、拖拽等)放在主线程,其他比较耗时的操作(图片下载、文件上传等)放在其他线程。在其他线程中完成了耗时操作之后需要再次返回主线程,此时就需要线程见得通信了。

1
2
3
4
5
6
7
8
9
10

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0) , ^{
在这里完成一些耗时操作

//回到主线程
dispatch_async(dispatch_get_main_queue() , ^{
刷新UI的一些操作
});

});

iOS 内存管理方式

发表于 2018-08-13

对于 iOS 的内存管理方式,大家一般都是只知其然而不知其所以然。下面就对 iOS 的内存管理做一个简单的总结。

iOS 内存管理的基本原则

iOS 的内存管理主要遵循以下的几个基本原则,这个原则无论在 ARC 时代还是在 MRC 时代都适用的:

  1. 自己生成的对象,自己持有;
  2. 不是自己生成的对象,自己也可以持有;
  3. 自己不再需要持有对象时,进行释放;
  4. 不能释放自己不持有的对象。

自己生成的对象,自己持有

在 iOS 内存管理中有这样的四个关键字: alloc、new、copy、mutableCopy,如果自身通过这几个关键字产生了对象,那么自身就持有了这个对象:

1
2
id obj = [[NSObject alloc] init];
id obj = [NSObject new];

使用了 alloc 或者 new 分配了内存,obj 指向了这个对象,这个对象本身的引用计数是1,不需要再进行 retain 操作。

不是自己生成的对象,自己也可以持有

1
2
id obj = [NSMutableArray array];
[obj retain];

NSMutableArray 通过类方法 array 产生了对象,这个对象不属于 obj 自身产生的。所以,这个时候需要使用 retain 方法使对象的引用计数+1,此时尽管这个对象不是 obj 自身产生的,但是它也可以持有这个对象了。

自己不再需要持有对象时,进行释放

1
2
3
4
id obj = [NSMutableArray array];
[obj retain];

[obj release];

当 obj 不需要再继续持有对象的时候,就可以发送 release 消息了。

不能释放自己不持有的对象

1
2
3
4
5
id obj = [[NSObject alloc] init];

[obj release];

[obj release];

释放一个已经被释放的对象是不允许的。

1
2
id obj1 = [obj object];
[obj1 release];

obj1 在没有进行 retain 操作的情况下进行了 release 操作,使得 obj 持有的对象被释放了,这样会造成野指针,也是不允许的。

引用计数

iOS 对于引用计数的管理是通过一个散列表来完成的。这个散列表的键值是对象的内存块地址的散列值。在操作引用计数器时,就是对引用计数的散列表进行操作:只要获取到这个散列表的地址和相应的对象的内存地址,就可以通过对象的内存地址在散列表中进行索引找到引用计数的值,然后根据用户的操作返回引用计数器,或者对引用计数器进行+1或者-1的操作。

Objective-C nil,Nil,NULL 和 NSNull 的区别

发表于 2018-08-13

在 OC 中有几种空类型的写法,他们之间是有一定的区别的

nil: 对象为空。

(id)0, literal null value for Objective-C objects

定义某一个实例对象的值为空:

1
2
3
4
5
6
7
NSObject * obj = nil;
if(nil == obj){
NSLog(@"对象为空");
}
else{
NSLog(@"对象不为空");
}

Nil: 类为空

(Class)0, literal null value for Objective-C classes

定义某一个类是空的:

1
2
Class oneClass = Nil;
Class anotherClass = [NSString class];

NULL: 基本数据类型指针为空

(void *)0, literal null value for C pointers

C 语言的各种数据类型的空指针:

1
2
3
int * pointerToInt = NULL;
char * pointerToChar = NULL;
struct TreeNode * root = NULL;

NSNull

[NSNull null], singleton object used to represent null

集合对象不能用 nil 来作为具体的一个值,所以用一个特定的对象 NSnull 来表示:

1
2
3
NSMutableDictionary * dic = [NSMutableDictionary dictionary];
dic[@"oneKey"] = [NSNull null];
NSLog(@"keys : %@", [dic allKeys]);

block 中的循环引用问题

发表于 2018-08-10

block 的使用,为程序的开发带来了很大的方便,但是与此同时,block 带来的循环引用问题也是要注意的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface MyBlockClass : NSObject

@property (nonatomic, copy) void (^block)();

@end

@implementation MyBlockClass

-(void)methodBlock{
self.block = ^{
[self methodOne];
};
}

@end

上面的代码生命了一个 block 属性,使得 self 对 block 有一个强引用。在 block 内又对 self 进行了强引用,形成一个闭环,即循环引用。这样内存不能被释放,就会造成内存泄漏。解决这样的循环引用通常是声明一个 weakSelf,更改后的代码:

1
2
3
4
5
6
-(void)methodBlock{
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf methodOne];
};
}

这样 block 对 self 的引用就是一个弱引用关系,这样就打破了循环引用的闭环。

虽然解决了循环引用的问题,但是还有一个问题值得注意:

1
2
3
4
5
6
7
-(void)methodBlock{
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf methodOne];
[weakSelf methodTwo];
};
}

在并发执行的时候 block 是可以被抢占的,对 weakSelf 指针的调用时序不同也会导致不同的结果,在特定的时序下 weakSelf 可能会变成 nil。为避免这个问题,采用 __strong 的方式进行避免,更改后的代码如下:

1
2
3
4
5
6
7
8
-(void)methodBlock{
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[weakSelf methodOne];
[weakSelf methodTwo];
};
}

这样就算是 block 被抢占,strongSelf 还是非 nil 的。
weakself 是为了 block 不持有 self,避免循环引用;strongSelf 是因为一旦进入 block 执行,就不允许 self 在这个执行过程中释放。block 执行完成,这个 strongSelf 会自动释放,没有循环引用问题。

C 语言的内存模型

发表于 2018-08-10
  1. 栈区(stack):

    栈区主要存放函数的参数值、局部变量的值,是由编译器进行自动的分配和释放的,通常在函数执行结束后就随之释放了,这种操作方式类似于数据结构中的栈。

  2. 堆区(heap):

    堆区就是通过new、malloc、realloc分配的内存块,编译器不管理他们的释放,有应用程序去负责释放。如果应用程序没有释放掉,操作系统会自动回收。

  3. 静态区:

    全局变量和静态变量是存储在静态区的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。在程序结束后,系统自动释放。

  4. 常量区:

    常量存放在常量区,不允许修改。

  5. 代码区:

    代码区存放函数的二进制代码。

声明 block 属性的时候为什么用 copy

发表于 2018-08-09

现在 iOS 开发中 block 横行,但是在使用的时候还是有很多要注意的。虽然一直在使用 block,但是一直有一个疑问:为什么声明 block 属性的时候要用 copy?

1
@property (nonatomic, copy) void (^blockName)(void);

block 的3种类型

全局块(_NSConcreteGlobalBlock)

全局快是存储在静态区的,这个静态区也叫做全局区,相当于 Objective-C 中的单例。

栈块(_NSConcreteStackBlock)

栈块是存储在栈区的,超出作用域就会被销毁。

堆块(_NSConcreteMallocBlock)

堆块会存储在堆区中,它是一个带有引用计数的对象,需要自行管理其内存。

判断 block 的存储位置

block 不访问外界变量,这个外界变量包括栈中和堆中的变量;

block 既不在栈中也不在堆中,这样就是全局块,在 ARC 和 MRC 下都是如此。

block访问外界变量

  1. MRC 环境下:访问外界变量的 block 默认是存储在栈中;
  2. ARC 环境下:访问外界变量的 block 默认存放在堆中;实际上是会先放在栈区,在ARC的情况下自动拷贝到堆区,自动释放。

使用 copy 修饰符的作用就是将 block 从栈区拷贝到堆区。复制到堆区主要是为了保存 block 的状态,延长它的生命周期,如果 block 在栈上的话,其所属的变量作用域一旦结束,这个 block 就会被释放掉,block 中的 __block 变量也会被释放掉。为防止 block 在其变量作用域结束之后被释放掉,将其复制到堆中。

关于 UITableView reloadSections 引起的崩溃问题

发表于 2018-08-06

在 iOS 开发过程中,UITableview 是一个经常被用到的组件之一。小白我原来自以为对他老人家已经是非常熟悉了。但是种种打脸情况的出现告诉我,还是太年轻。

使用 UITableView 的 reloadSections,很容易引起 NSInternalInconsistencyException 这样的崩溃,每次都快把我整奔溃了。为什么呢?

当 UITableView 的数据源发生变化的时候,一般会调用 reloadData 方法或者 reloadSections:withRowAnimation: 来进行 UITableview 的 UI 刷新。然而在有些时候并不需要刷新整个 UITableView,这个时候我们往往采用后者进行局部的刷新。但此时却有这样的一个隐患:

在调用 reloadSections:withRowAnimation: 方法的时候,UITableView 还会对其他 section 进行校验,此时如果发现 UITableView 记录的某个 section 的 row 的数量和 [dataSource tableView:numberOfRowsInSection] 方法返回的有差异的时候,就会抛出一个 NSInternalInconsistencyException 异常。

我的理解就是:当某个 section 所对应的数据源促使该 section 的 row 发生变化的时候,确没有对该 section 进行刷新的话,就会抛出异常。

123

汤圆红豆酥

22 日志
© 2019 汤圆红豆酥
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4