iOS知识点详解iOS Developer

Objective-C 内存管理基础

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

前言

之前的两篇拙文C语言-内存管理基础C语言-内存管理深入 介绍了关于C语言在内存管理方面的相关知识。但是对于从事iOS开发的同胞们来说,显然Objective-C用的更多,所以笔者想用两篇文章尽量完整的介绍一下Objective-C的内存管理,本文为第一部分,将从类和对象所有权策略及引用计数机制内存管理原则内存管理方式等几个方面展开。如果你能赏脸阅读此文,你会发现本文利用近一半的篇幅介绍ObjC对象的相关知识,这是因为Objective-C内存管理-管理的是继承自NSObject的对象的内存。阅读本文要求读者对C语言内存管理有一定的了解,尚不熟悉的同学请移步到这里:C语言-内存管理基础。话不多说,先附上本文内容的思维导图。

主要内容思维导图

类的结构与加载过程

Objective-C作为一门扩充C的面向对象编程语言,其和C语言的区别之一在于引入“面向对象”思想,能够灵活的使用类和对象进行编程。因此了解类的结构和本质对于学习Objective-C是非常重要的。

typedef struct objc_class *Class;

通过查看 objc/runtime.h文件,得知objc_class结构组成,其中包括了一个类很多信息。

struct objc_class {
  Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
  Class super_class                       OBJC2_UNAVAILABLE;  // 父类
  const char *name                        OBJC2_UNAVAILABLE;  // 类名
  long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
  long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
  long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
  struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
  struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
  struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
  struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;
ObjC对象结构图
Class classP = [Person class]; //classP现在一个`Class `类型的类对象
Person *objectP = [[Person alloc] init];
Class classP1 = [objectP class];

可以利用objectP类对象创建Person类的实例对象,并通过打印确定classPPerson类型。

Person *objectP1 = [[classP new] init];
NSLog(@"%@", NSStringFromClass(classP)); 

控制台输出:

//2017-02-05 15:49:57.916948 类和对象[4473:131075] Person

打印classPclassP 1内存地址发现数值相同,说明类在内存中只有一份。

NSLog(@"%p, %p",classP, classP1);
// 2017-02-05 15:28:53.340127 类和对象[4238:123830] 0x1000011e0, 0x1000011e0

【区别】:load方法类和分类都会调用,因为他们是分开加载的,分类的加载顺序和编译顺序有关;initialize方法是首次使用到该类时调用,调用顺序是先调用该类分类中initialize,再调用该类父类分类的initialize;如果没有会调用该类的initialize,再调用父类的initialize


对象的创建

Objective-C中创建对象,先调用类的alloc方法返回对象再调用对象的initinitWithSomething方法返回自己亦或是直接调用该类的new方法。例如下面代码:

  Person *p1 = [[Person alloc] init];
  Person *p2 = [Person new];

依次介绍下面三个方法:

1,将该新对象的引用计数 (Retain Count) 设置成 1。
2,将该新对象的 isa 成员变量指向它的类对象。
3,将该新对象的所有其它成员变量的值设置成零。(根据成员变量类型,零有可能是指 nil 或 Nil 或 0.0)

对象的所有权及引用计数

Person *p1 = nil;
 NSLog(@"%lu", (unsigned long)p1.retainCount); //p1.retainCount = 0

值得注意的是:retain操作无法使一个已经被释放的对象的引用计数器值+1,这也很好理解,retain无法让一个已经死去的人起死回生,就像环卫工人已经回收了你的矿泉水瓶,那么你将不能再持有该水瓶。

UIView *view = [[UIView alloc] init];
NSLog(@"创建之后的默认值是:%ld",(unsigned long)view.retainCount); //创建之后的默认值是:1
[view retain];
NSLog(@"通过一次retain操作之后:%ld",(unsigned long)view.retainCount); //通过一次retain操作之后:2
[view release];
NSLog(@"通过一次release操作之后:%ld",(unsigned long)view.retainCount); //通过一次release操作之后:1
上面的四种操作中除开`autorelease `方法外都比较好理解,关于`autorelease `可以点击[黑幕背后的Autorelease](http://blog.sunnyxx.com/2014/10/15/behind-autorelease/)。同时《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》书中有这样解释的:

在OC引用计数框架中,自动释放池是一项重要特性,调用release会立刻递减对象的引用计数(而且很可能令系统回收此对象)然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环“(even loop)时递减,不过也可能执行的更早。此特性很有用,尤其是在方法中返回对象时更应该用它。

该书中还举出了这样的例子:

- (NSString *)stringValue {
  NSString *str = [[NSString alloc] initWithFormat:@"i am this: %@",self];
  return str;
}

此时返回的str对象计数值比期望值要+1,因为方法内部调用了一次alloc操作而又没有与之对应的释放操作,计数器+1就意味着调用者要负责处理多出来的这一次保留操作。但是不能在- (NSString *)stringValue方法内部释放,否则还没等方法返回,系统就把该对象回收了,这时候应该使用autorelease,他能够延长对象的生命周期,保证方法返回后该对象一定有效并且在合适的时机得以释放。

【注意】:关于一个对象的retainCount大部分情况下是合理的,但是有些时候其数值是令人无法理解的。例如苹果官方文档对其做了下面的解释:

苹果官方对`retainCount`的解释
文档的大致内容是:此方法在调试内存管理问题上作用不大。 因为任意的框架对象可能引用该对象,同时自动释放池对对象有延迟释放作用,因此无法通过调用此方法获取到有用的信息。
@implementation Person
   - (void)dealloc
   {
    NSLog(@"对象被销毁了");
    [super dealloc];
   }
@end
 Person *p1 = [[Person alloc] init];
 NSLog(@"%lu",(unsigned long)p1.retainCount);
 [p1 release];

控制台输出信息

2017-01-28 18:11:29.431764 OC中对象内存管理[20948:160747] 1
2017-01-28 18:11:29.432173 OC中对象内存管理[20948:160747] 对象被销毁了

内存管理的范围

有了上面关于Objc对象知识的铺垫,我们可以很好的引入内存管理这个概念。

内存管理的方式

MRC和ARC的区别示意图

关于更多MAC和ARC机制的内容请参考以下博客:
王巍 (@onevcat):手把手教你ARC——iOS/Mac开发ARC入门和使用
HIT-Alibaba:Objective-C 中的内存分配
Apple:Transitioning to ARC Release Notes

从Mac OS X10.8开始,“垃圾收集器”(garbage collector)已经正式废弃了,以后Objective-C代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集。

由于关于Objective-C垃圾回收的资料本人收集的比较少,仅在《好学的Objective-C》一书中看到过相关介绍,而且该机制从未在iOS开发中使用到,所以笔者这里姑且认为Objective-C中是存在过三种内存管理的方式,只是没法介绍关于它的更多内容。有兴趣的同学可以查阅一下该书。

内存管理的原则

具体来说当使用allocnewcopy(生成一个接受对象的副本)创建对象时,其引用计数器值被置为1,当不需要使用该对象时需要做一次release操作;当使用retain操作持有一个对象时其引用计数器值+1,当不需要使用该对象时需要做一次release操作;一次创建对应着一次release,一次retain对应着一次release,这样对象才能“有始有终”。

内存管理不当导致的问题

Dog *yellowDog = [[Dog alloc] init];
[yellowDog release];
NSLog(@"%@", yellowDog);

上面的代码在yellowDog 的引用计数器值为0时已经将在堆区分配的 Dog 对象的内存释放掉了,再调用NSLog通过yellowDog指针访问堆区的对象就很可能会出问题。

Person *p1 = [Person new];
[p1 release];
[p1 release];

如上的代码,当第一次执行[p1 release]代码时,p1指向的对象的retainCount减为0,系统销毁该对象且其在堆区的存储空间会被回收,如果此时再次执行[p1 release]代码,尝试给已经被释放的Pweson对象发送release消息,很可能会触发运行时错误。具体来说程序运行成功但是控制台会输出这样的信息:

Person object 0x1002049e0 overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug

《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》一书是这样解释 “很可能”这个词的:

之所以说“很可能”没有说一定,是因为对象在堆区所占的内存在“解除分配”(deallocated)之后只是放回到“可用内存池”,如果此时执行NSLog时尚未覆盖对象内存,那么该对象依旧有效,这时程序不会奔溃。由此可见,因过早释放对象而导致的bug很难调试。

当我们开启Xcode的僵尸对象检测功能时,程序运行崩溃,在控制台输出如下信息:

*** -[Person release]: message sent to deallocated instance 0x100200640
//release 消息发送给了一个已经释放掉的对象

上面的情况在日常开发中常常体现为Thread:EXC_BAD_ACCESS(坏访问错误)
【坏访问错误】:即访问了一块坏内存(不可用的已经被回收的内存)。这样的错误我们也称为“野指针错误”,意思是利用野指针操作了一块不可用的内存。
【僵尸对象】:所占用的内存空间已经被回收不可用的对象。僵尸对象不应该再使用。
【解决方案】:为避免在不经意间使用了无效对象,一般调完realease之后都会清空指针。这样就能保证不会可能出现指向无效对象的指针。即在对象释放完毕后将指向对象的指针置为nil,给nil发消息不会产生任何反应。
综上所述:没有置为nil却指向了无效对象的指针就是野指针

所以正确的做法应该是:
Dog * yellowDog = [[Dog alloc] init];
[yellowDog release];
yellowDog = nil //对象释放后,将指向对象的指针置为nil
{   //代码块内创建Dog对象
      Dog *husky = [[Dog alloc] init];
}

形如上面的代码,当我们在创建Dog对象时系统分别在栈区为husky变量分配内存、在堆区为husky所指向的Dog对象分配内存,由于变量husky的作用域在大括号内属于局部变量,当代码块执行完毕,栈中的husky变量就被释放了,但是此时在代码块中并没有对husky变量指向的堆区的对象存储空间进行释放,所以我们说堆区中对应的存储空间就被泄露了。

总结为:基本的数据类型由于占用的存储空间是固定的,一般存在栈区中。由于栈中主要存放的是局部变量,局部变量占用的内存空间是其所在的代码块或者是函数结束的时候自动回收,指向对象的指针也会被回收,这个过程不需要程序员管理。但是对象创建完成后是存放在堆区的,由于此时指向对象的指针已经被回收,但是对象仍然存在内存中,就会造成内存泄漏(申请的空间已经不再使用却没有被及时合理的释放掉)。

文章最后

以上就是笔者对于Objective-C内存管理基础认识的全部内容,部分描述引自书籍和其它博客文中已经做出了说明,但凡是本人认为重要的概念均附上其它博客的链接用于扩展相关知识。

另外:本文涉及到MRC的代码需要在Xcode进行如下设置才能编译通过:在Build Settings中,找到"Objective-C Automatic Reference Counting"这个选项,将它的值改为"NO"。

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

下一篇:Objective-C 内存管理深入

上一篇下一篇

猜你喜欢

热点阅读