多线程基础

#一、多线程基础
基本概念

  • 进程
    进程是指在系统中正在运行的一个应用程序
    每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
    通过 活动监视器 可以查看 Mac 系统中所开启的进程

  • 线程
    进程要想执行任务,必须得有线程,进程至少要有一条线程
    程序启动会默认开启一条线程,这条线程被称为主线程或UI 线程
    线程是进程的基本执行单元,进程的所有任务都在线程中执行

  • 多线程

    • 一个进程中可以开启多条线程,每条线程可以同时执行不同的任务
      进程 -> 公司
      线程 -> 员工
      主线程 -> 老板(第一个员工)
    • 多线程技术可以提高程序的执行效率

  • 多线程原理
    • 同一时间,CPU只能处理一条线程,只有一条线程在执行
    • 多线程同时执行,其实是CPU快速地在多条线程之间切换
    • 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
    • 如果线程非常多,会在多条线程之间来回切换,消耗大量的 CPU 资源
      • 每个线程被调度的次数会降低
      • 线程的执行效率会下降

iOS 8.0 主线程的默认堆栈大小也是 512K
多线程基础

  • 多线程优缺点
    • 优点
      能适当提高资源利用率(CPU、内存利用率)
      能适当提高程序的执行效率

* 缺点
开启线程需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享

  • 主线程
    • 程序启动创建的线程,被称为主线程或UI 线程
    • 主线程的作用
      • 显示/刷新 UI 界面
      • 处理 UI 事件:点击、滚动、拖拽等事件
    • 注意:要将耗时操作放在后台线程执行,否则会影响 UI 的流畅度,破坏用户体验
    • 所有网络访问都是耗时操作!

  • iOS中多线程的实现方案

多线程基础

###1、多线程的优缺点
 多线程基础

###2、耗时操作示例

- (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 不能继续响应用户点击和拖拽事件。
学习多线程的目的:就是将耗时操作放到后台去执行。

###3、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

1、创建线程的方式(3种)
准备在后台线程调用的方法 longOperation:

  • (void)longOperation:(id)obj {

NSLog(@"%@ - %@", [NSThread currentThread], obj);

}

1.1、alloc / init - start

  • (void)threadDemo1 {
        // 创建线程
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download:) object:@“Alloc”];
        // 开启线程
        [thread start];
        NSLog(@“after %@”, [NSThread currentThread]);

}

小结

[thread start];执行后,会在另外一个线程执行 longOperation: 方法
在 OC 中,任何一个方法的代码都是从上向下顺序执行的
同一个方法内的代码,都是在相同线程执行的(block除外)

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 方法。

1.3、分类方法:performSelectorInBackground

  • (void)threadDemo3 {
        NSLog(@“before %@”, [NSThread currentThread]);
        [self performSelectorInBackground:@selector(longOperation:) withObject:@“PERFORM”];
        NSLog(@“after %@”, [NSThread currentThread]);

}

代码小结
performSelectorInBackground 是 NSObject 的分类方法。
会自动在后台线程执行 @selector 方法。
没有 thread 字眼,隐式创建并启动线程。
所有 NSObject 都可以使用此方法,在其他线程执行方法

=====================================
创建和启动线程
一个NSThread对象就代表一条线程
创建、启动线程

NSThread*thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(run) object:nil];
[thread start];
// 线程一启动,就会在线程thread中执行self的run方法
主线程相关用法

  • (NSThread*)mainThread;// 获得主线程
  • (BOOL)isMainThread;// 是否为主线程
  • (BOOL)isMainThread;// 是否为主线程

其他用法
获得当前线程

NSThread*current = [NSThreadcurrentThread];
线程的调度优先级

  • (double)threadPriority;
  • (BOOL)setThreadPriority:(double)p;
  • (double)threadPriority;

  • (BOOL)setThreadPriority:(double)p;
    调度优先级的取值范围是0.0~1.0,默认0.5,值越大,优先级越高
    线程的名字

  • (void)setName:(NSString*)n;

  • (NSString*)name;

其他创建线程方式
创建线程后自动启动线程

[NSThreaddetachNewThreadSelector:@selector(run) toTarget:selfwithObject:nil];
隐式创建并启动线程

[selfperformSelectorInBackground:@selector(run) withObject:nil];
上述2种创建线程方式的优缺点
优点:简单快捷
缺点:无法对线程进行更详细的设置

=====================================

2、NSThread的Target
NSThread 的实例化方法中的 target 指的是开启线程后,在线程中执行 哪一个对象 的 @selector 方法。

代码演练
准备对象

@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 对象的方法

3、线程状态
多线程基础
新建
实例化线程对象

就绪
向线程对象发送 start 消息,线程对象被加入 可调度线程池 等待 CPU 调度
detach 方法和 performSelectorInBackground 方法会直接实例化一个线程对象并加入 可调度线程池

运行
CPU 负责调度可调度线程池中线程的执行
线程执行完成之前,状态可能会在就绪和运行之间来回切换
就绪和运行之间的状态变化由 CPU 负责,程序员不能干预

