应用沙盒

为了缓解这个问题,App Sandbox 策略两个方面:

  1. 应用沙盒使您能够描述您的应用如何系统交互然后系统会授予您的应用完成工作所需的访问权限,仅此而已。
  2. App Sandbox 允许用户通过打开保存对话框拖放和其他熟悉的用户交互透明地授予您的应用额外访问权限

../艺术/about_sandboxing.png

应用沙盒基于一些简单原则

通过每个应用程序的基础上限制敏感资源的访问,如果攻击者成功利用应用程序中的安全漏洞,应用沙盒提供了最后一道防线,防止用户数据被盗、损坏或删除,或系统硬件被劫持. 例如,沙盒应用程序必须明确声明使用权利使用以下任何资源的意图:

系统运行时拒绝对项目定义中未明确请求的任何资源的访问。例如,如果您正在编写一个草图应用程序,并且您知道您的应用程序永远不需要访问麦克风,那么您就不需要请求访问权限,并且系统知道拒绝您的(可能受到威胁的)应用程序使用的任何尝试它。

另一方面,沙盒应用程序可以访问您请求的特定资源,允许用户通过以通常方式例如拖放执行典型操作扩展沙盒,并且可以自动执行许多被认为安全附加操作,包括:

解决应用沙盒违规问题

如果您的应用尝试执行应用沙盒不允许的操作,则会发生应用沙盒违规例如,您已经在本快速入门看到沙盒应用程序无法从 Web 检索内容对系统资源访问的细粒度限制是应用沙盒如何在应用受到恶意代码入侵时提供保护核心。解决此类违规涉及在 Xcode添加与您的应用程序需要功能相对应的特定权利。

通过添加适当的权利来解决应用沙盒违规

  1. 退出快速入门应用程序。

  2. 目标编辑器的 Capabilities 选项卡中,在 App Sandbox 部分中,选择与 Outgoing Connections (Client) 对应的权利。这样做会TRUE通过修改.entitlements属性列表文件将所需权利的值应用到 Xcode 项目

    注意 网络权利是根据谁建立连接而不是数据流的主要方向指定的。在此示例中,您需要出站连接功能,因为应用程序正在启动连接。充当服务器的应用程序需要入站连接权利。

  3. 构建并运行应用程序。

    预期的网页现在显示在应用程序中。

容器目录文件系统访问

当您采用 App Sandbox 时,您的应用程序可以访问以下位置

这些政策将在以下各节中进一步详述。

文档存档开发商

搜索

应用沙盒设计指南

  • 目录

介绍应用沙盒快速入门创建 Xcode 项目确认应用已被沙盒化解决应用沙盒违规问题下一步深度应用沙盒需要最后一道防线权利和系统资源访问容器目录和文件系统访问安全范围书签和持久资源访问应用沙盒和代码签名外部工具、XPC 服务和权限分离IPC 和 POSIX 信号量和共享内存为应用沙盒设计将应用程序迁移到沙盒应用沙盒清单修订记录

下一个以前的

深度应用沙盒

采用 App Sandbox 的具体步骤因您的 App 而异,但 App Sandbox 用于保护用户数据的访问控制机制保持一致:

需要最后一道防线

您可以按照*安全编码指南中*推荐的做法保护您的应用程序免受恶意软件攻击。但是,尽管您尽了最大努力来构建一个无懈可击的屏障——通过避免缓冲区溢出和其他内存损坏、防止用户数据泄露以及消除其他漏洞——您的应用程序仍可能恶意代码利用攻击者只需在您的防御或您链接的任何框架库中找到一个漏洞,即可控制您的应用程序与系统的交互。

App Sandbox 旨在通过让您描述您的应用程序与系统的预期交互来正面应对这种情况。然后,系统仅授予您的应用完成其工作所需的访问权限。如果恶意代码获得了正确沙盒应用程序的控制权,它只能访问应用程序沙盒中的文件和资源。

成功采用 App Sandbox,请使用不同于您可能习惯的思维方式,如表 2-1 所示

开发时… 采用 App Sandbox 时……
添加功能 最小化系统资源使用
利用整个应用程序的访问权限 分区功能然后信任每个部分
使用最方便的API 使用最安全的 API
限制视为限制 限制视为保护措施

权利和系统资源访问

未经沙盒化的应用程序可以访问所有用户可访问的系统资源,包括内置摄像头和麦克风、网络接字打印以及大部分文件系统。如果成功受到恶意代码攻击,此类应用程序可能会表现为具有广泛潜在危害的敌对代理

当您为您的应用程序启用应用程序沙盒时,您将删除最少量权限之外的所有权限,然后使用权利有意地逐个恢复它们。的权利是一个键-值对标识的特定功能,如能力以打开出站网络套接字

一项特殊权利——启用应用沙盒——开启了应用沙盒。当您启用沙箱时,Xcode创建一个.entitlements 属性列表文件并在项目导航器中显示它。

如果您的应用程序需要功能,请使用目标编辑器的“摘要选项卡向您的 Xcode 项目添加相应的权利来请求它。如果您不需要功能,请注意不要包含相应的权利。

您在逐个目标的基础上请求权利。如果您的应用程序只有一个目标(主应用程序),您只需为该目标请求授权。如果您将应用程序设计为使用主应用程序和帮助程序(以 XPC 服务的形式),您需要单独为每个目标请求权利。您可以在外部工具、XPC 服务和权限分离中了解更多相关信息

您可能需要对应用程序的权利进行比 Xcode 目标编辑器可用的更细粒度的控制。例如,您可能会请求临时例外权利,因为 App Sandbox 不支持您的应用程序所需的功能,例如向尚未提供任何脚本访问组的应用程序发送 Apple 事件的功能。要使用临时异常权限,请使用 Xcode 属性列表编辑器直接编辑目标.entitlements属性列表文件。

注意 如果您请求临时例外权利,请务必遵循iTunes Connect网站上提供的有关权利的指南。特别是,提交一个 bug,询问您需要的功能,并使用 iTunes Connect 中的 Review Notes 字段解释为什么您的应用程序需要临时异常。请务必提供错误编号

macOS App Sandbox 权利在*Entitlement Key Reference*中的启用 App Sandbox中进行了描述。有关为 Xcode 项目中的目标请求权利的演练,请参阅App Sandbox 快速入门

容器目录和文件系统访问

当您采用 App Sandbox 时,您的应用程序可以访问以下位置

  • **应用程序容器目录。**首次启动时,操作系统创建一个特殊目录供您的应用程序使用——并且仅供您的应用程序使用——称为容器。系统上的每个用户都在他们主目录中为您的应用程序获取一个单独的容器;您的应用程序对运行它的用户具有不受限制的容器读/写访问权限。
  • **应用组容器目录。**沙盒应用程序可以指定一项权利,使其能够访问一个或多个应用程序组容器目录,每个目录在具有该权利的所有应用程序之间共享。
  • **用户指定的文件。**当这些文件被用户明确打开或被用户拖放到应用程序上时,沙盒应用程序(具有适当的权利)会自动获取任意位置的文件的访问权限。
  • **相关项目。**通过适当的授权,您的应用程序可以访问与用户指定的文件同名但扩展名不同的文件。这可用于访问功能相关的文件(例如与电影关联的字幕文件)或以不同格式保存修改后的文件(例如在用户添加图片)。
  • **临时目录、命令工具目录和特定的世界可读位置。**沙盒应用程序对某些其他明确定义的位置中的文件具有不同程度的访问权限。

这些政策将在以下各节中进一步详述。

应用沙盒容器目录

应用沙盒容器目录具有以下特点:

容器外的 Powerbox 和文件系统访问

您的沙盒应用程序可以通过以下三种方式访问其容器外的文件系统位置:

与用户交互以扩展您的沙箱macOS 安全技术称为Powerbox。Powerbox 没有 API。当您使用NSOpenPanelNSSavePanel类时,您的应用程序会透明地使用 Powerbox 。您可以通过使用 Xcode 设置权利来启用 Powerbox,如*权利密钥参考*中的启用用户选择的文件访问中所述。

当您从沙盒应用程序调用打开或保存对话框时,出现窗口不是由 AppKit 而是由 Powerbox 显示的。当您采用 App Sandbox 时,Powerbox 的使用是自动的——它不需要对您的应用程序的 presandbox 版本进行代码更改。您为打开或保存而实现附件面板将忠实地呈现和使用。

IPC 和 POSIX 信号量共享内存

通常,沙盒应用程序不能使用 Mach IPC、POSIX 信号量共享内存或 UNIX 域套接字(有用)。但是,通过指定请求加入应用程序组的权利,应用程序可以使用这些技术与该应用程序组的其他成员进行通信

注意 沙盒应用程序不支持 System V 信号量

UNIX 域套接字简单;它们就像任何其他文件一样工作

您希望在沙盒应用程序中访问的任何信号量或 Mach 端口都必须根据特殊约定命名

例如,如果您的应用程序组的名称Z123456789.com.example.app-group,您可以创建两个名为Z123456789.myappgroup/rdyllwflg和 的信号量Z123456789.myappgroup/bluwhtflg。您可以创建一个名为Z123456789.com.example.app-group.Port_of_Kobe.

NSFileManager

    //NSFileManager 用于判断
    
    NSString *filePath = @"/Users/zhaoxiaohu/Desktop/arr.plist";
    
     NSString *filePath2 = @"/";
1) 判断文件是否存在
    //创建文件管理对象
    //调用defaultManager 创建一个文件管理单例对象
    //单例对象:在程序运行期间,只有一个对象存在
    NSFileManager *fm = [NSFileManager defaultManager];
    // YES 存在   NO 不存在
    BOOL isYES = [fm fileExistsAtPath:filePath];
    NSLog(@"-->%d",isYES);
2) 判断是否是一个目录
    if(isYES){
        BOOL isDir;
    
        // 2) 判断是否是一个目录
        [fm fileExistsAtPath:filePath isDirectory:&isDir];
        
        if (isDir) {
            NSLog(@"这是一个目录");
        }else{
        
            NSLog(@"这不是一个目录");
        }
    
    }
3) 判断文件是否可读
    isYES = [fm isReadableFileAtPath:filePath];
4) 是否可写
    isYES = [fm isWritableFileAtPath:filePath2];
5) 是否可删除
    isYES = [fm isDeletableFileAtPath:filePath2];
    
             NSLog(@"-->%d",isYES);
6)获取文件的信息(属性)
    //创建文件对象
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString *filePath = @"/Users/zhaoxiaohu/Desktop/arr.plist";
    
    NSString *dirPath = @"/Users/zhaoxiaohu/Desktop/a";
   
    //1)如何获取文件的信息(属性)
    NSDictionary *dict = [fm attributesOfItemAtPath:filePath error:nil];
    NSLog(@"%@",dict);
    NSLog(@"%@,%@",[dict objectForKey:@"NSFileOwnerAccountName"],dict[@"NSFileOwnerAccountName"]);
