前言
基础篇介绍了一些关于Objective-C内存管理的常见概念。本文将在前文的基础上扩展以下知识:成员变量set方法内存分析、属性、属性特质内存分析等内容,内容浅显易懂、属于学习总结。如果有需要浏览上一篇文章的同学请点击Objective-C 内存管理基础。希望本文能给正在学习Objective-C的小伙伴们更多启发。
成员变量的set方法
在属性(property) 这个语法提出之前,iOS开发者常常这样构建一个类:
@interface Person : NSObject
{
NSUInteger _number;
}
- (void)setNumber:(NSUInteger)number;
- (NSUInteger)number;
如上的代码为Person
类添加了一个成员变量_number
(添加下划线是为了命名规范)。同时手动申明和实现该成员变量的存取方法:setter
方法用于写入值,getter
方法用于读取值。 这样的写法看起来像是一个世纪之前的事情了,但我们能够从中探究到很多关于内存管理方面的问题。例如:
-
基本数据类型直接赋值
由于_number
是NSUInteger
类型,所以其不再内存管理的范围,对于基本类型的setter
方法只需要直接赋值就好了。
- (void)setNumber:(NSUInteger)number
{
_number = number;
}
- ObjC 对象类型需要进行内存管理
@class Book;
@interface Person : NSObject
{
Book *_book;
}
- (void)setBook:(Book *)book;
- (Book *)book;
上面的Person
类存在一个继承自NSObject
类型的成员变量,当实现其setter
方法时就需要做到以下几点了。
-
retain 传入的变量
根据内存管理原则:“需要持有对象时就对其做一次retain
操作”。所以应该对传入的book
做一次retain
操作。初步代码如下:
@implementation Person
- (void)setBook:(Book *)book
{
_book = [book retain]; //持有该对象对book做一次retain操作
}
@end
-
对象销毁时 release 其成员变量
Person
对象销毁时代表_book
没有人使用了,应该在Person
的delloc
方法里做一次release
操作。
@implementation Person
- (void)setBook:(Book *)book
{
_book = [book retain]; //持有该对象对book做一次retain操作
}
- (void)dealloc
{
[_book release]; //Person对象销毁时不再拥有_book,需要对其做一次release操作
[super dealloc];
}
@end
- ** release 旧的成员变量**
上面的代码非常不严谨,因为当出现下面的情况时,旧的成员变量无法被释放。
Book *b1 = [Book new];
[p setBook:b1];
Book *b2 = [Book new];
[p setBook:b2];; //重新为_book赋不同的值
[b2 release];
[b1 release];
由于setter
方法对传入的b1
、b2
都做了一次retain
,在Person
对象的delloc
方法里却只对当前的_book
(也就是b2
) release
,导致旧的成员变量b1
无法被释放,由此产生了内存泄露问题。对setter
方法优化后的代码应该如下:
- (void)setBook:(Book *)book
{
[_book release]; //对原来使用的成员变量做一次release操作
_book = [book retain]; //再持有新传入的变量
}
-
判断传入的变量
即便是上面的代码仍然还有问题,比如:
Person *p = [Person new];
Book *b1 = [Book new];
[p setBook:b1];
[b1 release];
[p setBook:b1];; //对_book 重新赋相同的值
[p release];
[b1 release]
代码执行完毕,b1
的引用计数器值为 1 ,接着执行[p setBook:b1]
为p.book
重新赋相同的值时,会进行下面的操作:
[_book release];
_book = [book retain];
【注意】:此时的_book
是第一次设置的变量b1
,其引用计数器值为1;再进行release
,变量b1
的引用计数变为0,系统回收该对象内存;接着执行
_book = [book retain]
将发生错误,因为此时的b1
已经是僵尸对象。 retain
无法将一个僵尸对象起死回生(需要开启僵尸对象检测功能)。
【解决方案】:应该对传入的变量(book
)进行判断,如果传入的变量(book
)和当前成员变量(_book
)是同一个变量,那么setter
方法不需要做任何操作。
最后的一个完整的setter
方法应该如下:
- (void)setBook:(Book *)book
{
if (book != _book) { //两者进行判断
[_book release]; //对原来的成员变量进行release
_book = [book retain]; //对传入的变量进行retain
}
}
属性
属性(property)是Objective-C 2.0引入的新特性,其通过将成员变量包装达到封装对象中数据的作用,并且提供了“点语法”使开发者更简单的依照类对象访问其中的数据。具体来说使用属性构建类主要有以下好处:
-
自动合成存取方法
使用属性封装数据可以让编译器自动申明和实现与属性相关的存取方法,此过程有一个专业名称--“自动合成”(synthesize)。而且该过程是在编译执行的期间自动完成的,因此无法看到 “合成方法(synthesized method)”的源码。
@class Book;
@interface Person : NSObject
@property Book *book;
@end
@class Book;
@interface Person : NSObject
{
Book *_book;
}
- (void)setBook:(Book *)book;
- (Book *)book;
上面两段代码实际上是等效的。
-
自动生成对应的成员变量
除了自动生成方法代码外,编译器还会自动向类中添加属性对应的成员变量,并且在属性名前面加上下划线,以此作为成员变量的名称并和属性名区分开来。
- (void)dealloc
{
[_book release];
NSLog(@"Person - dealloc");
[super dealloc];
}
在上面使用属性语法的Person
类中,重写dealloc
能调用[_book release]
说明的确是生成了属性对应的成员变量_book
。
-
支持自定义成员变量名
如果开发中对编译器自动生成的成员变量名不满意,使用@synthesize
语法在类的实现文件里可以自定义其名称。
注意:该语法是将已经存在的属性名替换成左边自定义的名称,以后属性对应的系统生成的带有下划线的成员变量名就被替换成了自定义的名字。同时其存取方法中使用到的成员变量名也将被替换。但是通过点语法调用self.property
是不受影响的。
@implementation Person
@synthesize book = _myBook;
//自定义的_myBook名称用来替换之前的属性名称,
//但是通过点语法调用self.book是不受影响的。
- (void)dealloc
{
[_myBook release];
// [self.book release]; 这行代码和上面那行代码是一样的。
NSLog(@"Person - dealloc");
[super dealloc];
}
@end
-
支持自定义成员变量存取方法
1,如果通过属性语法自动生成的getter
、setter
方法不能满足开发要求,我们可以重写属性对应的getter
、setter
方法,达到自定义存取方法的目的。
2,使用dynamic关键字,该语法告知编译器不要为某些属性创建对应的成员变量和默认实现的存取方法(这样做的后果是属性对应的成员变量和其存取方法都需要自己定义和实现)。
@implementation Person
@dynamic book;
- (void)dealloc
{
[_book release]; //此行代码报错,具体原因为_book成员变量没有申明
NSLog(@"Person - dealloc");
[super dealloc];
}
@end
通过上面的解释我们得出这样的结论:
属性 = 成员变量 + 存取方法 (@property = ivar + getter + setter)
属性语法极大的简化了开发人员在构建类时封装数据的工作量(编译器默认实现)。另外需要注意的是 synthesize、**dynamic **关键字用的较少,因为大部分时候编译器默认实现的代码还是比较符合开发需求的。
属性特质内存分析
在上面讲解属性特性时申明的属性,仍然是不符合要求的。
@class Book;
@interface Person : NSObject
@property Book *book;
@end
这样的定义的属性,其setter
方法仅仅类似于普通的基本数据类型直接赋值,验证代码如下:
Person *p = [Person new];
Book *b = [Book new];
p.book = b;
NSLog(@"%lu",b.retainCount); //打印b的引用计数器值为 1
[b release];
[p release];
通过打印b
的引用计数器值(b.retainCount
= 1),我们发现代码p.book = b
执行后,b
的引用计数并没有增加;如果此时进行的是“处理过的setter
方法”,b
必定会被retain
一次,其引用计数值应该为2,所以得出结论:单纯的@property Book *book
定义属性,其setter
方法只是进行了简单的赋值运算,并不符合Objc对象需要进行内存管理的原则。因此在MRC中我们常常这样改进:
@class Book;
@interface Person : NSObject
@property (retain) Book *book;
@end
通过使用retain
编译器会将该属性对应的setter
方法替换成上面提到的 “完整的setter
方法”,进而解决内存管理问题。 在ARC中对于属性后面的修饰词处理的更加严谨和丰富,具体来说引入了 属性特质(attributes)概念。
-
属性特质(attributes)
属性特质(attributes)也可被称为属性修饰词、属性特性。通过分析源码可以一探究竟。
property
在runtime
中是objc_property_t
,其结构如下:
typedef struct objc_property *objc_property_t;
而objc_property
是一个结构体,包括name
和attributes
,其结构如下:
struct property_t {
const char *name;
const char *attributes;
};
而attributes
本质是objc_property_attribute_t
,定义了property
的一些特性,其结构如下如下:
typedef struct {
const char *name; /**< The name of the attribute */
const char *value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
attributes
主要描述特性有:原子性和非原子性、读写权限、存取方法名、内存管理语义等。
修饰词对内存的影响(ARC)
-
原子性
atomic:原子性的(编译器默认该特性)。该特性会为属性的setter
方法加锁,以保证其操作是线程安全的,同时会影响性能。nonatomic:非原子性的。该特性不会为属性的
setter
方法加锁,非线程安全的,但是能极大的提升性能。由于在iOS开发中移动设备性能有限,所以绝大多数情况下使用的该特性。iOS中使用同步锁的开销加大,这会带来性能问题。一般情况下并不要求属性必须是“原子性”的,因为这并不能保证线程安全。若要实现“线程安全”的操作,还需采用更深层的锁定机制才行。开发iOS程序是一般都会使用
nonatomic
特性,因为atomic
会严重影响性能,但是在开发Mac OS X 程序时,使用atomic
不会有性能瓶颈。 读写权限
readwrite:可读可写的(编译器默认该特性)。该特性表明编译器会为其生成对应的getter
和setter
方法。同时你可以设置和读取该值。
readonly:只读的。该特性修饰的属性你将不能直接修改其值,例如使用点语法赋值时报错,提示不能为readonly
的属性赋值(但是可以通过KVC机制为该属性赋值)。
-
内存管理语义
assign:直接赋值。用于Objective-C中基本数据类型的变量作为属性,该特性修饰的属性其
setter
方法只会针对“纯量类型”进行简单的赋值操作。例如枚举类型、布尔类型、整型、浮点型等基本数据类型的变量(基本数据类型变量无需内存管理)。weak:非持有关系 。用于Objective-C中对象作为属性。同
assign
类似该特性修饰的属性其setter
方法既不保留新值,也不释放旧值;然而在属性所指的对象销毁时,属性值会被清空。ARC下,在有可能出现循环引用情况中往往要通过让其中一端使用
weak
来解决,比如:两个类相互为对方的属性、申明delegate
代理属性等。另外自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak
,比如:添加子控件是常常用到[self.view addSubView: self.imageView]
;self.imageView
这个属性就可以使用weak
定义。xib
中拖线时IBOutlet
控件属性一般也使用weak
(当然,也可以使用strong
)。如果你对 “属性所指的对象销毁时,属性值会被清空”不是很理解,可以参考下面的例子:
@interface ViewController ()
@property (nonatomic, weak) UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *redView = [[UIView alloc] init]; //创建子视图
redView.backgroundColor = [UIColor redColor]; //设置子视图背景色
[self.view addSubview:redView]; //添加到视图上
self.redView = redView; //属性赋值
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%@",self.redView);
self.redView.frame = CGRectMake(50, 50, 100, 100); //点击屏幕设置尺寸
}
@end
【分析】示例代码将`redView`添加的控制器的`View`上,当用户点击屏幕的时候设置其尺寸显示出来。对于一般继承自`NSObject`类型的变量作为属性时通常使用`strong`修饰。这里的`redView`明显是对象类型,其使用`weak`修饰是因为`[self.view addSubview:redView]`该行代码将`redView `作为一个元素加入到`self.view.subviews`数组中,`self.view`本身隐式的对`redView `做了一次持有操作。
(1)当将`[self.view addSubview:redView]`代码注释。
控制台输出:`self.redView = (null)`
【分析】`self.view`未对`self.redView`进行持有操作(`weak`修饰只是简单的赋值,未对传入的变量进行`retain`),当`viewDidLoad `方法调完,系统为`UIView`对象在堆区分配出来的空间(`[[UIView alloc] init]`)就被回收了,所以此时`self.redView`指向的对象已经被销毁,同时由于其是`weak`修饰,编译器将其指向`nil`,所以打印`self.redView`时其值为`null`。
(2)如果此时将`redView`声明为`assign`修饰,即:
@property (nonatomic, assign) UIView *redView;
点击屏幕时就会崩溃。
![坏内存访问错误](http://upload-images.jianshu.io/upload_images/2474121-15ca43eb54eabcc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
【分析】在属性所指的对象销毁时,`assign`不会将属性指向`nil`,所以此时`self.redView`已经是一个野指针其指向了一个僵尸对象,打印`self.redView`时访问了一块已经被回收的内存。所以崩溃。
希望能通过这个简单的例子解释一下`weak`和`assign`的区别。
**unsafe_unretained:**非持有关系(同`assign`)。用于Objective-C中对象或者是基本数据类型的变量作为属性。同`weak`一样该特性修饰的属性其`setter`方法既不保留新值(`unretained`),也不释放旧值。不同的是如果属性所指的对象被销毁,指向该对象的指针依然指向原来的内存地址,如果此时继续访问该对象很容易产生坏内存访问/野指针/僵尸对象访问(`unsafe`)。
**strong:**持有关系。用于Objective-C中对象作为属性。该特性修饰的属性的`setter`方法会对传入变量做一次`retain`并对之前的成员变量做一次`release`。其作用和MRC下的`retain`操作是一样的。
**copy:**拷贝关系。`copy` 的作用与`strong`类似,然而其修饰的属性的`setter`方法并非`retain`新值,而是将其“拷贝” (`copy`)一份。因为父类指针可以指向子类对象(多态性),使用`copy`的目的是为了让本对象的属性不受外界影响,无论给对象传入是一个可变对象还是不可对象,对象本身持有的就是一个不可变的副本。
>定义`NSString`、`NSArray`、`NSDictionary`等类型的变量作为属性时常使用`copy`关键字, 因为其有对应的可变类型:
`NSMutableString`、`NSMutableArray`、`NSMutableDictionary`(可变类型用`strong`)。
开发中常用的`block`代码块习惯使用`copy`关键字修饰(`strong`也是可以的)。
* **存取方法名**
**getter = <name>:**通常在为类定义布尔类型的属性时用于自定义其`getter`方法名。例如:
/// 是否正在工作
@property (nonatomic, assign, getter=isWorking) BOOL working;
/// 是否有订单有显示中
@property (nonatomic, assign, getter=isShowing) BOOL showing;
**setter = <name>:**该修饰词极少使用,除非特殊情况下,例如:
>在数据反序列化、转模型的过程中,服务器返回的字段如果以`init`开头,所以你需要定义一个`init`开头的属性,但默认生成的`setter`与`getter`方法也会以`init`开头,而编译器会把所有以`init`开头的方法当成初始化方法,而初始化方法只能返回`self`类型,因此编译器会报错。这时你就可以使用下面的方式来避免编译器报错:
@property(nonatomic, copy, getter=p_initBy, setter=setP_initBy:) NSString *initBy;
* **空值约束(iOS9推出的新特性)**
**nullable:**该属性值可以为空。用于Objective-C中对象作为属性,表示该属性的`getter`和`setter`方法中赋值和取值都是可以为空。
尽管在定义属性时写或不写`nullable `对该属性的赋值和操作没有任何影响,但写上`nullable `更多的作用在于程序员之间的沟通交流,时刻提醒开发人员该属性不一定是有值的,可能需要空值判断,要注意使用。
**nonnull:**该属性值不能为空。和`nullable `对立,该特质修饰的属性其`getter`和`setter`均不能为空值,否则会有警告。
另外下面的写法是等价的。
@property (nonnull,nonatomic, copy) NSArray *array;
@property (nonatomic, copy) NSArray * __nonnull array1;
`nullable`、`nonnull`除了在定义属性时使用,还可以用在函数和方法中对参数和返回值的空值进行约束。例如:
//函数
void text(NSArray * __nonnull array) {
}
//方法
- (NSString *__nonnull)creat:(NSArray * __nonnull)array {
return @"coderYQ";
}
![nullable和nonnull的使用](http://upload-images.jianshu.io/upload_images/2474121-8cb863891576d6cc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
当然如果你嫌麻烦,还可以使用宏一次性声明:
NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nonatomic, copy) NSArray *array;
@property (nonatomic, copy) NSArray *array1;
@property (nonatomic, copy) NSArray *array2;
@end
NS_ASSUME_NONNULL_END
如此两个宏`NS_ASSUME_NONNULL_BEGIN `、`NS_ASSUME_NONNULL_END `之间定义的属性均有`nonnull `提示,当然你也可以修改其中的约束,例如:
NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (nullable, nonatomic, copy) NSArray *array;
@property (null_resettable, nonatomic, copy) NSArray *array1;
@property (nonatomic, copy) NSArray *array2;
@end
NS_ASSUME_NONNULL_END
**null_resettable:**该特性修饰的属性表示其`setter`中可以赋值为空,但是`getter`方法中返回的值不会为空。使用该特性定义属性时编译器会提出警告:
应该重写`setter`方法处理赋值为空的情况。
![null_resettable的使用](http://upload-images.jianshu.io/upload_images/2474121-72ca8eb876123438.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一个`null_resettable `经典的使用示例就是苹果定义`UIViewController`的`view`属性:
@property(null_resettable, nonatomic,strong) UIView *view;
![null_resettable的使用](http://upload-images.jianshu.io/upload_images/2474121-c61f1fc4a4261aab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
大家都知道控制器的`view`是懒加载的,每次调用`getter`方法一旦发现`view`属性为空,系统会调用`loadView`方法创建并返回`view`。该关键字告诉开发人员`self.view`是永远不会为空的 ,你可以放心的使用。
由于在构建类时为其定义的各种属性有不同的类型,所以可以通过属性修饰词对存取方法进行微调进而满足内存管理规范。
>但是开发中有时候会重写`getter`或`setter`方法。这时我们应该保证实现的方法是具备相关属性的特质。例如将某个属性申明为`copy`,那么重写`setter`方法时应该拷贝传入的对象,否则误导该属性的使用者,严重时会产生bug。同理在其他方法中设置属性值时,也需要遵循属性修饰词的规定。
例如:在下面的自定义初始化方法中`name`应该使用`copy`,而`age`则直接赋值。
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (instancetype)initWithName:(NSString *)name
age:(int)age;
@end
@implementation Person
- (instancetype)initWithName:(NSString *)name age:(int)age
{
if (self = [super init]) {
_name = [name copy];
_age = age;
}
return self;
}
@end
---
##属性和成员变量的选择使用
在上面的`- (instancetype)initWithName: age:`初始化方法中,初学者可能会有疑问为什么使用的`_name`成员变量而不是`self.name`属性呢?这里本人通过查阅《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》对属性和成员变量的使用场景做了一些总结:
* 初始化方法中设置属性值时使用成员变量,因为子类可能会覆盖该属性的`setter`方法。例如:
- (instancetype)initWithName:(NSString *)name age:(int)age
{
if (self = [super init]) {
_name = [name copy];
_age = age;
}
return self;
}
* 懒加载方法中使用成员变量初始化,并通过属性访问。例如:
- (Book *)book
{
if (!_book) {
_book = [[Book alloc] init];
}
return _book;
}
self.book //懒加载的属性必须通过“getter”方法访问,否则成员变量永远不会被初始化
因为若没有使用getter
方法直接访问成员变量(_book),该成员变量(_book)此时尚未设置完成。
* 在`delloc`方法中使用成员变量
- (void)dealloc
{
[_book release];
[super dealloc];
}
* 在对象内部尽量直接访问成员变量
关于属性和成员变量使用的更多细节请阅读《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》第6、7条。
##文章最后
以上就是笔者对于Objective-C内存管理深入知识的学习总结,部分描述引自《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》这里做出说明。
如果文中有任何纰漏或错误欢迎在评论区留言指出,本人将在第一时间修改过来;喜欢我的文章,可以关注我以此促进交流学习; 如果觉得此文戳中了你的G点请随手点赞;转载请注明出处,谢谢支持。