iOS 基础iOS面试题

assign、weak、retain、strong、copy、_

2018-03-12  本文已影响258人  忠晓

assign

用于对基本数据类型进行赋值操作,不更改引用计数。也可以用来修饰对象,但是,被assign修饰的对象在释放后,指针的地址还是存在的,也就是说指针并没有被置为nil,成为野指针。如果后续在分配对象到堆上的某块内存时,正好分到这块地址,程序就会crash。之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。

weak

修饰Object类型,修饰的对象在释放后,指针地址会被置为nil,是一种弱引用。在ARC环境下,为避免循环引用,往往会把delegate属性用weak修饰;在MRC下使用assign修饰。weak和strong不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。

weak 与 assgin 的区别
assigin 可以用非 OC 对象,而 weak 必须用于 OC 对象

strong

ARC下的strong等同于MRC下的retain都会把对象引用计数加1。

copy

会在内存里拷贝一份对象,两个指针指向不同的内存地址。一般用来修饰NSString等有对应可变类型的对象,因为他们有可能和对应的可变类型(NSMutableString)之间进行赋值操作,为确保对象中的字符串不被修改 ,应该在设置属性是拷贝一份。而若用strong修饰,如果对象在外部被修改了,会影响到属性。

strong与copy的区别

CopyStrong的区别需要了解点内存管理的知识,Strong是ARC下引入的修饰,相当于手动管理内存(MRC)下的retain,在相关代码下,常常看到有的人用copy修饰NSString,NSArray,NSDictionary等存在可变与不可变之分的对象,常常会用copy,而不是strong,下面代码来解释一下strong与copy的区别:

先说明一下什么叫做浅拷贝,什么叫做深拷贝;

浅Copy:可以理解为指针的复制,只是多了一个指向这块内存的指针,共用一块内存。

深Copy:理解为内存的复制,两块内存是完全不同的,也就是两个对象指针分别指向不同的内存,互不干涉。

举例

首先在类延展中声明两个属性变量

@property (nonatomic, strong)NSString * stringStrong;   //strong修饰的字符串对象  
@property (nonatomic, copy)NSString * stringCopy;       //copy修饰的字符串对象  

接着创建两个不可变字符串(NSString)

//新创建两个NSString对象  
NSString * strong1 = @"I am Strong!";  
NSString * copy1 = @"I am Copy!";  

将两个属性分别进行赋值

//初始化两个字符串  
self.stringStrong = strong1;  
self.stringCopy = copy1;  

分别打印一下四个变量的内存地址:

NSLog(@"strong1 = %p",strong1);  
NSLog(@"stringStrong = %p",self.stringStrong);  
NSLog(@"copy1 = %p",copy1);  
NSLog(@"stringCopy = %p",self.stringCopy);  

结果如下:可以看出,此时无论是strong修饰的字符串还是copy修饰的字符串,都进行了浅Copy

2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] strong1 = 0x10a0b3078  
2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] stringStrong = 0x10a0b3078  
2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] copy1 = 0x10a0b3098  
2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] stringCopy = 0x10a0b3098  

如果创建两个可变字符串对象(NSMutableString)

//新创建两个NSMutableString对象  
NSMutableString * mutableStrong = [NSMutableString stringWithString:@"StrongMutable"];  
NSMutableString * mutableCopy = [NSMutableString stringWithString:@"CopyMutable"]; 

分别对属性再次进行赋值

self.stringStrong = mutableStrong;  
self.stringCopy = mutableCopy;

分别打印一下四个变量的地址:结果如下:这时就发现了,用strong修饰的字符串依旧进行了浅Copy,而由copy修饰的字符串进行了深Copy,所以mutableStrong与stringStrong指向了同一块内存,而mutableCopy和stringCopy指向的是完全两块不同的内存

2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] mutableStrong = 0x7fccba425d60  
2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] stringStrong = 0x7fccba425d60  
2018-03-11 18:59:06.332 StrongOrCopy[5046:421886] mutableCopy = 0x7fccba40d7c0  
2018-03-11 18:59:06.333 StrongOrCopy[5046:421886] stringCopy = 0x7fccba4149e0  

那么有什么用呢,实例来看一下有什么区别:

首先是对不可变字符串进行操作:

//新创建两个NSString对象  
NSString * strong1 = @"I am Strong!";  
NSString * copy1 = @"I am Copy!";  
  
//初始化两个字符串  
self.stringStrong = strong1;  
self.stringCopy = copy1;  
  
//两个NSString进行操作  
[strong1 stringByAppendingString:@"11111"];  
[copy1 stringByAppendingString:@"22222"];  

