iOS内存管理机制详解

机制

OC采用引用计数器对内存进行管理,当一个对象的引用计数(retainCount)为0,则被释放。


引用计数分为两种:

  • 手动引用计数(MRC)
// MRC代码
NSObject * obj = [[NSObject alloc] init]; //引用计数为1

//不需要的时候
[obj release] //引用计数减1

//持有这个对象
[obj retain] //引用计数加1

//放到AutoReleasePool
[obj autorelease]//在auto release pool释放的时候,引用计数减1
  • 自动引用计数(ARC)

比如如下ARC代码:

NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用计数为1
}
NSLog(@"%@",obj);

OC的内存机制可以简单概括为:谁持有(retain)谁释放(release)。retain引用计数+1,release反之。

我们先看看那ratain和release内部是如何实现的。

retain

- (id)retain {
    return ((id)self)->rootRetain();
}

inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

可以看出retain底层是调用了sidetable_retain()

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];//获取引用计数表
    
    table.lock(); // 加锁
    size_t& refcntStorage = table.refcnts[this]; // 根据对象的引用计数
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock(); // 解锁

    return (id)this;
}

SideTable数据结构:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    // 省略...
};

通过代码可以出,SideTable拥有一个自旋锁,一个引用计数map。这个引用计数的map以对象的地址作为key,引用计数作为value

release

- (oneway void)release {
    ((id)self)->rootRelease();
}

inline bool objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}
uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock(); // 加锁
    RefcountMap::iterator it = table.refcnts.find(this); // 先找到对象的地址
    if (it == table.refcnts.end()) {
        do_dealloc = true; //引用计数小于阈值,最后执行dealloc
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE; //引用计数减去1
    }
    table.unlock(); // 解锁
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

release过程:查找map,对引用计数减1,如果引用计数小于阈值,则调用SEL_dealloc


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

使用以下名称开头的方法意味着生成的对象会被自己持有,也就是内部会对象进行一次retain:

  • alloc
  • new
  • copy
  • mutableCopy

比如NSObject的alloc方法:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize()); // 在这里,obj创建的时候,obj的retainCount = 1
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

对于OC提供的方法,除了上面几种,比如说[NSMutableArray array],通过这些方法获取到的对象并不对其进行持有,内部会将生成的对象放入到自动释放池上

// 取得非自己生成而且不持有的对象
id obj = [NSMutableArray array];

// 进行retain之后,obj持有了对象
[obj retain];

再比如如果我们定义一个方法:

- (id)object {
    id obj = [[NSObject alloc] init];
    
    // 加入自动释放池,pool销毁的池销毁的同事对obj进行release一次
    [obj autorelease]; 
        
    return obj;
}
image.png

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

就像上面的 id obj1 = [obj1 object],obj并没有持有对象,如果这时候我们主动调用[obj1 release]就会发生崩溃。

还有一种情况就是已经被释放的对象再对其进行release操作的时候也会发生崩溃

id obj = [[NSObject alloc] init];
[obj release];

[obj release]; // crash

ARC中常见的所有权关键字

  • assign对应关键字__unsafe_unretained, 顾名思义,就是指向的对象被释放的时候,仍然指向之前的地址,容易引起野指针。

  • copy对应关键字__strong,只不过在赋值的时候,调用copy方法。

  • retain对应__strong

  • strong对应__strong

    __strong修饰符是id类型和对象类型默认的所有权修饰符。也就是说,以下源代码�中的id变量,实际上被附加了所有权修饰词:

    id obj = [[NSObject alloc] init];
    
  • weak 对应 __weak

    weak是用来替代unsafe_unretained,weak修饰符的变量(即弱引用)不持有对象,所以在超出其作用域时,对象就会释放,所以因为强引用而造成的循环引用,将其中的成员变量改为弱引用,就不会发生相同情况。

    在持有某若引用时,若该对象被废弃,则此弱引用将自动失效且处于nil被赋值状态(空弱引用)。

  • unsafe_unretained 对应 __unsafe_unretained

unsafe unretained与weak修饰符一样不会增加引用计数,自己生成的对象不能继续为自己所有,所以会立即释放。

iOS4以及OS X Snow Leopard的应用程序中,必须使用unsafe unretained修饰符来替代weak修饰符。赋值给附有__unsafe unretained修饰符变量的对象在通过该变量使用时,如果没有确保其存在,那么应用就会崩溃。

id __unsafe_unretained obj1 = nil;
{
    id __strong obj0 = [NSObject new];
    obj1 = obj0;
    NSLog(@"A: %@", obj1);
}
NSLog("%@", [obj1 description];);

如果像上面那样,程序就会崩溃,因为obj0被销毁之后,obj1并不会自动置为nil。


__bridge

id obj = [[NSObject alloc] init];
 
void *p = (__bridge void *)obj;
 
id o = (__bridge id)p;

将Objective-C的对象类型用 __bridge 转换为 void* 类型和使用 __unsafe_unretained 关键字修饰的变量是一样的

