Objective-C-(二)内存管理

由于Objective-C是基于C语言的,在了解Objective-C内存管理前应该先了解下C语言的内存模型。

简单回顾下C程序的占用空间的几个区域:

  • 程序代码区:存放程序执行代码的区域
  • 静态数据区:也称全局数据区,存放程序中的全局变量。例如:全局变量,静态变量,一般常量,字符串常量。静态数据区的内存是由程序终止时由系统自动释放。其中静态数据区具体又分为两块区域:
    • BSS段(Block Started by Symbol):未初始化的全局变量
    • 数据段(data segment):已初始化的全局变量
  • 堆区:由程序员手动管理分配和释放。通过malloc()、calloc、free()等函数操作的就是堆区的内存。
  • 栈区:函数的参数,局部变量等存放在栈区。栈区的内存由系统自动分配和释放。

在Objective-C中创建的对象都分配在堆区,内存管理针对的也是这块区域。

Objective-C内存管理的核心其实引用计数。系统通过对一个对象引用计数的计算来确认是否要释放对象回收内存。Objective-C有两种内存管理机制:手动管理(MRC)和自动管理(ARC)。ARC的原理其实跟MRC是一致的,只是系统自动帮我们在合适的地方键入了内存管理的方法,避免了手动管理带来了麻烦和失误。目前基本上开发用的都是ARC。最开始学习iOS的时候也用过MRC,先介绍下MRC的机制。

MRC

操作对象的四种方式:

  • 生成并持有对象:alloc/new/copy/mutableCopy等, retainCount :+1
  • 持有对象:retain,retainCount :+1
  • 释放对象:release,retainCount :-1
  • 废弃对象:dealloc, 自动释放内存

内存管理的四个法则:

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 不再需要自己持有对象的时候释放对象
  • 非自己持有的对象无法释放

示例代码:

自己生成的对象,自己持有:

/**
*  以 alloc/new/copy/mutableCopy 等方法创建的对象归调用者持有 
*/
id obj = [[NSObject alloc] init]; //创建一个NSObject对象返回给变量obj, 并且归调用者持有

非自己生成的对象,自己也能持有:

/**
*  alloc/new/copy/mutableCopy 等方法以外的方式创建的对象不归调用者持有 
*/
id obj = [NSMutableArray array]; // 非自己生成的对象,该对象存在,但不归调用者持有
[obj retain]; // 如果想持有该对象,需要执行retain方法

非自己生成的对象,且该对象存在是通过autorelease来实现的。autorelease提供了一种使得对象在超出生命周期后能正确的被释放(通过调用release方法)机制,以便于将对象返回给调用者,让调用者持有后再释放对象。否则对象还没来得及被调用者持有就被系统释放了。调用autorelease后对象不会立刻被释放,而是被注册到autoreleasepool中,然后当autoreleasepool结束被销毁的时候,才会调用对象的release方法释放对象。

不再需要自己持有对象的时候释放对象:

id obj = [[NSObject alloc] init];
[obj release]; // 释放自己生成并持有的对象

非自己持有的对象无法释放:

id obj = [NSMutableArray array]; 
[obj release];  //由于当前的调用者并不持有改对象,不能进行释放操作,否则导致程序崩溃。如果要释放该对象,需要先对对象进行retain操作。
/**
以上方法在Xcode9中经测试发现如果返回给obj的是NSMutableArray对象,会导致程序崩溃,但是如果是NSArray就不会。
*/

MRC下要注意属性的引用计数情况。虽然retainCount在获取引用计数的时候有时候不准确,但是也可以用来调试参考。例如我们给一个属性赋值如下:

@interface MemoryRefenceVC ()
@property (nonatomic, copy) NSArray *array;
@end

@implementation MemoryRefenceVC

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.array = [[NSArray alloc] initWithObjects:@1, nil];
    NSLog(@"array.retainCount = %ld", _array.retainCount);
}
@end

打印如下:

2018-12-12 17:25:57.607777+0800 XXX[9889:341414] array.retainCount = 2

我们创建了一个对象并且返回给调用者持有,为什么此时对象的引用计数是2呢?

因为在属性的赋值setter方法中,会对当前的对象多进行一次引用。

- (void)setArray:(NSArray *)array {
    [array retain];  //进行了一次retain操作
    _array = array;
}

所以此时对象的内存引用情况是:alloc创建时retainCount为1,setter方法中retain了一次引用计数加1,所以此时retainCount变为了2。

类似于如下操作:

NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 引用计数+1
self.array = temp; 引用计数+1

所以一般在使用属性赋值的时候一般这么写:

self.array = [[[NSArray alloc] initWithObjects:@2, nil] autorelease]; //用autorelease抵消一次retain操作

或者:

NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 
self.array = temp; 
[temp release];

ARC

ARC是苹果引入的一种自动管理内存的机制,实现的方式就是在编译的时候在代码合适的位置自动键入内存管理的代码。

ARC下内存管理思想跟MRC一样,同样遵守上面的四个法则。只是ARC下已经没有了上面的retain、release、autorelease等直接操作对象内存管理的方法。ARC下Objective-C采用所有权修饰符来管理对对象的引用情况。

  • __strong :变量的默认修饰符,默认不指定的话就是__strong。__strong表明了一种强引用的关系,表示当前修饰的变量持有对象,类似于MRC下的retain。
  • __weak:与__strong相反,__weak表明一种弱引用的关系,表示当前修饰的变量并不会持有该对象,当对象被系统释放后,__weak变量会自动置为nil,比较安全,常用于解决循环引用的情况。
  • __unsafe_unretained:同__weak一样,该修饰符同样不会持有对象,但是不同的是,当变量指向的对象被系统释放后,变量不会自动置为nil,该指针会变为野指针,如果再次访问该变量,会导致野指针访问错误。现在很少会用到该修饰符。
  • __autoreleasing:用于修饰引用传值的参数(id *, NSObject **),类似于调用autorelease方法,在函数返回该值时会被自动释放掉。常见于NSError的传递中:例如:error:(NSError *__autoreleasing *)error,传递error变量的引用,这样的话才可以在函数内部对error进行重新赋值然后返回给调用者,同时将内部的创建的error对象注册到Autorelease Pool中稍后释放。

具体用法就不举例了,平时写代码用的都是这些修饰符,不过大部分情况用的是__strong,默认省略了这个修饰符而已。

Autorelease Pool

MRC下,我们要使用自动释放池需要手动创建NSAutoreleasepool,并且要执行对象的autorelease方法和NSAutoreleasepooldrain方法销毁自动释放池。ARC下我们只需要使用@autoreleasepool语法就可以代替MRC下的NSAutoreleasepoolAutorelease Pool就是提供了一种延迟给对象发送release消息的机制。当你想放弃一个对象的所有权,但是又不想这个对象立刻被释放掉,就可以使用Autorelease Pool

ARC下使用Autorelease Pool的场景:当在循环遍历中创建大量临时对象的时候,为了避免内存峰值可以使用Autorelease Pool来避免。例如:

for (int i = 0; i < 100; i++) {
    @autoreleasepool {
        NSData *data = UIImageJPEGRepresentation(image, 0.7f);
        UIImage *image = [UIImage imageWithData:data];
    }
}

如果不使用@autoreleasepool,for循环内部创建出的大量UIImage对象需要等到循环结束时才能释放,这样会导致内存暴涨。当指定了@autoreleasepool后,每次循环结束的时候对象就会被释放掉,避免了内存峰值。

或者在方法中执行一段非常消耗资源的操作时,可以用@autoreleasepool及时释放掉资源。例如SDWebImage中对图像进行的解码预渲染操作。

//摘自SDWebImage
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    if (![[self class] shouldDecodeImage:image]) {
        return image;
    }
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        // device color space
        CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
        BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
        // iOS display alpha info (BRGA8888/BGRX8888)
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     bitmapInfo);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

循环引用

循环引用是指几个对象(至少两个对象)之间互相持有强引用形成了一个闭环,导致在超出对象的生命周期后谁都释放不掉的情况。

导致循环引用的可能情况:

  • 使用Block互相持有
  • NSTimer强引用Target目标对象
  • 使用delegate

解决循环引用的方法:

  • 使用弱引用weak(__weak)
  • 当持有的实例完成任务后赋值为nil

僵尸对象

最近又看了下《Effective Objective-C 2.0》,关于僵尸对象的具体实现仔细研究了下,做个笔记记录下。

僵尸对象是iOS开发中常用的内存管理调试功能。当我们给一个已经释放的对象发送消息的时候,通过僵尸对象能够很方便的了解到这个消息的相关信息,包括当前调用对象所属的类,消息名称等信息,便于我们查找问题的根源。

僵尸对象的原理:在runtime期间当一个对象被释放后,它不会真正的被系统回收,而是被转化成一个特殊的僵尸对象。这个对象所占用的内存不会被释放,当再次给这个对象发送消息时,僵尸对象会抛出异常,并且描述出当前消息的相关内容。例如:

*** -[__NSArrayI indexOfObject:]: message sent to deallocated instance 0x60000022b500

僵尸对象是如何实现的呢?具体来讲分以下几个步骤:首先runtime会替换掉基类NSObject的dealloc方法,在dealloc方法中进行下面步骤:

  • 获取当前的类名ClassName

  • 通过拼接_NSZombie_前缀创建一个新的僵尸类名:_NSZombie_ClassName (后缀ClassName为当前的类名)

  • 通过_NSZombie_类拷贝出一个新的类,并且类名命名为_NSZombie_ClassName

  • 销毁当前的对象,但是不释放内存(不调用free()方法)

  • 将当前对象的isa指针指向新创建的僵尸类(变更对象所属的类)

_NSZombie_类以及从其拷贝出来的新的僵尸类都没有实现任何方法,所以当给僵尸对象发送消息后,会进入消息转发流程。___forwarding___函数是实现消息转发流程的核心函数,在这个函数中先检测当前接收消息的对象所属的类名,如果类名的前缀是_NSZombie_,表明当前的消息发送的对象是僵尸对象,然后就会做特殊处理:先打印出当前消息的相关信息,然后终止程序抛出异常。

伪代码如下:

dealloc方法中创建僵尸类

- (void)createNSZombie {
 //获取当前对象的类名
 const char *className = object_getClassName(self);
 //创建新的僵尸对象类名
 const char *zombieClassName = strcat("_NSZombie_", className);
 //根据僵尸对象类名获取僵尸对象类(`objc_lookUpClass` 相比 `objc_getClass`,当类没有注册时不会去调用类处理回调)
 Class zombieClass = objc_lookUpClass(zombieClassName);

 //如果不存在,先创建僵尸对象类
 if (!zombieClass) {
 //获取_NSZombie_类
 Class baseZombieClass = objc_lookUpClass("_NSZombie_");
 //这里使用的是`objc_duplicateClass`创建新的类,`objc_duplicateClass`是直接拷贝目标类生成新的类然后赋予新的类名,新的类和_NSZombie_类结构相同,本类的父类,实例变量和方法都和复制前一样。
 zombieClass = objc_duplicateClass(baseZombieClass, zombieClassName, 0);
 }

 //销毁对象,但是不释放对象占用的内存
 objc_destructInstance(self);

 //重新设置当前对象所属的类,让其指向新创建的僵尸类
 object_setClass(self, zombieClass);
}

消息转发的实现,我把整个___forwarding___函数的实现都摘录了,顺便回顾下消息转发的流程

以下代码摘自:Objective-C 消息发送与转发机制原理

int __forwarding__(void *frameStackPointer, int isStret) {
 id receiver = *(id *)frameStackPointer;
 SEL sel = *(SEL *)(frameStackPointer + 8);
 const char *selName = sel_getName(sel);
 Class receiverClass = object_getClass(receiver);
​
 // 调用 forwardingTargetForSelector:
 if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
 id forwardingTarget = [receiver forwardingTargetForSelector:sel];
 if (forwardingTarget && forwarding != receiver) {
 if (isStret == 1) {
 int ret;
 objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
 return ret;
 }
 return objc_msgSend(forwardingTarget, sel, ...);
 }
 }
​
 // 僵尸对象
 const char *className = class_getName(receiverClass);
 const char *zombiePrefix = "_NSZombie_";
 size_t prefixLen = strlen(zombiePrefix); // 0xa
 if (strncmp(className, zombiePrefix, prefixLen) == 0) {
 CFLog(kCFLogLevelError,
 @"*** -[%s %s]: message sent to deallocated instance %p",
 className + prefixLen,
 selName,
 receiver);
 
 }
​
 // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
 if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
 NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
 if (methodSignature) {
 BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
 if (signatureIsStret != isStret) {
 CFLog(kCFLogLevelWarning ,
 @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'.  Signature thinks it does%s return a struct, and compiler thinks it does%s.",
 selName,
 signatureIsStret ? "" : not,
 isStret ? "" : not);
 }
 if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
 NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
​
 [receiver forwardInvocation:invocation];
​
 void *returnValue = NULL;
 [invocation getReturnValue:&value];
 return returnValue;
 } else {
 CFLog(kCFLogLevelWarning ,
 @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
 receiver,
 className);
 return 0;
 }
 }
 }
​
 SEL *registeredSel = sel_getUid(selName);
​
 // selector 是否已经在 Runtime 注册过
 if (sel != registeredSel) {
 CFLog(kCFLogLevelWarning ,
 @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
 sel,
 selName,
 registeredSel);
 } // doesNotRecognizeSelector
 else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
 [receiver doesNotRecognizeSelector:sel];
 } 
 else {
 CFLog(kCFLogLevelWarning ,
 @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
 receiver,
 className);
 }
​
 // The point of no return.
 kill(getpid(), 9);
}
​```

这就是整个僵尸对象的实现过程。

OC内存管理大概就是这些,要想更深入的理解,可以了解下内存管理方法是如何实现的。下一篇写下OC的内存管理的实现原理。

你可能感兴趣的