NSThread
- 实现多线程的技术方案之一.
- 面向对象的开发思想.
- 每个对象表示一条线程.
创建线程三种方式
准备新线程执行的方法
- (void)demo:(id)obj
{
NSLog(@"传入参数 => %@",obj);
NSLog(@"hello %@",[NSThread currentThread]);
}
对象方法创建
- 实例化线程对象的同时指定线程执行的方法
@selector(demo:)
. - 需要
手动开启线程
.
- (void)threadDemo1
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
// 手动启动线程
[thread start];
}
类方法创建
- 分离出一个线程,并且
自动开启线程
执行@selector(demo:)
. - 无法获取到线程对象
- (void)threadDemo2
{
[NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}
NSObject(NSThreadPerformAdditions)
的分类创建
- 方便任何继承自
NSObject
的对象,都可以很难容易的调用线程方法 - 无法获取到线程对象
-
自动开启线程
执行@selector(demo:)
.
- (void)threadDemo3
{
[self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}
总结
- 以上三种创建线程的方式,各有不同.随意选择.
- 使用哪种方式需要根据具体的需求而定.比如 : 如果需要线程对象,就使用对象方法创建.
target和@selector的关系
-
target
: 指方法从属于的对象.- 比如 : 本对象--
self
;其他对象--self.person
.
- 比如 : 本对象--
-
@selector
: 指对象里面的方法.- 比如 : 要执行的是
self
中或者self.person
中的哪个方法.
- 比如 : 要执行的是
-
提示 : 不要看见
target
就写self
. -
target
和@selector
的关系 : 执行哪个对象上的哪个方法.
代码演练
准备Person对象
@interface Person : NSObject
/// 人名
@property (nonatomic,copy) NSString *name;
/// 创建人得构造方法
+ (instancetype)personWithDict:(NSDictionary *)dict;
/// 人有个方法
- (void)personDemo:(id)obj;
@end
@implementation Person
+ (instancetype)personWithName:(NSString *)name
{
Person *person = [[Person alloc] init];
person.name = name;
return person;
}
- (void)personDemo:(id)obj
{
NSLog(@"创建的人名 => %@",self.name);
NSLog(@"hello %@",[NSThread currentThread]);
}
@end
控制器中的使用
定义属性
@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end
懒加载Person
@implementation ViewController
- (Person *)person
{
if (_person==nil) {
_person = [Person personWithName:@"zhangjie"];
}
return _person;
}
新的实例化方法
-
使用
self
调用@selector(personDemo:)
就会崩溃.因为self
中没有@selector(personDemo:)
. -
分类方法
// 崩溃
[self performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 正确的调用方式
[self.person performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
- 类方法
// 崩溃
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self withObject:@"detach"];
// 正确的调用方式
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self.person withObject:@"detach"];
- 对象方法
// 崩溃
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(personDemo:) object:@"alloc"];
// 手动开启线程
[thread start];
// 正确的调用方式
NSThread *thread = [[NSThread alloc] initWithTarget:self.person selector:@selector(personDemo:) object:@"alloc"];
// 手动开启线程
[thread start];
线程状态
线程生命周期的控制
- 新建
- 内存中创建了一个线程对象
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
- 就绪
- 将线程放进
可调度线程池
,等待被CPU调度
- 将线程放进
[thread start];
-
运行
- CPU负责调度"可调度线程池"中的处于"就绪状态"的线程
- 线程执行结束之前,状态可能会在"就绪"和"运行"之间来回的切换
- "就绪"和"运行"之间的状态切换由CPU来完成,程序员无法干涉
-
阻塞
-
正在运行的线程,当满足某个条件时,可以用
休眠
或者锁
来阻塞线程的执行- sleepForTimeInterval:休眠指定时长
[NSThread sleepForTimeInterval:1.0];
- sleepUntilDate:休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
- 互斥锁
@synchronized(self)
-
-
死亡
- 正常死亡:线程执行结束
- 非正常死亡
- 程序突然崩溃
- 当满足某个条件后,在线程内部强制线程退出,调用
exit
方法
代码演练
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 新建状态
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
// 就绪状态 : 将线程放进"可调度线程池",等待被CPU调度.
[thread start];
// 主线程中的危险操作,不能在主线程中调用该方法.会使主线程退出
// [NSThread exit];
}
- (void)threadDemo
{
for (int i = 0; i < 6; i++) {
NSLog(@"%d",i);
//1. 当前线程,每循环一次,就休眠一秒
[NSThread sleepForTimeInterval:1.0];
//2. 满足某一条件再次休眠一秒
if (2==i) {
NSLog(@"我还想再睡一秒");
// 休眠时间为从现在开始计时多少秒以后
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
}
//3. 满足某一条件线程死亡
if (4==i) {
NSLog(@"线程死亡");
// 在调用exit方法之前一定要注意释放之前由C语言框架创建的对象.
CGMutablePathRef path = CGPathCreateMutable();
CGPathRelease(path);
// 线程死亡
[NSThread exit];
// 当线程死亡之后,以后的代码都不会被执行
NSLog(@"线程已经死亡");
}
}
NSLog(@"循环结束");
}
关于exit
的结论
- 使
当前线程
退出. - 不能在主线程中调用该方法.会使主线程退出.
- 当
当前线程死亡
之后,这个线程中的代码都不会被执行. - 在调用此方法之前一定要注意释放之前由C语言框架创建的对象.
线程属性
属性
-
name - 线程名称
- 给线程起名字,可以方便运行调试,定位BUG
- 在大型的商业软件中,都会设计专门的线程做特定的事情,当程序崩溃时可以快速准确的定位BUG
-
threadPriority - 线程优先级
- 为浮点数整形,范围在0~1之间,1最高,默认0.5,不建议修改线程优先级
- 线程的"优先级"不是决定线程调用顺序的,他是决定线程备CPU调用的频率的
- 在开发的时候,不要修改优先级
- 多线程开发的原则是越简单越好
-
stackSize - 栈区大小
- 默认情况下,无论是主线程还是子线程,栈区大小都是512KB
- 栈区大小可以设置,最小16KB,但是必须是4KB的整数倍
代码演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"主线程栈区空间大小 => %tu",[NSThread currentThread].stackSize/1024);
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 给线程起名字,可以方便运行调试,定位BUG
// 在大型的商业软件中,都会设计专门的线程做特定的事情
thread1.name = @"download A";
// 线程调用优先级
// 线程的"优先级"不是决定线程调用顺序的,他是决定线程备CPU调用的频率的
// 范围在0~1之间,1最高,默认0.5,不建议修改线程优先级
thread1.threadPriority = 1.0;
// 线程就绪
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
thread2.name = @"download B";
thread2.threadPriority = 0;
[thread2 start];
}
- (void)demo
{
NSLog(@"子线程栈区空间大小 => %tu",[NSThread currentThread].stackSize/1024);
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[NSThread currentThread]);
}
}
补充
-
NSInteger
有符号整数(有正负数)用%zd
-
NSUInteger
无符号整数(没有负数)用%tu
- 是为了
自适应32位和64位CPU
的架构.
资源共享-线程安全
多线程操作共享资源的问题
-
共享资源
- 资源 : 一个全局的对象、一个全局的变量、一个文件.
- 共享 : 可以被多个对象访问.
- 共享资源 :可以被多个对象访问的资源.比如全局的对象,变量,文件.
-
在
多线程
的环境下,共享的资源
可能会被多个线程共享
,也就是多个线程可能会操作同一块资源. -
当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱.
-
经典案例 : 卖票.
-
线程安全
- 同一块资源,被多个线程同时读写操作时,任然能够得到正确的结果,称之为线程是安全的.
卖票逻辑
卖票系统的简单逻辑.png开发提示
- 实际开发中确定开发思路逻辑比及时的写代码更重要.
- 多线程开发的复杂度相对较高,在开发时可以按照以下套路编写代码
- 首先确保单个线程执行正确
- 然后再添加线程
代码实现卖票逻辑
- 先定义共享资源
@interface ViewController ()
/// 总票数(共享的资源)
@property (nonatomic,assign) int tickets;
@end
- 初始化余票数
共享资源
- (void)viewDidLoad {
[super viewDidLoad];
// 设置余票数
self.tickets = 10;
}
- 卖票逻辑实现
- (void)saleTickets
{
// while 循环保证每个窗口都可以单独把所有的票卖完
while (YES) {
// 模拟网络延迟
[NSThread sleepForTimeInterval:1.0];
// 判断是否有票
if (self.tickets>0) {
// 有票就卖
self.tickets--;
// 卖完一张票就提示用户余票数
NSLog(@"剩余票数 => %zd %@",self.tickets,[NSThread currentThread]);
} else {
// 没有就提示用户
NSLog(@"没票了");
// 此处要结束循环,不然会死循环
break;
}
}
}
单线程
- 先确保单线程中运行正常
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主线程中卖票
[self saleTickets];
}
多线程
- 如果单线程运行正常,就修改代码,实现多线程环境
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 在主线程中卖票
// [self saleTickets];
// 售票口 A
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
thread1.name = @"售票口 A";
[thread1 start];
// 售票口 B
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
thread2.name = @"售票口 B";
[thread2 start];
}
资源抢夺结果
- 数据错乱,数据增加.
出错原因分析
资源共享问题分析.png资源共享问题解决.png
解决多线程操作共享资源的问题
- 解决办法 : 使用
互斥锁/同步锁
.
添加互斥锁
- (void)saleTickets
{
// while 循环保证每个窗口都可以单独把所有的票卖完
while (YES) {
// // 模拟休眠网络延迟
[NSThread sleepForTimeInterval:1.0];
// 添加互斥锁
@synchronized(self) {
// 判断是否有票
if (self.tickets>0) {
// 有票就卖
self.tickets--;
// 卖完一张票就提示用户余票数
NSLog(@"剩余票数 => %zd",self.tickets);
} else {
// 没有就提示用户
NSLog(@"没票了");
// 此处要结束循环,不然会死循环
break;
}
}
}
}
互斥锁小结
- 互斥锁,就是使用了线程同步技术.
- 同步锁/互斥锁:可以保证被锁定的代码,同一时间,只能有一个线程可以操作.
-
self
:锁对象,任何继承自NSObject的对像都可以是锁对象,因为内部都有一把锁,而且默认是开着的. - 锁对象 : 一定要是全局的锁对象,要保证所有的线程都能够访问,
self
是最方便使用的锁对象. - 互斥锁锁定的范围应该尽量小,但是一定要锁住资源的
读写
部分. - 加锁后程序执行的效率比不加锁的时候要低.因为线程要等待解锁.
- 牺牲了性能保证了安全性.
原子属性
-
nonatomic
: 非原子属性 -
atomic
: 原子属性- 线程安全的,针对多线程设计的属性修饰符,是默认值.
- 保证同一时间只有一个线程能够写入,但是同一个时间多个线程都可以读取.
- **单写多读 : ** 单个线程写入
write
,多个线程可以读取read
. -
atomic
本身就有一把锁,自旋锁
.
-
nonatomic和atomic对比
-
nonatomic
: 非线程安全,适合内存小的移动设备. -
atomic
: 线程安全,需要消耗大量的资源.性能比非原子属性要差.
-
-
iOS开发的建议
- 所有属性都声明为
nonatomic
,性能更高. - 尽量避免多线程抢夺同一块资源.
- 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.
- 所有属性都声明为
模拟原子属性
- 定义属性
/// 非原子属性
@property (nonatomic,strong) NSObject *obj1;
/// 原子属性:内部有"自旋锁"
@property (atomic,strong) NSObject *obj2;
/// 模拟原子属性
@property (atomic,strong) NSObject *obj3;
- 重写非原子属性的
setter
和getter
方法- 重写了原子属性的
setter
方法之后,会覆盖原子属性内部的自旋锁
,使其失效.然后我们加入互斥锁
,来模拟但写多读
. - 重写了属性的
setter
和getter
方法之后,系统就不会再帮我们生成待下划线的成员变量.使用合成指令@synthesize
,就可以手动的生成带下划线的成员变量.
- 重写了原子属性的
// 合成指令
@synthesize obj3 = _obj3;
/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
@synchronized(self) {
_obj3 = obj3;
}
}
/// obj3的getter方法
- (NSObject *)obj3
{
return _obj3;
}
性能测试
/// 测试"非原子属性","互斥锁","自旋锁"的性能
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSInteger largeNum = 1000*1000;
NSLog(@"非原子属性");
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj1 = [[NSObject alloc] init];
}
NSLog(@"非原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
NSLog(@"原子属性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj2 = [[NSObject alloc] init];
}
NSLog(@"原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
NSLog(@"模拟原子属性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNum; i++) {
self.obj3 = [[NSObject alloc] init];
}
NSLog(@"模拟原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
}
测试结果
num08.png互斥锁和自旋锁对比
共同点
- 都能够保证同一时间,只有一条线程执行锁定范围的代码
不同点
-
互斥锁
:如果发现有其他线程正在执行锁定的代码,线程会进入休眠
状态,等待其他线程执行完毕,打开锁之后,线程会重新进入就绪
状态.等待被CPU重新调度. -
自旋锁
:如果发现有其他线程正在执行锁定的代码,线程会以死循环
的方式,一直等待锁定代码执行完成.
开发建议
- 所有属性都声明为
nonatomic
,原子属性和非原子属性的性能几乎一样. - 尽量避免多线程抢夺同一块资源.
- 要实现线程安全,必须要用到
锁
.无论什么锁,都是有性能消耗的. - 自旋锁更适合执行非常短的代码.死循环内部不适合写复杂的代码.
- 尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.
- 为了流畅的用户体验,UIKit类库的线程都是不安全的,所以我们需要在主线程(UI线程)上更新UI.
- 所有包含
NSMutable
的类都是线程不安全的.在做多线程开发的时候,需要注意多线程同时操作可变对象的线程安全问题.
异步下载网络图片
需求 : 异步下载网络图片并展示.图片可以滚动,滚动视图要是根视图.
ATS
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
代码实现
定义属性
@interface ViewController ()
/// 滚动视图
@property (nonatomic,strong) UIScrollView *scrollView;
/// 图片视图
@property (nonatomic,weak) UIImageView *imageView;
@end
加载视图层次
-
loadView
: 手动创建根视图,实现了这个方法视图控制器的view
就不会从SB
中加载了.当self.view==nil
的时候就会调用这个方法.
- (void)loadView
{
// 创建滚动视图
self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];
// 将滚动视图设置成根视图
self.view = self.scrollView;
self.scrollView.backgroundColor = [UIColor redColor];
// 创建图片视图
UIImageView *imageView = [[UIImageView alloc] init];
[self.view addSubview:imageView];
self.imageView = imageView;
}
异步下载网络数据
- (void)viewDidLoad {
[super viewDidLoad];
// 下载网络数据
// [self downloadImageData];
// 开启新线程异步下载图片
[self performSelectorInBackground:@selector(downloadImageData) withObject:nil];
}
/// 异步下载网络数据
- (void)downloadImageData
{
// 图片资源地址
NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/c995d143ad4bd1130c0ee8e55eafa40f4afb0521.jpg"];
// 所有的网络数据都是以二进制的形式传输的,所以用NSData来接受
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 设置图片视图
// [self setupImageViewWithImage:image];
// 回到主线程更新UI
// waitUntilDone:是否等待主线程执行结束再执行"下一行代码",一般设置成NO,不用等待
[self performSelectorOnMainThread:@selector(setupImageViewWithImage:) withObject:image waitUntilDone:NO];
// 测试 waitUntilDone:
NSLog(@"下一行代码");
}
设置图片视图
/// 设置图片视图
- (void)setupImageViewWithImage:(UIImage *)imgae
{
NSLog(@"setupImageView");
// 设置图片视图
self.imageView.image = image;
// 设置图片视图的大小跟图片一般大
[self.imageView sizeToFit];
// 设置滚动视图的滚动:滚动范围跟图片一样大
[self.scrollView setContentSize:image.size];
}
strong和weak补充
-
什么是strong,weak,分别什么时候使用?
- strong : 强指针.
- weak : 弱指针.
- OC对象,根视图或者父视图用strong.
- 子视图或者创建出来之后有强指针指向的对象用weak,
-
提问 :
-
scrollView
和imageView
都用strong
修饰.程序会出问题吗? - 连线的UI控件为什么用weak?
-
网友评论