分别对在字符串后面进行拼接,当然这个拼接对原字符串没有任何的影响,因为不可变自字符串调用的方法都是有返回值的,原来的值是不会发生变化的。打印如下,对结果没有任何的影响:

2018-03-11 19:15:26.729 StrongOrCopy[5146:439360] strong1 = I am Strong!  
2018-03-11 19:15:26.729 StrongOrCopy[5146:439360] stringStrong = I am Strong!  
2018-03-11 19:15:26.729 StrongOrCopy[5146:439360] copy1 = I am Copy!  
2018-03-11 19:15:26.729 StrongOrCopy[5146:439360] stringCopy = I am Copy!  

然后是对可变字符串进行操作:

//新创建两个NSMutableString对象  
NSMutableString * mutableStrong = [NSMutableString stringWithString:@"StrongMutable"];  
NSMutableString * mutableCopy = [NSMutableString stringWithString:@"CopyMutable"];  
  
//初始化两个字符串  
self.stringStrong = mutableStrong;  
self.stringCopy = mutableCopy;  
  
//两个MutableString进行操作  
[mutableStrong appendString:@"Strong!"];  
[mutableCopy appendString:@"Copy!"];  

再来看一下结果:对mutableStrong进行的操作,由于用strong修饰的stringStrong没有进行深Copy,导致共用了一块内存,当mutableStrong对内存进行了操作的时候,实际上对stringStrong也进行了操作; 相反,用copy修饰的stringCopy进行了深Copy,也就是说stringCopy与mutableCopy用了两块完全不同的内存,所以不管mutableCopy进行了怎么样的变化,原来的stringCopy都不会发生变化。这就在日常中避免了出现一些不可预计的错误。

2018-03-11 19:20:27.652 StrongOrCopy[5245:446189] stringStrong = StrongMutableStrong!  
2018-03-11 19:20:27.652 StrongOrCopy[5245:446189] mutableStrong = StrongMutableStrong!  
2018-03-11 19:20:27.652 StrongOrCopy[5245:446189] stringCopy = CopyMutable  
2018-03-11 19:20:27.652 StrongOrCopy[5245:446189] mutableCopy = CopyMutableCopy!  
总结

在不可变对象之间进行转换,strong与copy作用是一样的,但如果在不可变与可变之间进行操作,strong与copy就不同了。

__weak

作为程序猿还是代码具有说服力,上栗子:

首先定义一个类 MyObject 继承 NSObject,并添加一个属性 text,重写了description方法,返回 text 的值。这个主要是因为编译器本身对 NSString 是有优化的,创建的 string 对象有可能是静态存储区永不释放的,为了避免使用 NSString 引起一些问题,还是创建一个 NSObject 对象比较合适。

自定义了一个 ZXLog 方法输出对象相关值,定义如下:

#define ZXLog(prefix,Obj) {NSLog(@"变量内存地址:%p, 变量值:%p, 指向对象值:%@, --> %@",&Obj,Obj,Obj,prefix);}

代码:

MyObject *obj = [[MyObject alloc]init];
obj.text = @"my-object";
ZXLog(@"obj", obj);
   
__weak MyObject *weakObj = obj;
ZXLog(@"weakObj", weakObj);
   
void(^testBlock)(void) = ^(){
    ZXLog(@"weakObj - block", weakObj);
};
testBlock();
obj = nil;
testBlock();

打印结果:

变量内存地址:0x7fff510b7c78, 变量值:0x60000001a270, 指向对象值:<MyObject: 0x60000001a270>, --> obj
变量内存地址:0x7fff510b7c70, 变量值:0x60000001a270, 指向对象值:<MyObject: 0x60000001a270>, --> weakObj
变量内存地址:0x60400044cfe0, 变量值:0x60000001a270, 指向对象值:<MyObject: 0x60000001a270>, --> weakObj - block
变量内存地址:0x60400044cfe0, 变量值:0x0, 指向对象值:(null), --> weakObj - block

从上面的结果可以看到

接下来看第二段代码:

MyObject *obj = [[MyObject alloc]init];
obj.text = @"my-object";
ZXLog(@"obj", obj);
    
__weak MyObject *weakObj = obj;
ZXLog(@"weakObj-0", weakObj);
    
void(^testBlock)(void) = ^(){
   __strong MyObject *strongObj = weakObj;
   ZXLog(@"weakObj - block", weakObj);
   ZXLog(@"strongObj - block", strongObj);
};
    
