我去, 好蛋疼, 刚刚写好的博客就因为手贱在触控板上右划了一下, 写的全丢了, 还得重新写, 博客园就没有针对这种情况的解决方案吗?都不想写了
一、iOS中多线程的实现方案有四种
1、NSThread陷阱非常多, 有缺陷, 不过是OC的, 偶尔用一下
2、GCD是在iOS4推出的, 能充分利用设备的多核, 而且不用考虑线程, 性能比NSThread好的多
GCD研究起来就比较深了, 所以在面试的时候会经常被问到
3、NSOperation封装了很多实用的功能, 某些情况下, GCD实现起来比较难的反而用NSOperation实现起来很简单, 苹果推荐大家使用NSOperation
因为NSOperation封装的非常好, 所以使用起来非常简单, 没有什么技术难度, 在面试中就不怎么问
二、NSThread
NSThread使用起来简单, 比较灵活, 虽然不经常用, 但是必须要看看下面的用法, 有很多技术点要注意
(1) 通用的方法:currentThread
NSThread *thread = [NSThread currentThread];
当前线程, 在所有的(指NSThreadGCDNSOpertion)多线程技术中都可以使用来获得当前线程
(2) 第一种方法, 最简单的一种
/** 不带参数的实例化initWithTarget方法 */
// 1. 实例化线程对象, run 是一个耗时的任务,放在其他线程工作,就不会影响主线程了! NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; // 2. 启动线程,会立即调用run方法 [thread start];
实例化一个线程对象, 把耗时任务放在其它线程
必须start才会执行run方法
/** 带参数的实例化initWithTarget方法 */
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"hehe"]; thread.name = @"thread initWithTarget";
注意, 带参数@selector(run:)加冒号
可以设置thread对象的name属性
(3) 第二种方法
/** 不带参数的detach方法 */
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
- (void)run
会立即启动新的线程, 运行run方法
/** 带参数的detach方法 */
// object是调用方法的第一个参数 [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"hello"];
- (void)run:(id)obj
注意, 带参数@selector(run:)加冒号
(4) 第三种方法
// NSObject定义的方法,隐式的多线程调用 [self performSelectorInBackground:@selector(run:) withObject:@"NSObject"];
隐式的多线程调用, 隐式创建并启动线程
在NSThread.h文件中, 有这个performSelectorInBackground方法
注意: 是self调用此方法
(5) 说明: NSThread就这三种方式创建子线程, 其实并不难
(6) 线程的调度优先级
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority;
- (BOOL)setThreadPriority:(double)p;
调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高
自己开发时,不要修改优先级,否则一旦出现“优先级反转”会非常麻烦
(7) 控制线程状态
- 启动线程
- (void)start;
// 进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
- 阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 进入阻塞状态
比如让线程休眠3s
// 线程休眠(3s) [NSThread sleepForTimeInterval:3.0];
- 强制停止线程
+ (void)exit;
// 进入死亡状态
// 强行终止当前线程(杀掉) [NSThread exit];
注意:一旦线程停止(死亡)了,就不能再次开启任务
(8) NSThread 的注意事项 (非常重要)
① NSObject的多线程方法使用的是NSThread的多线程技术
② NSThread创建的线程,不会自动使用@autoreleasepool(主线程默认是有自动释放池的,在main.m文件中可以看到,但是NSThread创建的子线程默认没有自动释放池)
③ 在使用NSObject或NSThread的多线程技术时,如果涉及到对象分配,需要手动添加@autoreleasepool,不添加有可能内存泄露。
(9) 补充一下@autoreleasepool的知识
- iOS开发中的内存管理
① 在iOS开发中,并没有JAVA或C#中的垃圾回收机制
② 使用ARC开发,只是在编译时,编译器会根据代码结构自动添加了retain、release和autorelease
- 自动释放池的工作原理
① 标记为autorelease的对象在出了作用域范围后,会被添加到最近一次创建的自动释放池中
② 当自动释放池被销毁或耗尽时,会向自动释放池中的所有对象发送release消息
③ 每个线程都需要有@autoreleasepool,否则可能会出现内存泄漏,但是使用NSThread多线程技术,并不会为后台线程创建自动释放池
- 自动释放池常见面试代码
for (int i = 0; i < largeNumber; ++i) { NSString *str = @"Hello World"; str = [str stringByAppendingFormat:@" - %d", i]; str = [str uppercaseString]; NSLog(@"%@", str); }
问:以上代码存在什么样的问题?如果循环的次数非常大时,应该如何修改?
先要明白,str 存放在哪儿,因为 str 是常量字符串对象,所以存放在文字常量区(存放常量字符串,程序结束后由系统释放),即静态区
分别打印 str 的地址来分析一下
NSString *str = @"hello"; NSLog(@"1. %p", str); // 转换成大写 str = [str uppercaseString]; NSLog(@"2. %p", str); // 拼接字符串 str = [NSString stringWithFormat:@"%@-123", str]; NSLog(@"3. %p", str);
打印结果是:
1. 0x1000012c8
2. 0x100500320
3. 0x1005003a0
可以看到1的地址跟2、3的地址有很大不同,因为在调用 uppercaseString 这个方法的时候,就在堆中复制了一份 str ,调用拼接字符串方法中又复制了一份,因为已经不是在文字常量区操作数据了,常量字符串对象str已经不是当初的自己了,需要用到内存管理,因此这段代码的主要问题是没有使用自动释放池来管理对象。为 for 外部添加一个自动释放池:
@autoreleasepool { for (int i = 0; i < 1; i++) { NSString *str = @"hello"; NSLog(@"1. %p", str); // 转换成大写 str = [str uppercaseString]; NSLog(@"2. %p", str); // 拼接字符串 str = [NSString stringWithFormat:@"%@-123", str]; NSLog(@"3. %p", str); } }
如果循环的次数非常大,自动释放池无法存放所有循环产生的变量,那么就在 for 内部增加自动释放池,每次循环都释放一次变量,代码如下:
for (int i = 0; i < 1; i++) { @autoreleasepool { NSString *str = @"hello"; NSLog(@"1. %p", str); // 转换成大写 str = [str uppercaseString]; NSLog(@"2. %p", str); // 拼接字符串 str = [NSString stringWithFormat:@"%@-123", str]; NSLog(@"3. %p", str); } }