• iOS开发中多线程基础


    耗时操作演练

    代码演练

    • 编写耗时方法
    - (void)longOperation {
        for (int i = 0; i < 10000; ++i) {
            NSLog(@"%@ %d", [NSThread currentThread], i);
        }
    }
    • 直接调用耗时方法
    // 1> 直接调用耗时方法
    [self longOperation];

    运行測试效果

    • 在后台运行耗时方法
    // 2> 在后台运行耗时方法
    [self performSelectorInBackground:@selector(longOperation) withObject:nil];

    运行測试效果

    小结

    1. [NSThread currentThread]:当前线程对象
      • 能够在全部的多线程技术中使用!
      • 通经常使用来在多线程开发中。Log 代码是否在主线程运行
    2. number
      • number == 1 主线程
      • number != 1 后台线程
      • 不要纠结 number 的具体数字

    pthread演练

    • pthreadPOSIX 多线程开发框架,因为是跨平台的 C 语言框架。在苹果的头文件里并没有具体的凝视
    • 要查阅 pthread 有关资料,能够訪问 http://baike.baidu.com

    导入头文件

    #import <pthread.h>

    pthread演练

    // 创建线程。并且在线程中运行 demo 函数
    - (void)pthreadDemo {
    
        /**
         參数:
         1> 指向线程标识符的指针,C 语言中类型的结尾通常 _t/Ref,并且不须要使用 *
         2> 用来设置线程属性
         3> 线程运行函数的起始地址
         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;
    }

    小结

    1. 在 C 语言中,没有对象的概念。对象是以结构体的方式来实现的
    2. 通常,在 C 语言框架中,对象类型以 _t/Ref 结尾,并且声明时不须要使用 *
    3. C 语言中的 void * 和 OC 中的 id 是等价的
    4. 内存管理
      • 在 OC 中,假设是 ARC 开发,编译器会在编译时。依据代码结构,自己主动加入 retain/release/autorelease
      • 可是。ARC 仅仅负责管理 OC 部分的内存管理,而不负责 C 语言 代码的内存管理
      • 因此,开发过程中。假设使用的 C 语言框架出现 retain/create/copy/new 等字样的函数,大多都须要 release,否则会出现内存泄漏
    5. 在混合开发时,假设在 COC 之间传递数据,须要使用 __bridge 进行桥接,桥接的目的就是为了告诉编译器怎样管理内存
    6. 桥接的加入能够借助 Xcode 的辅助功能加入
    7. MRC 中不须要使用桥接

    三种创建线程的方法

    准备函数

    // MARK: - 后台线程调用函数
    - (void)longOperation:(id)obj {
        NSLog(@"%@ - %@", [NSThread currentThread], obj);
    }

    1. alloc / init - start

    // MARK: - NSThread 演练
    - (void)threadDemo1 {
        // 1. 实例化线程对象 => alloc(分配内存) / init(初始化)
        NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(longOperation:) object:@"alloc/init"];
    
        // 2. 启动线程
        [t start];
    
        // 3. 当前线程?
        NSLog(@"%@", [NSThread currentThread]);
    }

    演练小结

    1. [t start];运行后。会在另外一个线程运行 demo 方法
    2. 在 OC 中。不论什么一个方法的代码都是从上向下顺序运行的
    3. 同一个方法内的代码,都是在同样线程运行的(block除外)

    2. detachNewThreadSelector

    - (void)threadDemo2 {
        // detach => 分离一个子线程运行 demo: 方法
        [NSThread detachNewThreadSelector:@selector(longOperation:) toTarget:self withObject:@"Detach"];
    
        // 2. 当前线程?
        NSLog(@"%@", [NSThread currentThread]);
    }

    演练小结

    • detachNewThreadSelector 类方法不须要启动,创建线程后自己主动启动线程运行 @selector 方法

    3. 分类方法

    - (void)threadDemo3 {
        // 1. 在后台运行 @selector 方法
        [self performSelectorInBackground:@selector(longOperation:) withObject:@"category"];
    
        // 2. 当前线程?
        NSLog(@"%@", [NSThread currentThread]);
    }
    1. performSelectorInBackgroundNSObject 的分类方法
    2. 没有 thread 字眼,会马上在后台线程运行 @selector 方法
    3. 全部 NSObject 都能够使用此方法,在其它线程运行方法!

    自己定义对象

    Person 类

    // MARK: - Person 类
    @interface Person : NSObject
    /// 姓名
    @property (nonatomic, copy) NSString *name;
    @end
    
    @implementation Person
    
    /// 使用字典实例化对象
    + (instancetype)personWithDict:(NSDictionary *)dict {
        Person *p = [[Person alloc] init];
    
        [p setValuesForKeysWithDictionary:dict];
    
        return p;
    }
    
    /// 载入数据
    - (void)loadData {
        NSLog(@"载入数据 %@ %@", [NSThread currentThread], self.name);
    }
    
    @end

    Person 类使用分类方法

    - (void)threadDemo4 {
        Person * p = [Person personWithDict:@{@"name": @"zhangsan"}];
    
        [p performSelectorInBackground:@selector(loadData) withObject:nil];
    }

    线程状态

    演练代码

    // MARK: - 线程状态演练
    - (void)statusDemo {
    
        NSLog(@"睡会");
        [NSThread sleepForTimeInterval:1.0];
    
        for (int i = 0; i < 20; ++i) {
            if (i == 8) {
                NSLog(@"再睡会");
                [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
            }
    
            NSLog(@"%@ %d", [NSThread currentThread], i);
    
            if (i == 10) {
                NSLog(@"88");
                [NSThread exit];
            }
        }
        NSLog(@"能来吗?");
    }
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        // 注意不要在主线程上调用 exit 方法
    //    [NSThread exit];
    
        // 实例化线程对象(新建)
        NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(statusDemo) object:nil];
    
        // 线程就绪(被加入到可调度线程池中)
        [t start];
    }

    堵塞

    • 方法运行过程。符合某一条件时,能够利用 sleep 方法让线程进入 堵塞 状态

    1> sleepForTimeInterval

    • 从如今起睡多少

    2> sleepUntilDate

    • 从如今起睡到指定的日期

    死亡

    [NSThread exit];

    • 一旦强行终止线程,兴许的全部代码都不会被运行
    • 注意:在终止线程之前,应该注意释放之前分配的对象!

    就绪 -> 运行

    线程从就绪运行状态之间的切换是由 CPU 负责的。程序猿无法干预

    线程属性

    演练代码

    // 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", [NSThread currentThread], [NSThread currentThread].stackSize / 1024);
        }
    
        // 模拟崩溃
        // 推断是否是主线程
    //    if (![NSThread currentThread].isMainThread) {
    //        NSMutableArray *a = [NSMutableArray array];
    //
    //        [a addObject:nil];
    //    }
    }

    属性

    1. name - 线程名称

    • 在大的商业项目中。通常须要在程序崩溃时。获取程序准确运行所在的线程

    2. threadPriority - 线程优先级

    • 优先级,是一个浮点数,取值范围从 0~1.0
      • 1.0表示优先级最高
      • 0.0表示优先级最低
      • 默认优先级是0.5
    • 优先级高仅仅是保证 CPU 调度的可能性会高
    • 刀哥个人建议,在开发的时候。不要改动优先级
    • 多线程的目的:是将耗时的操作放在后台,不堵塞主线程和用户的交互。
    • 多线程开发的原则:简单

    3. stackSize - 栈区大小

    • 默认情况下,不管是主线程还是子线程。栈区大小都是 512K
    • 栈区大小能够设置
    [NSThread currentThread].stackSize = 1024 * 1024;

    4. isMainThread - 是否主线程

    资源共享-卖票

    多线程开发的复杂度相对较高,在开发时能够依照下面套路编写代码:

    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(@"没票了 %@", [NSThread currentThread]);
                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) {
            // 模拟休眠
            [NSThread sleepForTimeInterval:1.0];
    
            if (self.tickets > 0) {
                self.tickets--;
                NSLog(@"剩余票数 %d %@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"没票了 %@", [NSThread currentThread]);
                break;
            }
        }
    }

    运行測试结果

    相互排斥锁

    加入相互排斥锁

    - (void)saleTickets {
        while (YES) {
            // 模拟休眠
            [NSThread sleepForTimeInterval:1.0];
    
            @synchronized(self) {
                if (self.tickets > 0) {
                    self.tickets--;
                    NSLog(@"剩余票数 %d %@", self.tickets, [NSThread currentThread]);
                } else {
                    NSLog(@"没票了 %@", [NSThread currentThread]);
                    break;
                }
            }
        }
    }

    相互排斥锁小结

    1. 保证锁内的代码。同一时间,仅仅有一条线程能够运行!

    2. 相互排斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差。
    3. 速记技巧 [[NSUserDefaults standardUserDefaults] synchronize];

    相互排斥锁參数

    1. 能够加锁的随意 NSObject 对象
    2. 注意:锁对象一定要保证全部的线程都能够訪问
    3. 假设代码中仅仅有一个地方须要加锁,大多都使用 self。这样能够避免单独再创建一个锁对象

    原子属性

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

    演练代码

    @interface ViewController ()
    @property (atomic, strong) NSObject *obj1;
    @property (atomic, strong) NSObject *obj2;
    @end
    
    @implementation ViewController
    @synthesize obj1 = _obj1;
    
    // 原子属性模拟代码
    /// obj1 - getter
    - (NSObject *)obj1 {
        return _obj1;
    }
    
    /// obj1 - setter
    - (void)setObj1:(NSObject *)obj1 {
        @synchronized(self) {
            _obj1 = obj1;
        }
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        long largeNumber = 1000 * 1000;
    
        // 相互排斥锁測试
        CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
        for (int i = 0; i < largeNumber; ++i) {
            self.obj1 = [[NSObject alloc] init];
        }
        NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);
    
        // 自旋锁測试
        start = CFAbsoluteTimeGetCurrent();
        for (int i = 0; i < largeNumber; ++i) {
            self.obj2 = [[NSObject alloc] init];
        }
        NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);
    }
    
    @end

    原子属性内部的锁是自旋锁自旋锁的运行效率比相互排斥锁高

    自旋锁 & 相互排斥锁

    • 共同点

      • 都能够保证同一时间。仅仅有一条线程运行锁定范围的代码
    • 不同点

      • 相互排斥锁:假设发现有其它线程正在运行锁定的代码。线程会进入休眠状态,等待其它线程运行完毕。打开锁之后,线程会被唤醒
      • 自旋锁:假设发现有其它线程正在运行锁定的代码。线程会以死循环的方式,一直等待锁定代码运行完毕
    • 结论

      • 自旋锁更适合运行很短的代码
      • 不管什么锁。都是要付出代价

    线程安全

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

    约定:全部更新 UI 的操作都必须主线程上运行。

    • 因此。主线程又被称为UI 线程

    iOS 开发建议

    1. 全部属性都声明为 nonatomic
    2. 尽量避免多线程抢夺同一块资源
    3. 尽量将加锁、资源抢夺的业务逻辑交给server端处理,减小移动client的压力

    线程间通讯

    主线程实现

    定义属性

    /// 根视图是滚动视图
    @property (nonatomic, strong) UIScrollView *scrollView;
    /// 图像视图
    @property (nonatomic, weak) UIImageView *imageView;
    /// 网络下载的图像
    @property (nonatomic, weak) UIImage *image;

    loadView

    loadView 方法的作用:

    1. 载入视图层次结构
    2. 用纯代码开发应用程序时使用
    3. 功能和 Storyboard & XIB 是等价的

    假设重写了 loadViewStoryboard & XIB 都无效

    - (void)loadView {
        self.scrollView = [[UIScrollView alloc] init];
        self.scrollView.backgroundColor = [UIColor orangeColor];
        self.view = self.scrollView;
    
        UIImageView *iv = [[UIImageView alloc] init];
        [self.view addSubview:iv];
        self.imageView = iv;
    }

    viewDidLoad

    1. 视图载入完毕后运行
    2. 能够做一些数据初始化的工作
    3. 假设用纯代码开发,不要在此方法中设置界面 UI
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 下载图像
        [self downloadImage];
    }

    下载网络图片

    - (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;
    }

    设置图片

    - (void)setImage:(UIImage *)image {
        // 1. 设置图像视图的图像
        self.imageView.image = image;
    
        // 2. 依照图像大小设置图像视图的大小
        [self.imageView sizeToFit];
    
        // 3. 设置滚动视图的 contentSize
        self.scrollView.contentSize = image.size;
    }

    设置滚动视图的缩放

    1> 设置滚动视图缩放属性

    // 1> 最小缩放比例
    self.scrollView.minimumZoomScale = 0.5;
    // 2> 最大缩放比例
    self.scrollView.maximumZoomScale = 2.0;
    // 3> 设置代理
    self.scrollView.delegate = self;

    2> 实现代理方法 - 告诉滚动视图缩放哪一个视图

    - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
        return self.imageView;
    }

    3> 跟踪 scrollView 缩放效果

    - (void)scrollViewDidZoom:(UIScrollView *)scrollView {
        NSLog(@"%@", NSStringFromCGAffineTransform(self.imageView.transform));
    }

    线程间通讯

    • 在后台线程下载图像
    [self performSelectorInBackground:@selector(downloadImage) withObject:nil];
    • 在主线程设置图像
    [self performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
    }
  • 相关阅读:
    NIO通道的学习笔记
    Struts学习笔记(启动过程)
    Struts2学习笔记(ResultType)
    11
    编写类String的构造函数、析构函数和赋值函数(转载)
    new与malloc的区别
    不用判断语句,求两个数中大的那个
    delete p和delete[] p的区别(转)
    (转)虚函数和纯虚函数区别
    不借助第三个变量交换两个整数的值
  • 原文地址:https://www.cnblogs.com/blfbuaa/p/7301068.html
Copyright © 2020-2023  润新知