ZXLog(@"weakObj-1", weakObj);
testBlock();
ZXLog(@"weakObj-2", weakObj);
obj = nil;
testBlock();
ZXLog(@"weakObj-3", weakObj);

打印结果:

变量内存地址:0x7fff517bcc78, 变量值:0x60400000a420, 指向对象值:<MyObject: 0x60400000a420>, --> obj
变量内存地址:0x7fff517bcc70, 变量值:0x60400000a420, 指向对象值:<MyObject: 0x60400000a420>, --> weakObj-0
变量内存地址:0x7fff517bcc70, 变量值:0x60400000a420, 指向对象值:<MyObject: 0x60400000a420>, --> weakObj-1
变量内存地址:0x600000259df0, 变量值:0x60400000a420, 指向对象值:<MyObject: 0x60400000a420>, --> weakObj - block
变量内存地址:0x7fff517bcb28, 变量值:0x60400000a420, 指向对象值:<MyObject: 0x60400000a420>, --> strongObj - block
变量内存地址:0x7fff517bcc70, 变量值:0x60400000a420, 指向对象值:<MyObject: 0x60400000a420>, --> weakObj-2
变量内存地址:0x600000259df0, 变量值:0x0, 指向对象值:(null), --> weakObj - block
变量内存地址:0x7fff517bcb28, 变量值:0x0, 指向对象值:(null), --> strongObj - block
变量内存地址:0x7fff517bcc70, 变量值:0x0, 指向对象值:(null), --> weakObj-3

如果你看过 AFNetworking 的源码,会发现 AFN 中作者会把变量在 block 外面先用 __weak 声明,在 block 内把前面 weak 声明的变量赋值给 __strong 修饰的变量这种写法。

从上面例子我们看到即使在 block 内部用 strong 强引用了外面的 weakObj ,但是一旦 obj 释放了之后,内部的 strongObj 同样会变成 nil,那么这种写法又有什么意义呢?

下面再看一段代码:

MyObject *obj = [[MyObject alloc]init];
obj.text = @"my-object";
ZXLog(@"obj", obj);

__weak MyObject *weakObj = obj;
ZXLog(@"weakObj-0", weakObj);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    __strong MyObject *strongObj = weakObj;
    ZXLog(@"weakObj - block", weakObj);
    ZXLog(@"strongObj - block", strongObj);
    
    sleep(3);
    
    ZXLog(@"weakObj - block", weakObj);
    ZXLog(@"strongObj - block", strongObj);
});
NSLog(@"------ sleep 1s");
sleep(1);
obj = nil;
ZXLog(@"weakObj-1", weakObj);
NSLog(@"------ sleep 5s");
sleep(5);
ZXLog(@"weakObj-2", weakObj);

打印结果:

变量内存地址:0x7fff5f891c78, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> obj
变量内存地址:0x7fff5f891c70, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> weakObj-0
------ sleep 1s
变量内存地址:0x60000025a2a0, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> weakObj - block
变量内存地址:0x700000722d78, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> strongObj - block
变量内存地址:0x7fff5f891c70, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> weakObj-1
------ sleep 5s
变量内存地址:0x60000025a2a0, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> weakObj - block
变量内存地址:0x700000722d78, 变量值:0x6000000133f0, 指向对象值:<MyObject: 0x6000000133f0>, --> strongObj - block
变量内存地址:0x7fff5f891c70, 变量值:0x0, 指向对象值:(null), --> weakObj-2

代码中使用 sleep 来保证代码执行的先后顺序。

从结果中我们可以看到,只要 block 部分执行了,即使我们中途释放了 obj,block 内部依然会继续强引用它。对比上面代码,也就是说 block 内部的 __strong 会在执行期间进行强引用操作,保证在 block 内部 strongObj 始终是可用的。这种写法非常巧妙,既避免了循环引用的问题,又可以在 block 内部持有该变量。

综合两部分代码,我们平时在使用时,常常先判断 strongObj 是否为空,然后再执行后续代码,如下方式:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    __strong MyObject *strongObj = weakObj;
    if(strongObj){
        // do something ...
    }
});

这种方式先判断 Obj 是否被释放,如果未释放在执行我们的代码的时候保证其可用性。

__block

直接上代码:

MyObject *obj = [[MyObject alloc]init];
obj.text = @"my-object-1";
ZXLog(@"obj",obj);

__block MyObject *blockObj = obj;
obj = nil;
ZXLog(@"blockObj -1",blockObj);

