本节将介绍内存五大区
和多线程
:
- 内存五大区
- 多线程
- 互斥锁与自旋锁
- atomic与nonatomic的区别
- 线程与RunLoop
1. 内存五大区
按照地址
从高
到低
排列: 栈区
-> 堆区
-> 全局静态区
-> 常量区
-> 代码区
(内核区
和保留部分
不再考虑范围内)
补充说明:
内存五大区
,实际是指虚拟内存
,而不是
真实物理内存
。(详情可查看👉 本文第3点 虚拟内存与物理内存)iOS系统
中,应用
的虚拟内存
默认分配4G
大小,但五大区
只占3G
,还有1G
是五大区
之外的内核区
1.1 栈区
- 函数内部定义的
局部变量
和数组
,都存放在栈区
; (比如每个函数都有的(id self, SEL _cmd)
) - 栈区的
内存空间
由系统管理
。(函数调用
时开辟空间
,函数调用结束
时回收空间
) - 栈是从
高地址
向低地址
扩展,是一块连续的内存区域
,遵循FILO先进后出
原则,效率高。 - 栈区一般在
运行时
进行分配
缓冲区域
栈区
和堆区
中间有小块未使用
的内存区域
。用于给栈区
和堆区
之间创建一个缓冲区域
-
溢出:
到达缓冲区
的数据
向小缓冲区复制
的过程中,由于没有注意
小缓冲区的边界
,导致小缓存区满了
,从而覆盖
了和小缓存区相邻内存区域
的其他数据
而引起
的内存问题
。
(就像桶盛水,水多了,自然越界溢出来了。)
1.2 堆区
- 空间最大,由我们手动管理。(ARC自动管理)
- 堆是从
低地址
向高地址
扩展。 -
malloc、calloc、realloc开辟
: 堆区开辟空间
,可以是不连续
的内存区域
,以链表结构
存在(增删快,查找慢)。返回首地址
存放在栈区。 -
free回收
。释放
对象在堆区
的内存
,并将栈中
的地址指针置空
。
需要注意:
-
野指针
:提前释放了,查询时找不到内容 -
内存泄露
:没有释放,一直占用内存 -
过度释放
:对已释放的对象进行release操作。
1.3 全局静态区(.bss)
- 存放
全局变量
和静态变量
- 空间由
系统管理
。(程序启动
时,开辟空间
;程序结束
时,回收空间
;程序执行期间一直存在
) -
static
修饰的变量仅执行一次
,生命周期
为整个程序运行期
1.4 常量区(.data)
- 存放常量(
整型
、字符型
,浮点
,字符串
等),整个程序运行期
不能被改变。 - 空间由
系统管理
,生命周期
为整个程序运行期
。
1.5 代码区(.text)
- 存放
程序执行
的CPU指令
。(编译期
将代码
转换为CPU
指令)
define
和const
区别:
define
: 宏。编译期不会
进行语法识别
,没有类型
。编译期会分配内存
。每次使用
都会进行宏替换
和开辟内存
。
const
: 常量。编译期会
进行语法识别
,需要指定类型
。编译期不
会分配内存
,仅在第一次
使用时,开辟内存
并记录
内存地址
。后续
调用时不
会开辟内存
,直接返回
记录的内存地址
。效率
更快
。内存
占用更少
。
可以通过以下代码,加深印象:
- (void)test {
NSInteger i = 666;
NSLog(@"NSInteger i -> 内存地址:%p", &i); // 【局部变量】 栈区
NSString * name = @"HT";
NSLog(@"NSString name -> 内存地址: %p", name); // 【字符串内容】 存放在常量区
NSLog(@"NSString name -> 指针地址: %p", &name);// 【局部变量name的指针】 存放在栈区
NSObject * objc = [NSObject new];
NSLog(@"NSObject objc -> 内存地址: %p", objc);// 【对象的内容】 存放在堆区
NSLog(@"NSObject objc -> 指针地址: %p", &objc);//【对象的指针】 存放在栈区
}
- 打印结果: (
0x7
开头:栈区
、0x1
开头:常量区
、0x6
开头:堆区
)
2. 多线程
官方文档: 👉 相关链接
1. 线程和进程的定义
- 线程:
-
线程
是进程
的基本执行单元
,一个进程
的所有任务
都在线程
中执行
-
进程
想要执行任务,必须得有线程
,进程至少
要有一条
线程 -
程序启动
会默认开启
一条线程,这条线程被称为主线程
或UI线程
- 进程:
-
进程
是指在系统中正在运行
的一个应用程序
- 每个
进程
之间是独立
的,每个进程均运行
在其专用
的且受保护
的内存空间
内 - 通过
活动监视器
可以查看
Mac系统中所开启的线程
。
2. 线程与进程的关系
-
地址空间:
同一进程
的线程共享
本进程的地址空间
,而进程之间
则是独立
的地址空间。 -
资源拥有:
同一进程
内的线程共享
本进程内的资源
(如内存
、I/O
、cpu
等),但进程之间
资源是相互独立
的。
-
进程崩溃
后,保护模式下不会
对其他进程
产生影响
,但一个线程崩溃
会导致整个进程
都死掉
。所以多进程比多线程健壮
。 -
进程切换
时,消耗
的资源大
。涉及频繁切换
时,使用线程
要好过
于进程
。同样要求同时进行
且共享
某些变量
的并发操作
时,只能
用线程
不能用进程。 -
执行过程
:每个独立的进程
都有一个程序运行入口
和顺序执行序列
。但是线程不能独立执行
,必须依存
在应用程序
中,由应用程序
提供多个线程
执行控制
。 -
线程
是处理器
调度的基本单位
,但进程不是
。 -
线程
没有地址空间
,线程包含
在进程地址空间中
。
3. 多线程的意义
- 优点:
- 适当提高
执行效率
- 适当提高
资源
的利用率
(CPU、内存等) - 线程上的
任务执行完
后,线程会自动销毁
- 缺点:
-
开启线程
需要占用
一定的内存空间
(参照下面 第5点 线程成本 ) - 开启大量线程,会
占用
大量内存空间
,降低
程序性能
-
线程越多
,CPU
在调度线程上的开销越大
- 程序
设计
更加复杂
(如线程间的通讯,多线程的数据共享等)
4. 时间片
时间片的概念: CPU
在多个任务
之间进行快速切换
,这个时间间隔
就是时间片
。
(
单核CPU
)同一时间,CPU只能处理1个线程
多线程同时执行:
理论上,只要CPU
在多个线程切换
的足够快
(时间片足够小),就可以做出"同时执行"
的假象
。
(但实际上,一个CPU
单次只对一个线程
进行调度
。所以多线程同步
需要多个CPU
的处理器
(多核
)才可以做到。)如果
线程
数非常多
,CPU在多个线程之间切换,会消耗
大量CPU资源
。
每个线程
被调度的次数
会降低
,线程的执行效率
会降低
5. 线程成本
- 谷歌翻译:
-
内核数据结构
: 1KB -
堆空间
:iOS主线程:1MB,OSX主线程:8MB、其他辅助线程:512KB -
创建时间
:平均90微妙
6. 多线程技术方案
1. pthread
pthread
是一套通用
的多线程 API
,可以在Unix
/ Linux
/ Windows
等系统跨平台使用,使用 C 语言
编写,需要程序员自己管理
线程的生命周期
,使用难度较大
,我们在 iOS 开发中几乎不使用
。
- 简单使用实例:
#import <pthread.h>
- (void)createThread {
// 1. 创建线程:定义一个pthread_t类型变量
pthread_t thread;
// 2. 开启线程:执行任务
// 参数1: 要开的线程变量
// 参数2:线程的属性
// 参数3:子线程的执行函数(任务)
// 参数4:函数入参
pthread_create(&thread, NULL, run, @"入参");
// 3. 设置子线程的状态设置为detached,该线程运行结束后会自动释放所有资源
pthread_detach(thread);
}
void * run(void * param) {
NSLog(@"%@ %@", [NSThread currentThread], param);
return NULL;
}
- 打印结果:
- 其他方法:
pthread_create()
: 创建一个线程
pthread_exit()
: 终止当前线程
pthread_cancel()
: 中断另外一个线程的运行
pthread_join()
:阻塞当前的线程,直到另外一个线程运行结束
pthread_attr_init()
:初始化线程的属性
pthread_attr_setdetachstate()
:设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
pthread_attr_getdetachstate()
:获取脱离状态的属性
pthread_attr_destroy()
: 删除线程的属性
pthread_kill()
: 向线程发送一个信号
2. NSThread
NSThread
是苹果官方提供的,使用起来比pthread
更加面向对象,简单易用,可直接操作
线程对象
,需要自己管理
线程生命周期
。实际开发中偶尔使用
。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self createThread1];
// [self createThread2];
// [self createThread3];
}
//MARK: - 创建线程
//创建线程 (手动启动)
-(void)createThread1{
// 实例化一个线程对象
NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
// 线程名称
thread.name = @"Thread1Name";
/**
NSQualityOfServiceUserInteractive = 0x21, 用户交互 - 最高(21)
NSQualityOfServiceUserInitiated = 0x19, 用户马上执行的事件 - 较高(19)
NSQualityOfServiceUtility = 0x11, 普通任务 - 普通(11)
NSQualityOfServiceBackground = 0x09, 后台任务 - 较低 (9)
NSQualityOfServiceDefault = -1 常规 - 最低
*/
// 线程优先级
thread.qualityOfService = NSQualityOfServiceDefault;
// 线程启动
[thread start];
}
//创建线程 (自动启动)
-(void)createThread2{
//创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(run2:) toTarget:self withObject:@"createThread2"];
}
//创建线程 (自动启动)
-(void)createThread3{
//隐式创建线程并启动
[self performSelectorInBackground:@selector(run2:) withObject:@"createThread3"];
}
//MARK: - 耗时操作
- (void)run1{
for (int i=0; i<200; i++) {
NSLog(@"%d----%@",i,[NSThread currentThread]);
}
}
- (void)run2:(NSString*)param{
for (int i=0; i<200; i++) {
NSLog(@"%d----%@---%@",i,[NSThread currentThread],param);
}
}
- 创建:
-
createThread1
:手动
启动线程,可以配置
线程属性(名称、优先级); -
createThread2
:自动
启动线程,快捷便利,不支持配置
线程属性; -
createThread3
:隐式
创建线程并启动,快捷便利,不支持配置
线程属性;
- 任务:
run1
无参数,run2
带参数
其他方法:
+ (NSThread *)mainThread
: 获得主线程
- (BOOL)isMainThread
: 判断是否为主线程(对象方法)
+ (BOOL)isMainThread
: 判断是否为主线程(类方法)
NSThread *current = [NSThread currentThread]
: 获得当前线程
- (void)setName:(NSString *)n
: 线程的名字——setter方法
- (NSString *)name
: 线程的名字——getter方法
- (BOOL)isCancelled
:判断是否已取消
- (BOOL)isFinished
:判断是否已经结束
- (BOOL)isExecuting
:判断是否正在执行状态控制方法:
- (void)start
:线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
- (void)cancel
: 线程取消
- (void)setName:(NSString *)n
: 线程的名字——setter方法
+ (void)sleepUntilDate:(NSDate *)date
:阻塞(暂停)线程方法
+ (void)sleepForTimeInterval:(NSTimeInterval)ti
:线程进入阻塞状态
+ (void)exit
:线程进入死亡状态(立即终止除主线程以外所有线程)线程之间的通信:
在主线程上执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array
在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
- 售票案例(
线程安全
,多读单写
)
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount; // 剩余票数
@property (nonatomic, strong) NSThread *ticketSaleWindow1; // 线程1
@property (nonatomic, strong) NSThread *ticketSaleWindow2; // 线程2
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self initTicketStatusSave];
}
/**
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
// 1. 设置剩余火车票为 50
self.ticketSurplusCount = 50;
// 2. 设置北京火车票售卖窗口的线程
self.ticketSaleWindow1 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
self.ticketSaleWindow1.name = @"北京火车票售票窗口";
// 3. 设置上海火车票售卖窗口的线程
self.ticketSaleWindow2 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicketSafe) object:nil];
self.ticketSaleWindow2.name = @"上海火车票售票窗口";
// 4. 开始售卖火车票
[self.ticketSaleWindow1 start];
[self.ticketSaleWindow2 start];
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 互斥锁
@synchronized (self) {
//如果还有票,继续售卖
if (self.ticketSurplusCount > 0) {
self.ticketSurplusCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
}
//如果已卖完,关闭售票窗口
else {
NSLog(@"所有火车票均已售完");
break;
}
}
}
}
3. GCD
- GCD(
Grand Central Dispatch
),大中央调度
。
对线程操作
进行了封装
,加入了很多新的特性,内部进行了效率优化
,提供了简洁的C
语言接口
,使用简单高效
,是苹果推荐的方式。使用频率高
。
#pragma mark - GCD演练
/**
并发队列,同步执行
*/
- (void)gcdDemo4 {
// 1. 队列
dispatch_queue_t queue = dispatch_queue_create("itcast", DISPATCH_QUEUE_CONCURRENT);
// 2. 同步执行任务
for (int i = 0; i < 10; i++) {
dispatch_sync(queue, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
}
/**
并发队列,异步执行
*/
- (void)gcdDemo3 {
// 1. 队列
dispatch_queue_t queue = dispatch_queue_create("itcast", DISPATCH_QUEUE_CONCURRENT);
// 2. 异步执行任务
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
}
/**
串行队列,异步执行
*/
- (void)gcdDemo2 {
// 1. 队列
dispatch_queue_t queue = dispatch_queue_create("itcast", NULL);
// 2. 异步执行任务
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
}
/**
串行队列,同步执行(开发中非常少用)
*/
- (void)gcdDemo1 {
// 1. 队列
// dispatch_queue_t queue = dispatch_queue_create("icast", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue = dispatch_queue_create("icast", NULL);
NSLog(@"执行前----");
// 执行任务
for (int i = 0; i < 10; i++) {
NSLog(@"调度----");
// 在队列中"同步"执行任务,串行对列添加同步执行任务,会立即被执行
dispatch_sync(queue, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
NSLog(@"for 后面");
}
4. NSOperation
NSOperation
是基于GCD
的一个抽象基类
,将线程封装
成要执行的操作,不需要管理
线程的生命周期
和同步
,但比GCD可控性
更强
。例如可以加入操作依赖
(addDependency
)、设置操作队列
最大可并发执行的操作个数
(setMaxConcurrentOperationCount
)、取消
操作(cancel
)等。
NSOperation
作为抽象基类不具备
封装我们的操作的功能,需要使用两个它的实体子类:NSBlockOperation
和NSInvocationOperation
,或者继承NSOperation自定义子类
。
NSBlockOperation
和NSInvocationOperation
用法的主要区别
是:前者
执行指定的方法
,后者
执行代码块
,相对来说后者
更加灵活易用
。NSOperation
操作配置完
成后便可调用start
函数在当前线程执行
,如果要异步执行避免阻塞
当前线程则可以加入NSOperationQueue
中异步执行
。
- 测试代码
- (void)opDemo2 {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
[q addOperationWithBlock:^{
NSLog(@"耗时操作 %@", [NSThread currentThread]);
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"更新UI %@", [NSThread currentThread]);
}];
}];
}
- (void)opDemo1 {
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@"Invocation"];
// start 会立即在当前线程执行 selector 方法
// [op start];
// 将操作添加到队列,会自动异步执行
NSOperationQueue *q = [[NSOperationQueue alloc] init];
[q addOperation:op];
}
- (void)downloadImage:(id)obj {
NSLog(@"%@ %@", [NSThread currentThread], obj);
}
7. 线程生命周期
-
新建:
new
新建线程后,调用start
后,并不会立即执行
,而是进入就绪
状态,等待CPU
的调度。
-
新建:
-
运行:
CPU
调度当前线程
,进入运行状态
,开始执行任务。
如果当前线程
还在运行中
,CPU
从可调度池中调用其他线程
,来执行此任务。
-
运行:
-
阻塞:
运行中
的任务,被调用sleep
/等待同步锁
时,会进入阻塞状态
。所有线程都停止,等待sleep结束
/获取同步锁
,才会回到就绪状态
。
-
阻塞:
-
死亡:
运行中
的任务,在任务执行完
或被强制退出
时,线程自动进入Dead
销毁。
-
死亡:
线程池调度:
饱和策略:
3. 互斥锁与自旋锁
- 互斥锁:
- 保证
锁内代码
,同一时间
,只有一条线程
能够执行
; - 互斥锁的
锁定范围
,应该尽量小
,锁定范围越大
,效率越差
- 互斥锁参数:
- 能够
加锁
的任意NSObject
对象 -
锁对象
要保证所有线程
都能够访问
- 如果代码只有
一个地方
需要加锁
,大多都使用self
,这样可以避免
单独再创建
一个锁对象
-
自旋锁:
耗性能
,循环轮循
是否可执行
。自旋锁内容
应尽可能小
,保障尽快完成锁内任务
。
互斥锁与自旋锁的区别:
互斥锁
是被动等待
代码触发
,再上锁。自旋锁
是主动轮循
请求资源。所以自旋锁
更消耗资源。
- 要求
立即执行
,任务资源较小
(执行耗时短)时,可选择自旋锁
。被动触发
,任务资源较大
(执行耗时长)时,选择互斥锁
。
4. atomic与nonatomic的区别
nonatomic:
非原子
属性。非线程安全
,适合内存小的移动设备。atomic
原子
属性。线程安全
,需要消耗大量的资源。是默认值
。
atomic
是针对多线程设计的,本身有自旋锁
, 实现单写多读
:单个线程写入,多个线程可以读取。
iOS官方建议:
所有属性都声明为nonatomic
,避免
多线程抢夺
同一块资源
。
尽量将加锁
、资源抢夺
的业务逻辑交给服务器端处理
,减小
移动客户端的压力
**
5. 线程与RunLoop
-
RunLoop
与线程
是一一对应
的,一个runloop
对应一个核心
的线程。
为什么说是核心的,是因为
runloop
是可以嵌套的,但是核心
的只能有一个
,他们的关系
保存在一个全局字典
里。
Runloop
是来管理线程
的,当线程的runloop被开启
后,线程会在执行完
任务后进入休眠状态
,有任务
就会被唤醒
去执行任务。Runloop
在第一次
获取时被创建
,在线程结束
时被销毁
。对于
主线程
来说,runloop
在程序一启动
就默认创建好
了。对于
子线程
来说,runloop
是懒加载
的,只有当我们使用时
才会创建
,所以在子线程用定时器要注意:确保
子线程的runloop
被创建
,不然定时器不会回调
。