OC基础-BLock

299 阅读11分钟
本文已参与新人创作礼活动,一起开启掘金创作之路。

block

参考文章:

block本质、截获变量特性、__block修饰

一.本质

Block是将函数及其执行上下文封装起来的对象

image.png

block本质上也是一个OC对象,它内部也有个isa指针

block是封装了函数调用以及函数调用环境的OC对象

屏幕快照 2019-09-11 下午6.13.03.png

屏幕快照 2019-09-11 下午6.10.52.png

二.变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

  • 对于全局变量,不会捕获到 block 内部,访问方式为直接访问

  • 对于 auto 类型的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,用来存储这个变量的值,访问方式为值传递

  • 对于 static 类型的局部变量,会捕获到 block 内部,block 内部会自动生成一个成员变量,用来存储这个变量的地址,访问方式为指针传递

  • 对于对象类型的局部变量,block 会连同它的所有权修饰符一起捕获。\

屏幕快照 2019-09-11 下午6.12.20.png

截屏2022-05-25 上午11.17.56.png

为什么局部变量需要捕获,全局变量不用捕获呢?

  • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获;
  • 局部变量,外部不能直接访问,所以需要捕获;
  • auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值;
  • static 变量会一直保存在内存中, 所以捕获其地址即可。

Q:self 会不会捕获到 block 内部?

会捕获。
OC 方法都有两个隐式参数,方法调用者self和方法名_cmd
参数也是一种局部变量。

Q:_name 会不会捕获到 block 内部?

会捕获。
不是将_name变量进行捕获,而是直接将self捕获到 block 内部,因为_name是 Person 类的成员变量,_name来自当前的对象/方法调用者self(self->_name)
如果使用self.name即调用selfgetter方法,即给self对象发送一条消息,那还是要访问到selfself是局部变量,不是全局变量,所以self会捕获到 block 内部。

三.类型(block的内存管理)

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

image.png

impl.isa = NSConcteteStackBlock, isa会标记block是哪种类型

  • 全局block = NSConcreteGlobalBlock 存放在内存的已初始化数据区域中
  • 栈block = NSConcreteStackBlock 存放在内存的栈上面
  • 堆上面的block = NSConcreteMallocBlock 存放在内存的堆上面

截屏2022-05-09 上午11.23.56.png 我们在何时对Block进行copy操作

对于不同类型的Block,copy的效果

  • NSConcreteGlobalBlock(全局block) - copy后什么也不做
  • NSConcreteStackBlock(栈block) - copy后会在堆上产生一个block
  • NSConcreteMallocBlock(堆block) - copy后会增加其引用计数 问题:

P类中有个assign修饰的block,假如在方法A中,我们P.block = ^(int){***} 因为方法A是在栈上,执行完在内存中就销毁了,假如在后面我们又调用了P.block 就会崩溃!!!

比如现在声明一个成员变量Block,而在栈上创建这个Block,同时赋值给成员变量的Block。 如果没有对成员变量的Block进行Copy操作的话,当我们通过成员变量去访问对应的Block的时候, 可能会因为栈对应的函数退出之后在内存当中就销毁掉了,继续访问就会引起内存崩溃

遇到一个Block,我们怎么知道这个Block的存储位置呢?

(1)Block不访问外界变量(包括栈中和堆中的变量)

Block 既不在栈又不在堆中,在代码段中,ARC和MRC下都是如此。此时为全局块。

(2)Block访问外界变量

MRC 环境下:访问外界变量的 Block 默认存储栈中。
ARC 环境下:访问外界变量的 Block 默认存储在堆中(实际是放在栈区,然后ARC情况下自动又拷贝到堆区),自动释放。

四.block的copy

1.在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  • block作为函数返回值时

  • 将block赋值给__strong指针时

  • block作为Cocoa API中方法名含有usingBlock的方法参数时

  • block作为GCD API的方法参数时

2.MRC下block属性的建议写法

@property (copy, nonatomic) void (^block)(void);

3.ARC下block属性的建议写法

@property (strong, nonatomic) void (^block)(void);

@property (copy, nonatomic) void (^block)(void);

五.对象类型的auto变量

1.当block内部访问了对象类型的auto变量时

如果block是在栈上,将不会对auto变量产生强引用

屏幕快照 2019-09-11 下午6.22.43.png

2.如果block被拷贝到堆上

会调用block内部的copy函数

copy函数内部会调用_Block_object_assign函数