__bridge转换中还有另外两种转换,分别是" __bridge_retained转换"和" __bridge_transfer转换"

  • __bridge_retained转换可使要转换赋值的变量也持有所赋值的对象。
// MRC
void *p = 0;
{
    id obj = [NSObject new]; // retainCount = 1
    p = (__bridge_retained void *)obj; // retainCount = 2
}
NSLog(@"class=%@", [(__bridge id)p class]);
// log:class=NSObject
  • __bridge_transfer转换提供与次相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随之释放。
id p = (__bridge_transfer id)p;

等效于:

// MRC
id obj = (id)p;
[obj retain];
[(id)p release];

__bridge_retainedretain类似,__bridge_transferrelease相似。 在给id obj赋值时retain即相当于__strong修饰符的变量。

如果使用以上两种转换,那么不是用id类型或者对象型变量也可以生成、持有以及释放对象。虽然可以这样做,但是ARC中并不推荐。

void *p = (__bridge_retained void *)[NSObject new];
NSLog(@"class = %@", [(__bridge id)p class]);
(void)(__bridge_transfer id)p;

和下面代码等效

// MRC
id p = NSObject new];
NSLog(@"class = %@", [p class]);
[p release];

Objective-C对象与Core Foundation对象

Core Foundation对象主要使用在C语言编写的Core Foundation框架中,并是用引用计数的对象。在ARC无效时,Core Foundation框架中的retain/release分别是CFRetain/CFRelease。

因为Core Foundation对象与OC对象没有区别,所以在MRC时,只用简单的C语言的转换也能实现互换。另外这种转换不需要使用额外的CPU资源,因此也被称为"Toll-Free Bridge"

以下函数可用于OC对象和Core Foundation对象之间的相互变换,即Toll-Free Bridge转换:

CFTypeRef CFBridgingRetain(id x) {
    return (__bridge_retained CFTypeRef) x;
}

id CFBridgingRelease(CFTypeRef x) {
    return (__bridge_transfer id)x;
}
CFMutableArrayRef cfObj = NULL;
        {
            NSMutableArray *obj = [[NSMutableArray alloc] init];
            cfObj = CFBridgingRetain(obj);
            CFShow(cfObj);
            NSLog(@"retainCount = %ld", CFGetRetainCount(cfObj));
        }
        NSLog(@"after retainCount = %ld", CFGetRetainCount(cfObj));
        CFRelease(cfObj);

效果如下

image.png

还可以通过__bridge_retained来替代CFBridgingRetain

CFMutableArrayRef cfObject = (__bridge_retained CFMutableArrayRef)obj;

反过来

CFMutableArrayRef cfObj = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSLog(@"retainCount = %ld", CFGetRetainCount(cfObj));

id obj = CFBridgingRelease(cfObj);
// CFRelease(cfObj);
NSLog(@"after retainCount = %ld", CFGetRetainCount((__bridge CFTypeRef)(obj)));

打印出来:


image.png

和书上的结果好像不一样,如果加上CFRelsease就正常了,这个点没有搞清楚:

image.png

如果我们直接用__bridge_transfer进行转换,结果几就过就正常了:

CFMutableArrayRef cfObj = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);
NSLog(@"retainCount = %ld", CFGetRetainCount(cfObj));

//NSMutableArray *obj = CFBridgingRelease(cfObj);
            
NSMutableArray *obj = (__bridge_transfer NSMutableArray *)(cfObj);
//CFRelease(cfObj);
NSLog(@"after retainCount = %ld", CFGetRetainCount((__bridge CFTypeRef)(obj)));
image.png

ARC运行时的优化

ARC不只是在编译时由编译器进行内存管理,实际上在此基础还借助了OC运行时库。也就是说,ARC由以下工具、库实现:

  • clang(LLVM编译器)3.0以上
  • objc4 Objective-C运行时库493.9以上
__strong修饰符

赋值给__strong修饰的变量:

{
    id __strong obj = [[NSObject alloc] init];
}

可以看作成:

id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);

// 调用2次objc_msgSend方法,变量作用域结束时,objc_release释放对象。
// 由此看出编译器会自动插入release操作。

--
使用alloc/new/copy/mutableCopy以外的方法:

{
    id __strong obj = [NSMutableArray array];
}

可以看作:

id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleaseReturnValue(obj);
objc_release(obj);

这里和上面区别主要是objc_retainAutoreleaseReturnValue,该函数主要用于优化程序运行,objc_retainAutoreleaseReturnValue的入参是返回注册在autoreleasepool中的对象的方法/函数的返回值。编译器会在alloc/new/copy/mutableCopy以外的方法调用外部插入。

上面所说的功能实现的时候是需要objc_retainAutoreleaseReturnValueobjc_autoreleaseReturnValue配合完成,任何不在alloc/new/copy/mutableCopy组中的方法必须调objc_autoreleaseReturnValue。 例如,NSMutableArray类方法“array”调用此函数。

