iOS开发读书笔记:Objective-C高级编程 iOS与OS

2018-08-29  本文已影响34人  Ryan___

iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-中篇(Blocks)
iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-下篇(GCD)

阅读完Block此篇后,可以与iOS开发经验(25)-Block一块阅读,主要是可以加深对__forwarding的理解。

目录

2.1 Blocks概要

2.1.1 什么是Blocks

Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。
C语言的标准函数如下:

int func(int count);//声明函数
int result = func(10);//调用函数

如果像下面这样,使用函数指针来代替直接调用函数,必须使用该函数的名称func。

int result = (*funcptr)(10);

这样以来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
但其实使用函数指针也仍然需要知道函数名称。若不使用想赋值的函数的名称,就无法取得该函数的地址。

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);

通过Blocks,源代码中就能够使用匿名函数,而不带名称的函数。
到这里我们知道了"带有自动变量值的匿名函数"中"匿名函数"的概念。那么“带有自动变量值”究竟是什么呢?
首先回顾下函数中可能使用的变量:

虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。
另外,“带有自动变量值的匿名函数”这一概念并不仅指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包。

2.2 Blocks模式

2.2.1 Blocks语法

与一般的函数定义相比,仅有两点不同

^ 返回值类型 参数列表 表达式

^ int (int count) {
    return count + 1;
}

该源代码可省略胃如下形式
^ {
    return count + 1;
}

2.2.2 Blocks类型变量

在Block语法下,可将Block语法赋值给声明为Block类型的变量中。既源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。

int (^blk)(int);

与前面的使用函数指针的源代码对比可知,声明Block类型变量仅仅是将声明函数指针类型变量的“*”变为“^”。该Blcok类型变量与一般的C语言变量完全相同,可作为以下用途使用 :

那么,下面我们就试着使用Block语法将Block赋值为Block类型变量:

int (^blk) (int) = ^ (int count) {
    return count + 1;
}

也可以:

int (^blk1)(int) = blk;

但是此记述方式极为复杂。这时,我们可以像使用函数指针类型时那样,使用typedef来解决问题。

typedef int (^blk_t) (int);

如上所示,通过使用typedef可声明“blk_t”类型变量。这样函数定义就变得更容易理解了。
另外,将赋值给Block类型变量中的Block方法像C语言通常的函数调用那样使用,这种方法与使用函数指针类型变量调用函数的方法几乎完全相同。

2.2.3 截获自动变量值

通过以上说明,我们已经理解了“带有自动变量值的匿名函数”中的“匿名函数”。而“带有自动变量值”究竟是什么呢?“带有自动变量值”在Block中表现为“截获自动变量值”。截获自动变量值的实例如下:

int main() {
    int val = 10;
    void (^blk)(void) = ^ {
         printf(val);
    }
    val = 2;
    blk();
    //打印结果为10;
}

Block中,Block表达式截获所使用的自动变量的值,既保持该自动变量的瞬间值。这就是自动变量值的截获。

2.2.4 __block说明符

实际上,自动变量值截获指南保持秩序Block语法瞬间的值。保存后就不能改写该值。如果尝试在Block中改写截获的自动变量值,会发生编译错误。
若想在Block语法的表达式中将值赋值在Block语法外声明的自动变量,需要在该自动变量上添加__block说明符。该变量称为__block变量。

int main() {
    __block int val = 10;
    void (^blk)(void) = ^ {
        val = 1;
    }
    val = 2;
    blk();
    //打印结果为10;
}

2.2.5 截获的自动变量

截获OC对象,调用变更该对象的方法不会产生编译错误,而向截获的变量array赋值则会产生编译错误。

//编译正常
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
    id obj  = [[NSObject alloc] init];
    [array addObject:obj];
}
//编译错误
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^ {
    array = [[NSMutableArray alloc] init];
}

以上的第二段代码需要给截获的自动变量附加__block说明符。

2.3 Blocks的实现

2.3.1 Block的实质

Block上“带有自动变值的匿名函数”,但Block究竟是什么呢?
它实际上是作为极普通的C语言源代码来处理的,通过支持Block的编译器,含有Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极普通的C语言源代码被编译。
clang(LLVM编译器)具有转换为我们可读源代码的功能。通过“-rewrite-objc”选项就能将含有Block语法的源代码变换为C++的源代码。

clang -rewrite-objc xxx.m

