iOS基础-Block

系列文章目录



一、Block是什么

在 iOS 开发中,Block 是 Objective-C 和 Swift 中非常强大的一个特性,用于定义一段可以在任何时候执行的代码块。Block 可以捕获和存储其定义时所处上下文的状态,使得它们特别适用于处理异步操作、回调以及集合类的遍历。

Block 类似于其他语言中的匿名函数或闭包。在 Objective-C 中,Block 是一种特殊的数据类型,可以像对象一样被传递和存储。

在 Objective-C 中,你可以这样定义一个 Block:

//这里,myBlock 是一个接受一个整数参数并不返回任何值的 Block。
void (^myBlock)(int) = ^(int num) {
    NSLog(@"The number is %d", num);
};

//调用 Block,输出: The number is 10
myBlock(10);  

如果我们觉得定义一个 Block 很复杂,也可以用 typedef 去简化:

typedef void (^myBlock)(int);

myBlock b1 = ^(int num) {
    NSLog(@"wml->num:%d",num);
};

//调用 Block,输出: wml->num:100
b1(100);

二、Block的使用场景

1. 异步操作和完成处理器

//Block 是处理异步操作如网络请求、文件读写等的理想选择。它们通常用作回调,以处理异步操作完成后的数据或状态更新。
[self fetchDataWithURL:url completion:^(NSData *data, NSError *error) {
    if (error) {
        NSLog(@"Failed to fetch data: %@", error);
    } else {
        NSLog(@"Data fetched successfully.");
        // 处理数据
    }
}];

2. 动画

Block 在定义动画过程中非常有用,特别是使用 UIKit 的动画API时。它可以在动画结束时执行一段代码,非常适合于需要在动画序列完成后更新UI的场景。

[UIView animateWithDuration:1.0 animations:^{
    self.myView.alpha = 0.0;
} completion:^(BOOL finished) {
    self.myView.hidden = YES;
}];

3. 集合操作

Block 在处理数组、字典等集合类型时非常有用,例如执行过滤、转换、排序等操作。

NSArray *numbers = @[@1, @2, @3, @4, @5];
[numbers enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) {
    NSLog(@"Number: %@", number);
    if ([number integerValue] > 3) {
        *stop = YES; // 提前终止遍历
    }
}];

4. 定时器

Block 也可以用于创建定时器,特别是在需要简单任务重复执行时。

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"Timer fired");
});
dispatch_resume(timer);

5. 自定义控件的事件处理

Block 允许开发者为自定义控件提供灵活的事件处理机制,使得控件的使用更加灵活和强大。

[self.customButton handleTapWithBlock:^{
    NSLog(@"Button was tapped!");
}];

6.错误处理

在执行某些可能会失败的任务时,Block 可用于错误处理和恢复策略。

[self performTaskWithCompletion:^(BOOL success, NSError *error) {
    if (!success) {
        NSLog(@"Task failed: %@", error);
        // 处理错误,尝试恢复
    }
}];

三、Block的底层实现

1.结构分析

我们可以将下面的 oc 代码转换成 c++ 来看看Block的实现:

int main(int argc, const char * argv[]) {
    int age = 20;
    void (^block)(void) =  ^{
         NSLog(@"age is %d",age);
     };
            
    block();
    return 0;
}

关键代码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  //构造函数(类似于OC中的init方法) _age是外面传入的
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    //isa指向_NSConcreteStackBlock 说明这个block就是_NSConcreteStackBlock类型的
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy

         NSLog((NSString *)&__NSConstantStringImpl__var_folders_z2_5qyd6hbj171cdpwjgskps6kc0000gn_T_main_38b1a4_mi_0,age);
     }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    int age = 20;
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

我们可以将上述代码去除一些类型转换的逻辑,进一步简化为:

int age = 20;
void (*block)(void) = &__main_block_impl_0(
						__main_block_func_0, 
						&__main_block_desc_0_DATA, 
						age
						);

// block的调用
block->FuncPtr(block);

用一幅图来表示:
在这里插入图片描述

2.Block的类型

首先,Block 是一个 oc 对象,我们可以看到它的继承关系为:

在这里插入图片描述
Block 有三种基本类型,这些类型反映了 Block 在内存中的存储位置,以及它如何管理捕获的变量:

  1. _NSConcreteGlobalBlock:全局 Block,不捕获任何外部变量,或者只捕获全局变量和静态变量。存储在全局数据区。
  2. _NSConcreteStackBlock:栈 Block,捕获外部变量,并存储在栈上。这种 Block 如果需要在定义域之外使用,必须进行复制操作,将其复制到堆上。
  3. _NSConcreteMallocBlock:堆 Block,是通过复制栈 Block 得到的,存储在堆上,可以在定义域之外安全使用。

在 MRC 下测试:

因为ARC的时候,编译器做了很多的优化,往往看不到本质。

  • 改为MRC方法: Build Settings 里面的Automatic Reference Counting改为NO。

在这里插入图片描述

当 Block 被复制时(使用 [block1 copy]),它被转移到堆上,因此变成了 _NSConcreteMallocBlock 类型。

在 ARC 下测试:

在这里插入图片描述

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

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

情况总结如下:
在这里插入图片描述

3.Block的copy