7)获取指定目录下文件及子目录
    //使用递归方式 获取当前目录及子目录下的所有的文件及文件夹
    NSArray *subPaths = [fm subpathsAtPath:dirPath];
    
    //(推荐使用)subpathsOfDirectoryAtPath 不是使用递归方式获取的
    subPaths = [fm subpathsOfDirectoryAtPath:dirPath error:nil];
    NSLog(@"subPaths = %@",subPaths);
8)获取指定目录下的文件及目录信息(不在获取后代路径)
    subPaths = [fm contentsOfDirectoryAtPath:dirPath error:nil];
    NSLog(@"subPaths = %@",subPaths);
9)创建目录
   //创建文件管理对象
    NSFileManager *fm = [NSFileManager defaultManager];
    
    //如何创建目录 (路径  :/Users/zhaoxiaohu/Desktop/aaa)
    NSString *createDirPath = @"/Users/zhaoxiaohu/Desktop/aaa/ccc/bbb/love.txt";
//    fm createDirectoryAtPath:@"路径" withIntermediateDirectories:YES/NO 创建路径的时候,YES自动创建路径中缺少的目录,NO的不会创建缺少的目录 attributes:属性字典 error:错误对象
    
    BOOL isYES = [fm createDirectoryAtPath:createDirPath withIntermediateDirectories:YES attributes:nil error:nil];
   
    if (isYES) {
        NSLog(@"成功");
   }
10)创建文件
    //如何创建文件
    NSString *str = @"每当我错过一个女孩,我就向山上放一块砖,于是就有了长城";
    //writeToFile
    //fm createFileAtPath:@"路径" contents: NSData类型的数据 attributes:文件的属性字典
    //创建NSData?   是一个处理二进制数据的类
    //NSString -----> NSData  (把NSString转化成NSData)
    
    NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
    BOOL isYes;
    // createFileAtPath 创建文件
    isYes = [fm createFileAtPath:createDirPath contents:data attributes:nil];
    NSLog(@"isYes = %d",isYes);
11)copy文件
    //如何copy文件
    NSString *targetPath = @"/Users/zhaoxiaohu/Desktop/aaa/ccc/love.txt";
    [fm copyItemAtPath:createDirPath toPath:targetPath error:nil];
    
     NSString *targetPath = @"/Users/zhaoxiaohu/Desktop/aaa/love.txt";
12)移动文件
    //如何移动文件
    [fm moveItemAtPath:createDirPath toPath:targetPath error:nil];
13)删除文件
    //如何删除文件
    [fm removeItemAtPath:targetPath error:nil];

NSFileHandle

注意:NSFileHandle类主要对文件内容进行读取写入操作,可以使用NSFileHandle做文件的断点续传。
NSFileHandle 此类主要是对文件内容进行读取写入操作
NSFileMange 此类主要是对文件进行的操作以及文件信息的获取

