一、多线程基础
基本概念
进程
进程是指在系统中正在运行的一个应用程序
每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
通过 活动监视器 可以查看 Mac 系统中所开启的进程线程
进程要想执行任务,必须得有线程,进程至少要有一条线程
程序启动会默认开启一条线程,这条线程被称为主线程或UI 线程
线程是进程的基本执行单元,进程的所有任务都在线程中执行-
多线程
- 一个进程中可以开启多条线程,每条线程可以同时执行不同的任务
进程 -> 公司
线程 -> 员工
主线程 -> 老板(第一个员工) - 多线程技术可以提高程序的执行效率
- 一个进程中可以开启多条线程,每条线程可以同时执行不同的任务
- 多线程原理
- 同一时间,CPU只能处理一条线程,只有一条线程在执行
- 多线程同时执行,其实是CPU快速地在多条线程之间切换
- 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
- 如果线程非常多,会在多条线程之间来回切换,消耗大量的 CPU 资源
- 每个线程被调度的次数会降低
- 线程的执行效率会下降
iOS 8.0 主线程的默认堆栈大小也是 512K
-
多线程优缺点
优点
能适当提高资源利用率(CPU、内存利用率)
能适当提高程序的执行效率缺点
开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享
-
主线程
- 程序启动创建的线程,被称为主线程或UI线程
- 主线程的作用
- 显示/刷新 UI 界面
- 处理 UI 事件:点击、滚动、拖拽等事件
- 注意:要将耗时操作放在后台线程执行,否则会影响 UI 的流畅度,破坏用户体验
- 所有网络访问都是耗时操作!
- iOS中多线程的实现方案
二、耗时操作示例
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 在主线程执行
[self longOperation];
// 在后台线程执行
//[self performSelectorInBackground:@selector(longOperation) withObject:nil];
}
// 耗时操作
- (void)longOperation{
NSLog(@"start = %@",[NSThread currentThread]);
int largeNumber = 1000 * 1000 * 10;
for (int index = 0; index < largeNumber; index ++) {
// 栈区
// int num = 10;
// 静态区/常量区
// NSString *str = @"hello world";
// 在 oc 中,只要使用 @"" 定义的字符串,如果内容一样,无论在哪里,地址都一样。
// stringWithFormat:生成的字符串是保存在堆区的
// 栈区操作效率要比堆区快
// 程序员只需要管理堆区的内存
NSString *str = [NSString stringWithFormat:@"hello world - %d",index];
}
NSLog(@"over");
}
[NSThread currentThread] 是获取当前线程的对象。
最常用的就是根据 number 判断是否主线程。
number == 1 就是主线程 。
number != 1 就是后台线程。
不要纠结 number 的具体数字,由 CPU 决定。
演示因耗时操作导致按钮和 UITextView 不能继续响应用户点击和拖拽事件。
学习多线程的目的:就是将耗时操作放到后台去执行。
三、pthread
-
1、简介
- pthread 是 POSIX 多线程开发框架,由于是跨平台的 C 语言框架,在苹果的头文件中并没有详细的注释。
- 要查阅 pthread 有关资料,可以访问 http://baike.baidu.com。
2、导入头文件
#import <pthread.h>
- 3、pthread示例
// 创建线程,并且在线程中执行 demo 函数
- (void)pthreadDemo {
/**
参数:
1> 指向线程标识符的指针,C 语言中类型的结尾通常 _t/Ref,而且不需要使用 *
-- 在 C 语言中,没有对象的概念,对象是以结构体的方式来实现的。
---- 通常,在 C 语言框架中,对象类型以 _t/Ref 结尾,而且声明时不需要使用 *
2> 用来设置线程属性
3> 线程运行函数的起始地址
--- 在 C 语言中,函数名就是指向函数在内存中的起始地址
--- 类似的一个概念:数组名是指向数组第一个元素的地址。
在 C 语言中, void *(指向任何地址的指针) 和 OC 中的 id(万能指针) 是等价的
参数3的格式: void * (*) (void *)
返回值 (*函数指针) (参数)
4> 运行函数的参数
返回值:
- 若线程创建成功,则返回0
- 若线程创建失败,则返回出错编号
*/
pthread_t threadId = NULL;
NSString *str = @"Hello Pthread";
int result = pthread_create(&threadId, NULL, demo, (__bridge void *)(str));
if (result == 0) {
NSLog(@"创建线程 OK");
} else {
NSLog(@"创建线程失败 %d", result);
}
}
// 后台线程调用函数
void *demo(void *params) {
NSString *str = (__bridge NSString *)(params);
NSLog(@"%@ - %@", [NSThread currentThread], str);
return NULL;
}
- 4、小结
- 在 C 语言中,没有对象的概念,对象是以结构体的方式来实现的。
- 通常,在 C 语言框架中,对象类型以 _t/Ref 结尾,而且声明时不需要使用 *
- C 语言中的 void * 和 OC 中的 id 是等价的
- 内存管理
- 在 OC 中,如果是 ARC 开发,编译器会在编译时,会根据代码结构,自动添加retain/release/autorelease
- 但是,ARC 只负责管理 OC 部分的内存管理,而不负责 C 语言 代码的内存管理
- 因此,开发过程中,如果使用的 C 语言框架出现 retain/create/copy/new 等字样的函数,大多都需要 release,否则会出现内存泄漏
- 在混合开发时,如果在 C 和 OC 之间传递数据,需要使用 __bridge 进行桥接,桥接的目的就是为了告诉编译器如何管理内存。__bridge 表示什么特殊处理都不做。
- 桥接的添加可以借助 Xcode 的辅助功能添加。
- MRC 中不需要使用桥接。因为MRC的内存管理需要程序员手动管理。
四、NSThread
4.1、创建线程的方式(3种)
准备在后台线程调用的方法 longOperation:
- (void)longOperation:(id)obj {
NSLog(@"%@ - %@", [NSThread currentThread], obj);
}
4.1.1、alloc / init - start
- (void)threadDemo1 {
// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download) object:@"Alloc"];
// 开启线程
[thread start];
NSLog(@"after %@", [NSThread currentThread]);
}
- (void)download {
}
- 小结
1.[thread start];执行后,会在另外一个线程执行 longOperation: 方法
2.在 OC 中,任何一个方法的代码都是从上向下顺序执行的
3.同一个方法内的代码,都是在相同线程执行的(block除外)
4.1.2、detachNewThreadSelector
- (void)threadDemo2 {
NSLog(@"before %@", [NSThread currentThread]);
[NSThread detachNewThreadSelector:@selector(longOperation:) toTarget:self withObject:@"DETACH"];
NSLog(@"after %@", [NSThreadcurrentThread]);
}
或:
- (void)threadDemo2 {
// 在同一个方法中,代码是从上往下执行的.
// 同一个线程中,代码也是从上往下执行的(block除外)
// 多线程开发,不要相信第一次执行的结果
NSLog(@"start = %@",[NSThread currentThread]);
// detach:分离
// 创建线程,并启动线程.
// [self download:@"xxx"];
// 创建线程本身是在主线程创建,
[NSThread detachNewThreadSelector:@selector(download:) toTarget:self.person withObject:@"detach"];
NSLog(@"over");
}
- 小结
detachNewThreadSelector 类方法不需要启动,会自动创建线程并执行 @selector 方法。
4.1.3、分类方法:performSelectorInBackground
- (void)threadDemo3 {
NSLog(@"before %@", [NSThread currentThread]);
[self performSelectorInBackground:@selector(longOperation) withObject:@"PERFORM"];
NSLog(@"after %@", [NSThread currentThread]);
}
- (void)longOperation {
}
- 小结
1.performSelectorInBackground 是 NSObject 的分类方法。
2.会自动在后台线程执行 @selector 方法。
3.没有 thread 字眼,隐式创建并启动线程。
4.所有 NSObject 都可以使用此方法,在其他线程执行方法
4.1.4、创建和启动线程
一个NSThread对象就代表一条线程
创建、启动线程
NSThread*thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(run) object:nil];
[thread start];
// 线程一启动,就会在线程thread中执行self的run方法
- 主线程相关用法
+ (NSThread*)mainThread;// 获得主线程
- (BOOL)isMainThread;// 是否为主线程
+ (BOOL)isMainThread;// 是否为主线程
4.1.5、其他用法
获得当前线程
NSThread *current = [NSThreadcurrentThread];
线程的调度优先级
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
- (double)threadPriority;
- (BOOL)setThreadPriority:(double)p;
调度优先级的取值范围是0.0~1.0,默认0.5,值越大,优先级越高
线程的名字
- (void)setName:(NSString*)name;
- (NSString*)name;
4.1.6、其他创建线程方式
创建线程后自动启动线程
[NSThreaddetachNewThreadSelector:@selector(run) toTarget:selfwithObject:nil];
隐式创建并启动线程
[selfperformSelectorInBackground:@selector(run) withObject:nil];
- 上述2种创建线程方式的优缺点
优点:简单快捷
缺点:无法对线程进行更详细的设置
4.2、NSThread的Target
NSThread 的实例化方法中的 target 指的是开启线程后,在线程中执行 哪一个对象 的 @selector 方法。
4.3、示例
准备对象
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
+ (instancetype)personWithDict:(NSDictionary *)dict {
id obj = [[self alloc] init];
[obj setValuesForKeysWithDictionary:dict];
return obj;
}
- (void)longOperation:(id)obj {
NSLog(@"%@ - %@ - %@", [NSThreadcurrentThread], self.name, obj);
}
@end
定义属性 :
@property (nonatomic, strong) Person *person;
懒加载
- (Person *)person {
if (_person == nil) {
_person = [Person personWithDict:@{@"name": @"zhangsan"}];
}
return _person;
}
三种线程调度方法
1、alloc / init
NSThread *thread = [[NSThread alloc] initWithTarget:self.person(调用者) selector:@selector(longOperation:)(调用者调用此方法) object(参数): @"THREAD"];
[thread start];
2、Detach (分离)
[NSThread detachNewThreadSelector:@selector(longOperation:) toTarget:self.person withObject:@"DETACH"];
3、分类方法(创建一个后台子线程并运行)
[self.person performSelectorInBackground:@selector(longOperation:) withObject:@"PERFORM"];
- 小结
通过指定不同的 target 会在后台线程执行该对象的 @selector 方法
提示:不要看见 target 就写 self
performSelectorInBackground 可以让方便地在后台线程执行任意 NSObject 对象的方法
4.4、线程状态
新建
实例化线程对象就绪
向线程对象发送 start 消息,线程对象被加入 可调度线程池 等待 CPU 调度
detach 方法和 performSelectorInBackground 方法会直接实例化一个线程对象并加入 可调度线程池运行
CPU 负责调度可调度线程池中线程的执行
线程执行完成之前,状态可能会在就绪和运行之间来回切换
就绪和运行之间的状态变化由 CPU 负责,程序员不能干预阻塞
当满足某个预定条件时,可以使用休眠或锁阻塞线程执行:
sleepForTimeInterval:休眠指定时长
sleepUntilDate:休眠到指定日期
@synchronized(self):互斥锁
-
死亡
正常死亡
线程执行完毕非正常死亡
当满足某个条件后,在线程内部中止执行。
当满足某个条件后,在主线程中止线程对象。
[NSThread exit];
一旦强行终止线程,后续的所有代码都不会被执行
注意:在终止线程之前,应该注意释放之前分配的对象!
控制线程状态
启动线程
- (void)start;
// 进入就绪状态 ->运行状态。当线程任务执行完毕,自动进入死亡状态
阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate*)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 进入阻塞状态
强制停止线程
+ (void)exit;
// 进入死亡状态
注意:一旦线程停止(死亡)了,就不能再次开启任务
4.4.1、示例代码
- (void)statusDemo {
NSLog(@"先睡会");
[NSThread sleepForTimeInterval:1.0];
for (int i = 0; i < 20; i++) {
if (i == 9) {
NSLog(@"再睡会");
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
}
NSLog(@"%d %@", i, [NSThreadcurrentThread]);
if (i == 16) {
NSLog(@"88");
// 终止线程之前,需要记住释放资源
[NSThread exit];
}
}
NSLog(@"over");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 注意不要在主线程上调用 exit 方法
// [NSThread exit];
// 实例化线程对象(新建)
NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(statusDemo) object:nil];
// 线程就绪(被添加到可调度线程池中)
[t start];
}
4.4.2、取消线程
- (void)download{
NSThread *thread = [NSThread currentThread];
// 判断线程是否取消
if (thread.isCancelled) {
NSLog(@"1...888");
return;
}
// 睡0.2秒
[NSThread sleepForTimeInterval:0.2];
NSLog(@"睡会");
for (int index = 0; index < 10; index ++) {
if (thread.isCancelled) {
NSLog(@"2...888");
return;
}
NSLog(@"%@",[NSThread currentThread]);
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download) object:nil];
// 就绪 -> 进入 CPU 的可调用线程池
[thread start];
// 休眠一会
[NSThread sleepForTimeInterval:0.2];
// 取消线程 cancel 是给线程发送一个取消的消息。设置线程的状态为取消。
// 但是:如果要线程终止,需要在线程内部判断。
[thread cancel];
}
4.4.3、小结
- 阻塞
方法执行过程,符合某一条件时,可以利用 sleep 方法让线程进入 阻塞 状态
sleepForTimeInterval // 从现在起睡多少秒
sleepUntilDate // 从现在起睡到指定的日期
- 死亡
[NSThread exit];
一旦强行终止线程,后续的所有代码都不会被执行
注意:在终止线程之前,应该注意释放之前分配的对象!
- 注意:线程从就绪和运行状态之间的切换是由 CPU 负责的,程序员无法干预
4.5、线程的属性
name - 线程名称(需要设置)
在大的商业项目中,通常需要在程序崩溃时,获取程序准确执行所在的线程。threadPriority - 线程优先级
优先级,是一个浮点数,取值范围从 0~1.0
1.0表示优先级最高
0.0表示优先级最低
默认优先级是0.5
优先级高只是保证 CPU 调度频率的可能性会高
建议:在开发的时候,不要修改优先级,调度频率快慢由 CPU决定。
多线程的目的:是将耗时的操作放在后台,不阻塞主线程和用户的交互!
多线程开发的原则:简单
-
stackSize - 栈区大小
默认情况下,无论是主线程还是子线程,栈区大小都是 512K
栈区大小可以设置[NSThread currentThread].stackSize = 1024 * 1024;
- isMainThread - 是否主线程
4.5.1、示例代码
// MARK: - 线程属性
- (void)threadProperty {
NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 1. 线程名称
t1.name = @"Thread AAA";
// 2. 优先级
t1.threadPriority = 0;
[t1 start];
NSThread *t2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
// 1. 线程名称
t2.name = @"Thread BBB";
// 2. 优先级
t2.threadPriority = 1;
[t2 start];
}
- (void)demo {
for (int i = 0; i < 10; ++i) {
// 堆栈大小
NSLog(@"%@ 堆栈大小:%tuK", [NSThreadcurrentThread], [NSThread currentThread].stackSize / 1024);
}
// 判断是否是主线程
if (![NSThread currentThread].isMainThread) {
}
}
4.6、资源共享
4.6.1、多线程的安全隐患
资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
安全隐患解决 – 互斥锁
互斥锁使用格式
@synchronized(锁对象) { // 需要锁定的代码 }注意:锁定1份代码只用1把锁,用多把锁是无效的
互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源互斥锁的使用前提:多条线程抢夺同一块资源
相关专业术语:线程同步
线程同步的意思是:多条线程在同一条线上执行(按顺序地执行任务)
互斥锁,就是使用了线程同步技术
4.6.2、资源共享-卖票
- 多线程开发的复杂度相对较高,在开发时可以按照以下套路编写代码:
1.首先确保单个线程执行正确
2.添加线程
- 卖票逻辑
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.tickets = 20;
[self saleTickets];
}
/// 卖票逻辑 - 每一个售票逻辑(窗口)应该把所有的票卖完
- (void)saleTickets {
while (YES) {
if (self.tickets > 0) {
self.tickets--;
NSLog(@"剩余票数 %d %@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"没票了 %@", [NSThreadcurrentThread]);
break;
}
}
}
添加线程
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.tickets = 20;
NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
t1.name = @"售票员 A";
[t1 start];
NSThread *t2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
t2.name = @"售票员 B";
[t2 start];
}
添加休眠
- (void)saleTickets {
while (YES) {
// 模拟休眠
[NSThreadsleepForTimeInterval:1.0];
if (self.tickets > 0) {
self.tickets--;
NSLog(@"剩余票数 %d %@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"没票了 %@", [NSThreadcurrentThread]);
break;
}
}
}
运行测试结果
4.6.3、互斥锁
- (void)saleTickets {
while (YES) {
[NSThread sleepForTimeInterval:1.0];
@synchronized(self) {
if (self.tickets > 0) {
self.tickets--;
NSLog(@"剩余票数 %d %@", self.tickets, [NSThread currentThread]);
continue;
}
}
NSLog(@"没票了 %@", [NSThreadcurrentThread]);
break;
}
}
互斥锁小结
1.保证锁内的代码,同一时间,只有一条线程能够执行!
2.互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
3.速记技巧 [[NSUserDefaults standardUserDefaults] synchronize];互斥锁参数
1.能够加锁的任意 NSObject 对象
2.注意:锁对象一定要保证所有的线程都能够访问
3.如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象
4.7、原子属性
原子属性(线程安全),是针对多线程设计的,是默认属性
多个线程在写入原子属性时(调用 setter 方法),能够保证同一时间只有一个线程执行写入操作
原子属性是一种单(线程)写多(线程)读的多线程技术
原子属性的效率比互斥锁高,不过可能会出现脏数据
在定义属性时,必须显示地指定 nonatomic,否则默认为atomic
4.7.1、代码演练
1、定义属性
@property (nonatomic, strong) NSObject *obj1;
@property (atomic, strong) NSObject *obj2;
// 模拟原子属性
@property (atomic, strong) NSObject *obj3;
2、模拟原子属性
/**
如果重写了 atomic 属性的 setter方法,就必须重写 getter 方法。
- 如果同时重写了 setter 和 getter 方法,苹果就不再提供_成员变量
- @synthesize 合成指令,用处就是指定属性的 成员变量。
*/
@synthesize obj3 = _obj3;
- (void)setObj3:(NSObject *)obj3 {
@synchronized(self) {
_obj3 = obj3;
}
}
- (NSObject *)obj3 {
return _obj3;
}
3、性能测试
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
int largeNumber = 1000 * 1000;
NSLog(@"非原子属性");
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNumber; i++) {
self.obj1 = [[NSObject alloc] init];
}
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);
NSLog(@"原子属性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNumber; i++) {
self.obj2 = [[NSObject alloc] init];
}
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);
NSLog(@"模拟原子属性");
start = CFAbsoluteTimeGetCurrent();
for (int i = 0; i < largeNumber; i++) {
self.obj3 = [[NSObject alloc] init];
}
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);
}
原子属性内部的锁是自旋锁,自旋锁的执行效率比互斥锁高
atomic:原子属性.内部也会有一把锁,叫做自旋锁. 效率比互斥锁高
4.7.2、自旋锁&互斥锁
1、共同点
都能够保证同一时间,只有一条线程执行锁定范围的代码
2、不同点
互斥锁:如果发现有其他线程正在执行锁定的代码,线程会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程会被唤醒
自旋锁:如果发现有其他线程正在执行锁定的代码,线程会以死循环的方式,一直等待锁定代码执行完成。
3、结论
自旋锁更适合执行非常短的代码
无论什么锁,都是要付出代价
4.8、线程安全
多个线程进行读写操作时,仍然能够得到正确结果,被称为线程安全
要实现线程安全,必须要用到锁
为了得到更佳的用户体验,UIKit 不是线程安全的
约定:所有更新 UI 的操作都必须主线程上执行!因此,主线程又被称为UI 线程。
- iOS 开发建议
所有属性都声明为 nonatomic
尽量避免多线程抢夺同一块资源
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
4.9、线程间通讯
主线程实现
1、定义属性
/// 根视图是滚动视图
@property (nonatomic, strong) UIScrollView*scrollView;
/// 图像视图
@property (nonatomic, weak) UIImageView *imageView;
/// 网络下载的图像
@property (nonatomic, weak) UIImage *image;
2、loadView 方法
加载视图层次结构
用纯代码开发应用程序时使用
功能和 Storyboard & XIB 是等价的
- (void)loadView {
_scrollView = [[UIScrollView alloc] init];
_scrollView.backgroundColor = [UIColor orangeColor];
self.view = _scrollView;
UIImageView *iv = [[UIImageView alloc] init];
[self.view addSubview:iv];
_imageView = iv;
}
3、viewDidLoad 方法
视图加载完成后执行
可以做一些数据初始化的工作
如果用纯代码开发,不要在此方法中设置界面 UI
- (void)viewDidLoad {
[super viewDidLoad];
// 下载图像
[self downloadImage];
}
4、下载网络图片
- (void)downloadImage{
// 1. 网络图片资源路径
NSURL *url = [NSURL URLWithString:@"[http://c.hiphotos.baidu.com/image/pic/item/4afbfbedab64034f42b14da1aec379310a551d1c.jpg](http://c.hiphotos.baidu.com/image/pic/item/4afbfbedab64034f42b14da1aec379310a551d1c.jpg)"];
// 2. 从网络资源路径实例化二进制数据(网络访问)
NSData *data = [NSData dataWithContentsOfURL:url];
// 3. 将二进制数据转换成图像
UIImage *image = [UIImage imageWithData:data];
// 4. 设置图像
self.image = image;
}
5、设置图片
- (void)setImage:(UIImage *)image {
// 1. 设置图像视图的图像
self.imageView.image = image;
// 2. 按照图像大小设置图像视图的大小
[self.imageView sizeToFit];
// 3. 设置滚动视图的 contentSize
self.scrollView.contentSize = image.size;
}
6、设置滚动视图的缩放
设置滚动视图缩放属性
// 1> 最小缩放比例
self.scrollView.minimumZoomScale = 0.5;
// 2> 最大缩放比例
self.scrollView.maximumZoomScale = 2.0;
// 3> 设置代理
self.scrollView.delegate = self;
实现代理方法 - 告诉滚动视图缩放哪一个视图
#pragma mark - UIScrollViewDelegate 代理方法
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
7、线程间通讯
在后台线程下载图像
[self performSelectorInBackground:@selector(downloadImage) withObject:nil];
在主线程设置图像
// waitUntilDone:是否等待主线程执行完毕 setImage:方法。
// YES:等待 NO:不等待
// 一般不用等待,直接设置 NO 即可
[self performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];