其实,所谓Block就是Objective-C对象。Block指针赋值给Block的结构体成员变量isa。

struct _main_block_impl_0 {
    void *isa;   
    int flags;
    int Reserved; 
    void *FuncPtr;   
};

此_main_block_impl_0结构体相当于基于objc_object结构体的Objective-C类对象的结构体。另外,对其中的成员变量isa进行初始化,具体如下:

isa = &_NSConcreteStackBlock;

既_NSConcreteStackBlock相当于class_t结构体实例。在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSConcreteStackBlock中;

2.3.2 截获自动变量值

本节主要讲解如何截获自动变量值。将截获自动值的源代码用过clang进行转换(源代码略)。
我们注意到,Block语法表达式中使用的自动变量被作为成员变量追加到了_main_block_impl_0结构体中。

struct _main_block_impl_0 {
    struct __block_impl impl;   
    struct __main_block_desc_0 *Desc;   
    int var; 
};

_main_block_impl_0结构体内声明的成员变量类型与自动变量类型完全相同。请注意,Block语法表达式中没有使用的自动变量不会被追加。Block的自动变量截获只针对Block中使用的自动变量。
总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(既Block自身)中。

2.3.3 __block说明符

以上的截获自动变量的代码例子,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。因为在实现上不能改写被截获自动变量的值,所以会发生编译错误。
不过这样以来,无法在Block中保存值了,极为不便。但是有两个方法:

  1. 有如下几个变量,允许Block改写值:
    • 静态变量
    • 静态全局变量
    • 全局变量
  2. 使用__block修饰变量 :__block 存储域类说明符

C语言有如下存储域类说明符:

  1. typedef
  2. extern
  3. static:表示作为静态变量存在在数据区中
  4. auto:表示作为自动变量存储在栈中
  5. register

__block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中。

个人笔记

2.3.4 Block存储域

通过前面说明可知,Block转换为Block的结构体类型的自动变量,__block变量转换为__block变量的结构体类型的自动变量。所谓结构体类型的自动变量,既栈上生成的该结构体的实例。
另外通过之前的说明可知Block也是Objective-C对象,该Block的类为_NSConcreteStackBlock。有很多与之类似的类,如:

在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。例如

void (^blk)(void) = ^ {
    printf("Global Block");
}

此源代码通过声明全局变量blk来使用Block语法。如果转换该源代码,Block用结构体的成员变量isa的初始化如下:

impl.isa = & _NSConcreteGlobalBlock;

该Block的类为_NSConcreteGlobalBlock类。此Block既该Block用结构体实例设置在程序的数据区域中。
在以下情况下,Block为_NSConcreteGlobalBlock类对象

除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
配置在全局变量上的Block,从变量作用域外也可以通过指针安全的使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样的,如果其所属的变量作用域结束,则该__block变量也会被废弃。
Block提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block语法记述的变量作用域结束,堆上的Block还可以继续存在。
复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa。

impl.isa = & _NSConcreteMallocBlock;

而__block变量用结构体成员变量_forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block变量还是从堆上的__block变量都能够正确的访问。

那么Block提供的复制方法是什么呢?当ARC时,大多数情形下编译器会恰当的判断,自动生成将Block从栈上复制到堆上的代码。
当Block作为函数返回值返回时,执行objc_retainBlock方法,实际上是copy函数。
那么少数情形有几种呢?

  1. XXXX

另外,对于已配置在堆上的Block以及配置在程序的数据区域的Block,调用copy方法又会如何呢?

不管是Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可

2.3.5 __block变量存储域

上节只对Block的copy进行了说明,使用__block变量的Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部从栈复制到堆。此时,Block持有__block变量。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。
此思考方式与OC的引用计数内存管理完全相同。使用__block变量的Block持有__block变量。日光Block被废弃,它所持有的__block变量也就被释放。

那么在理解了__block变量的存储域之后,在回顾下之前讲过的使用__block变量用结构体成员变量__forwarding的原因。“不管__block变量配置在栈上还是在堆上,都能够正确的访问该变量”。正如这句话所述,通过Block的复制,__block变量也从栈复制到堆。此时可同时访问栈上的__block变量和堆上的__block变量。
源代码可转换为如下形式:

(val.__forwarding->val);