_Block_object_assign函数会根据auto变量的修饰符( **strong、 **weak、 __unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

3.如果block从堆上移除

会调用block内部的dispose函数

dispose函数内部会调用_Block_object_dispose函数 _Block_object_dispose函数会自动释放引用的auto变量(release)

屏幕快照 2019-09-11 下午6.22.43.png

4.__weak问题解决

(1)在使用clang转换OC为C++代码时,可能会遇到以下问题

cannot create __weak reference in file using manual reference

(2)解决方案:支持ARC、指定运行时系统版本,比如

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

六.__block

  • __block 可以用于解决 block 内部无法修改 auto 变量值的问题;

  • __block 不能修饰全局变量、静态变量;

  • 编译器会将 __block 变量包装成一个对象(struct __Block_byref_age_0(byref:按地址传递));

  • 加 __block 修饰不会修改变量的性质,它还是 auto 变量;

  • 一般情况下,对被捕获变量进行赋值(赋值!=使用)操作需要添加 __block 修饰符。比如给数组添加或者删除对象,就不用加 __block 修饰;

  • 在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题。

截屏2022-05-26 下午3.02.22.png

截屏2022-05-26 下午3.05.55.png

屏幕快照 2019-09-11 下午6.26.03.png

1.__block的内存管理

(1)当block在栈上时,并不会对__block变量产生强引用

(2)当block被copy到堆时

会调用block内部的copy函数 copy函数内部会调用_Block_object_assign函数 _Block_object_assign函数会对__block变量形成强引用(retain) 屏幕快照 2019-09-11 下午6.28.20.png

屏幕快照 2019-09-11 下午6.30.03.png

(3)当block从堆中移除时

会调用block内部的dispose函数 dispose函数内部会调用_Block_object_dispose函数

_Block_object_dispose函数会自动释放引用的__block变量(release) 屏幕快照 2019-09-11 下午6.31.09.png

2.__forwarding指针

屏幕快照 2019-09-11 下午6.31.59.png

__block 的__forwarding指针存在的意义?
为什么要通过 age 结构体里的__forwarding指针拿到 age 变量的值,而不直接 age 结构体拿到 age 变量的值呢?

__block 的__forwarding是指向自己本身的指针,为了不论在任何内存位置,都可以顺利的访问同一个 __block 变量。

  • block 对象 copy 到堆上时,内部的 __block 变量也会 copy 到堆上去。为了防止 age 的值赋值给栈上的 __block 变量,就使用了__forwarding
  • 当 __block 变量在栈上的时候,__block 变量的结构体中的__forwarding指针指向自己,这样通过__forwarding取到结构体中的 age 给它赋值没有问题;
  • 当 __block 变量 copy 到堆上后,栈上的__forwarding指针会指向 copy 到堆上的 _block 变量结构体,而堆上的__forwarding指向自己;

这样不管我们访问的是栈上还是堆上的 __block 变量结构体,只要是通过__forwarding指针访问,都是访问到堆上的 __block 变量结构体;给 age 赋值,就肯定会赋值给堆上的那个 __block 变量中的 age。

3.对象类型的auto变量、__block变量

(1)当block在栈上时,对它们都不会产生强引用

(2)当block拷贝到堆上时,都会通过copy函数来处理它们

__block变量(假设变量名叫做a)

_Block_object_assign((void** )&dst->a, (void )src->a, 8/ BLOCK_FIELD_IS_BYREF /);

对象类型的auto变量(假设变量名叫做p)

_Block_object_assign((void** )&dst->p, (void )src->p, 3/ BLOCK_FIELD_IS_OBJECT /);

(3)当block从堆上移除时,都会通过dispose函数来释放它们

__block变量(假设变量名叫做a)

_*Block_object_dispose((void** )src->a, 8/ BLOCK_FIELD_IS_BYREF /);

对象类型的auto变量(假设变量名叫做p)

_*Block_object_dispose((void** )src->p, 3/ BLOCK_FIELD_IS_OBJECT /);

4.被__block修饰的对象类型

(1)当__block变量在栈上时,不会对指向的对象产生强引用

(2)当__block变量被copy到堆时

会调用__block变量内部的copy函数

copy函数内部会调用_Block_object_assign函数

_Block_object_assign函数会根据所指向对象的修饰符( **strong、 **weak、 __unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)

(3)如果__block变量从堆上移除

会调用__block变量内部的dispose函数

dispose函数内部会调用_Block_object_dispose函数

_Block_object_dispose函数会自动释放指向的对象(release)

七.循环引用问题

1.ARC

屏幕快照 2019-09-11 下午6.41.02.png 注意:- ARC下 用__block解决(必须要调用 block):
缺点:必须要调用 block,而且 block 里要将指针置为 nil。如果一直不调用 block,对象就会一直保存在内存中,造成内存泄漏。

2.MRC
  • MRC下可以 用__block解决(在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题)

屏幕快照 2019-09-11 下午6.41.50.png

让 block 在使用完毕后立刻释放

一、Objective-C 中 Block 的立即释放

1. 避免循环引用

循环引用会让 block 的引用计数始终大于 0,进而导致内存无法释放。

objective-c

__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        // 使用 strongSelf 访问 self 成员
    }
};
2. 主动置空强引用