阻塞
当满足某个预定条件时,可以使用休眠或锁阻塞线程执行
sleepForTimeInterval:休眠指定时长
sleepUntilDate:休眠到指定日期
@synchronized(self):互斥锁

死亡
正常死亡
线程执行完毕

非正常死亡
当满足某个条件后,在线程内部中止执行。
当满足某个条件后,在主线程中止线程对象。

[NSThread exit];
一旦强行终止线程,后续的所有代码都不会被执行
注意:在终止线程之前,应该注意释放之前分配的对象!

控制线程状态

启动线程

  • (void)start;

// 进入就绪状态 ->运行状态。当线程任务执行完毕,自动进入死亡状态

阻塞(暂停)线程

  • (void)sleepUntilDate:(NSDate*)date;

  • (void)sleepForTimeInterval:(NSTimeInterval)ti;

// 进入阻塞状态

强制停止线程

  • (void)exit;

// 进入死亡状态

注意:一旦线程停止(死亡)了,就不能再次开启任务

3.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];
}

3.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];

}

3.3、代码小结
阻塞

方法执行过程,符合某一条件时,可以利用 sleep 方法让线程进入 阻塞 状态
sleepForTimeInterval 从现在起睡多少秒
sleepUntilDate 从现在起睡到指定的日期

死亡

[NSThread exit];
一旦强行终止线程,后续的所有代码都不会被执行
注意:在终止线程之前,应该注意释放之前分配的对象!

注意:线程从就绪和运行状态之间的切换是由 CPU 负责的,程序员无法干预

4、线程的属性
1)name - 线程名称(需要设置)
在大的商业项目中,通常需要在程序崩溃时,获取程序准确执行所在的线程。

2)threadPriority - 线程优先级
优先级,是一个浮点数,取值范围从 0~1.0
1.0表示优先级最高
0.0表示优先级最低
默认优先级是0.5

优先级高只是保证 CPU 调度频率的可能性会高
醒哥个人建议:在开发的时候,不要修改优先级,调度频率快慢由 CPU决定。
多线程的目的:是将耗时的操作放在后台,不阻塞主线程和用户的交互!
多线程开发的原则:简单

3)stackSize - 栈区大小
默认情况下,无论是主线程还是子线程,栈区大小都是 512K
栈区大小可以设置

[NSThread currentThread].stackSize = 1024 * 1024;

4)isMainThread - 是否主线程

4.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) {
       }

}

5、资源共享(掌握)

5.1、安全隐患介绍–PPT

多线程的安全隐患
资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
比如多个线程访问同一个对象、同一个变量、同一个文件
当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

多线程基础
多线程基础
多线程基础
多线程基础

安全隐患解决 – 互斥锁

互斥锁使用格式

@synchronized(锁对象) { // 需要锁定的代码  }

注意:锁定1份代码只用1把锁,用多把锁是无效的

互斥锁的优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:需要消耗大量的CPU资源

互斥锁的使用前提:多条线程抢夺同一块资源

相关专业术语:线程同步
线程同步的意思是:多条线程在同一条线上执行(按顺序地执行任务)
互斥锁,就是使用了线程同步技术

5.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;
            }
        }
    }

运行测试结果

5.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;
        }
    }
    互斥锁小结
    保证锁内的代码,同一时间,只有一条线程能够执行!
    互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
    速记技巧 [[NSUserDefaults standardUserDefaults] synchronize];

互斥锁参数
能够加锁的任意 NSObject 对象
注意:锁对象一定要保证所有的线程都能够访问
如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象

6、原子属性
原子属性(线程安全),是针对多线程设计的,是默认属性
多个线程在写入原子属性时(调用 setter 方法),能够保证同一时间只有一个线程执行写入操作
原子属性是一种单(线程)写多(线程)读的多线程技术
原子属性的效率比互斥锁高,不过可能会出现脏数据
在定义属性时,必须显示地指定 nonatomic,否则默认为atomic

6.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:原子属性.内部也会有一把锁,叫做自旋锁. 效率比互斥锁高

6.2、自旋锁&互斥锁
1、共同点
都能够保证同一时间,只有一条线程执行锁定范围的代码

2、不同点
互斥锁:如果发现有其他线程正在执行锁定的代码,线程会进入休眠状态,等待其他线程执行完毕,打开锁之后,线程会被唤醒
自旋锁:如果发现有其他线程正在执行锁定的代码,线程会以死循环的方式,一直等待锁定代码执行完成。

3、结论
自旋锁更适合执行非常短的代码
无论什么锁,都是要付出代价

7、线程安全
多个线程进行读写操作时,仍然能够得到正确结果,被称为线程安全
要实现线程安全,必须要用到锁
为了得到更佳的用户体验,UIKit 不是线程安全的

约定:所有更新 UI 的操作都必须主线程上执行!因此,主线程又被称为UI 线程。
 
iOS 开发建议
所有属性都声明为 nonatomic
尽量避免多线程抢夺同一块资源
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

8、线程间通讯(掌握)
主线程实现
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”];
        // 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];