【iOS】—— Block总结

Block总结

1. Block的使用规范

Block完整格式:声明变量+定义

objective-c 复制代码
//block声明
void (^blockName) (int a, int b);
//block实现
int ^(int a, int b){

 };
objective-c 复制代码
 int (^blockName)(int numA, int numB) = ^int (int a, int b){
      return a+b;
  };

Block变量类似于函数指针。

用途:自动变量(局部变量)

  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

截获自动变量:带有自动变量的值在Block中表现为"截获自动变量"。

值得注意是 :在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获

Block判空:

在iOS中对一个 Block 进行判空实际上是在检查这个 Block变量是否有指向创建出来Block对象。

正如下面的例子:

objective-c 复制代码
 void (^myBlock)(void);
  if (myBlock) {
      NSLog(@"2");
  } else {
      NSLog(@"1");
  }
  NSLog(@"%p", myBlock);

运行结果:说明该Block为空

这个是Block实际函数指针的结构体:

objective-c 复制代码
 struct __block_impl {
  void *isa;//用于保存Block结构体的实例指针
  int Flags;//标志位
  int Reserved;//今后版本升级所需的区域大小
  void *FuncPtr;//函数指针,FuncPtr指针指向Block的主体部分,也就是Block对应OC代码中的^{...}的部分
};

而对于判空的底层代码逻辑则要跟更详细一点:

objective-c 复制代码
 // 判空并执行 Block
  if (myBlock != NULL && myBlock->FuncPtr != NULL) {
      myBlock->FuncPtr();
  } else {
      printf("Block is NULL!\n");
  }

因此判断Block是否为空有两个步骤:检查 Block 变量是否为 nil检查 Block 中的函数指针是否为 NULL

2. __block修饰符

block可以截获变量,但是在block内部不能修改变量的值。

因此使用__block修饰符修饰变量,对需要在block内部赋值的变量,使用修饰符,确保可以对变量进行修饰。

objective-c 复制代码
id tyarray = @[@"blk", @"123", @"234"];
id __block arr = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
    arr = tyarray;
    NSLog(@"%@", arr);
};
__block修饰符的底层原理

我们都知道block捕获了带有__block修饰符的变量时可以修改变量的值,但是具体是怎么做到的?

以下面的变量为例:

objective-c 复制代码
 __block int b = 10;
 void (^block2)(void) = ^{
     NSLog(@"%d",b);
 };
 b = 100;
 block2();

原理:首先被__block修饰的变量b,声明变为b的__Block_byref_b_0结构体,加上__block修饰符的话捕获到的block内的变量为__Block_byref_b_0类型的结构体。

接下来看一下**__Block_byref_val_0**结构体的底层:

