iOS开发经验与总结iOS内存管理、多线程@IT·互联网

Objective-C 内存管理深入

2017-02-09  本文已影响530人  老板娘来盘一血

前言

基础篇介绍了一些关于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方法用于读取值。 这样的写法看起来像是一个世纪之前的事情了,但我们能够从中探究到很多关于内存管理方面的问题。例如:

    - (void)setNumber:(NSUInteger)number
{
        _number = number;
}
@class Book;
@interface Person : NSObject
{
    Book *_book;
}
  - (void)setBook:(Book *)book;
  - (Book *)book;

上面的Person类存在一个继承自NSObject类型的成员变量,当实现其setter方法时就需要做到以下几点了。

@implementation Person
    - (void)setBook:(Book *)book
{   
       _book = [book retain]; //持有该对象对book做一次retain操作
}
@end
@implementation Person
    - (void)setBook:(Book *)book
{   
       _book = [book retain]; //持有该对象对book做一次retain操作
}
    - (void)dealloc
{
       [_book release]; //Person对象销毁时不再拥有_book,需要对其做一次release操作
       [super dealloc]; 
}
@end
  Book *b1 = [Book new];
  [p setBook:b1];
  Book *b2 = [Book new];
  [p setBook:b2];; //重新为_book赋不同的值
  [b2 release];
  [b1 release];

由于setter方法对传入的b1b2都做了一次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引入的新特性,其通过将成员变量包装达到封装对象中数据的作用,并且提供了“点语法”使开发者更简单的依照类对象访问其中的数据。具体来说使用属性构建类主要有以下好处:

@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

@implementation Person
@synthesize book = _myBook;
 //自定义的_myBook名称用来替换之前的属性名称,
//但是通过点语法调用self.book是不受影响的。
    - (void)dealloc
{
      [_myBook release];
      // [self.book release]; 这行代码和上面那行代码是一样的。
      NSLog(@"Person - dealloc");
      [super dealloc];
}
@end
@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)概念。

typedef struct objc_property *objc_property_t;

objc_property是一个结构体,包括nameattributes,其结构如下:

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主要描述特性有:原子性和非原子性、读写权限、存取方法名、内存管理语义等。

readwrite:可读可写的(编译器默认该特性)。该特性表明编译器会为其生成对应的gettersetter方法。同时你可以设置和读取该值。

readonly:只读的。该特性修饰的属性你将不能直接修改其值,例如使用点语法赋值时报错,提示不能为readonly 的属性赋值(但是可以通过KVC机制为该属性赋值)。

@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:https://img.haomeiwen.com/i2474121/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:https://img.haomeiwen.com/i2474121/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:https://img.haomeiwen.com/i2474121/72ca8eb876123438.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一个`null_resettable `经典的使用示例就是苹果定义`UIViewController`的`view`属性:

@property(null_resettable, nonatomic,strong) UIView *view;

![null_resettable的使用](http:https://img.haomeiwen.com/i2474121/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;

@implementation Person


---
##属性和成员变量的选择使用
在上面的`- (instancetype)initWithName: age:`初始化方法中,初学者可能会有疑问为什么使用的`_name`成员变量而不是`self.name`属性呢?这里本人通过查阅《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》对属性和成员变量的使用场景做了一些总结:

 * 初始化方法中设置属性值时使用成员变量,因为子类可能会覆盖该属性的`setter`方法。例如:

 * 懒加载方法中使用成员变量初始化,并通过属性访问。例如:

self.book //懒加载的属性必须通过“getter”方法访问,否则成员变量永远不会被初始化
因为若没有使用getter方法直接访问成员变量(_book),该成员变量(_book)此时尚未设置完成。

 * 在`delloc`方法中使用成员变量

* 在对象内部尽量直接访问成员变量

关于属性和成员变量使用的更多细节请阅读《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》第6、7条。

##文章最后
以上就是笔者对于Objective-C内存管理深入知识的学习总结,部分描述引自《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》这里做出说明。

如果文中有任何纰漏或错误欢迎在评论区留言指出,本人将在第一时间修改过来;喜欢我的文章,可以关注我以此促进交流学习; 如果觉得此文戳中了你的G点请随手点赞;转载请注明出处,谢谢支持。
上一篇 下一篇

猜你喜欢

热点阅读