读写锁的补充
实现读写锁的两种方案
- 对底层
pthread
进行封装 - GCD封装
读写锁要实现的功能
- 多读单写,多读就是能够同时
拥有多条线程
或者多个任务读取
;单写就是同一时刻只有一条线程
操作这块内存空间,避免写的操作出现问题。 - 写入与写入进行
互斥
- 读取与写入也要
互斥
- 写的操作不能堵塞正常任务执行,
读写的操作是一个后台任务
可以通过栅栏函数
进行写的操作,保证上面读取与写入
、 写入与写入
之间互斥,而且还能保证主线业务流程正常执行。
多读的操作可以通过并发队列
实现
读写锁的实现
<!-- LGRWLock.h文件 -->
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGRWLock : NSObject
// 读数据
- (id)lg_objectForKey:(NSString *)key;
// 写数据
- (void)lg_setObject:(id)obj forKey:(NSString *)key forTime:(int)time;
@end
NS_ASSUME_NONNULL_END
<!-- LGRWLock.m文件 -->
#import "LGRWLock.h"
@interface LGRWLock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end
@implementation LGRWLock
- (id)init{
self = [super init];
if (self){
// 创建一个并发队列:
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据字典:
self.dataCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark - 读数据
- (id)lg_objectForKey:(NSString *)key{
__block id obj;
// 同步读取指定数据:
dispatch_sync(self.concurrent_queue, ^{
obj = [self.dataCenterDic objectForKey:key];
});
return obj;
}
#pragma mark - 写数据
- (void)lg_setObject:(id)obj forKey:(NSString *)key forTime:(int)time{
// 异步栅栏调用设置数据:
dispatch_barrier_async(self.concurrent_queue, ^{
sleep(time);
[self.dataCenterDic setObject:obj forKey:key];
NSLog(@"写情况: %@-%@",self.dataCenterDic[@"name"],[NSThread currentThread]);
});
}
@end
// 调用单写
self.lock = [[LGRWLock alloc] init];
[self.lock lg_setObject:@"kc1" forKey:@"name" forTime:4];
[self.lock lg_setObject:@"kc2" forKey:@"name" forTime:1];
[self.lock lg_setObject:@"kc3" forKey:@"name" forTime:2];
[self.lock lg_setObject:@"kc4" forKey:@"name" forTime:1];
// 多线程同步读
for (int i = 0; i<5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"读取情况: %@ - %d -%@",[self.lock lg_objectForKey:@"name"],i,[NSThread currentThread]);
});
}
block的分类
Block分为三种类型分别是GlobalBlock
、MallocBlock
、StackBlock
GlobalBlock:
- 位于
全局区
- 在Block内部不使用
外部变量
,或者只使用静态变量
和全局变量
void (^block)(void) = ^{
};
NSLog(@"%@",block);
// 控制台打印
2021-09-27 20:26:45.543569+0800 001---Block深入浅出[36628:22834052] <__NSGlobalBlock__: 0x1066b6100>
使用静态变量
、全局变量
、Block内部声明的变量
都没有问题
MallocBlock
- 位于
堆区
- 在Block内部使用
变量
或者OC属性
,并且可赋值给强引用
或者Copy修饰
的变量
int a = 10;
void (^block)(void) = ^{
NSLog(@"Cooci - %d",a);
};
NSLog(@"%@",block);
// 控制台打印
2021-09-27 20:31:15.654388+0800 001---Block深入浅出[36698:22840819] <__NSMallocBlock__: 0x60000063edc0>
对外部的a变量
进行捕获,赋值给强引用的block
;其中block持有的是堆区Block
的内存地址。
StackBlock
- 位于
栈区
- 与
MallocBlock
一样,可以在内部使用局部变量
或者OC属性
,但是不能赋值给强引用
或者Copy修饰
的变量
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"Cooci - %d",a);
};
NSLog(@"%@",block);
// 控制台打印
2021-09-27 20:36:17.254269+0800 001---Block深入浅出[36732:22844914] <__NSStackBlock__: 0x7ffeeac7c418>
同样对外部的a变量
进行捕获,和堆区Block的区别是赋值给弱引用的block
Block案例
-
案例一
:对外部变量的引用计数处理
- (void)blockDemo {
NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
void(^strongBlock)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
strongBlock();
void(^__weak weakBlock)(void) = ^{ // + 1
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
weakBlock();
void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();
}
// 控制台打印
2021-09-27 20:52:06.655815+0800 001---Block深入浅出[36856:22857674] 1
2021-09-27 20:52:06.655969+0800 001---Block深入浅出[36856:22857674] ---3
2021-09-27 20:52:06.656114+0800 001---Block深入浅出[36856:22857674] ---4
2021-09-27 20:52:06.656230+0800 001---Block深入浅出[36856:22857674] ---5
代码解读:
-
objc初始化
后的打印1,没有任何问题; - 在
strongBlock
中打印3,拆分成两步进行分析:
外部objc变量
,被栈区Block捕获
,引用计数+1;
栈区Block
赋值给强引用的strongBlock
,将栈区Block拷贝到堆区,底层进行深拷贝
,引用计数也会+1。 - 赋值给弱引用的
weakBlock
,属于栈区Block
,仅对外部objc变量
进行捕获,引用计数+1。 - 将
栈区Block
调用copy方法,赋值给mallocBlock
,仅对栈区Block进行了深拷贝,引用计数+1。
上面第3步
和第4步
,等同于第2步
的分解,最终打印结果:1、3、4、5
-
案例二
:内存拷贝的理解
- (void)blockDemo1{
int a = 0;
void(^ __weak weakBlock)(void) = ^{
NSLog(@"-----%d", a);
};
struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
id __strong strongBlock = weakBlock;
blc->invoke = nil;
void(^strongBlock1)(void) = strongBlock;
strongBlock1();
}
运行崩溃
代码解读:
-
weakBlock
为栈区Block
; - 按Block的底层源码,
自定义_LGBlock结构体
。只要内存结构一致
,即可将Block桥接为自定义对象
; - Block的本质是
结构体
,将结构体首地址赋值给__strong
修饰的对象; - 将结构体的
invoke置空
,即Block的函数指针
; - 将
strongBlock
赋值给强引用的strongBlock1
,然后对其进行调用。
闪退原因:
- 在
第3步
中,将结构体首地址赋值给对象
,二者指向相同内存空间
; - 在
第4步
中,将结构体的invoke置空,修改的是同一片内存空间; - 在
第5步
中,将invoke置空后的Block赋值给strongBlock1
,调用时坏地址访问
,程序闪退。
修改案例:[strongBlock copy];
strongBlock的内存由栈变为堆
依然闪退
:已经置为空了才去copy,这个时候已经没有意义了。
解决办法
,必须在invoke置空之前
,将Block进行深拷贝
- (void)blockDemo1{
int a = 0;
void(^ __weak weakBlock)(void) = ^{
NSLog(@"-----%d", a);
};
struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
// 深拷贝一份,变为堆block 调用无影响;如果不copy的话 操作同一个地址 调用崩溃
id __strong strongBlock = [weakBlock copy];
blc->invoke = nil;
void(^strongBlock1)(void) = strongBlock;
strongBlock1();
}
// 控制台打印
2021-09-27 21:34:19.425625+0800 001---Block深入浅出[37048:22884421] -----0
栈区Block
将invoke(指针)置空
,并不影响堆区Block
的调用
-
案例三
:堆栈Block的释放
- (void)blockDemo {
int a = 0;
void(^__weak weakBlock)(void) = nil;
{
void(^__weak strongBlock)(void) = ^{
NSLog(@"---%d", a);
};
weakBlock = strongBlock;
NSLog(@"1");
}
weakBlock();
}
// 控制台打印
2021-09-27 21:40:31.248816+0800 001---Block深入浅出[37089:22888896] 1
2021-09-27 21:40:31.248961+0800 001---Block深入浅出[37089:22888896] ---0
代码解读:
-
weakBlock
是一个未赋值
的栈区block,其生命周期与blockDemo
相同 -
strongBlock
也是一个栈区block,捕获变量a - 将捕获变量a的
栈区block
赋值给weakBlock
,此时weakBlock
依然是栈区block -
strongBlock
栈区block的生命周期也在blockDemo
的栈帧中,执行完赋值给weakBlock
-
weakBlock
起了误导作用,weakBlock
的赋值以及调用与{ }
中的代码执行并无关系,该方法只是单纯的执行了{ }
内的代码,而{ }
并不是block
注意:
栈区Block
的生命周期
与代码块无关,依赖于函数栈帧
修改案例:删除strongBlock
前面的__weak
代码解读:
-
weakBlock
使用__weak修饰,赋值为nil; - 定义代码块,实现一个
堆区Block
; - 将
堆区Block
赋值给代码块外面的weakBlock
; - 在代码块执行完毕后,调用weakBlock。
闪退原因:
- 在
第3步
中,堆区Block赋值__weak修饰的weakBlock,相当于映射关系; - 在
第4步
中,当代码块执行完毕,strongBlock由于是堆区Block,出了代码块就会被释放。作为映射的weakBlock
,自然也会被置为nil
。此时对其进行调用,出现坏地址访问,程序闪退。
block循环引用
Block拷⻉到堆区
如果Block为全局Block
,使用任何方式都不会拷贝到堆区
,即使手动copy也没用,它依然是全局Block
除了全局Block
,以下四种操作
系统会将Block复制到堆上
- 手动Copy
- Block作为返回值
- 被强引用或Copy修饰
- 系统API包含usingBlock
循环引用
//block1
self.block = ^(void){
NSLog(@"%@",self.name);
};
//block2
[UIView animateWithDuration:1 animations:^{
NSLog(@"%@",self.name);
}];
-
Block1会出现循环引用
,因为block被self持有,而block中使用self,所以self又被block持有。这种相互持有的情况下,就会出现循环引用。 -
Block2不会出现循环引用
,因为block的持有者是UIView,和self无关,不会出现相互持有的情况,所以不会循环引用。
对象正常释放的过程
image.png- 当A持有B,
B的retainCount进行+1
- 当A触发dealloc时,会给B放信号,
B的retainCount进行-1
。此时B的retainCount如果为0,就会调用dealloc,正常释放
对象循环引用的过程
image.png- 当A和B
相互持有
时,A的dealloc无法触发
,因为A要等B发信号才能对retainCount进行-1; -
B的dealloc也无法触发
,因为B也在等待A的信号。此时A和B都在等待对方的释放,最终出现循环引用。
避免循环引用的方式
weak-strong-dance
__weak typeof(self) weakSelf = self;
self.block = ^(void){
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%@",strongSelf.name);
};
self.block();
- 将
self
赋值给__weak
修饰的weakSel
f,此时weakSelf属于self的映射,指向同一片内存空间,并且self的引用计数不会发生变化
- 在
Block
中将weakSelf
赋值给__strong
修饰的strongSelf
,避免self提前释放导致访问为nil的情况 - 因为
strongSelf
为临时变量,在Block作用域结束后
,即可自动释放
,因此不会循环引用
- Block中强行切断持有者
__block ViewController *vc = self;
self.block = ^(void){
NSLog(@"%@",vc.name);
vc = nil;
};
self.block();
- 使用
__block
修饰对象,否则vc无法改变,也就是说无法置为nil - 在
Block中
使用结束,手动将对象置为nil。相当于手动切断持有关系,可以避免循环引用; - 缺陷:这种方式
Block必须调用
,否则将无法手动切断持有关系,self和block都无法释放,最终出现循环引用。
将持有者作为Block参数进行传递和使用
self.block = ^(ViewController *vc){
NSLog(@"%@",vc.name);
};
self.block(self);
- 将
self
作为参数,提供给Block
内部使用。当Block
执行结束,vc
会自动释放,然后相互持有关系会切断
,self也会释放
- 这种方式,self的释放依赖于
Block
的执行结束。如果Block中有延迟执行的代码
,self的释放也会延迟
block循环引用的面试题
案例一:下面代码是否会出现循环引用?
static ViewController *staticSelf_;
- (void)blockWeak_static {
__weak typeof(self) weakSelf = self;
staticSelf_ = weakSelf;
}
[self blockWeak_static];
答案:会出现循环引用
分析:将self
赋值__weak
修饰的对象,它们属于映射关系
,指向同一片内存空间。当weakSelf赋值全局静态变量
,staticSelf_在程序运行过程中不会主动释放,它会持续持有self
,导致self无法释放。
案例二:下面代码是否会出现循环引用?
- (void)block_weak_strong {
__weak typeof(self) weakSelf = self;
self.doWork = ^{
__strong typeof(self) strongSelf = weakSelf;
//捕获strongSelf 导致无法释放
weakSelf.doStudent = ^{
NSLog(@"%@", strongSelf);
};
weakSelf.doStudent();
};
self.doWork();
}
[self block_weak_strong];
答案:会出现循环引用
分析:在doWork
内部,strongSelf
持有的是self
。虽然strongSelf
是临时变量,但在doStudent
中又被持有,导致引用计数+1。在doWork执行完毕后引用计数-1,但doStudent
中的持有还存在,所以会出现循环引用。
网友评论