objective-c 复制代码
 struct __Block_byref_val_0 {
 void *__isa;
 __Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
  • __isa指针:Block_byref_age_0中也有isa指针也就是说__Block_byref_b_0本质也一个对象。
  • __forwarding__forwarding__Block_byref_b_0**结构体类型的,并且__forwarding存储的值为(__Block_byref_age_0 )&b,即结构体自己的内存地址。
  • __flags :0
  • __size sizeof(__Block_byref_b_0)即__Block_byref_b_0所占用的内存空间。
  • b :真正存储变量的地方,这里存储局部变量10。

接着将 __Block_byref_b_0结构体b存入__main_block_impl_0结构体中,并赋值给__Block_byref_b_0 *age

之后调用block,首先取出__main_block_impl_0中的b,通过b结构体拿到__forwarding指针,__forwarding中保存的就是__Block_byref_b_0结构体本身,这里也就是b(__Block_byref_b_0),在通过__forwarding拿到结构体中的b(10)变量并修改其值。

总结:block为什么能够修改变量的值?因为block把变量包装成了一个带有指针的对象,然后把b封装在结构体里面,block内部存储的变量为结构体指针,也可以通过指针找到内存地址修改变量的值。

3. Block的类型

Block的存储域

三种类型对应不同的区域:

NSGlobalBlock

一个Block没有访问外部的局部变量,或者访问的是全局变量,或者是静态局部变量。此时的Block是全局Block,数据存储在全局区。

objective-c 复制代码
    //block1没有引用到局部变量
    int a = 10;
    void (^block1)(void) = ^{
         NSLog(@"hello world");
    };
    NSLog(@"block1:%@", block1);

    //    block2中引入的是静态变量
    static int a1 = 20;
    void (^block2)(void) = ^{
        NSLog(@"hello - %d",a1);
    };
    NSLog(@"block2:%@", block2);

运行结果如下:

NSStackBlock

在捕获了局部变量之后,Block就会变成NSStackBlock,数据存储在栈中。

objective-c 复制代码
 int a1 = 20;
 void (^block2)(void) = ^{
     NSLog(@"hello - %d",a1);
 };
 NSLog(@"\nblock2:%@", block2);

运行结果如下:但是刚开始打印的时候,并不是NSStackBlock,原因是:**在ARC环境下系统会自动将block进行拷贝操作。**只要换成MRC就行了。

NSMallocBlock

什么时候栈上的 Block 会复制到堆呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的Cocoa框架方法或 Grand Central Dispatch的 API 中传递 Block 时。
objective-c 复制代码
    int a = 10;
    void (^block1)(void) = ^{
        NSLog(@"%d",a);
    };
    NSLog(@"block1:%@", [block1 copy]);

    __block int b = 10;
    void (^block2)(void) = ^{
        NSLog(@"%d",b);
    };
    NSLog(@"block2:%@", [block2 copy]);

运行结果如下:

简单来说,没有捕获自动变量的就是数据区,捕获了自动变量但是没有进行copy操作就是栈区,copy之后就变成了堆区。

4. Block的实现及本质

初始化部分

初始化部分就是Block结构体

objective-c 复制代码
 //Block结构体
struct __main_block_impl_0 {
  struct __block_impl impl;//impl:Block的实际函数指针,就是指向包含Block主体部分的__main_block_func_0结构体
  struct __main_block_desc_0* Desc;//Desc指针,指向包含Block附加信息的__main_block_desc_0()结构体
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {//__main_block_impl_0:Block构造函数(可以看到都是对上方两个成员变量的赋值操作)
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0结构体也就是Block结构体包含了三个部分:

  • 成员变量impl;
  • 成员变量Desc指针;
  • __main_block_impl_0构造函数;

struct __block_impl结构:包含Block实际函数指针的结构体

objective-c 复制代码
 struct __block_impl {
  void *isa;//用于保存Block结构体的实例指针
  int Flags;//标志位
  int Reserved;//今后版本升级所需的区域大小
  void *FuncPtr;//函数指针
};
  • _block_impl包含了Block实际函数指针FuncPtr,FuncPtr指针指向Block的主体部分,也就是Block对应OC代码中的^{...}的部分
  • 还包含了标志位Flags,在实现block的内部操作时可能会用到。
  • 今后版本升级所需的区域大小Reserved。
  • __block_impl结构体的实例指针isa。

struct __main_block_desc_0结构:

objective-c 复制代码
 static struct __main_block_desc_0 {
  size_t reserved;//今后版本升级所需区域大小
  size_t Block_size;//Block大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

Block构造函数__main_block_impl_0

作为构造函数注意和Block结构体是一个名字。

负责初始化__main_block_impl_0结构体(也就是Block结构体struct __block_impl)的成员变量

objective-c 复制代码
   //可以看到里面都是一些赋值操作
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

调用部分

函数原型 ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

逐步解析这段代码:

  • ((__block_impl *)myBlock)->FuncPtr:这部分将 myBlock 转换为 __block_impl 指针类型,并访问 FuncPtr 成员。它获取了块实现内部存储的函数指针。
  • ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr):在这里,函数指针被转换为一个函数类型,该函数接受一个类型为 __block_impl* 的参数,并返回 void。它将函数指针转换为可以调用的实际函数类型。
  • ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock):最后,使用 myBlock 作为参数,调用了所得到的函数指针。它使用块实现对象调用该函数。

本质

  1. 用一句话来说,Block是个对象(其内部第一个成员为isa指针)
  2. 在初始化函数里面:
objective-c 复制代码
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {//__main_block_impl_0:Block构造函数(可以看到都是对上方两个成员变量的赋值操作)
    impl.isa = &_NSConcreteStackBlock;  
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc; 

impl.isa = &_NSConcreteStackBlock;
_NSConcreteStackBlock相当于该block实例的父类 .将Block作为OC对象调用时,关于该类的信息放置于_NSConcretestackBlock中,这也证明了 block出生就是在栈上。

5. Block的捕获与内存管理

捕获变量

  • 全局变量:不捕获
  • 局部变量:捕获值
  • 静态全局变量:不捕获
  • 静态局部变量:捕获指针
  • const修饰的局部变量:捕获值
  • const修饰的静态局部常量:捕获指针

捕获对象

BLOCK 可以捕获对象,其中需要知道两个方法。

在捕获对象的时候代码出现了_main_block_copy_0_main_block_depose_0

  • main_block_copy_0作用就是调用_Block_object_assign,相当于retain,将对象赋值在对象类型的结构体变量main_block_impl_0中。在栈上的Block复制到堆时会进行调用。
  • main_block_dispose_0调用_Block_object_dispose,相当于release,释放赋值在对象类型的结构体变量中的对象。 在堆上的Block被废弃时会被调用。

内存管理

捕获外部变量引用计数的变化

objective-c 复制代码
 NSObject *objc = [NSObject new];
 NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1

 void(^strongBlock)(void) = ^{
     NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
 };
 strongBlock();

 void(^__weak weakBlock)(void) = ^{
     NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
 };
 weakBlock();
 void(^mallocBlock)(void) = [weakBlock copy];
 mallocBlock();

运行结果:

  • 第一个为1,就是简单创建引用计数+1
  • 第二个为3,strongBlock是在堆区的block,这里捕获objc时会对外部变量+1,这里将栈区的objc拷贝进堆区时,又进行了+1,所以为3。
  • 第三个为4,weakBlock是栈区的block,捕获objc没有进行拷贝就直接+1,所以为4。
  • 第四个为5,[weakBlock copy]进行了拷贝,因此引用计数再+1,所以为5。

6. 循环引用

什么是循环引用

对象持有导致对象不能及时的正常释放,容易造成内存泄漏。

objective-c 复制代码
#import "ViewController.h"
typedef void (^TBlock)(void);
@interface ViewController ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) TBlock block;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    self.name = @"View";
    self.block = ^() {
       NSLog(@"%@", self.name);
    };
    self.block();
    
}

@end

self持有了block,block持有了self,导致循环引用。 编译器也会提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle

如果单方面取消一方的持有即可取消循环。

objective-c 复制代码
  // 不会引起循环引用
    void(^blk1)(void);
    blk1 = ^() {
        NSLog(@"%@", self.name);
    };
    blk1();

这个案例就没有出现循环引用是因为当前self,也就是ViewController并没有对block进行强持有,block的生命周期只在viewDidLoad方法内,viewDidLoad方法执行完,block就会释放。

循环引用解决方法

之前提到过weak,可以解决循环引用问题。

weak的使用

objective-c 复制代码
  __weak typeof(self) weakSelf = self;

typeof(self)typeof 是一个运算符,用于获取表达式的类型。在这种情况下,表达式是 self,它代表当前对象的引用。

objective-c 复制代码
  __weak typeof(self) weakSelf = self;
  self.block = ^(){
      NSLog(@"%@", weakSelf.name);
  } ;
  self.block();

此时self持有blockblock弱引用self,弱引用会自动变为nil,强持有中断,所以不会引起循环引用。

之前学习GCD的时候将其他线程麻烦的操作执行完之后回到主线程,如果在执行其他线程的时ViewController被销毁,就会导致内部的函数来不及打印,导致想打印的数值为空。

objective-c 复制代码
  。。。。。。。
	__weak typeof(self) weakSelf = self;
    self.block = ^(){
    // 延迟2秒钟
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( 2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakSelf.name);
        });
    };
    self.block();
    [self fakeDealloc];
}
- (void)fakeDealloc {
    NSLog(@"调用了dealloc 模拟ViewController模拟销毁");
    // 模拟viewController被销毁
    self.name = nil;
}

