第一种场景:用NSString直接赋值
// 第一种场景:用NSString直接赋值
NSString *originStr1 = [NSString stringWithFormat:@"hello,everyone"];
_strongStr = originStr1;_
copyyStr = originStr1;
NSLog(@"第一种场景:用NSString直接赋值");
NSLog(@" 对象地址 对象指针地址 对象的值 ");
NSLog(@"originStr: %p , %p , %@", originStr1, &originStr1, originStr1);
NSLog(@"strongStr: %p , %p , %@", _strongStr, &_strongStr, _strongStr);
NSLog(@" copyyStr: %p , %p , %@", _copyyStr, &_copyyStr, _copyyStr);
结论:这种情况下,不管是用strong还是copy修饰的对象,其指向的地址都是originStr的地址。
当原字符串是NSString时,由于是不可变字符串,所以,不管使用strong还是copy修饰,都是指向原来的对象,copy操作只是做了一次浅拷贝。
而当源字符串是NSMutableString时,strong只是将源字符串的引用计数加1,而copy则是对原字符串做了次深拷贝,从而生成了一个新的对象,并且copy的对象指向这个新对象。另外需要注意的是,这个copy属性对象的类型始终是NSString,而不是NSMutableString,如果想让拷贝过来的对象是可变的,就要使用mutableCopy。
所以,如果源字符串是NSMutableString的时候,使用strong只会增加引用计数。
但是copy会执行一次深拷贝,会造成不必要的内存浪费。而如果原字符串是NSString时,strong和copy效果一样,就不会有这个问题。
但是,我们一般声明NSString时,也不希望它改变,所以一般情况下,建议使用copy,这样可以避免NSMutableString带来的错误。
顺便路过提一下assign与weak
我们都知道,assign用来修饰基本数据类型,weak用来修饰OC对象。
其实照理,assign也能修饰OC对象,但是assign修饰的对象在该对象释放后,其指针依然存在,不会被置为nil——这就造成了一个很严重的问题:出现了野指针。当访问这个野指针时,指向了原地址,而原地址是nil,所以会造成程序的crash。但是用weak来修饰的话,对象释放的时候会把指针置为nil,从而避免了野指针的出现。
那又有个疑问出现了,凭什么基本数据类型就可以使用assign。这就要扯到堆和栈的问题了,基本数据类型会被分配到栈空间,而栈空间是由系统自动管理分配和释放的,就不会造成野指针的问题。
一:概念
浅拷贝:指针拷贝,不会创建一个新的对象。浅拷贝简单点说就是对内存地址的复制,让目标对象指针和源对象指针指向同一片内存空间
深拷贝: 内容拷贝,会创建一个新的对象。深拷贝就是拷贝地址中的内容,让目标对象产生新的内存区域,且将源内存区域中的内容复制到目标内存区域中
深拷贝和浅拷贝的本质是内存地址是否相同
面试时经常问到给NSString字符串设置为strong属性有什么风险,从上面就测试可以看出:
stong修饰的字符串无论被赋值NSString还是NSMutableString都是浅拷贝,被赋值NSString类型时没有问题和copy效果一样,但是当被赋值为NSMutableString类型时,它会因为被赋的值的改变而改变,这在很多情况下是存在风险的,因为大部分情况下我们是不希望自己的属性值在初始化后又莫名的改变的,当然如果原本需求就是希望无论什么时候都随被赋值改变而改变那就必须就用stong修饰了
第二种情况(传递可变数据)
这里将局部的字符串改成了可变的NSMutableString,赋值用了self.语法,只有这样才会走自动生成的setter方法,strong,copy关键字才会生效
第三种情况(使用copy关键字的字符串为NSMutableString)
使用copy关键字的为可变字符串,但是结果是什么呢?
打印结果:
当执行到上图中的那一句时崩溃了,打印信息如下图
因为copy出来的仍然是不可变字符!如果使用NSMutableString的方法,就会崩溃。文章开始的时候那个问题的答案就在这里了。
assign:
assign一般用来修饰基本的数据类型,包括基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char, 等等),为什么呢?assign声明的属性是不会增加引用计数的,也就是说声明的属性释放后,就没有了,即使其他对象用到了它,也无法留住它,只会crash。但是,即使被释放,指针却还在,成为了野指针,如果新的对象被分配到了这个内存地址上,又会crash,所以一般只用来声明基本的数据类型,因为它们会被分配到栈上,而栈会由系统自动处理,不会造成野指针。
retain:
与assign相对,我们要解决对象被其他对象引用后释放造成的问题,就要用retain来声明。retain声明后的对象会更改引用计数,那么每次被引用,引用计数都会+1,释放后就会-1,即使这个对象本身释放了,只要还有对象在引用它,就会持有,不会造成什么问题,只有当引用计数为0时,就被dealloc析构函数回收内存了
weak:
weak其实类似于assign,叫弱引用,也是不增加引用计数。一般只有在防止循环引用时使用,比如父类引用了子类,子类又去引用父类。IBOutlet、Delegate一般用的就是weak,这是因为它们会在类外部被调用,防止循环引用。
strong:
相对的,strong就类似与retain了,叫强引用,会增加引用计数,类内部使用的属性一般都是strong修饰的,现在ARC已经基本替代了MRC,所以我们最常见的就是strong了。
nonatomic:
在修饰属性时,我们往往还会加一个nonatomic,这又是什么呢?它的名字叫非原子访问。对应的有atomic,是原子性的访问。我们知道,在使用多线程时为了避免在写操作时同时进行写导致问题,经常会对要写的对象进行加锁,也就是同一时刻只允许一个线程去操作它。如果一个属性是由atomic修饰的,那么系统就会进行线程保护,防止多个写操作同时进行。这有好处,但也有坏处,那就是消耗系统资源,所以对于iPhone这种小型设备,如果不是进行多线程的写操作,就可以使用nonatomic,取消线程保护,提高性能。
block本身是像对象一样可以retain,和release。但是,block在创建的时候,它的内存是分配在栈上的,而不是在堆上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。因为栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.
使用retain也可以,但是block的retain行为默认是用copy的行为实现的,
因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block拷贝(copy)到堆,所以说为了block属性声明和实际的操作一致,最好声明为copy。
iOS 内存分布,一般分为:栈区、堆区、全局区、常量区、代码区。其实 Block 也是一个 Objective-C 对象,常见的有以下三种 Block:
NSMallocBlock :存放在堆区的 Block
NSStackBlock : 存放在栈区的 Block
NSGlobalBlock : 存放在全局区的 Block
Block 内部没有引用外部变量,Block 在全局区,属于 GlobalBlock
注意:Block 引用普通外部变量,都是在栈区创建的,只是用 strong、copy 修饰的 Block 会把它从栈区拷贝到堆区一份,而 weak 修饰的 Block 不会;
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __block_desc_0 {
size_t reserved;
size_t Block_size;
} _block_desc_0_DATA = { 0, sizeof(struct __block_desc_0)};
struct _block_impl_0 {
struct __block_impl impl;
struct __block_desc_0* Desc;
int i; // 这个是引用外部变量 i
_block_impl_0(void *fp, struct __block_desc_0 *desc, int _i, int flags=0) :i(_i){
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
过分析上面源码,我们可以得到下面几点结论:
结构体中有 isa 指针,证明 Block 也是一个对象
Block 底层是用结构体来实现的,结构体 _block_impl_0 包含了 __block_impl 结构体和 __block_desc_0 结构体。
__block_impl 结构体中的 FuncPtr 函数指针,指向的就是我们的 Block 的具体实现。真正调用 Block 就是利用这个函数指针去调用的。
为什么能访问外部变量,就是因为将外部变量复制到了结构体中(上面的 int i),即自动变量会作为成员变量追加到 Block 结构体中。
分析具有 __block 修饰符外部变量的 Block 源代码
我们知道 Block 截获外部变量是将外部变量作为成员变量追加到 Block 结构体中,但是匿名函数存在作用域的问题,这个就是为什么我们不能在 Block 内部去修改普通外部变量的原因。所有就出现了 __block 修饰符来解决这个问题。
//Objective-C 代码
- (void)blockDataBlockFunction {
__block int a = 100; ///在栈区
void (^blockDataBlock)(void) = ^{
a = 1000;
NSLog(@"%d", a);
}; ///在堆区
blockDataBlock();
}
//C++ 代码
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __BlockStructureViewController__blockDataBlockFunction_block_impl_0 {
struct __block_impl impl;
struct __BlockStructureViewController__blockDataBlockFunction_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
};
具有 __block 修饰的变量,会生成一个 Block_byref_a_0 结构体来表示外部变量,然后再追加到 Block 结构体中,这里生成 Block_byref_a_0 这个结构体大概有两个原因:一个是抽象出一个结构体,可以让多个 Block 同时引用这个外部变量;另外一个好管理,因为 Block_byref_a_0 中有个非常重要的成员变量 forwarding 指针,这个指针非常重要(这个指针指向 Block_byref_a_0 结构体),这里是保证当我们将 Block 从栈拷贝到堆中,修改的变量都是同一份。
__block 变量的存储域
Block 从栈复制到堆上,_block 修饰的变量也会从栈复制到堆上;为了结构体 \_block 变量无论在栈上还是在堆上,都可以正确的访问变量,我们需要 forwarding 指针;
在 Block 从栈复制到堆上的时候,原本栈上结构体的 forwarding 指针,会改变指向,直接指向堆上的结构体。这样子就可以保证之后我们都是访问同一个结构体中的变量,这里就是为什么 __block 修饰的变量,在 Block 内部中可以修改的原因了。
Block 对于外部变量都会追加到结构体中
Block 为什么用 Copy 修饰
对于这个问题,得区分 MRC 环境 和 ARC 环境;首先,通过上面小节可知,Block 引用了普通外部变量,都是创建在栈区的;对于分配在栈区的对象,我们很容易会在释放之后继续调用,导致程序奔溃,所以我们使用的时候需要将栈区的对象移到堆区,来延长该对象的生命周期。
对于 MRC 环境,使用 Copy 修饰 Block,会将栈区的 Block 拷贝到堆区。
对于 ARC 环境,使用 Strong、Copy 修饰 Block,都会将栈区的 Block 拷贝到堆区。
所以,Block 不是一定要用 Copy 来修饰的,在 ARC 环境下面 Strong 和 Copy 修饰效果是一样的。
总结:
block内部没有调用外部局部变量时存放在全局区(ARC和MRC下均是)
block使用了外部局部变量,这种情况也正是我们平时所常用的方式。MRC:Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.所以在使用Block属性时使用copy修饰。但是ARC中的Block都会在堆上的,系统会默认对Block进行copy操作
用copy,strong修饰block在ARC和MRC都是可以的,都是在堆区
补充:一个block要使用self,会处理成在外部声明一个weak变量指向self,然而为何有时会出现在block里又声明一个strong变量指向weakSelf?
原因:block会把写在block里的变量copy一份,如果直接在block里使用self,(self对变量默认是强引用)self对block持有,block对self持有,导致循环引用,所以这里需要声明一个弱引用weakSelf,让block引用weakSelf,打破循环引用。
而这样会导致另外一个问题,因为weakSelf是对self的弱引用,如果这个时候控制器pop或者其他的方式引用计数为0,就会释放,如果这个block是异步调用而且调用的时候self已经释放了,这个时候weakSelf已就变成了nil。
当控制器(也可以是其他的控件)pop回来之后(或者一些其他的原因导致释放),网络请求完成,如果这个时候需要控制器做出反映,需要strongSelf再对weakSelf强引用一下。
但是,你可能会疑问,strongSelf对weakSelf强引用,weakSelf对self弱引用,最终不也是对self进行了强引用,会导致循环引用吗。不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放。