+ (id)array {
    return [[NSMutableArray alloc] init];
}
/* pseudo code by the compiler */ 
+ (id) array {
    id obj = objc_msgSend(NSMutableArray, @selector(alloc));    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

任何返回添加到自动释放池的对象的方法都将调用objc_autoreleaseReturnValue函数,如上例所示。 它将一个对象添加到自动释放池并返回。 但是实际上objc_autoreleaseReturnValue不会一直注册到自动释放池。

objc_autoreleaseReturnValue检查调用者的可执行代码,如果代码在调用此方法后调用objc_retainAutoreleasedReturnValue函数,它将跳过注册到自动释放池,并将对象返回给调用者。 即使objc_autoreleaseReturnValue没有将对象注册到自动释放池,objc_retainAutoreleasedReturnValue函数也可以正确地获得这样的对象。 通过objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue的合作,对象绕过被添加到自动释放池。

一般来说:检验了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。

image.png

__weak修饰符

  • 当引用对象被丢弃时,__weak修饰的变量会赋值为nil。
  • 通过__weak限定变量访问对象时,该对象将添加到自动释放池。
{
    id __weak obj1 = obj; // 假色obj诶__strong修饰且对象被赋值
}
/* pseudo code by the compiler */ 
id obj1;
objc_initWeak(&obj1, obj); 
objc_destroyWeak(&obj1);

通过objc_initWeak初始化__weak修饰的变量,在变量作用域结束时通过objc_destroyWeak释放该变量。

objc4源码中objc_initWeakobjc_destroyWeak的具体实现

id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}

void objc_destroyWeak(id *location)
{
    (void)storeWeak<true/*old*/, false/*new*/, false/*crash*/>
        (location, nil);
}

所以,本质上都是调用了storeWeak函数,storeWeak函数把第二参数的赋值对象的地址作为键值,将第一个参数的附有__weak修饰符的变量的地址注册到weak表中。如果第二个参数为0/nil,则把变量的地址从weak表中删。

这个函数总结起来主要做了以下事情:

  • 获取存储weak对象的map,这个map的key是对象的地址,value是weak引用的地址
  • 当对象被释放的时候,根据对象的地址可以找到对应的weak引用的地址,将其置为nil即可

weak表与引用计数表都是采用散列表实现。另外,由于一哥对象可以同时赋值给多个__waek对象修饰符的变量中,对于一个键值,可以注册多个变量的地址。

释放对象的时候,一般经历下面几个操作:

  1. objc_release
  2. 因为引用计数为0所以执行dealloc
  3. _objc_rootDealloc
  4. object_dispose
  5. objc_destructInstance
  6. objc_clear_deallocating

对象被废弃时最后调用的objc_ckear_deallocating函数动作如下:

  1. 从weak表中获取废弃对象的地址为键值的记录
  2. 将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil
  3. 从weak表伤处该记录
  4. 从应用计数表中删除废弃对象的地址为键值的记录

通过上面的步骤可以看出,如果大量的是用__weak修饰符的变量,会对cpu资源造成相应的消耗,一般只有在需要避免循环引用的时候是用__weak修饰符。

image.png

如果我们像上图那样,自己生成对象并复制给__weak变量,自己不能持有该对象,对象会马上被回收,引起编译器警告

编译器处理后代码:

/* pseudo code by the compiler 
    虽然自己生成并持有对象,但是编译器判断其没有持有责,因此被释放
*/
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init)); 
objc_initWeak(&obj, tmp);
objc_release(tmp);
objc_destroyWeak(&obj);

然后再来测试一下:使用__weak修饰符的变量是否会将对象注册到autoreleasepool。

{
    id __weak obj1 = obj;
    NSLog("%@", obj1);
}
/* 编译器的模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

与被赋值相比,在使用附有__weak修饰变量的情况下,增加了对objc_loadWeakRetained函数和objc_autorelease函数的调用。

  • objc_loadWeakRetained函数取出附有__weak修饰符变量所引用的对象并ratain
  • objc_autorelease函数将对象注册到autoreleasepool中

由于附有__weak修饰符变量所引用的对象能被注册到autoreleasepool中,所以在@autoreleasepool块结束前都能保证对象不被释放。但是,如果大流量地是用__weak变量会导致autoreleasepool的对象也会大量地添加,因此在使用__weak变量最好先暂时赋值给__strong变量再是用后者。

这样就能解释我们平时用到的weak-strong-dance的原理了。以AFN的源码为例子:

__weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;

        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
            strongSelf.networkReachabilityStatusBlock(status);
        }

    }

上面在闭包中利用把weakSelf赋值给strongSelf,保证在callback闭包执行的过程中,self不会被释放。

题外话

  • 1、内敛函数

内联函数是指用inline关键字修饰的函数。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。参考资料

参考文章:
黑幕背后的Autorelease
自动释放池的前世今生 ---- 深入解析 Autoreleasepool
深入理解Objective C的ARC机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容