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

简介: 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的销毁,这样就造成了循环引用,即使在外界已经没有任何指针能够访问到它们了,它们也无法被释放,如图所示。


2466108-9416fd65b24db035.webp.jpg


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;
    }
}


目录
相关文章
|
3天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
36 2
|
26天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
24天前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
39 10
|
26天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
20天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
20天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
43 3
|
24天前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
34 4
|
24天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
21小时前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
26天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
59 1