前言
之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了
iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。因此我们决定
进一步探究iOS底层原理的任务。继前面三篇文章对多线程知识进行了简单的回顾之后:
一、对GCD的快速回顾
因为NSOperation、NSOperationQueue是对GCD的OC封装,因此我们在这边探索底层原理是直接从GCD入手。
在 探索 GCD的底层实现之前,我们快速过一遍 GCD相关的知识点,如果你想更全面了解GCD也可以参考我的这篇文章:3-多线程方案 GCD
1. GCD的队列
1.1 GCD的队列可以分为2大类型
- 并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步
dispatch_async函数下才有效
- 串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
// 1.创建队列
/*
第一个参数:C语言的字符串,标签
第二个参数:队列的类型
DISPATCH_QUEUE_CONCURRENT:并发
DISPATCH_QUEUE_SERIAL:串行
*/
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_CONCURRENT);
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_SERIAL);
// 2.获得全局并发队列
// 第一个参数:可以设置优先级
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 3.获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 4.异步函数
dispatch_async(queue, ^{
NSLog(@"download1----%@",[NSThread currentThread]);
});
// 5.同步函数
dispatch_sync(queue, ^{
NSLog(@"download2----%@",[NSThread currentThread]);
});
1.2 同步、异步、并发、串行的注意点:
- 同步和异步主要影响:能不能开启新的线程
- 同步:在当前线程中执行任务,
不具备开启新线程的能力 - 异步:在新的线程中执行任务,
具备开启新线程的能力
- 同步:在当前线程中执行任务,
- 并发和串行主要影响:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
创建一个同步串行队列
// 不论是哪种队列,都不会开启新线程
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
// 打印输出:
// <NSThread: 0x6000020198c0>{number = 1, name = main}
// <NSThread: 0x6000020191c0>{number = 1, name = main}
创建一个异步并发队列
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
// 打印输出:
// <NSThread: 0x6000020198c0>{number = 4, name = (null)}
// <NSThread: 0x6000020191c0>{number = 5, name = (null)}
创建一个异步串行队列
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("com.hp", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
// 打印输出:
// <NSThread: 0x6000020198c0>{number = 5, name = (null)}
// <NSThread: 0x6000020191c0>{number = 5, name = (null)}
在主队列中,不论是同步还是异步都不会开启子线程
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
// 打印输出:
// <NSThread: 0x6000020198c0>{number = 1, name = main}
但是使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
综上所述可以用一张图来概述
1.3 dispatch_get_global_queue和dispatch_queue_create的区别
我们在代码里分别创建两种队列,然后打印发现,全局队列的地址都是同一个,而dispatch_queue_create的对象都不相同
dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue3 = dispatch_queue_create("queu3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue5 = dispatch_queue_create("queu5", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"%p %p %p %p %p", queue1, queue2, queue3, queue4, queue5);
// 分别输出:0x10c5d8080 0x10c5d8080 0x6000037c3180 0x6000037c1580 0x6000037c3200
2. GCD的队列组
第一种创建方式
// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 2.创建队列组
dispatch_group_t group = dispatch_group_create();
// 3.把任务添加到队列中
dispatch_group_async(group, queue, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
// 4.拦截通知,当队列组中所有的任务都执行完毕的时候回进入到下面的方法
dispatch_group_notify(group, queue, ^{
NSLog(@"-------dispatch_group_notify-------");
});
第二种创建方式
// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 2.创建队列组
dispatch_group_t group = dispatch_group_create();
// 3.在该方法后面的异步任务会被纳入到队列组的监听范围,进入群组
// dispatch_group_enter|dispatch_group_leave 必须要配对使用
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"1----%@",[NSThread currentThread]);
//离开群组
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"2----%@",[NSThread currentThread]);
//离开群组
dispatch_group_leave(group);
});
// 拦截通知
// 内部本身是异步的
dispatch_group_notify(group, queue, ^{
NSLog(@"-------dispatch_group_notify-------");
});
第三种方式
// 1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 2.创建队列组
dispatch_group_t group = dispatch_group_create();
// 3.把任务添加到队列中
dispatch_group_async(group, queue, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
// 4.会阻塞线程
// 直到队列组中所有的任务都执行完毕之后才能执行
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"----end----");
3. 队列函数的应用
讨论一下以下的函数执行顺序是怎样的,检验对GCD的掌握程度
- (void)textDemo2{
// 同步队列
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
我们知道无论同步还是异步函数都是一个耗时任务。
再来一个,据说这个是新浪的面试题
- (void)wbinterDemo{
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{ NSLog(@"3"); });
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
// A: 1230789
// B: 1237890
// C: 3120798
// D: 2137890
}
正确答案是A 分析:首先开启的是一个串行队列,12行的代码阻塞的是13行以下的,所以3在0之前,123没有顺序,789也没有顺序,使用排除法得到A
4. 死锁问题
如果把队列修改为串行队列那么此时调用的顺序为:
- (void)textDemo1{
dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
此时打印崩溃:
此时在这里:
- 2执行完之后,执行到了一个代码块(
dispatch_sync而sync的特点是阻塞,必须等到自己执行完之后才可以) - 而队列由于是先进先出的原则,所以此时造成了4等待块执行完成
- 块的执行完成需要3执行,而3又等待4执行,这样就造成了一个死锁的问题。
改进:那么我们把4的任务删除,还会造成死锁嘛?
- 答案是:还会死锁 观察调用栈发现死锁的函数是:
_dispatch_sync_f_slow - 实际上发生死锁的
dispatch_async和dispatch_sync这两个代码块
5. GCD创建队列四种方式
// OS_dispatch_queue_serial 串行
dispatch_queue_t serial = dispatch_queue_create("hb", DISPATCH_QUEUE_SERIAL);
// OS_dispatch_queue_concurrent 并发
dispatch_queue_t conque = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
// DISPATCH_QUEUE_SERIAL max && 1
// queue 对象 alloc init class
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
我们知道dispatch_get_main_queue是一个串行队列并且这个队列是在main()调用之前主线程自动创建的,dispatch_get_global_queue是一个并发队列。 打印输出可以得到这些信息:
6. 其他常用方法
// 1.延迟执行的几种方法
// 1.1
[self performSelector:@selector(task) withObject:nil afterDelay:2.0];
// 1.2
// repeats:是否重复调用
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
// 1.3
// 可以设置队列控制在哪个线程执行延迟
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), queue, ^{
NSLog(@"GCD----%@",[NSThread currentThread]);
});
// 2.一次性代码
// 整个程序运行过程中只会执行一次
// onceToken用来记录该部分的代码是否被执行过
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"---once----");
});
// 3.快速遍历
// 开多个线程进行遍历
/*
第一个参数:遍历的次数
第二个参数:队列(并发队列)
第三个参数:index 索引
*/
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
NSLog(@"%zd---%@",index,[NSThread currentThread]);
});
// 4.栅栏函数
// 栅栏函数不能使用全局并发队列
// 栅栏函数之后的线程都会延后执行
dispatch_queue_t queue = dispatch_queue_create("download", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"+++++++++++++++++++++++++++++");
});
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
});
二、探索GCD的底层实现
苹果官方是对GCD做了开源的,所以我们可以去下载一下GCD源码,阅读学习一下。
1. 源码下载
我们可以通过GCD的源码libdispatch.dylib来分析内部实现
libdispatch.dylib的下载地址:opensource.apple.com/release/mac…
然后找到libdispatch-1173.0.3进行下载
2.源码分析
2.1 主队列分析
查看主队列的 api如下图:
- 主队列是一个
特殊的串行队列 - 主队列在调用
main()函数之前自动创建的。 - 主队列在应用程序上下文中用于与主线程和
main runloop交互。
那么断点在
main函数处去验证一下
通过断点,确实验证了主队列是在调用
main()函数之前自动创建的。
那么我们要看底层源码,该怎么看啊,首先我们得知道 GCD是属于哪个源码的,才能进一步去探索分析。
- 再次通过断点寻找,如下图
通过
bt 打印堆栈信息,可以定位到libdispatch.dylib动态库,那么就去苹果开源网站去下载源码试试。
- 下载libdispatch.dylib源码,探索
GCD。
libdispatch源码
libdispatch源码阅读起来比较有挑战性
- 注释非常的少
- 宏定义非常的多
- 函数名非常的长
源码搜索dispatch_get_main_queue:
dispatch_get_main_queue是通过DISPATCH_GLOBAL_OBJECT返回的,是一个宏定义
DISPATCH_GLOBAL_OBJECT
- 通过
_dispatch_main_q参数搜索
dispatch_get_main_queue(void)
{
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}
- 还可以通过主队列的
dq_label搜索如下:
在源码中有
dq_serialnum = 1,这是不是意味着可以作为主队列就是 串行队列的依据呢? 现在还不得而知,那么去看看串行队列底层是怎么实现的,或许可以找到答案!
- 主队列的初始化是在
dispatch_init()方法中?
- 在
dispatch_init()中成功找到了主队列初始化的地方, - 获取默认队列,
- 并将主队列地址绑定到当前队列和主线程中
2.2 串行、并发队列分析
串行队列和并发队列都通过dispatch_queue_create创建的,那么去搜索一下
通过搜索定位到
dispatch_queue_create,在通过返回的是_dispatch_lane_create_with_target,再继续搜索
- 代码比较长,从返回值看,再推导
_dispatch_object_alloc申请内存空间_dispatch_queue_init构造函数初始化- 判断是否为并发队列,如果是,传入
DISPATCH_QUEUE_WIDTH_MAX,否则传入1。也就是说,串行队列这里传入1,如果是并发队列,则传入DISPATCH_QUEUE_WIDTH_MAX
- 对
dq进行设置,如dq_label、dq_priority等
_dispatch_queue_init
- 把前面的
width传进来,赋值dqf |= DQF_WIDTH(width) DQF_WIDTH(width),也就是用来确定队列的类型,以此来区分串行队列和并发队列
其他参数
vtable、dqai,分别是什么呢?继续探索
dqai初始化
在开头有这么一句代码
// dqai 创建 - dqa传入的属性串行还是并行
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
_dispatch_queue_attr_to_info
在这里进行初始化了
dqai,并判断dqa的类型,如果是并发队列,则设置并发队列为true,否则默认为串行队列。在调用_dispatch_queue_init对dq进行构造时,对队列类型进行了区分,也就是DQF_WIDTH(width)的传参,串行队列width=1,否则为并发队列。
vtable
const void *vtable; // - 设置类 类是通过宏定义拼接而成
if (dqai.dqai_concurrent) {
// OS_dispatch_##name##_class
// OS_dispatch_queue_concurrent - 宏定义拼接类类型
vtable = DISPATCH_VTABLE(queue_concurrent);
} else {
vtable = DISPATCH_VTABLE(queue_serial);
}
vtable可以理解为是一个类,或者说构造队列的模板类,qai来区分队列的类型,根据队列的类型来初始化不同的vtable。DISPATCH_VTABLE是一个宏定义的方法,全局搜索DISPATCH_VTABLE的定义
// DISPATCH_VTABLE定义
#define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)
// vtable symbols - 模板
#define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))
// 拼接形成类
#define DISPATCH_CLASS_SYMBOL(name) OS_dispatch_##name##_class
DISPATCH_VTABLE函数的传参根据不同的队列类型传参不一致。
并发队列:
传queue_concurrent参数,最终拼接后,队列类型对应的类为:OS_dispatch_queue_concurrent
串行队列:
传queue_serial参数,最终拼接后,队列类型对应的类为:OS_dispatch_queue_serial
所以vtable对应的就是队列的类型。通过拼接完成类的定义,这和我们在应用层使用的队列类型是一致的,如下图:
2.3 全局队列分析
进入
dispatch_get_global_queue的api
- 创建全局并发队列时可以传参数
- 根据不同服务质量或者优先等级提供不同的并发队列。
通过全局队列的标识 在源码里面搜索🔍
- 系统会维护一个
全局队列集合, - 根据不同的
服务质量或者优先等级提供不同的全局队列。 - 我们在开发工作中默认使用:
dispatch_get_global_queue(0, 0)。
专题系列文章
1.前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数、枚举、可选项、结构体、类、闭包、属性、方法、swift多态原理、String、Array、Dictionary、引用计数、MetaData等Swift基本语法和相关的底层原理文章有如下几篇:
其它底层原理专题
1.底层原理相关专题
2.iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案