运行结果:

1. 强弱共舞

为了解决上面的问题,由此引出了强弱共舞

objective-c 复制代码
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) void (^block)(void);
@end

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   // Do any additional setup after loading the view.
   
   self.name = @"View";
   __weak typeof (self) weakself = self;
   self.block = ^{
       __strong __typeof(weakself) strongself = weakself;
       dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
               NSLog(@"Block executed after 2 seconds. Name: %@", strongself.name);
       });
   };
   self.block();
   [self dealloc1];
}

- (void)dealloc1 {
   NSLog(@"Object is deallocating.");
}
@end

则一切就会正常打印。

因为__weak会自动置为nil,所以这里使用__strong(strong-weak-dance)暂时延长 self的生命周期,使得可以正常打印。

为什么强弱共舞能够避免循环引用,不是也调用了self? 因为这里strongself是一个临时的变量,出了作用域也跟着释放了,所以不会出现循环引用🐮

简单分析:

  • 在完成block中的操作之后,才调用了dealloc方法。添加strongWeak之后,持有关系为:self->block->strongWeak->weakSelf-> self。
  • weakSelf被强引用了就不会自动释放,因为strongWeak只是一个临时变量,它的声明周期只在block内部,block执行完毕后,strongWeak就会释放,而弱引用weakSelf也会自动释放。