在变换Block语法的函数中,该变量val为复制到堆上的__block变量用结构体实例,而使用的与Block无关的变量val,为复制前栈上的__block变量用结构体实例。
但是栈上的__block变量用结构体实例在__block变量从栈复制带堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址。
通过该功能,无论上在Block语法中、block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利的访问同一个__block变量。

2.3.6 截获对象

以下源代码生成并持有NSMutableArray类的对象,由于附有__strong修饰符的赋值目标变量的作用域立即结束,因此对象被立即释放并废弃。

{
    id array = [[NSMutableArray alloc] init];
}

下面我们看一下在Block语法中使用该变量array的代码:

//运行正常
blk_t blk;
{
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj) {
        [array addObject:obj];
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

该代码运行正常,执行结果如下

array count = 1;
array count = 2;
array count = 3;

请注意被赋值NSMutableArray类对象并被截获的自动变量array。我们可以发现它是Block用的结构体中附有__strong修饰符的成员变量。

struct _main_block_impl_0 {
    struct __block_impl impl;   
    struct __main_block_desc_0 *Desc;   
    id __strong array; 
};

在OC中,C语言结构体不能含有附有__strong修饰的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存。

但是OC的运行时库能够很准确把握Block从栈复制到堆以及堆上的Block被废弃的时机,因此Block用结构体中即使含有附有__strong修饰符或__weak修饰符的变量,也可以恰当的进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy和dispose,以及作为指针赋值给该成员变量的_main_block_copy_0函数和_main_block_dispose_0函数。
恰当管理赋值给变量array的对象:__main_block_copy_0函数使用_Block_object_assign函数将对象类型对象复制给Block用结构体的成员变量array中并持有该对象。

_Block_object_assign函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。

另外,__main_block_dispose_0函数使用_Block_object_dispose函数,释放赋值在Block用结构体成员变量array中的对象。
_Block_object_dispose函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

虽然此__main_block_copy_0函数(以下简称copy函数)和__main_block_dispose_0函数(以下简称dispose函数)指针被赋值在__main_block_desc_0结构体成员变量copy和dispose。在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。

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

在上面这些情况下栈上的Block赋值到堆上,其实可归结为_Block_copy函数被调用时Block从栈复制到堆。相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的dealloc实例方法。
有了这种构造,通过使用附有__strong修饰符的自动变量,因而Block中截获的对象就能够超出其变量作用域而存在。

2.3.7 __block变量和对象

__block说明符可指定任何类型的自动变量。

__block id obj = [[NSObject alloc] init];

其实该代码等同于

__block id __strong obj = [[NSObject alloc] init];

ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量。
在Block中使用附有__strong修饰符的id类型或对象类型自动变量的情况下,当Block从栈复制到堆时,使用Block_object_assign函数,持有Block截获的对象。当堆上的Block被废弃时,使用_block_object_dispose函数,释放Block截获的对象。
在__block变量为附有_strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block变量从栈复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。
由此可知,即使对象赋值复制到堆上的附有_strong修饰符的对象类型__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。

另外,我们前面用到的只有附有__strong修饰符的id类型或对象类型自动变量。如果使用__weak修饰符会如何呢?首先是在Block中使用附有__weak修饰符的id类型变量的情况。

blk_t blk;
{
    id array = [[[NSMutableArray alloc] init];
    id __weak array2 = array;
    blk = [^(id obj) {
        [array2 addObject:obj];
    } copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

该代码可正常执行。 执行结果,这与以上代码的结果不同:

array2 count = 0;
array2 count = 0;
array2 count = 0;

这是由于附有__strong修饰符的变量array在该变量作用域结束的同时被释放、废弃,nil被赋值在附有__weak修饰符的变量array2中。

2.3.8 Block循环引用

如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环利用。我们来看看下面的源代码:

typedef void (^blk_t)(void);

@interface MOyObject : NSObject
{
      blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^ {
        NSLog(@"self = %@",self);
    } ;
    return self;
}

- (void)dealloc {
    NSLog(@:dealloc:);
}
@end

int main() {
    id o = [[MyObject alloc] init];
    NSLog(@"%@",o);
   return 0;
}

该源代码中MyObject类的dealloc实例方法一定没有被调用。
MyObject类对象的Block类型成员变量blk_持有赋值为Block的强引用。既MyObject类对象持有Nlock。init实例方法中执行的Block语法使用附有_strong修饰符的id类型变量self。并且由于Block语法赋值在了成员变量blk中,因此通过Block语法生成在栈上的Block此时由栈复制到堆,并持有所使用的self。self持有Block,Block持有self。这正是循环引用。
另外,编译器在编译该源代码是能够查处循环引用,因此编译器能正确的进行警告。
为避免此循环引用,可声明附有__weak修饰符的变量,并将self赋值使用。

- (id)init {
    self = [super init];
    id __weak tmp = self;
    blk_ = ^ {
        NSLog(@"self = %@",tmp);
    } ;
    return self;
}

在该源代码中,由于Block存在时,持有该Block的MyObject类对象赋值在变量tmp中的self必须存在,因此不需要判断tmp的值是否为nil。
在面相iOS4(MRC),必须使用__unsafe_unretained修饰符代替__weak修饰符。在此源代码中也可使用__unsafe_unretained修饰符,且不必担心悬挂指针。

- (id)init {
    self = [super init];
    id __unsafe_unretained tmp = self;
    blk_ = ^ {
        NSLog(@"self = %@",tmp);
    } ;
    return self;
}

另外在以下源代码中Block内没有使用self也同样截获了self,引起了循环引用。

typedef void (^blk_t)(void);

@interface MOyObject : NSObject
{
      blk_t blk_;
      id obj_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^ {
        NSLog(@"obj_ = %@",obj_);
    } ;
    return self;
}

既Block语法内使用的obj_实际上截获了self。对编译器来说,obj_只不过是对象用结构体的成员变量。

blk_ = ^ {
    NSLog(@"obj_ = %@",self->obj_);
};

该源代码也基本与前面一样,声明附有_weak修饰符的变量并赋值obj使用来避免循环引用。在此源代码中也可安全的使用__unsafe_unretained修饰符,原因同上。

- (id)init {
    self = [super init];
    id __weak obj = obj_;
    blk_ = ^ {
        NSLog(@"obj = %@",obj);
    } ;
    return self;
}

在为避免循环引用而使用__weak修饰符时,虽说可以确认使用附有__weak修饰符的变量时是否为nil,但更有必要使之生成以使用赋值给附有__weak修饰符变量的对象。
另外,还可以使用__block变量来避免循环引用。

typedef void (^blk_t)(void);

@interface MOyObject : NSObject
{
      blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    __block id tmp = self;
    blk_ = ^ {
        NSLog(@"self = %@",tmp);
        tmp = nil;
    } ;
    return self;
}

- (void)execBlock {
    blk();
}

- (void)dealloc {
    NSLog(@:dealloc:);
}

@end

int main() {
    id o = [[MyObject alloc] init];
    [o execBlock];
   return 0;
}

该源代码没有循环引用。原因:通过执行execBlock实例方法,Block被实行,nil被赋值在__block变量tmp中。因此,_block变量tmp对MyObject类对象的强引用失效。但是如果不调用execBlock实例方法,既不执行赋值给成员变量blk的Block,便会循环引用并引起内存泄漏。
在生成并持有MyObject类对象的状态下会引起以下循环引用:

下面我们对使用__block变量避免循环引用的方法和使用__weak 修饰符及__unsafe_unretained修饰符避免循环引用的方法做个比较。
使用__block变量的优点如下:

在执行Block时可动态的决定是否将nil或其他对象赋值在__block变量中。

使用__block变量的缺点如下:

存在执行了Block语法,却不执行Block的路径时,无法避免循环引用。若有雨Block引发了循环引用时,根据Block的用途选择使用__block变量、__weak修饰符或__unsafe_unretained修饰符来避免循环引用。

2.3.9 copy/release

ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放赋值的Block。这时我们用copy实例方法用来赋值,用release实例方法来释放。

    [blk_ release];

只要Block有一次复制并配置在堆上,就可通过retain实例方法持有。

    [blk_ retain];

但是对于配置在栈上的Block调用retain实例方法则不起任何作用。

    [blk_ retain];

该源代码中,虽然堆赋值给blk_的栈上的Block调用了retain实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy实例方法来持有Block。

另外,ARC无效时,__block说明符被用来避免Block中的循环引用。这是由于当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符的id类型或对象类型的自动变量,则被retain。

注意:正好在ARC有效时能够同__unsafe_unretained修饰符一样来使用。由于ARC有效时和无效时__block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码是在ARC有效情况下编译还是在ARC无效情况下编译。

上一篇下一篇

猜你喜欢

热点阅读