iOS中的网络和多线程编程(八)

2022-02-21  本文已影响0人  anny_4243

摘自《iOS程序员面试笔试宝典》

block与GCD

“块”(block)与“大中枢派发”(GCD)是苹果公司为解决多线程编程而一起引入的解决方案。

block是一种可以在C、C++以及Objective-C代码中使用,类似于“闭包”(closure)的代码块,借助block机制,开发者可以将代码像对象一样在不同的上下文环境中进行传递。

GCD是一种与block有关的技术,它主要用于优化应用程序以支持多核处理器的调度。开发者可以将块排入队列中,由GCD负责处理所有的调度事宜。GCD会根据系统资源情况,适时地创建、复用、销毁后台线程(Background Thread),以便处理每个队列。和其他的多线程技术方案相比,使用起来更加简单和方便。

block有哪几种定义的方式

在Objective-C中,block定义包含了block的类型声明和实现,基本形式如下:

返回值类型(^block名称)(参数类型)=^(参数类型和参数名){};

其中,返回值类型和参数可以是空。如果有参数,那么在定义block的时候,必须要标明参数的类型和参数名。所以,block大致有3种细分的定义方式。

1)没有返回值,没有参数的定义方式。

void(^myBlock)() = ^{
    //代码
}

2)有返回值,有参数的定义方式。

int(^myBlock)(int) = ^(int a){
     return a;   
}

3)有返回值,没有参数的定义方式。

int(^myBlock)() = ^{
     return 100;
}

当然,block也有属于自己的类型,就像在Objective-C中,字符串对象属于NSString类型一样。block类型的格式就是:

返回值类型(^)(参数类型)

也就是说,上面第一种定义方式的block类型就是void(^)(),myBlock不是变量名,而是这种block类型的别名。在Objective-C中,可以使用typedef关键字定义block类型,也可以直接使用inline提示符来自动生成block格式。示例代码如下:

    /*使用typedef关键字定义block类型*/
    typedef void(^myBlock)();
        myBlock block = ^{
    };
    /*使用inline提示符来自动生成block格式*/
    <#returnType#>(^<#blockName#>)(<#parameterTypes#>) = ^(<#parameters#>){
        <#statements#>
    };

在ARC环境下,是否需要使用copy关键字来修饰block

先要明确的是,block其实包含两个组成部分,一部分是block所执行的代码,这一部分在编译的时候已经确定;另一部分是block执行时所需要的外部变量值的数据结构。根据block在内存中的位置,系统将block分为3类。

1)NSGlobalBlock:该类型的block类似函数,内存地址位于内存全局区。只要block没有对作用域中局部变量进行引用,此block会被系统设置为该类型。示例代码如下:

- (void)test{
    void(^gBlock1)(int, int) =^(int a, int b){
        NSLog(@"a + b = %d", a + b);
    };
    NSLog(@"%@", gBlock1);
}

以上代码的输出结果是:

<NSGlobalBlock:0x1025e8110>

事实上,对于NSGlobalBlock类型的block,无需做更多的处理,不需要使用retain和copy进行修饰。即使使用了copy,系统也不会改变block的内存地址,操作是无效的。

2)NSStackBlock:该类型的block内存位于栈,其生命周期由函数决定,函数返回后block将无效。

在MRC环境下,若block内部引用了局部变量,此block就会被系统设置为该类型。对于NSStackBlock类型的block,使用retain和release操作都是无效的,必须调用Block_copy()方法,或者使用copy进行修饰,其作用就是将block的内存从栈转移到堆,此时block就会转变为NSMallocBlock类型,这也是一直使用copy修饰block的原因。

在ARC环境下,若block内部引用了局部变量,系统默认使用了copy对block进行修饰,使其变成NSMallocBlock类型。所以在ARC环境下,不需要手动使用copy关键字来修饰block。

3)NSMallocBlock:当对NSStackBlock类型的block进行copy操作后,block就会转为此类型。在MRC环境下,可以使用retain、release等方法手动管理此类型block的生命周期。在ARC环境下,系统会帮助管理此类型block的生命周期。

在block内如何修改block外部变量

在block内部修改block外部变量会造成编译错误,提示变量缺少__block修饰,不可赋值。要想在block内部修改block外部变量,则必须在外部定义变量时,前面加上__block修饰符。示例代码如下:

/*block外部变量*/
__block int var1 = 0;
int var2 = 0;
/*定义block*/
void(^block)(void) =^{
    /*试图修改block外部变量*/
    var1 = 100;
    /*编译错误,在block内部不可对var2赋值*/
    //var2 = 1;
};
/*执行block*/
block();
NSLog(@"修改后的var1:%d",var1);//修改后的var1:100

block内部为何不能直接修改外部变量呢?因为当外部变量没有使用__block修饰符修饰时,block在截获外部的自动变量时会在内部新创建一个新的变量val来保存所截获的外部变量的瞬时值,新变量val成为block的成员变量(Objective-C中block也是对象),之后在block代码中修改的值是成员变量val的值,而不是截获的外部变量的值,所以外部变量的值不会受影响。此时,修改外部变量是先取值并赋值给成员变量val,然后修改val的值。可用下面的代码模拟其原理,假设block对外部变量var进行了加1操作,block使用一个名为block的函数来表示。

int var = 1;
void block(){
    int val = var;
    val += 1;
}

当外部变量使用了__block修饰符进行修饰的时候则是另外一种情形了,此时block并不是截获外部自动变量的瞬时值并保存到自己的新成员变量中,而是保存了对外部变量的指引引用,因此对指针变量的修改会直接影响外部变量的值。此时使用代码模拟其原理如下,依然是block对外部变量var进行加1操作。

__block int var = 1;
void block(){
    int *ptr = &var;
    *ptr += 1;
}

因此,block内部不可以直接修改外部变量,如果要修改外部变量,那么该外部变量必须使用__block修饰符进行修饰,否则编译器会直接进行报错提示。

需要注意的是,此处讨论的是自动变量,而静态变量由于默认传给block的就是地址值,所以是可以直接修改的。另外,全局变量和静态全局变量由于作用域很广,也是可以在block中直接被修改的,编译器也不会报错。

在block中使用self关键字是否一定导致循环引用

在block中使用self关键字并不总会引起循环引用。事实上,只有当block和self相互持有时,才会导致循环引用。由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,此时如果block中的对象又持有了该block,那么就会造成循环引用。典型的场景就是当block作为self的属性使用时,又在block内部调用了self的属性或者方法。示例代码如下:

typedef  void(^block)();
@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;
- (void)testBlock{
    self.myBlock = ^{
        /*其实注释中的代码,同样会造成循环引用*/
        NSString *localString = self.blockString;
    }
}

在上面这个例子中,myBlock和self相互引用了对方。此时,self的销毁依赖于myBlock的销毁,而myBlock的销毁又依赖self的销毁,这样就造成了循环引用,即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放,如图所示。

block循环引用

解决循环引用的关键是断开引用链。在实际开发中,主要使用弱引用(weak reference)的方法来避免循环引用的产生。在ARC环境下,使用__weak修饰符定义一个__weak self的引用,并且在里面使用这个弱引用。使用这种方式对示例代码修改如下:

typedef  void(^block)();
@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;

- (void)testBlock{
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
        /*其实注释中的代码,同样会造成循环引用*/
        NSString *localString = weakSelf.blockString;
    };
}

当使用__weak修饰的弱类型self时,block便不会再持有self的引用了,也就不会再产生循环引用了。

下面是不会造成循环引用的几种情况:

1)大部分GCD方法。

示例代码如下:

/* 使用GCD异步执行主队列任务*/
dispatch_async(dispatch_get_main_queue(), ^{
    [self doSomething];
});

在例子中,因为self并没有对GCD的block进行持有,只有block持有了self的引用,所以不会造成循环引用。

2)block作为临时变量。在这种情况下,同样self并没有持有block,所以也不会造成循环引用。

3)block执行过程中self对象被释放。事实上,block的具体执行时间不确定,当block被执行的时候block中被__weak修饰的self对象有可能已经被释放了(例如,控制器对象已经被POP了)。当在并发执行,涉及异步服务的时候,这种情况有可能会出现。

对于这种情况,应该在block中使用__strong修饰符修饰self对象,使得在block期间对对象持有,当block执行结束后,解除其持有。示例代码如下:

- (void)testBlock{
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
        __strong typeof(self)strongSelf = weakSelf;
        NSString *localString = strongSelf.blockString;
    }
}
上一篇 下一篇

猜你喜欢

热点阅读