下面测试是在 ARC 的环境下,编译器会根据情况自动将栈上的 block 复制到堆上

block作为函数返回值时:

在这里插入图片描述

Block 属性的声明

  • copy
    最常见且推荐的方式是使用 copy 修饰符。这是因为 Block 默认在栈上创建,而使用 copy 可以确保 Block 被复制到堆上,从而在其作用域之外也可以安全使用。
  • strong
    通常不推荐用于 Block,因为这不会将栈上的 Block 复制到堆上,可能会导致在 Block 执行时已经不在有效的作用域内。
  • weak
    用于避免循环引用,特别是当 Block 内部需要引用 self,且 self 同时持有这个 Block 时。通常,你会在 Block 内部使用一个弱引用的 self,而不是将 Block 本身声明为 weak。

4.变量捕捉

对于不同类型的变量,有不同的捕捉方式:
在这里插入图片描述

我们还可以用一个例子来证明变量捕捉的情况:

在这里插入图片描述

查看底层代码,可以看到对于 auto 局部变量是用值传递,static 局部变量是用指针传递,全局变量则是直接读取:
在这里插入图片描述

四、Block的使用细节

编译器默认是 ARC 环境,所以未作声明的都是 ARC 环境。

1.auto变量的生命周期

在 ARC 环境下,auto 变量在出了作用域后会被销毁:

@interface MyObj : NSObject
@property (nonatomic ,assign) int age;
@end


@implementation MyObj
- (void)dealloc { NSLog(@"%s",__func__); }
@end

int main(int argc, const char * argv[])
{
    {
        MyObj *person = [[MyObj alloc]init];
        person.age = 10;
    }
    NSLog(@"----------------");
    return 0;
}

结果如下:

在这里插入图片描述

我们在 Block 中创建一个对象类型的 auto 变量:

// 定义block
typedef void (^MyBlock)(void);

int main(int argc, const char * argv[])
{
    MyBlock block;
    
    {
        MyObj *obj = [[MyObj alloc]init];
        obj.age = 10;
        
        block = ^{
            NSLog(@"---------%d", obj.age);
        };
        
         NSLog(@"block.class = %@",[block class]);
    }
    NSLog(@"block销毁");

    return 0;
}

运行后我们发现,Block 类型为__NSMallocBlock__时,延长了变量的生命周期,在 Block 销毁后,变量才被销毁:
在这里插入图片描述

我们将 oc 代码转换为 cpp 代码,发现变量被捕捉到了 Block 内部:

在这里插入图片描述

在 MRC 环境下

我们发现 Block 为__NSStackBlock__类型时,并没有延长变量的生命周期:

在这里插入图片描述

我们通过对 Block 进行 copy,将类型转换为__NSMallocBlock__时,变量的生命周期延长了:

在这里插入图片描述

2.__weak修饰变量

在 MRC 环境下

当 Block 类型为__NSMallocBlock__时,用 __weak修饰变量时,Block 持有变量的弱引用,不影响变量的生命周期。
左图变量属性为 weak,右图属性为 strong。

在这里插入图片描述

当 Block 类型为__NSStackBlock__时,用 __weak修饰变量时,并不起作用。
左图变量属性为 weak,右图属性为 strong。

在这里插入图片描述

在 ARC 环境下

使用__weak 可以让 Block 对变量由默认的强引用变为弱引用,从而影响变量的生命周期。

在这里插入图片描述

无论是 MRC 还是 ARC:

  • 当 block 为__NSStackBlock__类型时候,是在栈空间,无论对外面使用的是 strong 还是 weak 都不会对外面的对象进行强弱引用。

  • 当 block 为__NSMallocBlock__类型时候,是在堆空间,block是内部的_Block_object_assign函数会根据strong或者 weak对外界的对象进行强引用或者弱引用。

3.修改局部变量

我们有三种方式可以在 Block 中去修改一个变量:

a.定义成全局变量

在这里插入图片描述

b.定义成static变量

在这里插入图片描述

c.__block修饰auto变量

在这里插入图片描述

4.循环引用

例如下面这段代码,在对象中持有了 Block,而 Block 又持有了对象的指针,出现了循环引用问题,导致资源泄露:

@interface MyObj : NSObject
@property (nonatomic ,assign) int age;
@property void (^MyBlock)(void);
@end

@implementation MyObj
- (void)dealloc { NSLog(@"%s",__func__);}
@end

int main(int argc, const char * argv[])
{
    MyObj *obj = [[MyObj alloc] init];

    obj.MyBlock = ^{
        NSLog(@"age->%d",obj.age);
    };
    
    return 0;
}

我们可以用 __weak 来修饰这个在 Block 中用到的指针:

在这里插入图片描述

用__unsafe_unretained 也可以解决循环引用问题,但它是不安全的:

  • __weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil。
  • __unsafe_unretained:不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。

在这里插入图片描述

用__block 也可以解决循环引用,Block 需要被调用一次,来执行 obj = nil:

在这里插入图片描述

总结:

在 ARC 环境下,最好使用 __weak 来修饰变量避免循环引用。
在 MRC 环境下,因为不支持弱指针__weak,所以,只能是 __unsafe_unretained 或者 __block 来解决循环引用。

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值