美文网首页
block分析(上)

block分析(上)

作者: 浅墨入画 | 来源:发表于2021-09-27 23:41 被阅读0次

读写锁的补充

实现读写锁的两种方案
  • 对底层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分为三种类型分别是GlobalBlockMallocBlockStackBlock

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

代码解读:

  1. objc初始化后的打印1,没有任何问题;
  2. strongBlock中打印3,拆分成两步进行分析:
    外部objc变量,被栈区Block捕获,引用计数+1;
    栈区Block赋值给强引用的strongBlock,将栈区Block拷贝到堆区,底层进行深拷贝,引用计数也会+1。
  3. 赋值给弱引用的weakBlock,属于栈区Block,仅对外部objc变量进行捕获,引用计数+1。
  4. 栈区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();
}
运行崩溃

代码解读:

  1. weakBlock栈区Block
  2. 按Block的底层源码,自定义_LGBlock结构体。只要内存结构一致,即可将Block桥接为自定义对象
  3. Block的本质是结构体,将结构体首地址赋值给__strong修饰的对象;
  4. 将结构体的invoke置空,即Block的函数指针
  5. strongBlock赋值给强引用的strongBlock1,然后对其进行调用。

闪退原因:

  • 第3步中,将结构体首地址赋值给对象,二者指向相同内存空间
  • 第4步中,将结构体的invoke置空,修改的是同一片内存空间;
  • 第5步中,将invoke置空后的Block赋值给strongBlock1,调用时坏地址访问,程序闪退。

修改案例:[strongBlock copy]; strongBlock的内存由栈变为堆

image.png

依然闪退:已经置为空了才去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

栈区Blockinvoke(指针)置空,并不影响堆区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

代码解读:

  1. weakBlock是一个未赋值的栈区block,其生命周期与blockDemo相同
  2. strongBlock也是一个栈区block,捕获变量a
  3. 将捕获变量a的栈区block赋值给weakBlock,此时weakBlock依然是栈区block
  4. strongBlock栈区block的生命周期也在blockDemo的栈帧中,执行完赋值给weakBlock
  5. weakBlock起了误导作用,weakBlock的赋值以及调用与{ }中的代码执行并无关系,该方法只是单纯的执行了{ }内的代码,而{ }并不是block

注意:
栈区Block生命周期与代码块无关,依赖于函数栈帧

修改案例:删除strongBlock前面的__weak

image.png

代码解读:

  1. weakBlock使用__weak修饰,赋值为nil;
  2. 定义代码块,实现一个堆区Block
  3. 堆区Block赋值给代码块外面的weakBlock
  4. 在代码块执行完毕后,调用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();
  1. self赋值给__weak修饰的weakSelf,此时weakSelf属于self的映射,指向同一片内存空间,并且self的引用计数不会发生变化
  2. Block中将weakSelf赋值给__strong修饰的strongSelf,避免self提前释放导致访问为nil的情况
  3. 因为strongSelf为临时变量,在Block作用域结束后,即可自动释放,因此不会循环引用
  • Block中强行切断持有者
__block ViewController *vc = self; 

self.block = ^(void){ 
    NSLog(@"%@",vc.name);
    vc = nil; 
};
self.block();
  1. 使用__block修饰对象,否则vc无法改变,也就是说无法置为nil
  2. Block中使用结束,手动将对象置为nil。相当于手动切断持有关系,可以避免循环引用;
  3. 缺陷:这种方式Block必须调用,否则将无法手动切断持有关系,self和block都无法释放,最终出现循环引用。
将持有者作为Block参数进行传递和使用
self.block = ^(ViewController *vc){ 
    NSLog(@"%@",vc.name);
};

self.block(self);
  1. self作为参数,提供给Block内部使用。当Block执行结束,vc会自动释放,然后相互持有关系会切断self也会释放
  2. 这种方式,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无法释放。

image.png

案例二:下面代码是否会出现循环引用?

- (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中的持有还存在,所以会出现循环引用。

相关文章

  • block分析(上)

    读写锁的补充 实现读写锁的两种方案 对底层pthread进行封装 GCD封装 读写锁要实现的功能 多读单写,多读就...

  • Block内存管理实例分析

    Block内存管理实例分析 Block内存管理实例分析

  • iOS Block 原理解析

    一 : block要点分析 [ block是个闭包 ] block他的本质就是闭包功能在iOS上的实现。而闭包功能...

  • Objective-C的Block实质与实现探究 part-5

    Block截获对象的情况 输出结果: 代码分析:在Block语法中使用array变量,因此array会被在栈上的B...

  • iOS Block学习笔记(五) -- Block截获静态变量(

    上一节分析了Block截获int, char*类型自动变量的源码, 发现Block内部用成员变量的形式保存了被截获...

  • Block探索

    Block内存关系Block经典问题循环引用&解决Block底层分析Block底层HooK 程序占用内存分类 栈区...

  • Block底层分析

    Block内存关系Block经典问题循环引用&解决Block底层分析Block底层HooK 1. 研究工具:cla...

  • block分析(下)

    block通过clang分析 带着下面的疑问,我们去探索block原理 探索block底层源码 block在底层是...

  • Block底层hook

    Block内存关系Block经典问题循环引用&解决Block底层分析Block底层HooK 前言 如何反编译出微信...

  • Block经典问题循环引用&解决

    Block内存关系Block经典问题循环引用&解决Block底层分析Block底层HooK 1.循环引用怎么产生的...

网友评论

      本文标题:block分析(上)

      本文链接:https://www.haomeiwen.com/subject/weixnltx.html