常用处理方法
+ (id)fileHandleForReadingAtPath:(NSString *)path //打开一个文件准备读取`
 `+ (id)fileHandleForWritingAtPath:(NSString *)path //打开一个文件准备写入`   
 `+ (id)fileHandleForUpdatingAtPath:(NSString *)path //打开一个文件准备更新`
 `- (NSData *)availableData; //从设备通道返回可用的数据`            
 `- (NSData *)readDataToEndOfFile; //从当前节点读取到文件的末尾`              
 `- (NSData *)readDataOfLength:(NSUInteger)length; // 从当前节点开始读取指定的长度数据`                           
 `- (void)writeData:(NSData *)data; //写入数据`       
 `- (unsigned long long)offsetInFile; //获取当前文件的偏移量`          
 `- (void)seekToFileOffset:(unsigned long long)offset; //跳到指定文件的偏移量`    
 `- (unsigned long long)seekToEndOfFile; //跳到文件末尾`      
 `- (void)truncateFileAtOffset:(unsigned long long)offset; //将文件的长度设为offset字节`
 `- (void)closeFile; 关闭文件
基本用法追加数据
 NSString *homePath  = NSHomeDirectory( );
 NSString *sourcePath = [homePath stringByAppendingPathConmpone:@"testfile.text"];                
 NSFileHandle *fielHandle = [NSFileHandle fileHandleForUpdatingAtPath:sourcePath];                       
 [fileHandle seekToEndOfFile];节点跳到文件的末尾          
 NSString *str = @"追加的数据"                   
 NSData* stringData  = [str  dataUsingEncoding:NSUTF8StringEncoding];          
 [fileHandle writeData:stringData]; 追加写入数据       
 [fileHandle closeFile];
基本用法一 定位数
 NSFileManager *fm = [NSFileManager defaultManager];              
 NSString *content = @"abcdef";                      
 [fm createFileAtPath:path contents:[content dataUsingEncoding:NSUTF8StringEncoding] attributes:nil];                                                   
 NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:path];      
 NSUInteger length = [fileHandle availabelData] length]; 获取数据长度       
 [fileHandle seekToFileOffset;length/2]; 偏移量文件的一半           
 NSData *data = [fileHandle readDataToEndOfFile];                
 [fileHandle closeFile];
基本用法一 复制文件
NSFileHandle *infile, *outfile;   //输入文件、输出文件          
NSData *buffer;    //读取的缓冲数据                    
NSFileManager *fileManager = [NSFileManager defaultManager];   
NSString *homePath = NSHomeDirectory( );              
NSString *sourcePath = [homePath  stringByAppendingPathComponent:@"testfile.txt"];   // 源文件路径                                          
NSString *outPath = [homePath stringByAppendingPathComponent:@"outfile.txt"];   //输出文件路径                               
BOOL sucess  = [fileManager createFileAtPath:outPath contents:nil attributes:nil];  
 if (!success)          
  {                                                      
     return N0;                                                                                                   
  }                 
infile = [NSFileHandle fileHandleForReadingAtPath:sourcePath];   //创建读取源路径文件
 if (infile == nil)                          
  {                                          
    return NO;                      
  }                           
outfile = [NSFileHandle fileHandleForReadingAtPath:outPath];   //创建并打开要输出的文件                                                                                                                
  if (outfile == nil)                            
  {                                                               
      return NO;                                                    
  }                                             
[outfile truncateFileAtOffset:0]; //将输出文件的长度设为0         
buffer = [infile readDataToEndOfFile];  //读取数据           
[outfile writeData:buffer];  //写入输入                        
[infile closeFile];        //关闭写入输入文件               
[outfile closeFile]

数据库管理系统

SQL语言概念
SQL是一种结构化查询语言,TA是专为数据库建立的操作命令集,是一种功能齐全的数据库语言


数据库管理系统

数据库特征

  1. 以一定方式存储在一起
  2. 能为多个用户共享
  3. 具有尽可能少的冗余代码
  4. 与程序彼此独立的数据集合

讲了这么多数据库相关知识,那么到底什么数据库

数据库分类
关系数据库(主流)对象数据库、层次式数据库

常用关系数据库
PC端:Oracle、MySQL、SQL Server、Access、DB2、Sybase
嵌入式移动客户端:SQLite

SQLite是一个轻量级关系数据库。SQLite最初的设计目标是用于嵌入式系统,TA占用资源非常少,在嵌入设备中,只需要几百K的内存就够了,目前应用于Android、iOS、Windows Phone等智能手机。iOS使用时SQLite,只需要加入libsqlite3.0.tbd依赖以及引入sqlite3.h头文件即可


数据库中有几个很重要的概念

表:是数据库中一个非常重要的对象,是其他对象的基础。根据信息的分类情况,一个数库中可能包含若干个数据表

字段:表的“列”称为字段”,每个字段包含某一专题的信息

记录:是指对应于数据表一行信息的一组完整相关信息


SQL中很重要的一点,一定要记住,*SQL对大小写不敏感*


SQL语句后面的分号

数据库系统中分号是作为分隔每条SQL语句的标准方法,这样就可以在对服务器的相同请求中执行一条以上的语句
需要我们注意的是,某些数据库会有要求在每条SQL命令的末尾加上分号,而SQLite则属于另一类,TA的语句末尾不使用分号

  • PS:在我们学习Objective-C时每段语句的末尾都要求加上分号,但是SQLite不需要我们要区分开来

SQLite数据库数据类型
SQLite是无类型的数据库,可以保存任何类型的数据,对于SQLite来说对字段不指定类型是完全有效
为了使SQLite和其他数据库间的兼容性最大化,SQLite支持“类型近似”的观点,列的类型近似指的是存储列上数据的推荐类型。

SQLite近似类型规则

  1. 如果类型字符串包含int”,那么该字段的亲缘类型是INTEGER
  2. 如果类型字符串包含char”、“clob”或“text”,那么该字段的亲缘类型是TEXT,如VARCHAR
  3. 如果类型字符串中包含“blob”,那么该字段的亲缘类型是none
  4. 如果类型字符串中包含“real”、“floa”或“doub”,那么该字段的亲缘类型是real
  5. 其余情况下,字段的亲缘类型为NUMERIC

SQLite字段约束条件


SQL语句

  • SQL的语句我们可以分成两个部分来看,分别是:数据操作语言(DML)和数据定义语言(DDL)

查询更新指令构成了SQL的DML部分:

DDL部分是我们有能力创建活删除表格,我们也可以定义索引,规定表之间链接,以及施加表间的约束


三、iOS的数据库技术实现

开始使用SQLite所需要的几个步骤

需要的框架libsqlite3.0.tbd

  1. 引入<sqlite3.h>头文件
  2. 打开数据库
  3. 执行SQL命令——创建表增删改查等操作
  4. 关闭数据库

打开与关闭数据库

  • 需要注意的是我们的iOS程序中,一般情况下只有一个数据库,我们可以在数据库中创建多张表来保存不同的信息,但是千万不要创建多个数据库,每个数据库中只有一张表,因为不断的连接,关闭数据库是很耗性能

创建一个DB类用来进行对数据库的操作

打开数据库

#import "DB.h"

@implementation DB

// 创建数据库指针
static sqlite3 *db = nil;

// 打开数据库
+ (sqlite3 *)open {
    
    // 此方法的主要作用是打开数据库
    // 返回值是一个数据库指针
    // 因为这个数据库在很多的SQLite API(函数)中都会用到,我们声明一个类方法来获取,更加方便

    // 懒加载
    if (db != nil) {
        return db;
    }
    
    // 获取Documents路径
    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentationDirectory, NSUserDomainMask, YES) lastObject];
    
    // 生成数据库文件在沙盒中的路径
    NSString *sqlPath = [docPath stringByAppendingPathComponent:@"studentDB.sqlite"];
    
    // 创建文件管理对象
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    // 判断沙盒路径中是否存在数据库文件,如果不存在执行拷贝操作,如果存在不在执行拷贝操作
    if ([fileManager fileExistsAtPath:sqlPath] == NO) {
        // 获取数据库文件在包中的路径
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"studentDB" ofType:@"sqlite"];
        
        // 使用文件管理对象进行拷贝操作
        // 第一个参数是拷贝文件的路径
        // 第二个参数是将拷贝文件进行拷贝的目标路径
        [fileManager copyItemAtPath:filePath toPath:sqlPath error:nil];
        
    }
    
    // 打开数据库需要使用一下函数
    // 第一个参数是数据库的路径(因为需要的是C语言的字符串,而不是NSString所以必须进行转换
    // 第二个参数指向指针的指针
    sqlite3_open([sqlPath UTF8String], &amp;db);
    
    return db;
}

关闭数据库

// 关闭数据库
+ (void)close {
    
    // 关闭数据库
    sqlite3_close(db);
    
    // 将数据库的指针置空
    db = nil;
    
}

创建一个学生

建表的方法

// 创建表方法
- (void)createTable {
    
    // 将建表sql语句放入NSString对象中
    NSString *sql = @"create table if not exists stu (ID integer primary key, name text not null, gender text default '男')";
    
    // 打开数据库
    sqlite3 *db = [DB open];
    
    // 执行sql语句
    int result = sqlite3_exec(db, sql.UTF8String, nil, nil, nil);
    
    if (result == SQLITE_OK) {
        NSLog(@"建表成功");
    } else {
        NSLog(@"建表失败");
    }
    
    // 关闭数据库
    [DB close];

}

获取表中所有学生

// 获取表中保存的所有学生
+ (NSArray *)allStudents {
    
    // 打开数据库
    sqlite3 *db = [DB open];
    
    // 创建一个语句对象
    sqlite3_stmt *stmt = nil;
    
    // 声明数组对象
    NSMutableArray *mArr = nil;
    
    // 此函数作用生成一个语句对象,此时sql语句并没有执行,创建的语句对象,保存了关联的数据库,执行的sql语句,sql语句的长度等信息
    int result = sqlite3_prepare_v2(db, "select * from Students", -1, &amp;stmt, nil);
    if (result == SQLITE_OK) {
        
        // 为数组开辟空间
        mArr = [NSMutableArray arrayWithCapacity:0];
        
        // SQLite_ROW仅用于查询语句,sqlite3_step()函数执行后的结果如果是SQLite_ROW,说明结果里面还有数据,会自动跳到下一条结果,如果已经是最后一条数据,再次执行sqlite3_step(),会返回SQLite_DONE,结束整个查询
        while (sqlite3_step(stmt) == SQLITE_ROW) {
            
            // 获取记录中的字段值
            // 第一个参数是语句对象,第二个参数是字段的下标,从0开始
            int ID = sqlite3_column_int(stmt, 0);
            const unsigned char *cName = sqlite3_column_text(stmt, 1);
            const unsigned char *cGender = sqlite3_column_text(stmt, 2);
            
            // 将获取到的C语言字符串转换成OC字符串
            NSString *name = [NSString stringWithUTF8String:(const char *)cName];
            NSString *gender = [NSString stringWithUTF8String:(const char *)cGender];
            Student *student = [Student studentWithID:ID name:name gender:gender];
            
            // 添加学生信息到数组
            [mArr addObject:student];
        }
    }
    
    // 关闭数据库
    sqlite3_finalize(stmt);
    
    return mArr;
    
}

查找对应ID的学生

// 根据指定的ID,查找相对应的学生
+ (Student *)findStudentByID:(int)ID {
    
    // 打开数据库
    sqlite3 *db = [DB open];
    
    // 创建一个语句对象
    sqlite3_stmt *stmt = nil;
    
    Student *student = nil;
    
    // 生成语句对象
    int result = sqlite3_prepare_v2(db, "select * from Students where ID = ?", -1, &amp;stmt, nil);
    
    if (result == SQLITE_OK) {
        
        // 如果查询语句或者其他sql语句有条件,在准备语句对象的函数内部,sql语句中用?来代替条件,那么在执行语句之前,一定要绑定
        // 1代表sql语句中的第一个问号,问号的下标是从1开始的
        sqlite3_bind_int(stmt, 1, ID);
        
        if (sqlite3_step(stmt) == SQLITE_ROW) {
            
            // 获取记录中的字段信息
            const unsigned char *cName = sqlite3_column_text(stmt, 1);
            const unsigned char *cGender = sqlite3_column_text(stmt, 2);
            
            // 将C语言字符串转换成OC字符串
            NSString *name = [NSString stringWithUTF8String:(const char *)cName];
            NSString *gender = [NSString stringWithUTF8String:(const char *)cGender];
            
            student = [Student studentWithID:ID name:name gender:gender];
            
        }
    }
    
    // 先释放语句对象
    sqlite3_finalize(stmt);
    return student;
    
}

表中添加一条记录

// 插入一条记录
+ (void)insertStudentWithID:(int)ID name:(NSString *)name gender:(NSString *)gender {
    
    // 打开数据库
    sqlite3 *db = [DB open];
    
    sqlite3_stmt *stmt = nil;
    
    int result = sqlite3_prepare_v2(db, "insert into Students values(?,?,?)", -1, &amp;stmt, nil);
    
    if (result == SQLITE_OK) {
        // 绑定
        sqlite3_bind_int(stmt, 1, ID);
        sqlite3_bind_text(stmt, 2, [name UTF8String], -1, nil);
        sqlite3_bind_text(stmt, 3, [gender UTF8String], -1, nil);
        
        // 插入查询不一样,执行结果没有返回值
        sqlite3_step(stmt);
        
    }
    
    // 释放语句对象
    sqlite3_finalize(stmt);
    
}

更新记录

// 更新指定ID下的姓名和性别
+ (void)updateStudentName:(NSString *)name gender:(NSString *)gender forID:(int)ID {
    
    // 打开数据库
    sqlite3 *db = [DB open];
    
    sqlite3_stmt *stmt = nil;
    
    int result = sqlite3_prepare_v2(db, "update Student set name = ?, gender = ? where ID = ?", -1, &stmt, nil);
    if (result == SQLITE_OK) {
        sqlite3_bind_text(stmt, 1, [name UTF8String], -1, nil);
        sqlite3_bind_text(stmt, 2, [gender UTF8String], -1, nil);
        sqlite3_bind_int(stmt, 3, ID);
        
        sqlite3_step(stmt);
    }
    sqlite3_finalize(stmt);
}

删除记录

// 根据指定ID删除学生
+ (void)deleteStudentByID:(int)ID {
    
    // 打开数据库
    sqlite3 *db = [DB open];
    
    sqlite3_stmt *stmt = nil;
    
    int result = sqlite3_prepare_v2(db, "delete from Students where ID = ?", -1, &stmt, nil);
    
    if (result == SQLITE_OK) {
        sqlite3_bind_int(stmt, 1, ID);
        sqlite3_step(stmt);
    }
    
    sqlite3_finalize(stmt); 
    
}

FMDB


1.FMDB基本使用

FMDB简介

FMDB是iOS平台的SQLite数据库框架
FMDB以OC的方式封装了SQLite的C语言API

使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码
提供了多线程安全的数据库操作方法,有效地防止数据混乱

FMDB的安装方式

pod ‘FMDB’

1.导入FMDB文件
2.导入系统依赖库sqlite3.0.tbd


FMDB核心

FMDatabase数据库

一个FMDatabase对象就代表一个单独的SQLite数据库
用来执行SQL语句

FMResultSet查询结果

使用FMDatabase执行查询后的结果

FMDatabaseQueue线程安全数据库操作

用于在多线程中执行多个查询或更新,它是线程安全


FMDB使用步骤

  • 1.打开数据库
    通过指定SQLite数据库文件路径来创建FMDatabase对象

    • 具体文件路径

    如果不存在会自动创建.

    会在临时目录创建一个空的数据库当FMDatabase连接关闭时,数据库文件也被删除

    • nil

    会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁

// 1.打开数据库
FMDatabase *db = [FMDatabase databaseWithPath:path];
if ([db open])  { NSLog(@"打开成功!"); }

在FMDB中,除查询以外的所有操作,都称为更新
create、dropinsertupdatedelete
使用executeUpdate:方法执行更新

// FMDB更新方法
-(BOOL)executeUpdate:(NSString*)sql, …
-(BOOL)executeUpdateWithFormat:(NSString*)format, …
-(BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments

// 2.执行更新
[db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @20, @"Jack"]

  • 3.执行查询
    • 查询方法
    - (FMResultSet *)executeQuery:(NSString*)sql, ...
    - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
    - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"];

// 遍历结果
while ([rs next]) {
    NSString *name = [rs stringForColumn:@"name"];
    int age = [rs intForColumn:@"age"];
    double score = [rs doubleForColumn:@"score"];
}

  • 4.关闭数据库
[self.db close];

2.FMDabaseQueue的简单使用

FMDabaseQueue简介

FMDatabase这个类是线程不安全的,如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题
为了保证线程安全,FMDB提供方便快捷的FMDatabaseQueue类

FMDabaseQueue的使用

  • 1.FMDatabaseQueue的使用
    FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
简单使用
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
        // 查询
        FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    }];
使用事务
    [queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
        //查询
        FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    }];

多线程

NSThread

NSThread使用

1、 实例初始化、属性和实例方法
   //创建线程
   NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(demo:) object:@"Thread"];
   //或者
   NSThread  *newThread=[[NSThread alloc]init];
   NSThread  *newThread= [[NSThread alloc]initWithBlock:^{
       NSLog(@"initWithBlock");
   }];
  • 属性
  1. 线程字典
/**
每个线程都维护了一个键-值的字典,它可以在线里面的任何地方被访问。
你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
比如,你可以使用它来存储在你的整个线程过程中 Run loop 里面多次迭代状态信息。
NSThread实例可以使用一下方法
*/
@property (readonly, retain) NSMutableDictionary *threadDictionary;
NSMutableDictionary *dict = [thread threadDictionary];  
  1. 优先级
 double threadPriority ; //优先级
  1. 线程优先级
/** NSQualityOfService:
  NSQualityOfServiceUserInteractive:最高优先级,主要用于提供交互UI的操作,比如处理点击事件,绘制图像屏幕上
  NSQualityOfServiceUserInitiated:次高优先级,主要用于执行需要立即返回任务
  NSQualityOfServiceDefault:默认优先级,当没有设置优先级的时候,线程默认优先级
  NSQualityOfServiceUtility:普通优先级,主要用于不需要立即返回任务
  NSQualityOfServiceBackground后台优先级,用于完全不紧急任务
*/
@property NSQualityOfService qualityOfService; 
  1. 线程名称
@property (nullable, copy) NSString *name;
  1. 线程使用栈大小默认是512K
@property NSUInteger stackSize ;
  1. 线程正在执行
@property (readonly, getter=isExecuting) BOOL executing;
  1. 线程执行结束
@property (readonly, getter=isFinished) BOOL finished;
  1. 线程是否可以取消
@property (readonly, getter=isCancelled) BOOL cancelled;
  1. -(void)start; 启动线程
    实例化线程需要手动启动才能运行
    [thread start];
  1. -(BOOL)isMainThread; 是否主线
 isMain=[thread isMainThread];
  1. -(void)setName:(NSString *)n; 设置线程名称
[thread setName=@"The Second Thread"];
  1. -(void)cancel ; 取消线程
[thread cancel];
  1. -(void)main ; 线程的入口函数
[thread main];
  1. -(void)isExecuting; 判断线程是否正在执行
BOOL isRunning=[thread isExecuting];
  1. -(void)isFinished;判断线程是否已经结束
BOOL isEnd=[thread isFinished];
  1. -(void)isCancelled; 判断线程是否撤销
isCancel=[thread isCancelled];
2、类方法
  1. 创建子线程并开始,注意以下两个类方法创建后就可执行,不需手动开启
/**
  block方式
*/
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
/**
  SEL方式
*/
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
  1. +(NSThread)currentThread;获取当前线程
[NSThread currentThread]
  1. +(BOOL)isMultiThreaded; 当前代码运行所在线是否是子线程
BOOL isMulti = [NSThread isMultiThreaded];
  1. +(void)sleepUntilDate:(NSDate *)date; 当前代码所在线程睡到指定时
   [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
  1. +(void)sleepForTimeInterval:(NSTimeInterval)ti; 当前线程睡多长时间
    [NSThread sleepForTimeInterval:1.0];
  1. +(void)exit; 退出当前线程
[NSThread exit];
  1. +(double)threadPriority; 设置当前线程优先级
double dPriority=[NSThread threadPriority];
  1. +(BOOL)setThreadPriority:(double)p; 给当前线程设定优先级,调度优先级的取值范围是0.0 ~ 1.0,默认0.5,值越大,优先级越高。
BOOL isSetting=[NSThread setThreadPriority:(0.0~1.0)];
  1. +(NSArray *)callStackReturnAddresses;线程的调用都会有函数的调用函数的调用就会有栈返回地址记录,在这里返回的是函 数调用返回的虚拟地址,说白了就是在该线程中函数调用虚拟地址数组
NSArray *addressArray=[NSThread callStackReturnAddresses];
  1. +(NSArray *)callStackSymbols 同上面的方法一样,只不过返回的是该线程调用函数的名字数字
NSArray* nameNumArray=[NSThread callStackSymbols];

注意:callStackReturnAddresscallStackSymbols两个函数可以同NSLog联合使用来跟踪线程的函数调用情况,是编程调试的重要手段

3、隐式创建&线程间通讯

以下方法位于NSObject (NSThreadPerformAdditions)分类中,所有继承NSObject 实例化对象都可调用以下方法

/**
  指定方法在主线程中执行
参数1. SEL 方法
    2.方法参数
    3.是否等待当前执行完毕
    4.指定的Runloop model
*/
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    // equivalent to the first method with kCFRunLoopCommonModes
/**
  指定方法在某个线程中执行
参数1. SEL 方法
    2.方法参数
    3.是否等待当前执行完毕
    4.指定的Runloop model
*/
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
    // equivalent to the first method with kCFRunLoopCommonModes
/**
  指定方法在开启的子线程中执行
参数1. SEL 方法
    2.方法参数
*/
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

注意:我们经常提到的“线程间通讯”其实就是上面几个方法,并不是多高大上,也没有多复杂!!!

再注意:苹果声明UI更新一定要在UI线程(主线程)中执行,虽然不是所有后台线程更新UI都会出错

4、线程间资源共享&线程加锁

程序运行过程中,如果存在多线程,那么各个线程读写资源就会存在先后、同时读写资源的操作,因为是在不同线程,CPU调度过程中我们无法保证哪个线程会先读写资源,哪个线程后读写资源。因此为了防止数据读写混乱和错误的发生,我们要将线程在读写数据时加锁,这样就能保证操作同一个数据对象的线程只有一个,当这个线程执行完成之后解锁,其他的线程才能操作此数据对象。NSLock / NSConditionLock / NSRecursiveLock / @synchronized都可以实现线程上锁的操作。

  1. @synchronized
    直接例子:相信12306卖火车票的例子大家了解
    首先:开启两个线程同时售票
    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];
        //互斥锁 -- 保证锁内的代码在同一时间内只有一个线程在执行
        @synchronized (self){
            //1.判断是否有票
            if (self.tickets > 0) {
                //2.如果有就卖一张
                self.tickets --;
                NSLog(@"还剩%d张票  %@",self.tickets,[NSThread currentThread]);
            }else{
                //3.没有票了提示
                NSLog(@"卖完了 %@",[NSThread currentThread]);
                break;
            }
        }
    }

}
  1. NSLock
    -(BOOL)tryLock;//尝试加锁,成功返回YES ;失败返回NO ,但不会阻塞线程的运行
    -(BOOL)lockBeforeDate:(NSDate *)limit;//在指定的时间以前得到锁。YES:在指定时间之前获得了锁;NO:在指定时间之前没有获得锁。
     该线程将被阻塞,直到获得了锁,或者定时过期- (void)setName:(NSString*)newName//为锁指定一个Name
  - (NSString*)name//**返回锁指定的**name
    @property (nullable, copy) NSString *name;线程锁名称 

举个例子

 NSLock* myLock=[[NSLock alloc]init];
NSString *str=@"hello";
[NSThread detachNewThreadWithBlock:^{
            [myLock lock];
NSLog(@"%@",str);
str=@"world";
            [myLock unlock];
    }];
[NSThread detachNewThreadWithBlock:^{
            [myLock lock];
NSLog(@"%@",str);
str=@"变化了";
            [myLock unlock];
    }];

输出结果加锁之前,两个线程输出一样 hello加锁之后,输出分辨为hello 与world

  1. NSConditionLock
    使用此锁,在线程没有获得锁的情况下,阻塞,即暂停运行,典型用于生产者消费者模型
- (instancetype)initWithCondition:(NSInteger)condition;//初始化条件锁
- (void)lockWhenCondition:(NSInteger)condition;//加锁 (条件是:锁空闲,即没被占用;条件成立)
- (BOOL)tryLock; //尝试加锁,成功返回TRUE,失败返回FALSE
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//在指定条件成立的情况下尝试加锁,成功返回TRUE,失败返回FALSE
- (void)unlockWithCondition:(NSInteger)condition;//在指定的条件成立时,解锁
- (BOOL)lockBeforeDate:(NSDate *)limit;//在指定时间前加锁,成功返回TRUE,失败返回FALSE,
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;//条件成立的情况下,在指定时间前加锁,成功返回TRUE,失败返回FALSE,
@property (readonly) NSInteger condition;//条件锁的条件
@property (nullable, copy) NSString *name;//条件锁的名称

举个例子

  NSConditionLock* myCondition=[[NSConditionLock alloc]init];
    [NSThread detachNewThreadWithBlock:^{
        for(int i=0;i<5;i++)
        {
            [myCondition lock];
            NSLog(@"当前解锁条件:%d",i);
            sleep(2);
            [myCondition unlockWithCondition:i];
            BOOL isLocked=[myCondition tryLockWhenCondition:2];
            if(isLocked)
            {
                NSLog(@"加锁成功!!!!!");
                [myCondition unlock];
            }
        }
    }];

输出结果,在条件2 解锁之后,等待条件2 的锁加锁成功。

  1. NSRecursiveLock
    此锁可以在同一线程中多次被使用,但要保证加锁与解锁使用平衡,多用于递归函数,防止死锁
- (BOOL)tryLock;//尝试加锁,成功返回TRUE,失败返回FALSE
- (BOOL)lockBeforeDate:(NSDate *)limit;//在指定时间前尝试加锁,成功返回TRUE,失败返回FALSE
@property (nullable, copy) NSString *name;//线程锁名称

使用示例

-(void)initRecycle:(int)value
{
   [myRecursive lock];
   if(value>0)
   {
       NSLog(@"当前的value值:%d",value);
       sleep(2);
       [self initRecycle:value-1];
   }
   [myRecursive unlock];
}

输出结果: 从你传入的数值一直到1,不会出现死锁


5、线程安全之原子属性 atomic

原子属性(线程安全)与非原子属性,平时我们@property声明对象属性时会用到nonatomic,是什么意思呢?
苹果系统在我们声明对象属性时默认atomic,也就是说在读写这个属性的时候,保证同一时间内只有一个线程能够执行。当声明时用的是atomic,通常会生成 _成员变量 如果同时重写getter&setter _成员变量 就不自动生成。实际上原子属性内部有一个锁,叫做“自旋锁”。
首先我们比较一下“自旋锁” & “互斥锁”的异同然后回答上面的问题

无论什么锁,都很消耗性能效率不高,所以在我们平时开发过程中,会使用nonatomic

@property (strong, nonatomic) NSObject *myNonatomic;
@property (strong,    atomic) NSObject *myAtomic;

根据上面描述,我们得出结论,当我们重写了myAtomic的setter和getter方法

- (void)setMyAtomic:(NSObject *)myAtomic{
      _myAtomic = myAtomic;
}
- (NSObject *)myAtomic{
    return _myAtomic;
}

那么我们就必须声明一个_myAtomic静态变量

@synthesize myAtomic = _myAtomic;

否则系统在编译时候找不到 _myAtomic


6、子线程上的Runloop
  1. 介绍子线程上的Runloop之前先来一个有意思的小插曲,我们来介绍一下Runloop,甚至模拟一个Runloop
    Runloop 运行循环
    -在目前iOS开发中,几乎用不到,在以前iOS黑暗时代程序员用到
    目的:
    保证程序不退出
    监听事件
    没有事件让程序进入休眠
    区分模式
    NSDefaultRunLoopMode – 时钟、网络事件
    NSRunLoopCommonModes – 用户交互

模拟Runloop

void click(int type){
    printf("正在运行第%d",type);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        while (YES) {
            printf("请输入选项 0 表示退出");
            int result = -1;
            scanf("%d",&result);
            if (result == 0) {
                printf("程序结束n");
                break;
            }else{
                click(result);
            }
        }
    }
    return 0;
}
  1. 在iOS中,开辟的子线程上的Runloop默认开启的,并且子线程中的Runloop开启之后是手动无法关闭的。那么当我们给子线程中重复添加不同任务时并且Runloop没有开启的情况下,子线程无法监听事件(确切说是子线程的Runloop),我们后来添加的任务就无法执行。
    但是我们如果让子线程Runloop一直工作浪费资源,下面介绍一个OC中常用到的可以控制子线程Runloop的例子
    首先,Runloop就是一个死循环,那么我们就创建一个死循环,然后声明一个可以判断是否应该退出Runloop循环的属性
@property (assign, nonatomic, getter=isFinished) BOOL finished;

创建子线程并添加任务

    NSThread *t = [[NSThread alloc]initWithTarget:self selector:@selector(demo) object:nil];
    [t start];
    self.finished = NO;
    [self performSelector:@selector(otherMethod) onThread:t withObject:nil waitUntilDone:NO];

第一个任务加入死循环

- (void)demo{
    NSLog(@"%@",[NSThread currentThread]);
    //在OC中使用比较多的,退出循环的方式
    while (!self.isFinished) {
        [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
    }
    NSLog(@"能来吗?");
}

最后添加的任务结束后结束死循环

- (void)otherMethod{
    for (int i = 0; i < 10; i ++) {
        NSLog(@"%s   %@",__FUNCTION__,[NSThread currentThread]);

    }
  //让上面方法中的死循环结束
   self.finished = YES;
}

1. GCD 简介

什么『GCD』 ?我们先来看看百度百科的解释简单了解下相关概念

引自 百度百科
Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

为什么我们要使用 GCD 呢?

因为使用 GCD 有很多好处啊,具体如下

GCD 拥有以上这么多的好处,而且在多线程中处于举足轻重的地位。那么我们就很有必要系统地学习一下 GCD 的使用方法。


2. GCD 任务和队列

学习 GCD 之前,先来了解 GCD 中两个核心概念:『任务』『队列』

任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行』异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力

  • 同步执行(sync

    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力
  • 异步执行(async

    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力

举个简单例子:你要打电话给小明小白
同步执行』 就是:你打电话给小明时候,不能同时打给小白。只有等到给小明打完了,才能打给小白(等待任务执行结束)。而且只能用当前的电话(不具备开启新线程的能力)。
异步执行』 就是:你打电话给小明时候不用等着和小明通话结束(不用等待任务执行结束),还能同时给小白打电话。而且除了当前电话,你还可以使用其他一个或多个电话(具备开启新线程的能力)。

注意:**异步执行(async)**虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(下面会讲)。

队列(Dispatch Queue)这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。队列的结构参考下图

img

队列(Dispatch Queue).png

在 GCD 中有两种队列:串行队列』并发队列』。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

  • 串行队列(Serial Dispatch Queue)

    • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 并发队列(Concurrent Dispatch Queue)

    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

注意:并发队列并发功能只有在异步dispatch_async)方法下才有效

两者具体区别如下两图所示

img

串行队列(Serial Dispatch Queue).png

img

并发队列(Concurrent Dispatch Queue).png


3. GCD 的使用步骤

GCD 的使用步骤其实很简单,只有两步:

  1. 创建一个队列(串行队列或并发队列);
  2. 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)。

下边来看看队列的创建方法 / 获取方法,以及任务的创建方法。

3.1 队列的创建方法 / 获取方法

// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
  • 对于串行队列,GCD 默认提供了:

    『主队列(Main Dispatch Queue)』

    • 所有放在主队列中的任务,都会放到主线程中执行。
    • 可使用 dispatch_get_main_queue() 方法获得主队列。

注意:主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象。

// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
  • 对于并发队列,GCD 默认提供了

    全局并发队列(Global Dispatch Queue)』

    • 可以使用 dispatch_get_global_queue 方法来获取全局并发队列。需要传入两个参数。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT第二个参数暂时没用,用 0 即可
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

3.2 任务的创建方法

GCD 提供了同步执行任务的创建方法 dispatch_sync异步执行任务创建方法 dispatch_async

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里异步执行任务代码
});

虽然使用 GCD 只需两步,但是既然我们有两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:

  1. 同步执行 + 并发队列
  2. 异步执行 + 并发队列
  3. 同步执行 + 串行队列
  4. 异步执行 + 串行队列

实际上,刚才还说了两种默认队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是当前代码默认放在主队列中,所以主队列很有必要专门来研究一下,所以我们就又多了两种组合方式。这样就有六种不同的组合方式了。

  1. 同步执行 + 主队列
  2. 异步执行 + 主队列

那么这几种不同组合方式各有什么区别呢?

这里我们先上结论,后面再来详细讲解。你可以直接查看 3.3 任务和队列不同组合方式的区别 中的表格结果,然后跳过 4. GCD的基本使用 继续往后看。


3.3 任务和队列不同组合方式的区别

我们先来考虑最基本的使用,也就是当前线程为 『主线程』环境下,『不同队列』+『不同任务』 简单组合使用的不同区别。暂时不考虑 『队列中嵌套队列』 的这种复杂情况。

『主线程』中,『不同队列』+**『不同任务』**简单组合的区别:

区别 并发队列 串行队列 主队列
同步(sync 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行
异步(async 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 没有开启新线程,串行执行任务

注意:从上边可看出: 『主线程』 中调用 『主队列』+『同步执行』 会导致死锁问题
这是因为 主队列中追加的同步任务主线程本身的任务 两者之间相互等待,阻塞『主队列』,最终造成了主队列所在的线程(主线程)死锁问题
而如果我们在 『其他线程』 调用 『主队列』+『同步执行』,则不会阻塞 『主队列』,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务


3.4 队列嵌套情况下,不同组合方式区别

除了上边提到的『主线程』中调用『主队列』+『同步执行』会导致死锁问题。实际在使用『串行队列』的时候,也可能出现阻塞『串行队列』所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。

比如下面代码这样:在『异步执行』+『串行队列』的任务中,又嵌套了『当前的串行队列』,然后进行『同步执行』。

dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{    // 异步执行 + 串行队列
    dispatch_sync(queue, ^{  // 同步执行 + 当前串行队列
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
});

执行上面的代码会导致 串行队列中追加的任务串行队列中原有的任务 两者之间相互等待,阻塞了『串行队列』,最终造成了串行队列所在的线程(子线程)死锁问题。

主队列造成死锁也是基于这个原因,所以,这也进一步说明了主队列其实并不特殊。

关于 『队列中嵌套队列』这种复杂情况,这里也简单做一个总结。不过这里只考虑同一个队列的嵌套情况,关于多个队列的相互嵌套情况还请自行研究或者等我最新文章发布

『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:

区别 『异步执行+并发队列』嵌套『同一个并发队列』 『同步执行+并发队列』嵌套『同一个并发队列』 『异步执行+串行队列』嵌套『同一个串行队列』 『同步执行+串行队列』嵌套『同一个串行队列』
同步(sync) 没有开启新的线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行 死锁卡住不执行
异步(async) 有开启新线程,并发执行任务 有开启新线程,并发执行任务 有开启新线程(1 条),串行执行任务 有开启新线程(1 条),串行执行任务

好了,关于『不同队列』+『不同任务』 组合不同区别总结就到这里。

3.5 关于不同队列和不同任务的形象理解

因为前一段时间看到了有朋友留言说对 异步执行并发队列 中创建线程能力有所不理解,我觉得这个问题的确很容易造成困惑,所以很值得拿来专门分析一下。

他的问题:

在 异步 + 并发 中的解释
(异步执行具备开启新线程的能力。且并发队列可开启多个线程,同时执行多个任务)

以及 同步 + 并发 中的解释
(虽然并发队列可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务不具备开启新线程的能力)

这个地方看起来有点疑惑,你两个地方分别提到:异步执行开启新线程,并发队列也可以开启新线程,想请教下,你的意思是只有任务才拥有创建新线程的能力,而队列只有开启线程的能力,并不能创建线程 ?这二者是这样的关联吗?

关于这个问题,我想做一个很形象的类比,来帮助大家队列任务 以及 线程 之间关系理解

假设现在有 5 个人要穿过一道门禁,这道门禁总共有 10 个入口,管理员可以决定同一时间打开几个入口,可以决定同一时间让一个人单独通过还是个人一起通过。不过默认情况下,管理员只开启一个入口,且一个通道一次只能通过一个人

  • 这个故事里,人好比是 任务管理员好比是 系统,入口则代表 线程
    • 5 个人表示有 5 个任务,10 个入口代表 10 条线程。
    • 串行队列 好比是 5 个人排成一支长队。
    • 并发队列 好比是 5 个人排成多支队伍,比如 2 队,或者 3 队。
    • 同步任务 好比是管理员只开启了一个入口(当前线程)。
    • 异步任务 好比是管理员同时开启了多个入口(当前线程 + 新开的线程)。
  • 『异步执行 + 并发队列』 可以理解为:现在管理员开启了多个入口(比如 3 个入口),5 个人排成了多支队伍(比如 3 支队伍),这样这 5 个人就可以 3 个人同时一起穿过门禁了。
  • 『同步执行 + 并发队列』 可以理解为:现在管理员只开启了 1 个入口,5 个人排成了多支队伍。虽然这 5 个人排成了多支队伍,但是只开了 1 个入口啊,这 5 个人虽然都想快点过去,但是 1 个入口一次只能过 1 个人,所以大家就只好一个接一个走过去了,表现的结果就是:顺次通过入口。
  • 换成 GCD 里的语言就是说:
    • 『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
    • 『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。

下边我们来研究一下上边提到的六种简单组合方式的使用方法。


4. GCD 的基本使用

先来讲讲并发队列的两种执行方式。

4.1 同步执行 + 并发队列

  • 在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
/**
 * 同步执行 + 并发队列
 * 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncConcurrent---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_sync(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_sync(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"syncConcurrent---end");
}

输出结果:
2019-08-08 14:32:53.542816+0800 YSC-GCD-demo[16332:4171500] currentThread—<NSThread: 0x600002326940>{number = 1, name = main}
2019-08-08 14:32:53.542964+0800 YSC-GCD-demo[16332:4171500] syncConcurrent—begin
2019-08-08 14:32:55.544329+0800 YSC-GCD-demo[16332:4171500] 1—<NSThread: 0x600002326940>{number = 1, name = main}
2019-08-08 14:32:57.545779+0800 YSC-GCD-demo[16332:4171500] 2—<NSThread: 0x600002326940>{number = 1, name = main}
2019-08-08 14:32:59.547154+0800 YSC-GCD-demo[16332:4171500] 3—<NSThread: 0x600002326940>{number = 1, name = main}
2019-08-08 14:32:59.547365+0800 YSC-GCD-demo[16332:4171500] syncConcurrent—end

同步执行 + 并发队列 中可看到

  • 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
  • 所有任务都在打印的 syncConcurrent---beginsyncConcurrent---end 之间执行的(同步任务 需要等待队列的任务执行结束)。
  • 任务按顺序执行的。按顺序执行的原因:虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务 不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。

4.2 异步执行 + 并发队列

  • 可以开启多个线程,任务交替(同时)执行。
/**
 * 异步执行 + 并发队列
 * 特点:可以开启多个线程,任务交替(同时)执行。
 */
- (void)asyncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncConcurrent---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"asyncConcurrent---end");
}

输出结果:
2019-08-08 14:36:37.747966+0800 YSC-GCD-demo[17232:4187114] currentThread—<NSThread: 0x60000206d380>{number = 1, name = main}
2019-08-08 14:36:37.748150+0800 YSC-GCD-demo[17232:4187114] asyncConcurrent—begin
2019-08-08 14:36:37.748279+0800 YSC-GCD-demo[17232:4187114] asyncConcurrent—end
2019-08-08 14:36:39.752523+0800 YSC-GCD-demo[17232:4187204] 2—<NSThread: 0x600002010980>{number = 3, name = (null)}
2019-08-08 14:36:39.752527+0800 YSC-GCD-demo[17232:4187202] 3—<NSThread: 0x600002018480>{number = 5, name = (null)}
2019-08-08 14:36:39.752527+0800 YSC-GCD-demo[17232:4187203] 1—<NSThread: 0x600002023400>{number = 4, name = (null)}

异步执行 + 并发队列 中可以看出:

  • 除了当前线程(主线程),系统又开启了 3 个线程,并且任务是交替/同时执行的。(异步执行 具备开启新线程的能力。且 并发队列 可开启多个线程,同时执行多个任务)。
  • 所有任务是在打印的 syncConcurrent---beginsyncConcurrent---end 之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行 不做等待,可以继续执行任务)。

接下来再来讲讲串行队列的两种执行方式。


4.3 同步执行 + 串行队列

  • 不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
/**
 * 同步执行 + 串行队列
 * 特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
 */
- (void)syncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncSerial---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_sync(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"syncSerial---end");
}

输出结果为:
2019-08-08 14:39:31.366815+0800 YSC-GCD-demo[17285:4197645] currentThread—<NSThread: 0x600001b5e940>{number = 1, name = main}
2019-08-08 14:39:31.366952+0800 YSC-GCD-demo[17285:4197645] syncSerial—begin
2019-08-08 14:39:33.368256+0800 YSC-GCD-demo[17285:4197645] 1—<NSThread: 0x600001b5e940>{number = 1, name = main}
2019-08-08 14:39:35.369661+0800 YSC-GCD-demo[17285:4197645] 2—<NSThread: 0x600001b5e940>{number = 1, name = main}
2019-08-08 14:39:37.370991+0800 YSC-GCD-demo[17285:4197645] 3—<NSThread: 0x600001b5e940>{number = 1, name = main}
2019-08-08 14:39:37.371192+0800 YSC-GCD-demo[17285:4197645] syncSerial—end

同步执行 + 串行队列 可以看到

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行 不具备开启新线程的能力)。
  • 所有任务都在打印的 syncConcurrent---beginsyncConcurrent---end 之间执行(同步任务 需要等待队列的任务执行结束)。
  • 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。

4.4 异步执行 + 串行队列

  • 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
/**
 * 异步执行 + 串行队列
 * 特点:会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务。
 */
- (void)asyncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncSerial---begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"asyncSerial---end");
}

输出结果为:
2019-08-08 14:40:53.944502+0800 YSC-GCD-demo[17313:4203018] currentThread—<NSThread: 0x6000015da940>{number = 1, name = main}
2019-08-08 14:40:53.944615+0800 YSC-GCD-demo[17313:4203018] asyncSerial—begin
2019-08-08 14:40:53.944710+0800 YSC-GCD-demo[17313:4203018] asyncSerial—end
2019-08-08 14:40:55.947709+0800 YSC-GCD-demo[17313:4203079] 1—<NSThread: 0x6000015a0840>{number = 3, name = (null)}
2019-08-08 14:40:57.952453+0800 YSC-GCD-demo[17313:4203079] 2—<NSThread: 0x6000015a0840>{number = 3, name = (null)}
2019-08-08 14:40:59.952943+0800 YSC-GCD-demo[17313:4203079] 3—<NSThread: 0x6000015a0840>{number = 3, name = (null)}

异步执行 + 串行队列 可以看到

  • 开启了一条新线程(异步执行 具备开启新线程的能力,串行队列 只开启一个线程)。
  • 所有任务是在打印的 syncConcurrent---beginsyncConcurrent---end 之后才开始执行的(异步执行 不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。

下边讲讲刚才我们提到过的:主队列

  • 主队列:GCD 默认提供的

    串行队列

    • 默认情况下,平常所写代码是直接放在主队列中的。
    • 所有放在主队列中的任务,都会放到主线程中执行。
    • 可使用 dispatch_get_main_queue() 获得主队列。

我们再来看看主队列的两种组合方式。


4.5 同步执行 + 主队列

同步执行 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。

4.5.1 在主线程中调用 『同步执行 + 主队列』

/**
 * 同步执行 + 主队列
 * 特点(主线程调用):互等卡主不执行。
 * 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
 */
- (void)syncMain {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"syncMain---begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_sync(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_sync(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"syncMain---end");
}

输出结果
2019-08-08 14:43:58.062376+0800 YSC-GCD-demo[17371:4213562] currentThread—<NSThread: 0x6000026e2940>{number = 1, name = main}
2019-08-08 14:43:58.062518+0800 YSC-GCD-demo[17371:4213562] syncMain—begin
(lldb)

在主线程中使用 同步执行 + 主队列 可以惊奇的发现

  • 追加到主线程的任务 1、任务 2、任务 3 都不再执行了,而且 syncMain---end 也没有打印,在 XCode 9 及以上版本上还会直接崩溃。这是为什么呢?

这是因为我们在主线程中执行 syncMain 方法,相当于把 syncMain 任务放到了主线程的队列中。而 同步执行 会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务 1 追加到主队列中,任务 1 就在等待主线程处理syncMain 任务。而syncMain 任务需要等待 任务 1 执行完毕,才能接着执行。

那么,现在的情况就是 syncMain 任务和 任务 1 都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,而且 syncMain---end 也没有打印。

要是如果不在主线程中调用,而在其他线程中调用会如何呢?

4.5.2 在其他线程中调用『同步执行 + 主队列』

  • 不会开启新线程,执行完一个任务,再执行下一个任务
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];

输出结果:
2019-08-08 14:51:38.137978+0800 YSC-GCD-demo[17482:4237818] currentThread—<NSThread: 0x600001dd6c00>{number = 3, name = (null)}
2019-08-08 14:51:38.138159+0800 YSC-GCD-demo[17482:4237818] syncMain—begin
2019-08-08 14:51:40.149065+0800 YSC-GCD-demo[17482:4237594] 1—<NSThread: 0x600001d8d380>{number = 1, name = main}
2019-08-08 14:51:42.151104+0800 YSC-GCD-demo[17482:4237594] 2—<NSThread: 0x600001d8d380>{number = 1, name = main}
2019-08-08 14:51:44.152583+0800 YSC-GCD-demo[17482:4237594] 3—<NSThread: 0x600001d8d380>{number = 1, name = main}
2019-08-08 14:51:44.152767+0800 YSC-GCD-demo[17482:4237818] syncMain—end

在其他线程中使用 同步执行 + 主队列看到

  • 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
  • 所有任务都在打印的 syncConcurrent---beginsyncConcurrent---end 之间执行(同步任务 需要等待队列的任务执行结束)。
  • 任务是按顺序执行的(主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

为什么现在就不会卡住了呢?

因为syncMain 任务 放到了其他线程里,而 任务 1任务 2任务3 都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务 在其他线程中执行到追加 任务 1 到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的 任务1,等 任务1 执行完毕,再接着执行 任务 2任务 3。所以这里不会卡住线程,也就不会造成死锁问题。

4.6 异步执行 + 主队列

  • 只在主线程中执行任务,执行完一个任务,再执行下一个任务。
/**
 * 异步执行 + 主队列
 * 特点:只在主线程中执行任务,执行完一个任务,再执行下一个任务
 */
- (void)asyncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncMain---begin");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    NSLog(@"asyncMain---end");
}

输出结果:
2019-08-08 14:53:27.023091+0800 YSC-GCD-demo[17521:4243690] currentThread—<NSThread: 0x6000022a1380>{number = 1, name = main}
2019-08-08 14:53:27.023247+0800 YSC-GCD-demo[17521:4243690] asyncMain—begin
2019-08-08 14:53:27.023399+0800 YSC-GCD-demo[17521:4243690] asyncMain—end
2019-08-08 14:53:29.035565+0800 YSC-GCD-demo[17521:4243690] 1—<NSThread: 0x6000022a1380>{number = 1, name = main}
2019-08-08 14:53:31.036565+0800 YSC-GCD-demo[17521:4243690] 2—<NSThread: 0x6000022a1380>{number = 1, name = main}
2019-08-08 14:53:33.037092+0800 YSC-GCD-demo[17521:4243690] 3—<NSThread: 0x6000022a1380>{number = 1, name = main}

异步执行 + 主队列 可以看到

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然 异步执行 具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
  • 所有任务是在打印的 syncConcurrent---beginsyncConcurrent---end 之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(因为主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

弄懂了难理解、绕来绕去的**『不同队列』+『不同任务』**使用区别之后,我们来学习一个简单的东西:5. GCD 线程间的通信


5. GCD 线程间的通信

在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击滚动拖拽事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

/**
 * 线程间通信
 */
- (void)communication {
    // 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
        // 异步追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        // 回到主线程
        dispatch_async(mainQueue, ^{
            // 追加在主线程中执行的任务
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
    });
}

输出结果:
2019-08-08 14:56:22.973318+0800 YSC-GCD-demo[17573:4253201] 1—<NSThread: 0x600001846080>{number = 3, name = (null)}
2019-08-08 14:56:24.973902+0800 YSC-GCD-demo[17573:4253108] 2—<NSThread: 0x60000181e940>{number = 1, name = main}

  • 可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。

6. GCD 的其他方法

6.1 GCD 栅栏方法:dispatch_barrier_async

  • 我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于

    栅栏
    

    一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到

    dispatch_barrier_async
    

    方法在两个操作组间形成栅栏。

    dispatch_barrier_async
    

    方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在

    dispatch_barrier_async
    

    方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。具体如下图所示

    img

    dispatch_barrier_async.png

/**
 * 栅栏方法 dispatch_barrier_async
 */
- (void)barrier {
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_barrier_async(queue, ^{
        // 追加任务 barrier
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 4
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
    });
}

输出结果:
2019-08-08 14:59:02.540868+0800 YSC-GCD-demo[17648:4262933] 1—<NSThread: 0x600001ca4c40>{number = 3, name = (null)}
2019-08-08 14:59:02.540868+0800 YSC-GCD-demo[17648:4262932] 2—<NSThread: 0x600001c84a00>{number = 4, name = (null)}
2019-08-08 14:59:04.542346+0800 YSC-GCD-demo[17648:4262933] barrier—<NSThread: 0x600001ca4c40>{number = 3, name = (null)}
2019-08-08 14:59:06.542772+0800 YSC-GCD-demo[17648:4262932] 4—<NSThread: 0x600001c84a00>{number = 4, name = (null)}
2019-08-08 14:59:06.542773+0800 YSC-GCD-demo[17648:4262933] 3—<NSThread: 0x600001ca4c40>{number = 3, name = (null)}

dispatch_barrier_async 执行结果中可以看出:

  • 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。

6.2 GCD 延时执行方法:dispatch_after

我们经常会遇到这样的需求:在指定时间(例如 3 秒)之后执行某个任务。可以用 GCD 的dispatch_after 方法来实现
需要注意的是:dispatch_after 方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after 方法是很有效的。

/**
 * 延时执行方法 dispatch_after
 */
- (void)after {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"asyncMain---begin");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2.0 秒后异步追加任务代码到主队列,并开始执行
        NSLog(@"after---%@",[NSThread currentThread]);  // 打印当前线程
    });
}

输出结果:
2019-08-08 15:01:33.569710+0800 YSC-GCD-demo[17702:4272430] currentThread—<NSThread: 0x600001ead340>{number = 1, name = main}
2019-08-08 15:01:33.569838+0800 YSC-GCD-demo[17702:4272430] asyncMain—begin
2019-08-08 15:01:35.570146+0800 YSC-GCD-demo[17702:4272430] after—<NSThread: 0x600001ead340>{number = 1, name = main}

可以看出:在打印 asyncMain---begin 之后大约 2.0 秒的时间,打印了 after---<NSThread: 0x600001ead340>{number = 1, name = main}

6.3 GCD 一次性代码(只执行一次):dispatch_once

  • 我们在创建单例或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 方法。使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程环境下,dispatch_once 也可以保证线程安全。
/**
 * 一次性代码(只执行一次)dispatch_once
 */
- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行 1 次的代码(这里面默认是线程安全的)
    });
}

6.4 GCD 快速迭代方法:dispatch_apply

  • 通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的方法 dispatch_applydispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。但是这样就体现不出快速迭代的意义了。

我们可以利用并发队列进行异步执行。比如说遍历 0~5 这 6 个数字,for 循环的做法是每次取出一个元素,逐个遍历dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。

还有一点,无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

/**
 * 快速迭代方法 dispatch_apply
 */
- (void)apply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    NSLog(@"apply---begin");
    dispatch_apply(6, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}

输出结果:
2019-08-08 15:05:04.715266+0800 YSC-GCD-demo[17771:4285619] apply—begin
2019-08-08 15:05:04.715492+0800 YSC-GCD-demo[17771:4285619] 0—<NSThread: 0x600003bd1380>{number = 1, name = main}
2019-08-08 15:05:04.715516+0800 YSC-GCD-demo[17771:4285722] 1—<NSThread: 0x600003b82340>{number = 3, name = (null)}
2019-08-08 15:05:04.715526+0800 YSC-GCD-demo[17771:4285720] 3—<NSThread: 0x600003ba4cc0>{number = 5, name = (null)}
2019-08-08 15:05:04.715564+0800 YSC-GCD-demo[17771:4285721] 2—<NSThread: 0x600003bb9a80>{number = 7, name = (null)}
2019-08-08 15:05:04.715555+0800 YSC-GCD-demo[17771:4285719] 4—<NSThread: 0x600003b98100>{number = 6, name = (null)}
2019-08-08 15:05:04.715578+0800 YSC-GCD-demo[17771:4285728] 5—<NSThread: 0x600003beb400>{number = 4, name = (null)}
2019-08-08 15:05:04.715677+0800 YSC-GCD-demo[17771:4285619] apply—end

因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是 apply---end 一定在最后执行。这是因为 dispatch_apply 方法会等待全部任务执行完毕。

6.5 GCD 队列组:dispatch_group

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。

  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enterdispatch_group_leave 组合来实现 dispatch_group_async
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。

6.5.1 dispatch_group_notify

  • 监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
/**
 * 队列组 dispatch_group_notify
 */
- (void)groupNotify {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    
    dispatch_group_t group =  dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程

        NSLog(@"group---end");
    });
}

输出结果:
2019-08-08 15:07:21.601734+0800 YSC-GCD-demo[17813:4293874] currentThread—<NSThread: 0x600003aad380>{number = 1, name = main}
2019-08-08 15:07:21.601871+0800 YSC-GCD-demo[17813:4293874] group—begin
2019-08-08 15:07:23.604854+0800 YSC-GCD-demo[17813:4294048] 2—<NSThread: 0x600003add100>{number = 4, name = (null)}
2019-08-08 15:07:23.604852+0800 YSC-GCD-demo[17813:4294053] 1—<NSThread: 0x600003ace4c0>{number = 3, name = (null)}
2019-08-08 15:07:25.606067+0800 YSC-GCD-demo[17813:4293874] 3—<NSThread: 0x600003aad380>{number = 1, name = main}
2019-08-08 15:07:25.606255+0800 YSC-GCD-demo[17813:4293874] group—end

dispatch_group_notify 相关代码运行输出结果可以看出:
当所有任务都执行完成之后,才执行 dispatch_group_notify 相关 block 中的任务。

6.5.2 dispatch_group_wait

  • 暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。
/**
 * 队列组 dispatch_group_wait
 */
- (void)groupWait {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    
    dispatch_group_t group =  dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    NSLog(@"group---end");
    
}

输出结果:
2019-08-08 15:09:12.441729+0800 YSC-GCD-demo[17844:4299926] currentThread—<NSThread: 0x6000013e2940>{number = 1, name = main}
2019-08-08 15:09:12.441870+0800 YSC-GCD-demo[17844:4299926] group—begin
2019-08-08 15:09:14.445790+0800 YSC-GCD-demo[17844:4300046] 2—<NSThread: 0x600001389780>{number = 4, name = (null)}
2019-08-08 15:09:14.445760+0800 YSC-GCD-demo[17844:4300043] 1—<NSThread: 0x600001381880>{number = 3, name = (null)}
2019-08-08 15:09:14.446039+0800 YSC-GCD-demo[17844:4299926] group—end

dispatch_group_wait 相关代码运行输出结果可以看出:
当所有任务执行完成之后,才执行 dispatch_group_wait 之后的操作。但是,使用dispatch_group_wait 会阻塞当前线程。

6.5.3 dispatch_group_enter、dispatch_group_leave

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
  • 当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务。
/**
 * 队列组 dispatch_group_enter、dispatch_group_leave
 */
- (void)groupEnterAndLeave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"group---begin");
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程

        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 等前面的异步操作都执行完毕后,回到主线程.
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    
        NSLog(@"group---end");
    });
}

输出结果:
2019-08-08 15:13:17.983283+0800 YSC-GCD-demo[17924:4314716] currentThread—<NSThread: 0x600001ee5380>{number = 1, name = main}
2019-08-08 15:13:17.983429+0800 YSC-GCD-demo[17924:4314716] group—begin
2019-08-08 15:13:19.988898+0800 YSC-GCD-demo[17924:4314816] 2—<NSThread: 0x600001e9ca00>{number = 3, name = (null)}
2019-08-08 15:13:19.988888+0800 YSC-GCD-demo[17924:4314808] 1—<NSThread: 0x600001e94100>{number = 4, name = (null)}
2019-08-08 15:13:21.990450+0800 YSC-GCD-demo[17924:4314716] 3—<NSThread: 0x600001ee5380>{number = 1, name = main}
2019-08-08 15:13:21.990711+0800 YSC-GCD-demo[17924:4314716] group—end

dispatch_group_enter、dispatch_group_leave 相关代码运行结果中可以看出:当所有任务执行完成之后,才执行 dispatch_group_notify 中的任务。这里的dispatch_group_enterdispatch_group_leave 组合,其实等同于dispatch_group_async

6.6 GCD 信号量:dispatch_semaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,不可通过。计数为 0 或大于 0 时,计数减 1 且不等待,可通过。
Dispatch Semaphore 提供了三个方法:

  • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加 1
  • dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

6.6.1 Dispatch Semaphore 线程同步

我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。比如说:AFNetworking 中 AFURLSessionManager.m 里面tasksForKeyPath: 方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

下面,我们来利用 Dispatch Semaphore 实现线程同步,将异步执行任务转换为同步执行任务。

/**
 * semaphore 线程同步
 */
- (void)semaphoreSync {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        number = 100;
        
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %zd",number);
}

输出结果:
2019-08-08 15:16:56.781543+0800 YSC-GCD-demo[17988:4325744] currentThread—<NSThread: 0x60000298d380>{number = 1, name = main}
2019-08-08 15:16:56.781698+0800 YSC-GCD-demo[17988:4325744] semaphore—begin
2019-08-08 15:16:58.785232+0800 YSC-GCD-demo[17988:4325867] 1—<NSThread: 0x6000029eba80>{number = 3, name = (null)}
2019-08-08 15:16:58.785432+0800 YSC-GCD-demo[17988:4325744] semaphore—end,number = 100

从 Dispatch Semaphore 实现线程同步的代码可以看到

  • semaphore---end
    

    是在执行完

    number = 100;
    

    之后才打印的。而且输出结果 number 为 100。这是因为

    异步执行
    

    不会做任何等待,可以继续执行任务。

    执行顺如下

    1. semaphore 初始创建时计数为 0。
    2. 异步执行任务 1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态
    3. 然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行。
    4. 最后打印 semaphore---end,number = 100

这样就实现了线程同步,将异步执行任务转换为同步执行任务。

6.6.2 Dispatch Semaphore 线程安全和线程同步(为线程加锁)

线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

若每个线程中对全局变量静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。

下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题。

场景:总共有 50 张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。

6.6.2.1 非线程安全(不使用 semaphore)

先来看看考虑线程安全的代码:

/**
 * 非线程安全:不使用 semaphore
 * 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售卖火车票(非线程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {
        
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }
        
    }
}

输出结果(部分):
2019-08-08 15:21:39.772655+0800 YSC-GCD-demo[18071:4340555] currentThread—<NSThread: 0x6000015a2f40>{number = 1, name = main}
2019-08-08 15:21:39.772790+0800 YSC-GCD-demo[18071:4340555] semaphore—begin
2019-08-08 15:21:39.773101+0800 YSC-GCD-demo[18071:4340604] 剩余票数:48 窗口:<NSThread: 0x6000015cc600>{number = 4, name = (null)}
2019-08-08 15:21:39.773115+0800 YSC-GCD-demo[18071:4340605] 剩余票数:49 窗口:<NSThread: 0x6000015f8600>{number = 3, name = (null)}
2019-08-08 15:21:39.975041+0800 YSC-GCD-demo[18071:4340605] 剩余票数:47 窗口:<NSThread: 0x6000015f8600>{number = 3, name = (null)}
2019-08-08 15:21:39.975037+0800 YSC-GCD-demo[18071:4340604] 剩余票数:47 窗口:<NSThread: 0x6000015cc600>{number = 4, name = (null)}
2019-08-08 15:21:40.176567+0800 YSC-GCD-demo[18071:4340604] 剩余票数:46 窗口:<NSThread: 0x6000015cc600>{number = 4, name = (null)}

可以看到在不考虑线程安全,不使用 semaphore 的情况下,得到票数是错乱的,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。

6.6.2.2 线程安全(使用 semaphore 加锁)

考虑线程安全的代码:

/**
 * 线程安全:使用 semaphore 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);
    
    self.ticketSurplusCount = 50;
    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }
        
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }
}

输出结果为:
2019-08-08 15:23:58.819891+0800 YSC-GCD-demo[18116:4348091] currentThread—<NSThread: 0x600000681380>{number = 1, name = main}
2019-08-08 15:23:58.820041+0800 YSC-GCD-demo[18116:4348091] semaphore—begin
2019-08-08 15:23:58.820305+0800 YSC-GCD-demo[18116:4348159] 剩余票数:49 窗口:<NSThread: 0x6000006ede80>{number = 3, name = (null)}
2019-08-08 15:23:59.022165+0800 YSC-GCD-demo[18116:4348157] 剩余票数:48 窗口:<NSThread: 0x6000006e4b40>{number = 4, name = (null)}
2019-08-08 15:23:59.225299+0800 YSC-GCD-demo[18116:4348159] 剩余票数:47 窗口:<NSThread: 0x6000006ede80>{number = 3, name = (null)}

2019-08-08 15:24:08.355977+0800 YSC-GCD-demo[18116:4348157] 剩余票数:2 窗口:<NSThread: 0x6000006e4b40>{number = 4, name = (null)}
2019-08-08 15:24:08.559201+0800 YSC-GCD-demo[18116:4348159] 剩余票数:1 窗口:<NSThread: 0x6000006ede80>{number = 3, name = (null)}
2019-08-08 15:24:08.759630+0800 YSC-GCD-demo[18116:4348157] 剩余票数:0 窗口:<NSThread: 0x6000006e4b40>{number = 4, name = (null)}
2019-08-08 15:24:08.965100+0800 YSC-GCD-demo[18116:4348159] 所有火车票均已售完
2019-08-08 15:24:08.965440+0800 YSC-GCD-demo[18116:4348157] 所有火车票均已售完

可以看出,在考虑了线程安全的情况下,使用 dispatch_semaphore
机制之后,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。

AFN

#import "ViewController.h"
#import "AFNetworking.h"
#define Kboundary @"----WebKitFormBoundaryjv0UfA04ED44AhWx"
#define KNewLine [@"rn" dataUsingEncoding:NSUTF8StringEncoding]
@interface ViewController () <NSXMLParserDelegate>
 
@end
 
@implementation ViewController
 
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self https];
}
 
- (void)get {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 发送get请求
    NSDictionary *paramDict = @{
                                @"username":@"520it",
                                @"pwd":@"520it",
                                @"type":@"JSON"
                                };
    /*
        第一个参数:请求路径(不包含参数)
        第二个参数:参数字典发送给服务器的数据)
        第三个参数:进度回调
        第四个参数:成功回调
            task:请求任务
            responseObject:响应体信息(已将JSON转为OC对象)
        第五个参数:失败回调
            error:错误信息
    */
    //响应头:task.response
    [manager GET:@"http://120.25.226.186:32812/login" parameters:paramDict progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"%@----%@", [responseObject class], responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"请求失败----%@", error);
    }];
}
 
- (void)post {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 发送post请求
    NSDictionary *paramDict = @{
                                @"username":@"520it",
                                @"pwd":@"520it",
                                @"type":@"JSON"
                                };
    [manager POST:@"http://120.25.226.186:32812/login" parameters:paramDict progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"%@----%@", [responseObject class], responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"请求失败----%@", error);
    }];
}
 
- (void)download {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 下载文件
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    /*
        第一个参数:请求对象
        第二个参数:进度回调
        第三个参数:目标位置回调
            targetPath:临时文件路径
            response:响应头信息
        第四个参数:下载完成后的回调
            filePath:最终的文件路径
    */
    NSURLSessionDownloadTask *download = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
        //监听下载进度
        NSLog(@"%f", 1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
        NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
        NSLog(@"targetPath:%@", targetPath);
        NSLog(@"fullPath:%@", fullPath);
        return [NSURL fileURLWithPath:fullPath];
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
        NSLog(@"%@", filePath);
    }];
    [download resume];
}
 
- (void)upload {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 发送post请求上传文件
    /*
        第一个参数:请求路径
        第二个参数:字典(非文件参数)
        第三个参数:处理要上传的文件数第四个参数:进度回调
        第五个参数:成功回调
            responseObject:响应体信息
        第六个参数:失败回调
    */
    [manager POST:@"http://120.25.226.186:32812/upload" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        //第一种方式:
        //UIImage *image = [UIImage imageNamed:@"timg"];
        //NSData *imageData = UIImagePNGRepresentation(image);
        //使用formData来拼接数据
        /*
            第一个参数:二进制数据,要上传的文件参数
            第二个参数:服务器规定的
            第三个参数:该文件上传到服务器以什么名称保存
        */
        //[formData appendPartWithFileData:imageData name:@"file" fileName:@"xxxx.png" mimeType:@"image/png"];
        //第二种方式
        //[formData appendPartWithFileURL:[NSURL fileURLWithPath:@"/Users/xieyang/Code/AFN/AFN/timg.jpeg"] name:@"file" fileName:@"xxxx.png" mimeType:@"image/png" error:nil];
        //第三种方式
        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"/Users/xieyang/Code/AFN/AFN/timg.jpeg"] name:@"file" error:nil];
    } progress:^(NSProgress * _Nonnull uploadProgress) {
        NSLog(@"%f",1.0 * uploadProgress.completedUnitCount/uploadProgress.totalUnitCount);
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"上传成功----%@",responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"上传失败----%@",error);
    }];
}
 
-(NSData *)getBodyData
{
    NSMutableData *fileData = [NSMutableData data];
    //1 文件参数
    /*
        --分隔符
        Content-Disposition: form-data; name="file"; filename="Snip20160225_341.png"
        Content-Type: image/png(MIMEType:大类型/小类型)
        空行
        文件参数
    */
    [fileData appendData:[[NSString stringWithFormat:@"--%@",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    //name:file 服务器规定的参数
    //filename:Snip20160225_341.png 文件保存到服务器上面的名称
    //Content-Type:文件的类型
    [fileData appendData:[@"Content-Disposition: form-data; name="file"; filename="Sss.png"" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:[@"Content-Type: image/png" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:KNewLine];
    UIImage *image = [UIImage imageNamed:@"Snip20160227_128"];
    //UIImage --->NSData
    NSData *imageData = UIImagePNGRepresentation(image);
    [fileData appendData:imageData];
    [fileData appendData:KNewLine];
    //2 非文件参数
    /*
        --分隔符
        Content-Disposition: form-data; name="username"
        空行
        123456
    */
    [fileData appendData:[[NSString stringWithFormat:@"--%@",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:[@"Content-Disposition: form-data; name="username"" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    [fileData appendData:KNewLine];
    [fileData appendData:[@"123456" dataUsingEncoding:NSUTF8StringEncoding]];
    [fileData appendData:KNewLine];
    //3 结尾标识
    /*
        --分隔符--
    */
    [fileData appendData:[[NSString stringWithFormat:@"--%@--",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
    return fileData;
}
 
- (void)XML {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 发送get请求
    //注意:如果返回的是xml数据,应该修改AFN的解析方案
    manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
    NSDictionary *paramDict = @{
                                @"type":@"XML"
                                };
    [manager GET:@"http://120.25.226.186:32812/video" parameters:paramDict progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSXMLParser *parser = (NSXMLParser *)responseObject;
        //设置代理,需遵守协议<NSXMLParserDelegate>
        parser.delegate = self;
        //开始解析
        [parser parse];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"请求失败----%@", error);
    }];
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict {
    NSLog(@"%@----%@", elementName, attributeDict);
}
 
- (void)httpData1 {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 发送get请求
    //注意:如果返回的数据既不是XML也不是JSON,应该修改解析方案为
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    [manager GET:@"http://120.25.226.186:32812/resources/images/minion_01.png" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"%@", responseObject);
        //UIImage *image = [UIImage imageWithData:responseObject];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"请求失败----%@", error);
    }];
}
 
- (void)httpData2 {
    //1 创建会话管理者
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2 发送get请求
    //注意:如果返回的数据既不是XML也不是JSON,应该修改解析方案为
    //告诉AFN可以接收text/html类型的数据
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    [manager GET:@"http://www.baidu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"%@----%@", [responseObject class], [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
        //UIImage *image = [UIImage imageWithData:responseObject];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"请求失败----%@", error);
    }];
}
 
- (void)https {
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //更改解析方式
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    //设置对证书处理方式(允许无效证书)
    //manager.securityPolicy.allowInvalidCertificates = YES;
    //manager.securityPolic.validatesDomainName = NO;
    [manager GET:@"https://kyfw.12306.cn/otn" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"sussess----%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"error----%@", error);
    }];
}
 
@end


原文地址:https://blog.csdn.net/core37/article/details/126999010

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_47196.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注