在 block 执行完成之后,把持有 block 的强引用设置为nil

objective-c

void (^block)(void) = ^{
    // 执行任务
};
block();  // 执行 block
block = nil;  // 释放 block
3. 使用局部变量而非实例变量

局部变量的作用域结束后,系统会自动释放其内存。

objective-c

- (void)doSomething {
    void (^localBlock)(void) = ^{
        // 局部 block
    };
    localBlock();  // 执行后局部变量出栈,block 被释放
}
4. 使用 dispatch_once 执行单次任务

对于只需执行一次的 block,可以借助dispatch_once来保证其只执行一次,并且会自动释放。

objective-c

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行一次的代码
});

二、Swift 中闭包的立即释放

1. 避免循环引用(弱引用 / 无主引用)

swift

self.completionHandler = { [weak self] in
    guard let self = self else { return }
    // 使用 self
}

2. 主动置空强引用

swift

var closure: (() -> Void)? = {
    // 闭包任务
}
closure?()  // 执行闭包
closure = nil  // 释放闭包

3. 使用局部闭包

swift

func doSomething() {
    let localClosure = { [weak self] in
        guard let self = self else { return }
        // 使用 self
    }
    localClosure()  // 执行后闭包被释放
}
4. 使用 autoreleasepool 加速内存回收

对于占用内存较大的操作,可以把闭包放在autoreleasepool中执行,以此加速内存的回收。

swift

autoreleasepool {
    let heavyClosure = {
        // 执行占用内存大的任务
    }
    heavyClosure()
    // 闭包在此作用域结束后被释放
}

三、底层机制与注意事项

1. Block 的内存管理机制
  • 栈 Block__NSStackBlock__):在栈上分配内存,超出作用域后会被自动释放,但这种 block 无法复制到堆上。
  • 堆 Block__NSMallocBlock__):通过复制操作从栈转移到堆,需要通过释放引用的方式来回收内存。
  • 全局 Block__NSGlobalBlock__):存储在全局数据区,在程序结束时才会被释放。
2. 常见导致 Block 无法释放的情况
  • 实例变量对 block 有强引用,同时 block 内部又捕获了实例的强引用,从而形成循环引用。
  • block 被添加到集合(如NSArray)中,而集合的生命周期较长。
  • 使用NSTimerCADisplayLink时,target 对 block 有强引用。
3. 验证 Block 是否被释放

可以通过在 deinit 方法中添加打印语句来验证 block 是否被释放。

objective-c

// Objective-C
- (void)dealloc {
    NSLog(@"Object deallocated");
}

// Swift
deinit {
    print("Object deallocated")
}

四、特殊场景处理

1. 异步任务中的 Block

在使用dispatch_async等异步方法时,要保证在任务执行完成后,不再有强引用指向 block。

objective-c

__block typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 后台任务
    dispatch_async(dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            // 更新 UI
        }
        // 任务完成,无强引用后 block 会被释放
    });
});
2. NSTimer/CADisplayLink 中的 Block

在使用基于 block 的定时器时,要在合适的时机 invalidate 定时器。

swift

// Swift
var timer: Timer?

func startTimer() {
    timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
        self?.doSomething()
    }
}

func stopTimer() {
    timer?.invalidate()
    timer = nil  // 释放 timer 及其 block
}

总结

要让 block 在使用后立即释放,核心在于:

  1. 防止循环引用,采用弱引用或无主引用来捕获 self。

  2. 执行完毕后,主动断开对 block 的强引用。

  3. 优先使用局部变量而非实例变量来持有 block。

  4. 针对特殊场景(如定时器、异步任务),在合适的时机终止对 block 的引用。