深入理解 Objective-C 属性内存管理语义
一文彻底搞懂 assign、weak、copy、strong 这些内存管理语义的用法与原理
举个例子:
//
// Dog.h
// oc-demo
//
// Created by 姚明振 on 2022/8/17.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Dog : NSObject
@property(nonatomic, assign, nullable) NSObject *assignProperty;
@property(nonatomic, weak, nullable) NSObject *weakProperty;
@property(nonatomic, copy, nullable) NSString *propertyCopy;
@property(nonatomic, strong, nullable) NSObject *strongProperty;
@end
NS_ASSUME_NONNULL_END
assign:
想要它不 crash 其实是有办法的,我们在它所指向对象被销毁时手动帮它为属性赋 nil:
当然这没有什么实际意义,不能实现我们预期的目的。
原因是 alloc、init 方法返回的是一个 autorelease 的 NSObject 实例,当走完 init 后如果没有人对他进行 retain 操作,则立马会被销毁。
让临时变量持有该 NSObject 实例,即在作用域内被强引用着,所以 assign 修饰的属性在这种情况下也能正常访问指向的对象。例子只为让大家易于理解,千万不要在项目里这么用。
总结:
- 只进行简单的赋值操作(把一个指针赋值给该属性,如果是对象则是对象地址,如果是基本数据类型则是该值的地址)
- 只适用于基本数据类型(非 Objective-C 对象「语法上是支持修饰 Objective-C 对象的」,如:int,float,NSInterge,CGFloat,及结构体类型)
- 不影响该属性所指向对象的生命周期(不改变retaincount)
- 在所指向对象销毁时不会自动为该属性赋 nil 值
weak:
参考 assign,做个对比
我们发现跟 assign 不同的是,这里没有发生 crash,原因是 weak 修饰的属性在所指向对象销毁时自动为该属性赋 nil 处理。
使用临时变量强引用 NSObject,此时能正常访问该属性指向的对象。
总结:
- 只适用于 Objective-C 对象(语法上就不支持修饰基本数据类型)
- 不影响该属性所指向对象的生命周期(不改变retaincount)
- 在所指向对象销毁时会自动为该属性赋 nil 值(防止野指针访问导致 crash)
strong:
可以看到属性值正常输出字符串 "123",说明对字符串进行了强引用。
不过当我们赋值 NSMutableString 实例时,发现这违背了该属性的设计意图。明明是不可变的字符串,刚赋值为123,讲道理在不走 setter 方法时它的值是不允许改变的,可这种事真的发生了,它被修改为 123456。于是引入了 copy 来解决这个问题(下面介绍)。
总结:
-
只适用于 Objective-C 对象(语法上就不支持修饰基本数据类型)
-
会强引用指向的对象(retaincount + 1)
-
会释放之前指向的对象(retaincount - 1)
-
当修饰 block 类型属性时(ARC 下表现与 copy 一致)
- 当指向的对象为 StackBlock 时会把 block 对象复制到堆区成为 MallocBlock
- 当指向的对象为 MallocBlock 时会增加引用计数(retaincount + 1)
- 当指向的对象为 GlobalBlock 时什么都不会发生
copy:
当用 copy 修饰一个 NSObject 类型的属性,运行时发生了 crash,并抛出 NSObject 未实现 copyWithZone: 方法的异常。这是因为 copy 修饰的属性会在 setter 方法内对老值进行 release,对新值进行 copy 操作,也就是它真正指向的是传入对象 copy 后的返回值(可能是深 copy 也可能是浅 copy )。这一操作避免了给不可变类型(如:NSString)属性赋值对应可变子类(如:NSMutableString)导致的逻辑异常。把属性类型改为 NSString(它实现了copyWithZone: 方法)。
这里打印了字符串 “123”,说明了 copy 修饰的属性会对它指向的值进行强引用。
另一方面当为该属性赋值后,继续修改原值时该属性的值不受影响,因为此时已经把 NSMutableString copy 为了 NSString。
总结:
-
只适用于 Objective-C 对象(语法上就不支持修饰基本数据类型)
-
会强引用指向的对象(retaincount + 1)
-
会释放之前指向的对象(retaincount - 1)
-
当修饰 NSString,NSArray 等存在可变类型的子类时(这是 copy 与 strong 的最大区别)
- 使用 copy 修饰,把可变类型深 copy 为不可变类型,避免不安全修改
- 不安全的原因是:可变类型的对象可以在不改变对象地址的情况下修改值,因为你的属性是 NSString 类型,此时值被意外修改是不符合预期的。
-
当修饰 block 类型属性时
- 当指向的对象为 StackBlock 时会把block对象复制到堆区成为 MallocBlock
- 当指向的对象为 MallocBlock 时会增加引用计数(retaincount + 1)
- 当指向的对象为 GlobalBlock 时什么都不会发生
总结
今天开始复习,决定把自己的理解以文章的形式输出,一方面能加深理解做个笔记,另一方面也希望能给大家带来点帮助。笔者水平有限,难免有理解偏差与遗漏,希望大家批评指正。