线程概述
有些程序是一条直线,起点到终点;有些程序是一个圆,不断循环,直到将它切断
一个运行着的程序就是一个进程或者叫做一个任务,一个进程至少包含一个线程,线程就是程序的执行流。Mac和iOS中的程序启动,创建好一个进程的同时, 一个线程便开始运行,这个线程叫主线程。主线程在程序中的地位和其他线程不同,它是其他线程最终的父线程,且所有界面的显示操作即AppKit或 UIKit的操作必须在主线程进行。
系统中的每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则共用进程的内存空间。每创建一个新的线程,都需要一些内存(如每个线程有自己的Stack空间)和消耗一定的CPU时间。另外当多个线程对同一个资源出现争夺的时候需要注意线程安全问题
多线程的实现原理:虽然在同一时刻,CPU只能处理1条线程,但是CPU可以快速地在多条线程之间调度(切换),造成了多线程并发执行的假象。
多线程的优点
能适当提高程序的执行效率。
能适当提高资源利用率(CPU、内存利用率)。
多线程的缺点
创建线程是需要成本的:iOS下主要成本包括:在栈空间的子线程512KB、主线程1MB,创建线程大约需要90毫秒的创建时间。
线程越多,CPU在调度线程上的开销就越大。
线程越多,程序设计就越复杂:因为要考虑到线程之间的通信,多线程的数据共享。
下面开始撸代码:
------------------------------------------------------------------华丽的分割线
1.耗时操作的问题演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self longOperation];
}
- (void)longOperation
{
NSLog(@"start");
NSTimeInterval start = CACurrentMediaTime();
for (int i = 0; i < 10000000; i++) {
// 存储在栈区
// int num = 10;
// 存储在常量区
// NSString *str1 = @"hello";
// 存储在堆区
// NSString *str2 = [NSString stringWithFormat:@"hello_%d",i];
// I/O操作 : 把数据从内存输出到外接设备,或者由外接设备输入到内存;
NSLog(@"%d",i);
}
NSLog(@"over %f", CACurrentMediaTime() - start);
}
结论
- 空的for循环不耗时
- 操作内存的栈区速度很快;栈区存储空间地址是连续的;
- 操作内存的常量区速度很快;内存空间只开辟一次;
- 操作内存的堆区速度相对栈区和常量区要慢些;堆区内存空间不连续,需要寻址;
- I/O操作是很耗时的; (把数据从内存输出到外接设备,或者由外接设备输入到内存)
- 耗时操作对UI交互的影响 : 卡死了主屏幕,直到耗时操作执行完,屏幕的交互才能正常进行;
- 解决耗时操作卡顿UI的办法 : 多线程技术;
- 学习多线程的目的 : 把耗时操作放在后台执行,不让耗时操作卡顿UI;
2.解决耗时操作卡顿UI的办法
使用多线程
技术 : 解决屏幕卡死的问题
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self longOperation];
// 使用多线程技术
[self performSelectorInBackground:@selector(longOperation) withObject:nil];
}
3.多线程基本概念
同步 & 异步
同步
和异步
是任务执行的两种方式
-
同步
- 我们之前写程序的时候代码都是从上往下,顺序执行的,就叫做
同步执行
. - 1个人执行多个任务,是要依次执行的.因为1个人同一时间只能执行1个任务.
-
多个任务按序依次执行
,就是同步执行
.
- 我们之前写程序的时候代码都是从上往下,顺序执行的,就叫做
-
异步
-
多个任务同时执行
,就是异步执行
. - 异步是多线程的代名词.
- 我们学习多线程就是为了实现如何让任务异步执行.
-
进程 & 线程
-
进程
- 在系统中
正在运行
的一个应用程序叫进程. - 通过
活动监视器
可以查看MAC系统中正在运行
的所有应用程序. - 每个进程之间都是
独立
的,均运行在其专用
且受保护
的内存空间内. - 两个进程之间是无法通信的,迅雷无法帮助酷我下载正在播放的音乐.
-
进程
可以类比成正在正常运营
的公司
.
- 在系统中
-
线程
- 线程可以类比成公司中的
员工
. - 进程要想执行任务,必须要有线程,且每个进程至少有一条线程.
- 线程是进程的
基本执行单元
,进程中的所有任务都在线程中执行. - 程序启动(进程开启)会默认开启一条线程.
- 1个进程中可以有多个线程.
- 线程可以类比成公司中的
多线程
-
多线程 : 一个进程中可以开启多条线程,多条线程可以
**同时**
执行不同的任务. - 进程-公司,线程-员工,老板是什么?
- 多线程可以解决程序阻塞的问题
- 多线程可以提高程序的执行效率,给用户良好的使用体验.
- 比如,酷我音乐的边下载边听歌,迅雷的边下载边播放.
4.多线程执行原理
-
单核CPU
同一时间,CPU只能处理1个线程,只有1个线程在执行任务. 多线程的同时执行 : 其实是CPU在多条线程之间快速切换(调度任务).
- 如果CPU调度线程的速度足够快,就造成了多线程
**同时**
执行的**假象**
- 如果线程非常多,CPU会在多条线程之间不断的调度任务,结果就是消耗了大量的CPU资源,CPU会累趴下.
- 每个线程调度的频率会降低
- 线程的执行效率会下降
5.多线程优缺点
优点
- 能"适当"提高程序的执行效率.
- 能"适当"提高CPU和内存的利用率.
- 线程上的任务执行完成后,线程会自动销毁,节省内存.
缺点
- 开启线程需要占用一定的内存空间,如果开启的线程过多,会占用大量的CPU资源,降低程序的性能
- 占用内存空间:默认情况下,子线程512KB,主线程1M.PS:iOS8中,主线程512KB.
- 线程越多,CPU调度线程的开销就越大.
- 时间开销
- 空间开销
- 程序设计更加复杂:比如线程之间的通信,多线程的数据共享
6.主线程
-
一个程序运行后,默认会开启1个线程,称为
主线程
或UI线程
.- 关注
main
函数的执行
- 关注
主线程一般用来
刷新UI界面
,处理UI事件
.处理UI事件
:点击
、滚动
、拖拽
等事件-
主线程使用注意
- 别将耗时的操作放到主线程中
- 耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种卡的坏体验,影响UI交互质量.
7.多线程的实现方案
(二)创建线程三种方式
1.准备新线程执行的方法
- (void)demo:(id)obj
{
NSLog(@"传入参数 => %@",obj);
NSLog(@"hello %@",[NSThread currentThread]);
}
2.对象方法创建
- 实例化线程对象的同时指定线程执行的方法
@selector(demo:)
. - 需要
手动开启线程
.
- (void)threadDemo1
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
// 手动启动线程
[thread start];
}
3.类方法创建
- 分离出一个线程,并且
自动开启线程
执行@selector(demo:)
. - 无法获取到线程对象
- (void)threadDemo2
{
[NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}
4.NSObject(NSThreadPerformAdditions)
的分类创建
- 方便任何继承自
NSObject
的对象,都可以很容易的调用线程方法 - 无法获取到线程对象
-
自动开启线程
执行@selector(demo:)
.
- (void)threadDemo3
{
[self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}
5.总结
- 以上三种创建线程的方式,各有不同.随意选择.
- 使用哪种方式需要根据具体的需求而定.比如 : 如果需要线程对象,就使用对象方法创建.
(三)target和selector的关系
1.target和selector的关系分析
-
target
: 指方法从属于的对象.- 比如 : 本对象--
self
;其他对象--self.person
.
- 比如 : 本对象--
-
@selector
: 指对象里面的方法.- 比如 : 要执行的是
self
中或者self.person
中的哪个方法.
- 比如 : 要执行的是
-
提示 : 不要看见
target
就写self
. -
target
和@selector
的关系 : 执行哪个对象上的哪个方法.
2.代码演练
准备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];
}
新线程执行的方法
- (void)threadDemo
{
// 提示 : 能执行到这里说明线程是运行状态
NSLog(@"%@",[NSThread currentThread]);
// 使当前线程休眠2秒钟 : 休眠指定时长
[NSThread sleepForTimeInterval:2.0];
NSLog(@"第一次睡醒");
// 使当前线程休眠到指定日期
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
NSLog(@"第二次睡醒");
// 使当前线程退出 : 当前线程一旦退出,后续的所有代码都不会执行
// 注意 : 该方法不能在主线程使用,会使主线程退出
[NSThread exit];
NSLog(@"没戏了?");
}
关于exit
的结论
- 使
当前线程
退出. - 不能在主线程中调用该方法.会使主线程退出.
-
当前线程死亡
之后,这个线程中的剩下的所有代码都不会被执行. - 在调用此方法之前一定要注意释放之前由C语言框架创建的对象.
- 调用
exit
方法属于在线程内部取消线程,有时候需要在线程外部,当某一条件满足时就取消线程
线程的取消 (在线程执行的方法的外部取消)
- 取消线程的方法
- (void)cancel NS_AVAILABLE(10_5, 2_0);
- 使用
[thread cancel]
- 这个方法只是修改了线程的状态而已,并没有真正的取消线程
- 如果想真正的取消线程需要在线程执行的过程中判断线程的状态是否是已取消
- 如果该线程已经被取消,就直接返回,不再执行后面的代码
if ([NSThread currentThread].isCancelled) {
NSLog(@"该线程已经被取消");
return;
}
(五)线程属性
1.常用属性
-
name
- 线程名称- 设置线程名称可以当线程执行的方法内部出现异常时,记录异常和当前线程
-
stackSize
- 栈区大小- 默认情况下,无论是主线程还是子线程,栈区大小都是
512K
- 栈区大小可以设置
[NSThread currentThread].stackSize = 1024 * 1024;
- 必须是 4KB 的倍数
- 默认情况下,无论是主线程还是子线程,栈区大小都是
isMainThread
- 是否主线程-
threadPriority
- 线程优先级- 优先级,是一个浮点数,取值范围从
0~1.0
-
1.0
表示优先级最高 -
0.0
表示优先级最低 - 默认优先级是
0.5
- 优先级高只是保证
CPU
调度的可能性会高
- 优先级,是一个浮点数,取值范围从
-
qualityOfService
- 服务质量(iOS 8.0 推出)-
NSQualityOfServiceUserInteractive
- 用户交互,例如绘图或者处理用户事件 -
NSQualityOfServiceUserInitiated
- 用户需要 -
NSQualityOfServiceUtility
- 实用工具,用户不需要立即得到结果 -
NSQualityOfServiceBackground
- 后台 -
NSQualityOfServiceDefault
- 默认,介于用户需要和实用工具之间
-
关于优先级和服务质量
- 多线程的目的:是将耗时的操作放在后台,不阻塞主线程和用户的交互!
- 多线程开发的原则:简单
- 在开发时,最好不要修改优先级,不要相信 用户交互 服务质量
- 内核调度算法在决定该运行哪个线程时,会把线程的优先级作为考量因素
* 较高优先级的线程会比较低优先级的线程具有更多的运行机会
* 较高优先级不保证你的线程具体执行的时间,只是相比较低优先级的线程,更有可能被调度器选择执行而已
2.代码演示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"主线程栈区空间大小 == %tu KB 是否是主线程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 给线程起名字
thread1.name = @"download A";
// 设置线程优先级
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 KB 是否是主线程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[NSThread currentThread]);
}
}
3.补充
-
NSInteger
有符号整数(有正负数)用%zd
-
NSUInteger
无符号整数(没有负数)用%tu
- 是为了
自适应32位和64位CPU
的架构.
(六)线程安全-资源共享
1.多线程操作共享资源的问题
-
共享资源
- 资源 : 一个全局的对象、一个全局的变量、一个文件.
- 共享 : 可以被多个对象访问.
- 共享资源 :可以被多个对象访问的资源.比如全局的对象,变量,文件.
在
多线程
的环境下,共享的资源
可能会被多个线程共享
,也就是多个线程可能会操作同一块资源.当多个线程操作同一块资源时,很容易引发数据错乱和数据安全问题,数据有可能丢失,有可能增加,有可能错乱.
经典案例 : 卖票.
-
线程安全
- 同一块资源,被多个线程同时读写操作时,任然能够得到正确的结果,称之为线程是安全的.
开发提示
- 实际开发中确定开发思路逻辑比及时的写代码更重要.
- 多线程开发的复杂度相对较高,在开发时可以按照以下套路编写代码
- 首先确保单个线程执行正确
- 然后再添加线程
代码实现卖票逻辑
- 先定义共享资源
@interface ViewController ()
/// 总票数(共享的资源)
@property (nonatomic,assign) int tickets;
@end
- 初始化余票数
共享资源
- (void)viewDidLoad {
[super viewDidLoad];
// 设置余票数
self.tickets = 20;
}
- 卖票逻辑实现
- (void)saleTickets
{
// while 循环保证每个窗口都可以单独把所有的票卖完
while (YES) {
// 判断是否有票
if (self.tickets>0) {
// 模拟网络延迟 : 放大出错时的效果,没有实际意义
[NSThread sleepForTimeInterval:1.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];
}
资源抢夺结果
-
数据错乱,数据增加.
出错原因分析
2.解决多线程操作共享资源的问题
- 解决办法 : 使用
互斥锁/同步锁
.
添加互斥锁
- (void)saleTickets
{
// while 循环保证每个窗口都可以单独把所有的票卖完
while (YES) {
// 添加互斥锁
@synchronized(self) {
// 判断是否有票
if (self.tickets>0) {
// 模拟网络延迟 : 放大出错时的效果,没有实际意义
[NSThread sleepForTimeInterval:1.0];
// 有票就卖一张
self.tickets--;
// 卖完一张票就提示用户余票数
NSLog(@"剩余票数 => %zd",self.tickets);
} else {
// 没有就提示用户
NSLog(@"没票了");
// 此处要结束循环,不然会死循环
break;
}
}
}
}
互斥锁小结
- 互斥锁,就是使用了线程同步技术.
- 同步锁/互斥锁:可以保证被锁定的代码,同一时间,只能有一个线程可以操作.
-
self
:锁对象,任何继承自NSObject的对像都可以是锁对象,因为内部都有一把锁,而且默认是开着的. - 锁对象 : 一定要是全局的锁对象,要保证所有的线程都能够访问,
self
是最方便使用的锁对象. - 互斥锁锁定的范围应该尽量小,但是一定要锁住资源的
读写
部分. - 加锁后程序执行的效率比不加锁的时候要低.因为线程要等待解锁.
- 牺牲了性能保证了安全性.
(七)原子属性
1.原子属性相关概念
nonatomic
: 非原子属性-
atomic
: 原子属性- 线程安全的,针对多线程设计的属性修饰符,是默认值.
- 特点 : 单写多读
- **单写多读 : **保证同一时间,只有一个线程能够执行
setter
方法,但是可以有多个线程执行getter
方法. -
atomic
属性的setter
里面里面有一把锁,叫做自旋锁
. - 原子属性的
setter
方法是线程安全的;但是,getter
方法不是线程安全的.
-
nonatomic
和atomic
对比-
nonatomic
: 非线程安全,适合内存小的移动设备. -
atomic
: 线程安全,需要消耗大量的资源.性能比非原子属性要差一点儿点儿.
-
2.模拟原子属性
模拟原子属性的核心思想 : 在属性的
setter
方法里面加锁.但是getter
方法里面不加锁;定义属性
/// 非原子属性
@property (nonatomic,strong) NSObject *obj1;
/// 原子属性:内部有"自旋锁"
@property (atomic,strong) NSObject *obj2;
/// 用于模拟原子属性
@property (atomic,strong) NSObject *obj3;
- 重写非原子属性的
setter
和getter
方法- 重写了原子属性的
setter
方法之后,会覆盖原子属性内部的自旋锁
,使其失效.然后我们加入互斥锁
,来模拟单写多读
. - 重写了属性的
setter
和getter
方法之后,系统就不会再帮我们生成待下划线的成员变量.使用合成指令@synthesize
,就可以手动的生成带下划线的成员变量.
- 重写了原子属性的
3.模拟原子属性
// 合成指令
@synthesize obj3 = _obj3;
/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
// 使用互斥锁替代看不见的自旋锁
@synchronized(self) {
_obj3 = obj3;
}
}
/// obj3的getter方法
- (NSObject *)obj3
{
return _obj3;
}
4.性能测试
/// 测试"非原子属性","互斥锁","自旋锁"的性能
- (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);
}
测试结果
5.互斥锁和自旋锁对比
共同点
- 都能够保证同一时间,只有一条线程执行锁定范围的代码
不同点
-
互斥锁
:如果发现有其他线程正在执行锁定的代码,线程会进入休眠
状态,等待其他线程执行完毕,打开锁之后,线程会重新进入就绪
状态.等待被CPU重新调度. -
自旋锁
:如果发现有其他线程正在执行锁定的代码,线程会以死循环
的方式,一直等待锁定代码执行完成.
6.开发建议
- 所有属性都声明为
nonatomic
,原子属性和非原子属性的性能几乎一样. - 尽量避免多线程抢夺同一块资源.
- 要实现线程安全,必须要用到
锁
.无论什么锁,都是有性能消耗的. - 自旋锁更适合执行非常短的代码.死循环内部不适合写复杂的代码.
- 尽量将加锁,资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力.
- 为了流畅的用户体验,UIKit类库的线程都是不安全的,所以我们需要在主线程(UI线程)上更新UI.
- 所有包含
NSMutable
的类都是线程不安全的.在做多线程开发的时候,需要注意多线程同时操作可变对象的线程安全问题.
(八)NSThread线程间通信
1.ATS
使用http地址时Xcode会认为不够安全从而保存,为解决此问题需要在info文件的Xml文件内添加下列代码
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
2.代码实现
定义属性
@interface ViewController ()
/// 滚动视图
@property (nonatomic,strong) UIScrollView *scrollView;
/// 图片视图
@property (nonatomic,weak) UIImageView *imageView;
@end
loadView
方法复习
- 当
self.view == nil
时,会调用; - 先于
viewDidLoad
调用; - 一旦重写了这个方法,
storyboard
里面就不会去加载根视图了;
加载视图层次
- (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];
// 回到主线程更新UI
// waitUntilDone:是否等待主线程中的`updateUIwWithImage`方法执行结束再执行"下一行代码",一般设置成NO,不用等待
[self performSelectorOnMainThread:@selector(updateUIwWithImage:) withObject:image waitUntilDone:NO];
// 测试 waitUntilDone:
NSLog(@"下一行代码");
}
刷新UI
- (void)updateUIwWithImage:(UIImage *)imgae
{
NSLog(@"updateUIwWithImage");
// 设置图片视图
self.imageView.image = image;
// 设置图片视图的大小跟图片一般大
[self.imageView sizeToFit];
// 设置滚动视图的滚动:滚动范围跟图片一样大
[self.scrollView setContentSize:image.size];
}
线程间通信
- 因为多线程共享地址空间和数据空间
所以一个线程的数据可以直接提供给其他线程使用,叫做线程间通信;