1、Block
1.1 Block底层:OC对象-封装一段代码-Block地址+参数传递
本质其实就是个OC对象,内部也有isa指针,封装了函数调用以及函数调用环境的OC对象。 block的底层结构是 __main_block_impl_0->__block_impl; functr调用了刚刚初始化时传入的保存在impl.FuncPtr的函数(函数指针),如下图
- impl->isa:就是isa指针,可见它就是一个OC对象。
- impl->FuncPtr:是一个函数指针,也就是底层将block中要执行的代码封装成了一个函数,然后用这个指针指向那个函数。
- Desc->Block_size:block占用的内存大小。
- age:捕获的外部变量age,可见block会捕获外部变量并将其存储在block的底层结构体中。
调用block时(block()),实际上就是通过函数指针FuncPtr找到封装的函数并将block的地址作为参数传给这个函数进行执行,把block传给函数是因为函数执行中需要用到的某些数据是存在block的结构体中的 (比如捕获的外部变量) 。如果定义的是带参数的block,调用block时是将block地址和block的参数一起传给封装好的函数。
1.2 block的变量捕获机制
block外部的变量是可以被block捕获的,这样就可以在block内部使用外部的变量了。不同类型的变量的捕获机制是不一样的。 `int c = 1000; // 全局变量 static int d = 10000; // 静态全局变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10; // 局部变量
static int b = 100; // 静态局部变量
void (^block)(void) = ^{
NSLog(@"a = %d",a);
NSLog(@"b = %d",b);
NSLog(@"c = %d",c);
NSLog(@"d = %d",d);
};
a = 20;
b = 200;
c = 2000;
d = 20000;
block();
}
return 0;
}
// ***************打印结果***************
2020-01-07 15:08:37.541840+0800 CommandLine[70672:7611766] a = 10
2020-01-07 15:08:37.542168+0800 CommandLine[70672:7611766] b = 200
2020-01-07 15:08:37.542201+0800 CommandLine[70672:7611766] c = 2000
2020-01-07 15:08:37.542222+0800 CommandLine[70672:7611766] d = 20000
// 只有2个局部变量被捕获,而且2个局部变量的捕获方式还不一样。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *b;
};`
1.2.1 不同变量的捕获方式:全局变量/静态局部变量/普通局部变量
- 全局变量--不会捕获,是直接访问。
- 静态局部变量--是捕获变量地址。
- 普通局部变量--是捕获变量的值。
全局变量的捕获
不管是普通全局变量还是静态全局变量,block都不会捕获。因为全局变量在哪里都可以访问,所以block内部不捕获也是可以直接访问全局变量的,所以外部更改全局变量的值时,block内部打印的就是最新更改的值。
静态局部变量的捕获
我们发现定义的静态局部变量b被block捕获后,*在block结构体里面是以int b;的形式来存储的,也就是说block其实是捕获的变量b的地址,block内部是通过b的地址去获取或修改b的值。所以block外部更改b的值会影响block里面获取的b的值,block里面更改b的值也会影响block外面b的值。所以上面会打印b = 200。
普通局部变量的捕获
普通局部变量就是在一个函数或代码块中定义的类似int a = 10;的变量,它其实是省略了auto关键字,等价于auto int a = 10,所以也叫auto变量。和静态局部变量不同的是,普通局部变量被block捕获后在block底层结构体中是以int a;的形式存储,也就是说block捕获的其实是a的值(也就是10)。并且在block内部重新定义了一个变量来存储这个值,这个时候block外部和里面的a其实是2个不同的变量,所以外面更改a的值不会影响block里面的a。所以打印的结果是a = 10。
1.2.2 面试题
Q:为什么普通局部变量要捕获值,跟静态局部变量一样捕获地址不行吗?
A:不行。因为普通局部变量a在出了大括号后就会被释放掉了,这个时候如果我们在大括号外面调用这个block,block内部通过a的指针去访问a的值就会抛出异常,因为a已经被释放了。而静态局部变量的生命周期是和整个程序的生命周期是一样的,也就是说在整个程序运行过程中都不会释放b,所以不会出现这种情况。
Q:既然静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?
这是因为静态局部变量作用域只限制在这个大括号类,出了这个大括号,虽然它还存在,但是外面无法访问它。而前面已经介绍过,block里面的代码在底层是被封装成了一个函数,那这个函数肯定是在b所在的大括号外面,所以这个函数是无法直接访问到b的,所以block必须将其捕获。
Q:静态局部变量一直都不会被释放,会导致内存泄漏吗?
不会。静态局部变量(Static Local Variables)在C语言或类C语言(如C++、Objective-C)中是一种特殊的局部变量,它们具有局部作用域,但是在程序的整个运行期间都保持其值。这意味着静态局部变量只在第一次执行到它们被声明的代码块时被初始化,之后即使代码块执行结束,它们的值也不会被销毁,而是保留到下次该代码块被执行时使用。
静态局部变量(Static Local Variables)在C语言及其衍生语言(如C++、Objective-C)中,存储在程序的数据段(Data Segment)中。 具体来说,是在初始化数据段(Initialized Data Segment)的一部分,这部分也被称为.data段或.bss`段,取决于变量是否被显式初始化。
数据段(Data Segment)是程序二进制文件的一部分,用于存储程序的全局变量、静态变量等数据。它在程序启动时被加载到内存中,并在程序的整个生命周期内保持不变。数据段可以进一步细分为:
1、初始化数据段(.data):用于存储程序中已经初始化的全局变量和静态变量。如果你声明了一个静态局部变量并显式地初始化它,那么它将被存储在这个段中。
void function() {
static int initializedVar = 10; // 存储在.data段
}
2、未初始化数据段(.bss):用于存储未初始化的全局变量和静态变量。如果一个静态局部变量没有被显式初始化,它通常会被放在这个段中,并在程序启动时被自动初始化为0。
void function() {
static int uninitializedVar; // 存储在.bss段,默认初始化为0
}
特点
1、生命周期:静态局部变量的生命周期贯穿程序的整个执行过程。它们在程序启动时被初始化(如果有显式的初始化表达式),并在程序结束时被销毁。
2、作用域:尽管静态局部变量在数据段中并且具有程序生命周期的存储期,它们的作用域仍然局限于声明它们的代码块内。
这意味着你只能在特定的函数或代码块中访问这些变量,这有助于保护数据,避免全局变量可能带来的命名冲突和数据安全问题。
总结
静态局部变量存储在程序的数据段中,具体是在初始化数据段(.data)或未初始化数据段(.bss)中,这取决于变量是否被显式初始化。
它们在程序的整个生命周期内都存在,但只能在声明它们的代码块内被访问。`
扩展-对内存泄漏的误解
关于静态局部变量可能导致内存泄漏的担忧,实际上是一个误解。内存泄漏通常指的是程序在运行过程中不断分配内存而没有适时释放,导致可用内存逐渐减少的情况。而静态局部变量的行为并不符合这一定义:
1、有限的分配:静态局部变量在程序运行期间只被分配一次内存,且通常占用的内存量是有限的。 2、生命周期管理:静态局部变量的生命周期由编译器自动管理,它们在程序启动时被初始化,在程序结束时才被销毁。因此,它们不会导致运行时的内存泄漏。 `void function() { static int count = 0; // 静态局部变量 count++; printf("Count: %d\n", count); }
int main() {
function(); // 输出: Count: 1
function(); // 输出: Count: 2
return 0;
}
// 上面的示例中,即使function函数执行完毕,变量count的值也不会被销毁,下次调用function时,count将保持上次的值。`
静态局部变量的设计是为了在函数调用之间保持状态,而不需要使用全局变量。它们的内存分配是由编译器控制的,因此不会导致内存泄漏。
正确理解静态局部变量的行为有助于开发者更好地利用它们来实现特定的功能,而不必担心内存泄漏的问题。
1.3 block三种类型:NSGlobalBlock/NSStackBlock/NSMallocBlock
block有三种类型 :_NSConcreteGlobalBlock(全局block)、_NSConcreteStackBlock(栈block)和_NSConcreteMallocBlock(堆block) 。
当你创建一个block时,它默认是在栈上的(_NSConcreteStackBlock)。栈上的block是临时的,会在定义它的作用域结束时被销毁。为了让block有更长的生命周期,可以将其复制到堆上(_NSConcreteMallocBlock),这样block就可以在定义它的作用域之外被使用。
在Objective-C的自动引用计数(ARC)环境下,当你将一个栈上的block赋值给一个使用strong/copy修饰的属性时,编译器会自动将这个block从栈复制到堆上。这是ARC的一个特性,旨在帮助管理block的内存,确保block在需要时仍然有效。
判断一个变量是否会被block捕获关键就在于这个变量是局部变量还是全局变量。TODO: 测试:以下几种情况中block会捕获self:----self是局部变量 + 外部使用block+布局变量
- (void)blockTest{
// 第一种
void (^block1)(void) = ^{
NSLog(@"%p",self);
};
// 第二种
void (^block2)(void) = ^{
self.name = @"Jack";
};
// 第三种
void (^block3)(void) = ^{
NSLog(@"%@",_name);
};
// 第四种
void (^block4)(void) = ^{
[self name];
};
}
// 要搞清楚这个问题,我们主要是要搞清楚self是局部变量还是全局变量。可能很多人以为它是全局变量,其实他是一个局部变量。
// 再强调一遍,self是局部变量。那有人就疑惑了,它为什么是局部变量?它是在哪里定义的。要搞清楚这个问题,我们需要对OC的方法调用机制有一定了解。
OC中一个对象调用方法,其实就是给这个这个对象发送消息。比如我调用[self blockTest],它转成C语言后就变成了objc_msgSend(self, @selector(blockTest))。
OC的blockTest方法是没有参数的,但是转成objc_msgSend后就多出来了2个参数,一个就是self,是指向函数调用者的,另一个参数就是要调用的这个方法。
所以对于所有的OC方法来说,它们都有这2个默认的参数,第一个参数就是self,所以self就是这么通过参数的形式传进来的,它的确是一个局部变量。
这个问题解决了,那上面几种情况就简单了,这4中情况下block都会捕获self。对第一、二、四这3种情况都好理解。
第三种情况可能有人会有疑惑,block里面都没有用到self为什么还会捕获self呢。其实_name这种写法并不是没有self,它只是将self给省略了,它其实等同于self->_name,所以这种情况要格外注意。
block既然是一个OC对象,那它就有类。我们通过调用block的class方法或object\_getClass()函数来得到block的类。
__NSGlobalBlock__- (void)test{
int age = 10;
void (^block1)(void) = ^{
NSLog(@"-----");
};
NSLog(@"block1的类:%@",[block1 class]);
NSLog(@"block2的类:%@",[^{
NSLog(@"----%d",age);
} class]);
NSLog(@"block3的类:%@",[[^{
NSLog(@"----%d",age);
} copy] class]);
}
有人可能会疑惑,打印第2和3这两个block时为不像block1那样先定义一个block1然后再打印block1。
这是因为在ARC模式下,如果一个__NSStackBlock__类型的block被一个强指针指着,那系统会自动对这个block进行一次copy操作将这个block变成__NSMallocBlock__类型,这样会影响运行的结果。
// ***************打印结果***************
2020-01-08 09:07:46.253895+0800 AppTest[72445:7921459] block1的类:__NSGlobalBlock__
2020-01-08 09:07:46.254027+0800 AppTest[72445:7921459] block2的类:__NSStackBlock__
2020-01-08 09:07:46.254145+0800 AppTest[72445:7921459] block3的类:__NSMallocBlock__
1.3.1 _NSGlobalBlock
如果一个block里面没有访问普通局部变量(也就是说block里面没有访问任何外部变量或者访问的是静态局部变量或者访问的是全局变量),那这个block就是__NSGlobalBlock__。
__NSGlobalBlock__类型的block在内存中是存在数据区的(也叫全局区或静态区,全局变量和静态变量是存在这个区域的)。__NSGlobalBlock__类型的block调用copy方法的话什么都不会做。
- (void)test{
void (^block)(void) = ^{
NSLog(@"-----");
};
NSLog(@"--- %@",[block class]);
NSLog(@"--- %@",[[block class] superclass]);
NSLog(@"--- %@",[[[block class] superclass] superclass]);
NSLog(@"--- %@",[[[[block class] superclass] superclass] superclass]);
}
// ***************打印结果***************
2020-01-08 11:03:34.331652+0800 AppTest[72667:7957820] --- __NSGlobalBlock__
2020-01-08 11:03:34.331777+0800 AppTest[72667:7957820] --- __NSGlobalBlock
2020-01-08 11:03:34.331883+0800 AppTest[72667:7957820] --- NSBlock
2020-01-08 11:03:34.331950+0800 AppTest[72667:7957820] --- NSObject
1.3.2 _NSStackBlock
如果一个block里面访问了普通的局部变量,那它就是一个__NSStackBlock__,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__类型block时要注意,一定要确保它还没被释放。如果对一个__NSStackBlock__类型block做copy操作,那会将这个block从栈复制到堆上。
__NSStackBlock__的继承链是:NSStackBlock : __NSStackBlock : NSBlock : NSObject。
1.3.3 _NSMallocBlock
NSStackBlock__类型block做调用copy,那会将这个block从栈复制到堆上,堆上的这个block类型就是__NSMallocBlock**,所以__NSMallocBlock__类型的block是存储在堆区**。如果对一个__NSMallocBlock__类型block做copy操作,那这个block的引用计数+1。 __NSMallocBlock__的继承链是:NSMallocBlock : __NSMallocBlock : NSBlock : NSObject。
在ARC环境下,编译器会根据情况,**自动将栈上的block复制到堆上。有一下4种情况会将栈block复制到堆上**:
a. block作为函数返回值时:
void (^MyBlock)(void);
- (MyBlock)createBlock{
int a = 10;
return ^{
NSLog(@"******%d",a);
};
}
b. 将block赋值给强指针时:--将定义的栈上的block赋值给强指针myBlock,就变成了堆block。
- (void)test{
int a = 10; // 局部变量
void (^myBlock)(void) = ^{
NSLog(@"a = %d",a);
};
block();
}
c. 当block作为参数传给Cocoa API时:
[UIView animateWithDuration:1.0f animations:^{
}];
d. block作为GCD的API的参数时:
dispatch_async(dispatch_get_main_queue(), ^{
});
另外在MRC环境下,定义block属性建议使用copy关键字,这样会将栈区的block复制到堆区。
@property (copy,nonatomic) void(^block)void;
在ARC环境下,定义block属性用copy或strong关键字都会将栈区block复制到堆上,所以这两种写法都可以。
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
### block对对象型的局部变量的捕获
block对对象类型和对基本数据类型变量的捕获是不一样的,对象类型的变量涉及到强引用和弱引用的问题,强引用和弱引用在block底层是怎么处理的呢?
如果block是在栈上,不管捕获的对象时强指针还是弱指针,block内部都不会对这个对象产生强引用。所以我们主要来看下block在堆上的情况。
首先来看下强引用的对象被block捕获后在底层结构体中是如何存储的。这里用下面这条命令来将OC代码转成c/c++代码:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
// OC代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
void (^block)(void) = ^{
NSLog(@"age--- %ld",person.age);
};
block();
}
return 0;
}
// 底层结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
};
可以看到和基本数据类型不同的是,person对象被block捕获后,在结构体中多了一个修饰关键字__strong。
我们再来看下弱引用对象被捕获后是什么样的:
// OC代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
void (^block)(void) = ^{
NSLog(@"age--- %ld",weakPerson.age);
};
block();
}
return 0;
}
// 底层block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
};
可见此时block中weakPerson的关键字变成了__weak。
在block中修饰被捕获的对象类型变量的关键字除了__strong、__weak外还有一个__unsafe_unretained。那这结果关键字起什么作用呢?
当block被拷贝到堆上时是调用的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数就会根据这3个关键字来进行操作。
如果关键字是__strong,那block内部就会对这个对象进行一次retain操作,引用计数+1,也就是block会强引用这个对象。也正是这个原因,导致在使用block时很容易造成循环引用。
如果关键字是__weak或__unsafe_unretained,那block对这个对象是弱引用,不会造成循环引用。
所以我们通常在block外面定义一个__weak或__unsafe_unretained修饰的弱指针指向对象,然后在block内部使用这个弱指针来解决循环引用的问题。
block从堆上移除时,则会调用block内部的dispose函数,dispose函数内部调用_Block_object_dispose函数会自动释放强引用的变量。
1.3.2 __block修饰符的作用--内部修改外部变量 TODO
__block修饰符是Objective-C中处理block变量捕获的一个重要特性,正确使用它可以使你的block更加灵活和强大。当需要在block内部修改外部变量的值时,这个变量需要使用__block修饰符进行声明。这是因为默认情况下,block会捕获外部变量的值,但这些变量在block内部是只读的,不能被修改( 不可变的副本 ) 。 注意事项:
- 使用__block修饰符时,需要注意内存管理的问题。在MRC(手动引用计数)环境下,__block变量不会被自动retain,可能需要手动管理其生命周期。而在ARC(自动引用计数)环境下,__block变量会被自动retain。
- 在ARC环境下,如果__block变量捕获了对象,而且这个block被拷贝到了堆上,那么这个对象也会被retain。这可能会导致循环引用,需要使用弱引用(__weak)来避免。
不使用__block修饰符:
int count = 10;
void (^myBlock)(void) = ^{
// count = 20; // 这里会报错,因为默认情况下block内部不能修改外部变量的值
NSLog(@"Count = %d", count);
};
myBlock();
使用__block修饰符:
__block int count = 10;
void (^myBlock)(void) = ^{
count = 20; // 使用__block修饰符后,可以在block内部修改count的值
NSLog(@"Count = %d", count);
};
myBlock();
// 输出:Count = 20
<!---->
- (void)test{
int age = 10;
void (^block)(void) = ^{
age = 20;
};
}
这段代码有什么问题吗?编译器会直接报错,在block中不可以修改这个age的值。为什么呢?
因为age是一个局部变量,它的作用域和生命周期就仅限在是test方法里面,而前面也介绍过了,block底层会将大括号中的代码封装成一个函数,也就相当于现在是要在另外一个函数中访问test方法中的局部变量,这样肯定是不行的,所以会报错。
如果我想在block里面更改age的值要怎么做呢?我们可以将age定义成静态局部变量static int age = 10;。虽然静态局部变量的作用域也是在test方法里面,但是它的生命周期是和程序一样的,而且block捕获静态局部变量实际是捕获的age的地址,所以block里面也是通过age的地址去更改age的值,所以是没有问题的。
但我们并不推荐这样做,因为静态局部变量在程序运行过程中是不会被释放的,所以还是要尽量少用。那还有什么别的方法来实现这个需求呢?这就是我们要讲的__block关键字。
- (void)test1{
__block int age = 10;
void (^block)(void) = ^{
age = 20;
};
block();
NSLog(@"%d",age);
}
当我们用__block关键字修饰后,底层到底做了什么让我们能在block里面访问age呢?下面我们来看下上面代码转成c++代码后block的存储结构是什么样的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
};
struct __Block_byref_age_0 {
void *__isa; // isa指针
__Block_byref_age_0 *__forwarding; // 如果这block是在堆上那么这个指针就是指向它自己,如果这个block是在栈上,那这个指针是指向它拷贝到堆上后的那个block
int __flags;
int __size; // 结构体大小
int age; // 真正捕获到的age
};
age用__block修饰后,在block的结构体中变成了__Block_byref_age_0 *age;,而__Block_byref_age_0是个结构体,里面有个成员int age;,这个才是真正捕获到的外部变量age,
实际上外部的age的地址也是指向这里的,所以不管是外面还是block里面修改age时其实都是通过地址找到这里来修改的。
所以age用__block修饰后它就不再是一个test1方法内部的局部变量了,而是被包装成了一个对象,age就被存储在这个对象中。
之所以说是包装成一个对象,是因为__Block_byref_age_0这个结构体的第一个成员就是isa指针。
1.3.3 _block修饰变量的内存管理: __strong、__weak、__unsafe_unretained
__block不管是修饰基础数据类型还是修饰对象数据类型,底层都是将它包装成一个对象(我这里取个名字叫__blockObj),然后block结构体中有个指针指向__blockObj。既然是一个对象,那block内部如何对它进行内存管理呢?
当block在栈上时,block内部并不会对__blockObj产生强引用。
当block调用copy函数从栈拷贝到堆中时,它同时会将__blockObj也拷贝到堆上,并对__blockObj产生强引用。
当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部又会调用_Block_object_dispose函数来释放__blockObj。
在Objective-C中,管理内存的一个重要概念是所有权修饰符,它们定义了对象之间的引用关系。 __strong、__weak、__unsafe_unretained TODO
__strong、__weak和__unsafe_unretained是三种常见的所有权修饰符,它们在自动引用计数(ARC)环境下有着不同的作用。
- __strong修饰符用于创建对象的强引用,保证对象在强引用存在时不被销毁。
- __weak修饰符用于创建对象的弱引用,当对象被销毁时,弱引用会自动置为nil,有助于防止循环引用。
- __unsafe_unretained修饰符也创建对象的弱引用,但不会在对象销毁时将变量置为nil,使用时需要小心野指针问题。
在ARC环境下,正确使用这些修饰符对于管理内存和防止内存泄漏至关重要。
__strong是默认的所有权修饰符。当你使用`__strong`修饰一个对象时意味着你拥有这个对象的强引用。只要强引用存在,对象就不会被销毁,从而保证了对象的生命周期。
__strong NSObject *obj = [[NSObject alloc] init]; // 在ARC环境下,当`__strong`修饰的变量超出其作用域时,ARC会自动释放对象。
__weak修饰符用于创建一个对象的弱引用。与强引用不同,弱引用不会增加对象的引用计数。即使你持有对象的弱引用,对象也可以被销毁。
当对象被销毁时,所有的弱引用会自动置为`nil`,这有助于防止野指针错误。
__weak NSObject *obj = someStrongReferenceToObject; // `__weak`修饰符通常用于防止循环引用,特别是在父子对象或代理关系中。
__unsafe_unretained`修饰符也用于创建对象的弱引用,但与`__weak`不同的是,当对象被销毁时,`__unsafe_unretained`修饰的变量不会自动置为`nil`。
这意味着,如果你尝试访问一个已经被销毁的对象,程序会崩溃,因为你访问了一个野指针。
__unsafe_unretained NSObject *obj = someStrongReferenceToObject; // `__unsafe_unretained`修饰符的使用场景较少,主要出现在需要与不支持ARC的代码交互时。
1.3.5 面试图
Q:block里不加入strong会导致crash么。实测部分场景会有,为什么?
在 Objective-C 中,块(block)捕获外部变量的行为取决于这些变量的类型和修饰符。对于对象类型的变量,默认情况下,块会以强引用(strong reference)的形式捕获它们。这意味着,只要块本身还存在,它所捕获的对象就不会被释放,从而避免了使用已释放对象的风险。然而,在某些情况下,如果不正确地管理块中的强引用,确实可能导致应用崩溃或内存泄漏。
Q:为什么不加 __strong 会导致崩溃?
- 循环引用:最常见的问题是循环引用,尤其是当块捕获
self(或其他可能形成引用环的对象)时。如果块以强引用形式捕获了self,而self又以强引用形式持有这个块,就形成了一个循环引用,导致self和块都无法被释放。这不会直接导致崩溃,但会导致内存泄漏。解决方法是在块内部使用弱引用(__weak)来捕获self。 - 访问已释放的对象:在某些情况下,如果块被执行的时候,它捕获的对象已经被释放了,那么尝试访问这个对象就会导致崩溃。这种情况通常发生在块异步执行且生命周期管理不当时。例如,一个对象启动了一个异步操作,并在操作的完成块中访问了自身的属性,但如果这个对象在操作完成前就被释放了,那么在完成块中访问这个已释放对象的属性就会导致崩溃。
__weak typeof(self) weakSelf = self;
[self performAsyncOperationWithCompletion:^{
// 如果self在异步操作完成前被释放,这里的strongSelf可能为nil
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething]; // 如果strongSelf为nil,这里不会崩溃
}];
在这个示例中,我们首先通过 __weak 修饰符创建了 self 的一个弱引用 weakSelf,以避免循环引用。然后,在异步操作的完成块中,我们通过 __strong 修饰符将 weakSelf 升级为强引用 strongSelf。这样做的目的是确保在完成块执行期间 self 不会被释放。如果 self 在异步操作完成前被释放了,weakSelf 会变成 nil,因此 strongSelf 也是 nil,这样就避免了访问已释放对象的风险。
1.3.6 结论
在块中不加 __strong 通常不会直接导致崩溃,因为块默认以强引用捕获对象类型的变量。崩溃通常是由于循环引用导致的内存泄漏或访问已释放对象引起的。正确管理块中的引用(使用 __weak 和 __strong)是避免这些问题的关键。
Block捕获外部变量的行为取决于变量的类型以及Block本身是如何被存储的。
对于对象类型的变量,如果在Block内部使用,Block默认会将其以strong引用的形式捕获,以确保在Block执行期间,这些对象不会被释放。
因此,通常情况下,仅仅因为Block内部没有显式使用strong修饰符,并不会直接导致崩溃。然而,存在一些特定的场景,如果不正确地管理Block中的对象引用,可能会导致崩溃或其他意外行为。以下是一些可能导致问题的场景:
- 循环引用
如果Block内部捕获了外部的
strong引用对象,而这个对象又strong持有了这个Block,就形成了一个循环引用。这会导致内存泄漏,因为参与循环引用的对象和Block都无法被正常释放。虽然这不直接导致崩溃,但如果循环引用中的对象是资源密集型的,长时间积累可能会导致应用性能问题或崩溃。 - 使用已释放的对象
如果Block被延迟执行(例如,提交到一个异步队列) ,而Block内部使用的对象在Block执行前就被释放了,那么在Block执行时访问这些已释放对象的成员变量或方法可能会导致崩溃。这种情况下,确保Block执行期间对象仍然存活的一种方法是在Block内部使用对象的
strong引用,或者确保对象的生命周期覆盖了Block的执行周期。 - __block修饰符和对象的释放
使用
__block修饰符的变量在Block中是可变的。如果一个__block修饰的对象在Block执行过程中被释放,然后又在Block中被访问,这同样可能导致崩溃。在ARC环境下,__block修饰的对象变量在Block捕获时也会被strong持有,但在特定情况下,如果Block没有被执行,而外部环境已经释放了这个对象,可能会出现问题。
解决方案:
1、避免循环引用:可以通过在Block内部使用weak或__weak修饰符来捕获对象,或者使用__strong修饰符在Block内部临时强引用这些对象。
2、管理对象生命周期:确保Block执行期间所需的对象不会被提前释放。如果需要,可以在Block内部创建对象的strong引用。
3、使用__block修饰符时小心:确保理解__block修饰符的语义,特别是在ARC环境下,以及它如何影响对象的生命周期。
总之,Block中不加入strong不会直接导致崩溃,崩溃通常是由于循环引用、使用已释放的对象或不当的对象生命周期管理等问题导致的。正确地管理Block和对象的引用关系是避免这些问题的关键。
Q:用__weak修复后导致block对person弱引用,那person在外部释放后,block才调用,会有crssh么?--TODO:不会
// person对象被声明为一个强引用(strong),然后创建了一个指向person的弱引用(weak)weakPerson。
// 在block内部,你通过这个弱引用来访问person的age属性。如果person在block被调用之前被释放,那么weakPerson将会自动变为nil,这是弱引用的特性之一,用以避免悬挂指针(dangling pointer)。
Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
void (^block)(void) = ^{
NSLog(@"age--- %ld",weakPerson.age);
};
// 如果weakPerson是nil,weakPerson.age将会被解释为发送消息给nil,这将返回0。因此,NSLog将会打印age--- 0。这种写法是安全的,不会导致崩溃(crash)。
//然而,你应该意识到,如果person在block执行之前被释放,你可能无法按预期访问person的数据,因为weakPerson会变成nil。
// 这种情况下,最好在使用weakPerson之前检查它是否为nil。
void (^block)(void) = ^{
if (weakPerson) {
NSLog(@"age--- %ld", weakPerson.age);
} else {
NSLog(@"person已经被释放");
}
};
// 更好的解决方法
1. 使用强引用(Strong Reference)局部变量
在block内部,你可以创建一个强引用的局部变量指向弱引用对象,这样可以确保在block执行期间对象保持存活。
这种方法的关键是在block执行完毕后,强引用的局部变量会被释放,从而避免了潜在的循环引用问题。
void (^block)(void) = ^{
Person *strongPerson = weakPerson;
if (strongPerson) {
NSLog(@"age--- %ld", strongPerson.age);
} else {
NSLog(@"person已经被释放");
}
};
2. 明确生命周期管理:确保在block执行之前,相关对象的生命周期是被正确管理的。这可能意味着需要重新考虑你的对象管理策略,确保在block执行时对象仍然有效。
3. 使用@autoreleasepool
如果你担心在block执行期间对象被释放可能导致的内存问题,可以考虑在block内部使用@autoreleasepool块来更精细地管理内存。
void (^block)(void) = ^{
@autoreleasepool {
// 使用weakPerson
if (weakPerson) {
NSLog(@"age--- %ld", weakPerson.age);
} else {
NSLog(@"person已经被释放");
}
}
};
4. 检查对象是否仍然存在:在访问对象之前,总是检查它是否仍然存在。这是一种保守的做法,可以避免因对象已被释放而导致的问题。
5. 使用Optional Chaining(Swift) 如果你在使用Swift,可以利用Optional Chaining来安全地访问可能为nil的对象属性或方法。
block = {
if let age = weakPerson?.age {
print("age--- (age)")
} else {
print("person已经被释放")
}
}
6. 重新考虑设计
如果你经常遇到这种情况,可能需要重新考虑你的应用架构或对象生命周期管理策略。可能有更合适的方式来组织你的代码,以避免这种潜在的问题。
总之,正确管理block中对象的生命周期是避免潜在问题的关键。通过上述方法,你可以更安全地在block中使用对象,同时避免内存泄漏或访问已释放对象的风险。
在Objective-C中,当你尝试通过一个nil对象发送消息时,这个操作是安全的,不会导致崩溃。 Objective-C允许向nil发送消息,并且简单地忽略它,返回0、nil或NO,具体取决于返回值的类型。 所以,按照你的代码示例,即使person在block调用之前被释放,也不会导致崩溃。这是因为:
- 弱引用weakPerson在person被释放后会自动变为nil。
- 向nil发送消息在Objective-C中是安全的,不会导致崩溃。
1.4 为什么使用copy关键字?
在ARC之前,管理block的内存是开发者的责任。为了保持block的生命周期,开发者需要手动将栈上的block复制到堆上。这是通过copy操作来完成的。因此,当声明一个block属性时,使用copy关键字是一种最佳实践,它明确表示这个属性持有的block应该在堆上。如下:
@property (copy, nonatomic) void (^block)(void);
在ARC环境下,虽然将block赋值给使用strong修饰的属性时编译器会自动进行复制操作,但使用copy关键字仍然是推荐的做法。这样做的原因有:
- 代码意图清晰:使用copy明确表示block应该被复制。这对于代码的阅读者来说是一个清晰的信号,表明这个属性持有的block是在堆上的。
- 向后兼容:对于不使用ARC的代码,或者当你的代码库需要与旧代码兼容时,使用copy可以确保block被正确管理。
- 一致性:即使在ARC环境下,对于block属性仍然使用copy,可以保持与非ARC代码的一致性,也避免了在不同内存管理环境下切换时的混淆。
虽然在ARC环境下,将栈上的block赋值给使用strong修饰的属性时编译器会自动进行复制操作,但使用copy关键字声明block属性仍然是一种最佳实践。这样做可以提高代码的清晰度和一致性,同时确保block的内存管理在不同的编译环境下都是正确的。
Q1:使用strong修饰block可以么?--ARC下可以,MRC下不行
通常建议使用copy而不是strong来修饰block属性。 这是因为block在创建时默认存储在栈(stack)上,而栈上的block在函数返回后就会被销毁。
为了在函数返回后仍能保持block的可用性,需要将其复制到堆(heap)上。使用copy修饰符可以实现这一点,因为它告诉编译器在将block赋值给属性时需要将其从栈复制到堆上。
栈上的Block: 当你在代码中创建一个block时,它默认是存储在栈上的。栈上的block有一个非常短的生命周期,只在定义它的那个作用域内有效。一旦超出这个作用域,栈上的block就会被销毁。
堆上的Block: 为了让block在其原始作用域之外也能使用,需要将其复制到堆上。堆上的block有更长的生命周期,直到没有任何强引用指向它时才会被销毁。使用copy操作可以将block从栈复制到堆上。
ARC环境下的变化
在自动引用计数(ARC)环境下,编译器会自动管理block的内存。当你将一个block赋值给一个使用strong修饰的属性时,如果这个block是在栈上,编译器会自动将其复制到堆上。因此,在ARC环境下,使用strong修饰block属性通常也是安全的,因为ARC会自动处理block的复制操作。
然而,即使在ARC下,使用copy仍然是最佳实践。这是因为copy明确表达了将block从栈复制到堆的意图,这在非ARC环境下是必要的,并且即使在ARC环境下,这也是一种更清晰、更明确的代码表达方式。
总结
- 在非ARC环境下,必须使用copy来修饰block属性,以确保block被复制到堆上,从而延长其生命周期。
- 在ARC环境下,虽然使用strong修饰block属性通常也是安全的,但使用copy仍然是最佳实践,因为它更明确地表达了复制block的意图,并且保持了与非ARC代码的兼容性。
1.5 block声明与使用
在Objective-C中声明一个属性为block时,需要正确地指定block的语法。
下面是如何正确声明一个无参数且无返回值的block属性的示例:
@property (copy, nonatomic) void (^block)(void);
// - `@property`:用于声明一个属性。
// - `(copy, nonatomic)`:属性的特性。使用`copy`是因为block在从栈复制到堆时需要复制,以保持其生命周期,特别是当你将一个栈上的block赋值给这个属性时。
// `nonatomic`表示非原子性,用于提高访问速度。
// - `void (^block)(void)`:block的类型。这表示这个block没有参数并且返回`void`。
// 设置block属性
self.block = ^{
NSLog(@"这是一个block");
};
// 调用block属性:在调用block之前,你应该检查它是否被设置(即它不是nil):
if (self.block) {
self.block();
}
// 当使用block时,特别是在block内部引用self(即所在的对象)时,要注意避免循环引用。
// 循环引用会导致内存泄漏。为了避免这种情况,可以使用弱引用(__weak类型):
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf someMethod];
};
// 首先创建了self的一个弱引用weakSelf,然后在block内部将其转换为强引用strongSelf。这样可以在block执行期间保持对象,同时避免循环引用。
// 注意,这种方法适用于可能导致循环引用的情况。如果你的block不会捕获self或者捕获的self不会持有这个block,那么你可能不需要担心循环引用的问题。
在Objective-C中,使用带有block参数的方法是一种常见的编程模式,特别是在需要异步执行代码或回调时。下面是如何定义和调用带有block参数的方法的示例。
1、定义带有Block参数的方法
假设你想定义一个方法,该方法执行一些操作,然后通过block回调返回结果。你可以这样定义这个方法:
@interface MyClass : NSObject
- (void)performOperationWithCompletion:(void (^)(BOOL success, NSError *error))completionBlock;
@end
在这个例子中,`performOperationWithCompletion:`方法接受一个block作为参数,这个block没有返回值(`void`),并接受两个参数:一个`BOOL`类型的`success`和一个`NSError`对象。
2、实现带有Block参数的方法
// MyClass.m
#import "MyClass.h"
@implementation MyClass
- (void)performOperationWithCompletion:(void (^)(BOOL, NSError *))completionBlock {
// 执行一些操作...
// 假设操作成功完成
BOOL success = YES;
NSError *error = nil;
// 检查是否提供了block,然后调用它
if (completionBlock) {
completionBlock(success, error);
}
}
@end
3、调用带有Block参数的方法
MyClass *myObject = [[MyClass alloc] init];
[myObject performOperationWithCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"操作成功完成");
} else {
NSLog(@"操作失败: %@", error);
}
}];
在这个例子中,当`performOperationWithCompletion:`方法完成操作时,它会调用提供的block,并传递操作的结果。这种模式在处理异步操作和回调时非常有用,比如网络请求、文件读写等。
4、注意事项
- 当在block内部使用外部变量时,默认情况下这些变量是只读的。如果你需要在block内部修改外部变量,可以通过在变量前加上`__block`修饰符来实现。
- 要避免循环引用(retain cycle),特别是当你在block内部引用`self`时。可以通过使用弱引用(`__weak`类型的变量)来避免这个问题。
通过这种方式,你可以在Objective-C中灵活地使用带有block参数的方法,以实现异步操作和回调逻辑。
Q2:__weak typeof(self) weakSelf = self; 和@weakify(self);有什么区别
__weak typeof(self) weakSelf = self; 和 @weakify(self); 都是在Objective-C中用来避免循环引用的常见方法,尤其是在block中使用self时。它们的目的相同,但是实现方式和使用的上下文有所不同。
-
依赖:
__weak typeof(self) weakSelf = self;不依赖于任何外部库,是Objective-C语言的一部分。 而@weakify(self);需要引入支持这些宏的库,如ReactiveObjC或libextobjc。 -
简洁性:
@weakify(self);和@strongify(self);提供了一种更简洁的方式来避免循环引用,使代码更易读。但是,它隐藏了背后的实现细节,对于不熟悉这些宏的开发者可能会造成一定的困惑。 -
通用性:
__weak typeof(self) weakSelf = self;是一种更通用的方法,因为它不依赖于特定的库。
选择使用哪种方式主要取决于个人偏好和项目中是否已经包含了支持@weakify和@strongify宏的库。如果你倾向于不引入额外的依赖,或者在一个不使用ReactiveObjC或libextobjc的项目中工作,那么使用__weak typeof(self) weakSelf = self;可能是更好的选择。
如果你的项目已经使用了这些库,那么@weakify(self); 和 @strongify(self);可以提供一种更简洁的语法。
1、__weak typeof(self) weakSelf = self;
手动声明一个weakSelf变量,它是self的一个弱引用。这里使用了__weak修饰符来避免循环引用,同时使用typeof(self)来确保weakSelf和self类型相同。
这是一种更为直接和底层的方式,不依赖于任何第三方库。
2、@weakify(self); 是一个宏,通常来自于第三方库,如ReactiveObjC或libextobjc。
这个宏在背后做了类似的事情:创建一个self的弱引用。与手动声明相比,@weakify(self); 提供了一种更简洁的语法。
为了配合@weakify使用,通常还会使用@strongify(self);宏来在block内部创建一个强引用。
@weakify(self);
[self doSomethingWithCompletion:^{
@strongify(self);
if (self) {
// 使用self
}
}];
参考: