iOS中的内存管理总结

内存管理

iOS通过以下三种方式来管理内存方案:

TaggedPointer
//非指针型的isa
NONPOINTER_ISA
//包含了引用计数表 和 弱引用表
散列表

TaggedPointer

TaggedPointer对象是为了改进在64位架构下内存的占用和效率问题。比如NSNumber和NSDate,使用4个字节就可以基本满足需要,剩余的4个字节作为特殊的标记,表示这是一个特别的指针,不指向任何地址。

NONPOINTER_ISA

在arm64位下iOS操作系统,苹果也对isa指针进行了优化。Objective-C对象的isa区域不再只是一个指针.
NONPOINTER_ISA中,只有33位作为指针部分,其余的31位使用了类似于TaggedPointer的概念。在31位中,其中19位(extra_rc)被用来存储该对象额外的引用计数值,其余的位数用来标识是否关联过对象,是否有弱引用对象等标志。

散列表方式

散列表的这种方案是通过sideTables()这种hash表来存储的。在sideTables下面挂载了若干个sideTable这种数据结构。sideTable下共包含3个元素:
1.自旋锁spinlock_t、2.引用计数表RefcountMap、3.弱引用表weak_table_t.

为什么不是一个sideTable?

如果只有一张表时,当有多个线程来操作其中的一个对象,来更改它的引用计数时,为了保证数据的正确性,就需要为这张表进行加锁操作来保证安全,更改完成后再释放锁让其他的对象操作自己的引用计数。那么效率就会十分的低下。所以系统引入了分离锁这个方案,将内存对象存入到不同的sidetable来提高效率。

那么我们如何快速的定位到它属于哪个sideTable呢?

sideTables其实是个hash表。系统通过hash查找,来找到对应的索引。

自旋锁spinlock_t

自旋锁是一个忙等的锁。是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

引用计数表RefcountMap

RefcountMap实际上是一个哈希表。通过同哈希算法可以避免循环遍历,达到快速查找的目的。size_t用来存储一个对象的引用计数值(64)位。
其中第一位用weakly_referenced表示该对象是否有弱引用。第二位用deallocating表示当前对象是否正在进行dealloc。其余的用来保存引用计数。

弱引用表weak_table_t

weak_table_t也是一个哈希表。对应的hash值用weak_entry_t来表示。


ARC

ARC是LLVM编译器和Runtime共同协作来为我们实现引用计数的管理。ARC禁止手动调用retain/release/retaionCount/dealloc.ARC中新增了weak和strong属性关键字。

引用计数管理

alloc的实现

alloc的实现比较简单,就是经过一系列的调用,最终调用了C函数的calloc。注意,此时并没有设置引用计数为1.

retain的实现
SideTable& table = SideTables()[this];
size_t refcntStorage = table.refcnts[this];
refcntStorage += SIDE_TABLE_RC_ONE;

我们可以知道,通过对象的this指针到SideTables中查找到该对象所在的table。然后再通过this指针查找到该对象所对应的引用计数值。因为是做retain操作,所以系统再通过SIDE_TABLE_RC_ONE这个宏对原有的refcntStorage引用计数再加上1.

我们在进行retain操作的时候,系统是怎样查找它对应的引用计数的呢?

通过上面的分析,我们可以知道,系统是通过两次哈希查找,来查找到对应的引用计数值,然后再做加一操作。

release实现
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
it->second -= SIDE_TABLE_RC_ONE;

同样的,系统首先通过this指针查找到对象所在的table表。然后再在table表中查找到对应的引用计数,再对该对象的引用计数做减一操作。

retainCount实现
SideTable& table = SideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find(this);
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;

同样的,系统首先通过this指针查找到对象所在的table表,并初始化一个局部变量,然后再在table表中查找到对应的引用计数,再对该对象z做一次向右偏移操作,最后与局部变量refcnt_result的值1想加。所以呢,当我们新alloc一个对象时,虽然在引用计数表中是没有这个对象的,但由于refcnt_result初始化为1,所以alloc之后调用retainCount的结果为1.

dealloc实现

image

如上图所示,调用dealloc时,系统会在内部调用_objc_rootDealloc函数,_objc_rootDealloc函数内部又会调用rootDealloc函数。rootDealloc会做以下5种判断:

    1. nonpointer_isa:判断当前对象是否使用了非指针型的isa
    1. weakly_referenced:判断当前对象是否有weak指针指向它
    1. has_assoc:判断当前对象是否有关联对象
    1. has_cxx_dtor:判断当前对象的实现是否涉及到C++相关的内容,以及呢当前对象是否使用ARC来管理内存
    1. has_sidetable_rc:判断当前对象的引用计数是否是通过sidetable表来维护的。因为有的对象是通过nonpointer_isa这种非指针型的isa指针来维护的,nonpointer_isa中有19位被用来存储对象的引用计数,当超过最大阈值时,再通过sidetable来维护引用计数。

当该对象根据以上5个条件判断均不满足时,那么系统会直接调用C中的函数free来直接释放对象。否则的话,系统会调用objct_dispose()来做进一步的操作。

objct_dispose内部实现

image

如上图所示,系统会调用objc_destructInstance函数来销毁对象的实例,然后再执行C函数的free函数。

objc_destructInstance内部实现

image

如上图所示,系统会首先判断hasCxxDtor,上面已经说过hasCxxDtor是用来判断对象是否有c++的实现以及是否通过ARC来管理内存。如果有的话,系统会调用object_cxxDestruct函数来做进一步操作。如果没有的话,会接着判断hasAssociatedObjects是否有关联对象。如果有关联对象的话,系统会调用_object_remove_associations函数,接着如果没有关联对象的话,系统会调用clearDeallocating函数结束调用流程。

clearDeallocating内部实现

image

如上图所示,系统会首先调用sidetable_clearDeallocating函数,接着系统会调用weak_clear_no_lock,weak_clear_no_lock会将指向该对象的弱引用指针置为nil(这也就可以解释“为什么对象有weak指针指向时,当该对象dealloc时,它的weak指针为什么会自动被置为nil?”原因就是在dealloc的内部实现中,有做对象的弱引用指针置为nil的操作。 )。在调用了weak_clear_no_lock函数之后,系统会调用table_refcnts.erase来对对象的引用计数进行擦除,将该对象在引用计数表中的相关存储给清除掉,最后结束调用流程。

弱引用管理

那么一个weak修饰的变量是怎样被添加到弱引用表当中的呢?

image

如上图所示,当我们通过一个弱引用指针指向一个变量obj1时,代码经过编译后就入上图所示,调用了objc_initWeak函数。

objc_initWeak函数的调用栈

image

如上图所示, objc_initWeak函数会首先调用storeWeak函数,接着调用weak_register_no_lock函数,到弱引用表中进行注册添加。在这个函数中,系统通过哈希算法得到需要插入的位置,如果该位置已经有了该对象对应的弱引用数组,那么系统就讲这个新的弱引用变量添加到该数组中,如果没有对应的数组,系统会重新创建一个弱引用数组,然后在该数组下标为0的位置添加这个新的弱引用指针。

我们知道,在销毁weak修饰的对象时,系统会同时将这个weak指针置为nil。那么系统是怎样实现的呢?

通过dealloc的分析,我们知道系统会在dealloc的底层调用weak_clear_no_lock函数。在这个函数中,系统会根据当前对象的弱引用指针查找弱引用表,把当前对象对应的弱引用数组给取出,然后遍历这个弱引用数组,将数组中的每一个弱引用指针分别置为nil。

总结:为什么weak指针指向的对象被废弃后会被自动置为nil?

当对象被废弃后,对象的dealloc方法会有做对象的弱引用指针置为nil的方法(也就是weak_clear_no_lock方法)。在这个方法中,系统会根据当前对象的弱引用指针,通过哈希方法查找到当前对象对应的弱引用数组,然后遍历这个弱引用数组,将数组中的每一个弱引用指针分别置为nil。


自动释放池

我们在OC中的main函数中会发现自动释放池的使用:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

@autoreleasepool经过编译器编译之后,会改写为以下的代码:

void* ctx = objc_autoreleasePoolPush();
{

    //在xcode中被{}包裹的代码
}
objc_autoreleasePoolPop(ctx);

objc_autoreleasePoolPush和objc_autoreleasePoolPop的实现

objc_autoreleasePoolPush内部会调用C++类的AutoreleasePoolPage中的push方法。对应的objc_autoreleasePoolPop会调用AutoreleasePoolPage类中的pop方法:

void* objc_autoreleasePoolPush(void)      ----->

void* AutoreleasePoolPage::push(void)



void* objc_autoreleasePoolPop(void* ctx)      ----->

void* AutoreleasePoolPage::pop(void* ctx)

实际上,一次pop操作相当于一次批量的pop操作。也就是说,在@autoreleasepool中的花括号{}所包含的对象都会被添加到自动释放池中,当进行pop操作时,花括号中所有的对象都会被发送一次release消息。

自动释放池的数据结构

实际上,自动释放池是以栈为节点通过双向链表的形式组合而成,并且它是和线程一一对应的。

image

image

AutoreleasePoolPage的内部实现

如下图所示,AutoreleasePoolPage主要有4个成员变量:next指向下一个可以填充的位置;而parent和next则是双向链表中的父指针和子指针;thread则很明显是一个线程相关的变量,所以我们才说autoreleasePool是和线程一一对应的。

image

如下图所示,在这个栈中,存放的就是@autoreleasepool中花括号中创建的autorelease对象。

image

AutoreleasePoolPage::push的实现

当调用AutoreleasePoolPage::push函数时,意味着会向栈中添加新的一批对象。此时,会将当前的next置为nil,我们称它为哨兵对象。同时将next指针指向下一个可以入栈的位置,然后将这一批对象依次添加到栈中。这也就是AutoreleasePoolPage的push方法的实现过程。实际上,我们每次进行一个@autoreleasepool的代码块的创建,就相当于不断的先在栈中插入哨兵对象,然后添加一批新的autorelease对象的过程。

image

对象调用autorelease的流程

如下图,当我们调用了一个对象的autorelease,那么系统会先判断栈中的next指针是否指向了栈顶,如果没有指向栈顶,说明还有存储空间,便直接做入栈的push操作,将该对象添加到next的位置;如果next已经位于栈顶,那么系统会先新增加一个栈节点到链表上,然后在新的栈上添加对象。

image

image

AutoreleasePoolPage::pop

如下图所示:在进行pop操作的时候,系统会根据传入的哨兵对象找到对应的位置。而这个哨兵对象,就是上面提到过的void* ctx = objc_autoreleasePoolPush();,然后将哨兵对象ctx传给pop函数objc_autoreleasePoolPop(ctx);。通过哨兵对象找到对应的位置之后,系统就会给上次push操作之后添加的对象依次发送release消息,最后再将next指针回退到正确的位置。

image

image

下面代码,array对象的内存是什么时候释放的呢?
- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableArray *array = [NSMutableArray array];
    NSLog(@"%@", array);
}

实际上,在每一次的runloop循环当中,runloop都会在它将要结束的时候,对前一次创建的autoreleasePool进行pop操作,然后再push进来一个新的autoreleasePool。所以说,我们在viewDidLoad中创建的array对象,会在当次runloop将要结束的时候调用AutoreleasePoolPage::pop(),对array进行释放。

autoreleasePool的实现原理是怎样的?

autoreleasePool是以栈为节点通过双向链表形式组合而成的数据结构。

autoreleasePool为什么可以嵌套使用呢?

实际上,多层嵌套调用就是多次插入哨兵对象。我们每次进行@autoreleasepool中代码块创建的时候,系统就会为我们进行一次哨兵对象的插入,然后将这批对象添加到AutoreleasePoolPage当中。

循环引用

iOS中的循环引用可以分为3种:自循环引用、相互循环引用、多循环引用。

自循环引用

如下图,创建一个对象,该对象有个成员变量obj,当我们将该对象赋值给obj的话,就会产生自循环引用的问题。

image

相互循环引用

如下图,有两个对象,他们均有一个成员变量引用了对方,就会造成相互循环引用。

image

多循环引用

如下图,多循环引用其实跟相互循环引用的情况是一样的,就是多个对象依次引用下一个对象造成的。

image

如何解决循环引用?

我们在开发中,经常涉及到的循环引用有代理、Block、NSTimer等情况。一方面,我们可以通过避免循环引用,例如我们在使用代理时,通过弱引用来避免产生循环引用。比如使用__weak __block __unsafe_unretained.另一方面,我们也可以在合适的时机通过手动来断开循环引用。

通过block方式来避免循环引用需要注意block在MRC和在ARC中不同的表现。在MRC中,block不会增加引用计数,避免了循环引用。但是在ARC下,block修饰的对象会被强引用,无法避免循环引用,需要我们手动解环。

通过unsafe_unretained来避免循环引用时,unsafe_unretained修饰的对象不会增加引用计数,从而避免了循环引用。但是,如果被unsafe_unretained修饰的对象在某一时机被释放,会产生悬垂指针。所以我们也不建议使用unsafe_unretained来解决循环引用的问题。

解决NSTimer造成的循环引用

定时器有重复定时器和非重复定时器的分别。非重复定时器使用时一般是在回调方法中调用[timer invalidate]; timer = nil;来解决循环引用的问题。这里我们讨论的是重复定时器。首先,我们要明白NSTimer造成循环引用的原因。例如我们要实现一个无限滚动的广告栏View。那么我们的控制器VC会持有这个View,而View为了实现定时滚动,则会运用到了定时器。我们知道定时器会通过targe持有这个View。这个时候就产生了View和Timer相互引用的问题。

[NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];

如下图,如果是想通过View对Timer使用弱引用的方式来解决循环引用的方式是行不通的。因为当View中的Timer被创建启动后,Timer会被当前的runloop进行强引用,如果timer是在主线程创建的,那么这个timer便会被主线程的runloop所持有。由于主线程的runloop常住内存,所以runloop会对timer进行强引用,而timer又通过targe对view进行强引用,这就形成了主线程的runloop对view对象的强引用。所以,当控制器VC销毁后,它的这个循环滚动的子视图view由于被runloop强持有,从而无法销毁。进而造成了内存泄露。

image

如下图,我们可以通过创建中间对象来解决Timer造成的循环引用问题。中间对象对Timer对象和view视图对象都采用弱引用的方式。由此轮播图的view只被控制器VC所持有。当控制器VC退出后就会释放对轮播图View。当定时器Timer进行下次循环的回调时,会判断视图对象是否被释放废弃掉了,如果被释放了则调用timer的invalidate方法。由于Timer和中间对象并没有形成相互引用,从而解决了内存泄露的问题(实际上,我们运用中间对象的思路,就是利用视图对象被释放后,中间对象对视图的弱引用会被置为nil这个原理,来在这个时机解决循环引用的问题)。

image

#import <Foundation/Foundation.h>

@interface NSTimer (WeakTimer)

+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)repeats;

@end

#import "NSTimer+WeakTimer.h"

@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;

- (void)fire:(NSTimer *)timer;
@end

@implementation TimerWeakObject

- (void)fire:(NSTimer *)timer
{
    if (self.target) {
        if ([self.target respondsToSelector:self.selector]) {
            [self.target performSelector:self.selector withObject:timer.userInfo];
        }
    }
    else{
        [self.timer invalidate];
        self.timer = nil;
    }
}

@end

@implementation NSTimer (WeakTimer)

+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)repeats
{
    TimerWeakObject *object = [[TimerWeakObject alloc] init];
    object.target = aTarget;
    object.selector = aSelector;
    object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];

    return object.timer;
}

@end

   转载规则


《iOS中的内存管理总结》 刘星星 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
数据库入门1 数据库入门1
基本概念什么是数据库? 数据库(database) 保存有组织的数据的容器(通常是一个文 件或一组文件)。数据库软件称为DBMS(数据库管理系统)。数据库 是通过DBMS创建和操纵的容器。 什么是SQL? SQL(发音为字母S-
2018-10-01
下一篇 
iOS中的GCD使用总结 iOS中的GCD使用总结
iOS中的GCD使用总结GCD是iOS开发者必须要熟练掌握和使用的异步执行任务的技术之一。下面是我自己总结的GCD的使用总结。 GCD的APIDispatch QueueGCD中,执行时存在2种Dispatch Queue。一种是串行
2018-08-03
  目录