01-多线程

一、多线程基础

基本概念

  • 进程
    进程是指在系统中正在运行的一个应用程序
    每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
    通过 活动监视器 可以查看 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];

推荐阅读更多精彩内容