void(^testBlock)(void) = ^(){
    ZXLog(@"blockObj - block",blockObj);
    MyObject *obj2 = [[MyObject alloc]init];
    obj2.text = @"my-object-2";
    ZXLog(@"obj2",obj2);
    blockObj = obj2;
    ZXLog(@"blockObj - block",blockObj);
};
ZXLog(@"%@",testBlock);
ZXLog(@"blockObj -2",blockObj);
testBlock();
ZXLog(@"blockObj -3",blockObj);

打印结果:

变量内存地址:0x7fff5ddc1c78, 变量值:0x60000000ac60, 指向对象值:<MyObject: 0x60000000ac60>, --> obj
变量内存地址:0x7fff5ddc1c70, 变量值:0x60000000ac60, 指向对象值:<MyObject: 0x60000000ac60>, --> blockObj -1
变量内存地址:0x7fff5ddc1c30, 变量值:0x60400045ce00, 指向对象值:<__NSMallocBlock__: 0x60400045ce00>, --> %@
变量内存地址:0x6040004588f8, 变量值:0x60000000ac60, 指向对象值:<MyObject: 0x60000000ac60>, --> blockObj -2
变量内存地址:0x6040004588f8, 变量值:0x60000000ac60, 指向对象值:<MyObject: 0x60000000ac60>, --> blockObj - block
变量内存地址:0x7fff5ddc1ba8, 变量值:0x60000000ace0, 指向对象值:<MyObject: 0x60000000ace0>, --> obj2
变量内存地址:0x6040004588f8, 变量值:0x60000000ace0, 指向对象值:<MyObject: 0x60000000ace0>, --> blockObj - block
变量内存地址:0x6040004588f8, 变量值:0x60000000ace0, 指向对象值:<MyObject: 0x60000000ace0>, --> blockObj -3

可以看到在 block 声明前后 blockObj 的内存地址是有所变化的,这涉及到 block 对外部变量的内存管理问题,大家可以看扩展阅读中的几篇文章,对此有较深入的分析。

下面来看看 __block 能不能避免循环引用的问题:

MyObject *obj = [[MyObject alloc]init];
obj.text = @"11111111111111";
ZXLog(@"obj",obj);

__block MyObject *blockObj = obj;
obj = nil;
void(^testBlock)(void) = ^(){
    ZXLog(@"blockObj - block",blockObj);
};
obj = nil;
testBlock();
ZXLog(@"blockObj",blockObj);

打印结果:

变量内存地址:0x7fff57e48c78, 变量值:0x60000001b520, 指向对象值:<MyObject: 0x60000001b520>, --> obj
变量内存地址:0x604000457818, 变量值:0x60000001b520, 指向对象值:<MyObject: 0x60000001b520>, --> blockObj - block
变量内存地址:0x604000457818, 变量值:0x60000001b520, 指向对象值:<MyObject: 0x60000001b520>, --> blockObj

当外部 obj 指向 nil 的时候,obj 理应被释放,但实际上 blockObj 依然强引用着 obj,obj 其实并没有被真正释放。因此使用 __block 并不能避免循环引用的问题。

但是我们可以通过手动释放 blockObj 的方式来释放 obj,这就需要我们在 block 内部将要退出的时候手动释放掉 blockObj ,如下这种形式:

MyObject *obj = [[MyObject alloc]init];
obj.text = @"11111111111111";
ZXLog(@"obj",obj);

__block MyObject *blockObj = obj;
obj = nil;
void(^testBlock)(void) = ^(){
    ZXLog(@"blockObj - block",blockObj);
    blockObj = nil;
};
obj = nil;
testBlock();
ZXLog(@"blockObj",blockObj);

必须记住在 block 底部释放掉 block 变量,这其实跟 MRC 的形式有些类似了,不太适合 ARC这种形式既能保证在 block 内部能够访问到 obj,又可以避免循环引用的问题,但是这种方法也不是完美的,其存在下面几个问题

当在 block 外部修改了 blockObj 时,block 内部的值也会改变,反之在 block 内部修改 blockObj 在外部再使用时值也会改变。这就需要在写代码时注意这个特性可能会带来的一些隐患
__block 其实提升了变量的作用域,在 block 内外访问的都是同一个 blockObj 可能会造成一些隐患。

总结

__weak 本身是可以避免循环引用的问题的,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 __strong 的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

__block 本身无法避免循环引用的问题,但是我们可以通过在 block 内部手动把 blockObj 赋值为 nil 的方式来避免循环引用的问题。另外一点就是 __block 修饰的变量在 block 内外都是唯一的,要注意这个特性可能带来的隐患。

但是__block有一点:这只是限制在ARC环境下。在非arc下,__block是可以避免引用循环的。

上一篇下一篇

猜你喜欢

热点阅读