2. 手动中断循环
objective-c 复制代码
//手动中断
    self.name = @"ViewController";
    __block ViewController *vc = self;
    __weak typeof (self) weakself = self;
    self.block = ^{
        __strong __typeof(weakself) strongself = weakself;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongself.name);
            vc = nil;
        });
    };
    self.block();
    [self dealloc1];

这里借助临时变量vc之后,持有关系变为:self->block->vc->selfvc在block使用完成之后就被置为nilblock不构成对self的持有关系了,因此这里就不构成循环引用问题。

3. 参数形式解决循环引用(block传参)
objective-c 复制代码
 //Block传值
  self.name = @"ViewController";
  self.block1 = ^(ViewController *vc) {
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
          NSLog(@"%@", vc.name);
      });
  };
  self.block1(self);
  [self dealloc1];

self作为参数参入block中,进行指针拷贝,并没有对self进行持有。

因为使用捕获self实际上会将self转换为弱引用,从而避免了循环引用。

循环引用的案例

  • 静态变量持有
objective-c 复制代码
   // staticSelf_定义:
  static ViewController *staticSelf_;

  - (void)blockWeak_static {
      __weak typeof(self) weakSelf = self;
      staticSelf_ = weakSelf;
  }

weakSelf虽然是弱引用,但是staticSelf_静态变量,并对weakSelf进行了持有,staticSelf_释放不掉,所以weakSelf也释放不掉!导致循环引用!

  • __strong持有问题
objective-c 复制代码
 - (void)block_weak_strong {

    __weak typeof(self) weakSelf = self;

    self.doWork = ^{
        __strong typeof(self) strongSelf = weakSelf;
        NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));

        weakSelf.doStudent = ^{
            NSLog(@"%@", strongSelf);
            NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));
        };

       weakSelf.doStudent();
    };

   self.doWork();
}
  • 在doWork内部,__strong typeof(self) strongSelf = weakSelf;
  • 用强引用持有了weakSelf,和前的情况类似,strongSelf的生命周期也就在doWork方法内;
  • 这里需要注意的是,doStudent这个内部block调用了外部变量,所以他会从栈block copy到堆中,从而导致strongSelf的引用计数增加,无法释放掉,进而导致循环引用!
相关推荐
用户091 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan1 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸2 天前
macOS自带截图命令ScreenCapture
macos
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918412 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
TESmart碲视2 天前
Mac 真正多显示器支持:TESmart USB-C KVM(搭载 DisplayLink 技术)如何实现
macos·计算机外设·电脑
00后程序员张2 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h2 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa