OC计数内存管理(1)

0.知识预备

一般一个进程由以下几个部分组成: .text(code代码段,只读)、.data(初始化的非零全局变量)、.bss(初始化的和零值全局变量)、heap(堆)、stack(栈)

大致内存模型如图所示(copy自black)

内存模型

动态分配的是堆&栈这两部分空间

用于alloc/new/copy等方法创建一个对象时,分配给进程,常见的便是伙伴算法,用一种链表数组的方式进行内存分配,堆内存分配需要程序员自己调用或者通过某种机制调用dealloc/delete方法归还。需要程序员管理。

一般的函数内部储存临时变量的所在区域,在函数结束后,其生命周期结束,系统栈指针自动pop,原来的栈区空间变为可用,等待新的临时变量覆盖。不需要程序员管理。

举例如下:

- (returnType) func (void)
{
    NSString * st = [[NSString alloc] initWithString : @"hello!\n"];
    //do something
    return st;
}

那么st指针这个临时变量申明在栈区,函数结束返回后,栈指针改变,占用的栈区重新变为可用区域,其生命周期结束,该内存区域的值将会在下一次生成的临时变量覆盖掉。而alloc申请的NSString对象分配在堆区,如果没有dealloc等方法释放,将会一直存在,直到进程结束被系统回收。所以进程申请的堆区内存管理很有必要,不然进程运行期间会造成大量的内存泄露。当然不能忽视的是,即使进程结束操作系统会对分配给进程的内存回收,但是有些内存比如通过Linux系统APIshmget获取的共享内存,进程结束后操作系统并不回收。

1.计数机制管理内存

(1)Objective-C的计数机制

Objective-C语言使用了一种内存计数机制来管理内存,通俗地讲就是统计堆区分配的对象的拥有者数量,为零时调用dealloc(自动调用)将其释放。通过retain计数+1,release计数-1,如果release后计数为0,则在release中调用dealloc方法;举例如下

@interface Car:NSObject
- (void) setEngine: ((Engine *) newEngine);
- (Engine *) engine;
@end

@implementation Engine
- (void) setEngine : (Engine *) newEngine
{
    [newEngine retain];
    [engine release];
    engine = newEngine;
}
@end

这是一个car对象的setEngine方法,修改其指向的Engine对象,所以先让newEngine指向的对象拥有者计数+1,然后让自己之前指向的engine对象-1,释放拥有权。为什么先retain,假设engine指向的对象计数值为1,考虑到newEngine和当前engine指向的是同一个对象的状况,先release将会导致对象被释放,这时engine和newEngine都将会指向释放内存产生错误。

(2) MRC机制

<1>MRC原理

计数值为0释放对象,retain增加计数,release减少计数。内存管理由程序员手动添加retain和release进行程序管理。当对象引用计数为0时,对象被销毁时,系统发送一条dealloc消息。一般开发中我们将重写dealloc方法,释放相关资源。同时一旦调用了dealloc方法,就必须在最后调用[super dealloc];

MRC内存原则

  1. 需要引用时,+1;不需要引用了,-1;
  2. 谁创建,谁release;
  3. 谁retain,谁release;
  4. 只要调用了alloc,必须有release(autorelease)
  5. 成员变量的set方法中需要注意的:基本数据不用管,对象数据需要注意。
<2>autorelease概念

有时候会出现并不能很好地确定合适的release时机,如下列代码

- (NSString *) description
{
    NSString * description;
    description = [[NSString alloc ] 
                   initWithFormat: @ "hello beauty !"];
    return (description);
    
}//description

这段代码中返回了一个NSString对象指针,使得生成的NSString对象retainCount值为1,那么让谁负责释放它?根据内存管理原则,谁创建谁释放,description指针在其生命周期结束时,ARC插入了release语句;谁retain,谁释放,返回的临时指针变量赋值给调用方后生命周期结束,相当于调用方变量接棒了临时变量retain的持有,应该由调用方释放,那么调用代码应该如此写成如下

NSString * desc = [someobject description];
NSLog(@"%@",desc);
[desc release];

哦,Dear,这样未免太过于不精致了,一行代码却变成了三行,更好的解决办法是什么?是的,autorelease pool是时候出来了,先看autorelease方法

-(id) autorelease;

autorelease方法预先设定了一条会在未来发送的release消息,返回值是接受这条消息的对象,当给一个对象发送autorelease消息时,实际上将该对象添加到了自动释放池中。当自动释放池被销毁时,会向该池中所有对象发送release信息。

这时我们更新我们的description代码

- (NSString *) description
{
    NSString * description;
    description = [[NSString alloc ] 
                   initWithFormat: @ "hello beauty !"];
    return ([description autorelease]);
    
}//description

现在我们只用调用这一段代码就够了

NSLog(@"%@",[someObject description]);
<3>autorelease pool

自动释放池的用法有两种

@autoreleasepool{
    //code in here,关键字方法
}

NSAutoreleasePool *pool
pool = [NSAutoreleasePool new];
//code in here,NSAutoreleasePool对象方法
[pool release]

后者是通过创建一个NSAutoreleasePool对象的方式,当池子被释放后,其保留计数器值归0,然后池子被销毁,销毁过程中该池子将释放在其中的每一个对象。

两个方法中,@autoreleasepool方法更快,因为一般而言Objective-C语言创建和释放内存的能力在我们之上。

观察以下代码

#import <Foundation/Foundation.h>
__weak id p;
NSString * func()
{
    NSString * a = [[NSString alloc]
                    initWithFormat:@"hello beauty!"];
    p = a;
    return a;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString * b = func();
        NSLog(@"the addr is %p",p);
        NSLog(@"%@",b);
        NSLog(@"the addr is %p",p);
    }
    NSLog(@"the addr is %p",p);
    return 0;
}

运行结果如下

2018-04-02 17:07:16.750504+0800 test[3571:293795] the addr is 0x100428150
2018-04-02 17:07:16.750705+0800 test[3571:293795] hello beauty!
2018-04-02 17:07:16.750743+0800 test[3571:293795] the addr is 0x100428150
2018-04-02 17:07:16.750766+0800 test[3571:293795] the addr is 0x0

从这一角度看autorelease是一种延迟释放,释放的时机是在每一次runloop之后/autoreleasepool{}代码块结束。

<4>autoloop概念

IOS时是由一个一个runloop组成的,每个runloop都会执行下图的一些步骤:

runloop

可以看到,每个runloop中都创建一个Autorelease Pool,并在runloop的末尾进行释放,所以,一般情况下,每个接受autorelease消息的对象,都会在下个runloop开始前被释放。也就是说,在一段同步的代码中执行过程中,生成的对象接受autorelease消息后,一般是不会在代码段执行完成前释放的。

那通过们自己创建的Pool,

@autoreleasepool{
    //code in here,关键字方法
}

可以提前释放掉一些对象,避免内存占用过大的情况。如下面的循环情况

for (int i = 0; i < 1000; i++)
{
    @autoreleasepool
    {
        //大量的临时对象
    }
}

下面是苹果的Using Autorelease Pool Blocks说明

Cocoa总是希望代码在自动释放池块中执行,否则自动释放的对象不会被释放,并且您的应用程序会泄漏内存。(如果你autorelease在自动释放池块外发送消息,Cocoa会记录一条合适的错误消息。)AppKit和UIKit框架处理自动释放池块中的每个事件循环迭代(例如鼠标放下事件或点击,即会runloop改变)。因此,您通常不必自己创建自动释放池块,甚至也看不到用于创建自动释放池的代码。但是,有三种情况您可以使用自己的自动释放池块:

  • 如果您正在编写不基于UI框架的程序,例如命令行工具。

  • 如果你编写一个创建许多临时对象的循环。

    您可以在循环中使用自动释放池块来在下一次迭代之前处理这些对象。在循环中使用自动释放池块有助于减少应用程序的最大内存占用量。

  • 如果你产生了一个辅助线程。

    一旦线程开始执行,您必须创建自己的自动释放池块; 否则,你的应用程序会泄漏对象。(有关详细信息,请参阅自动释放池块和线程。)

值得注意的是,在实际的IOS开发中,苹果公司一般不推荐我们在自己的代码中使用autorelease方法,也不要使用会返回自动释放对象的一些便利方法,一般这些方法的会返回一个新对象的类方法,如NSString的stringWith开头的方法。

关于runloop的具体机制,这个感觉很吃时间和基础,我现在学得还不够扎实,还需要啃,暂时不做更多的阐述,等后面继续阐述。

标记下可以参考的文章

iOS线下分享《RunLoop》by 孙源@sunnyxx

深入理解RunLoop

(3) ARC机制

ARC机制是在编译时候为我们主动添加retain和release,ARC自动标记autorelease的对象会在autoreleasepool的runloop更新时release掉。有时候runloop内会产生大量的临时对象,这时候我们就通过自己建立的@autoreleasepool进行提前释放,避免内存占用过高。

ARC机制的有效对象

  1. 代码块指针
  1. Objective-C对象指针
  1. 通过_attribute((NSObject))类型定义的指针

如char *指针,CF对象(如CFStringREf)是不支持ARC特性的我们需要自己编写内存管理代码,协同ARC机制共同发挥作用。

2.深入ARC内存管理

(1)ARC的实现

使用ARC,开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码,再结合Objective C的runtime,实现自动引用计数。如ARC代码段

{
    //code here
    NSObject * obj =[[NSObject alloc] init];
    //doing sth
}

等同于MRC

{
    //code here
    NSObject * obj =[[NSObject alloc] init];
    //doing sth
    [obj release];
}

属性所有权关键字如下

  • assign对应关键字__unsafe_unretained, 顾名思义,就是指向的对象被释放的时候,仍然指向之前的地址,容易引起野指针。
  • copy对应关键字__strong,只不过在赋值的时候,调用copy方法。
  • retain对应__strong
  • strong对应__strong
  • unsafe_unretained对应__unsafe_unretained
  • weak对应__weak。

(2)计数方法的实现

引用计数主要依赖于这三个方法:

  1. retain 增加引用计数
  2. release 降低引用计数,引用计数为0的时候,释放对象。
  3. autorelease 在当前的auto release pool结束后,降低引用计数。

在Cocoa Touch中,NSObject协议中定义了这三个方法,由于Cocoa Touch中,绝大部分类都继承自NSObject(NSObject类本身实现了NSObject协议),所以可以“免费”获得NSObject提供的运行时和ARC管理方法,这就是为什么适用OC开发iOS的时候,你的类要继承自NSObject。通过开源的runtime代码我们可以一探究竟。

地址runtime2.0

<1>retain与release

先看NSObject的retain

//来自NSObject.mm文件

// Replaced by ObjectAlloc
- (id)retain {
    return ((id)self)->rootRetain();
}

//rootretain()定义简化如下,原代码位于objc-object.h
inline id objc_object::rootRetain()
{
    return rootRetain(false, false);
}

inline id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this; //Taggedpointer是假对象

    isa_t oldisa;
    isa_t newisa;
    do{
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;  //原值
        
        if(xx) sidetable_unlock();
        
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  
        // rc计数++
        
        if(slowpath(carry)){ 
            //省略一段判定
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            //省略。。
        }
    }while(xxx);
    //省略一段代码
}

可以发现retain的操作主要是通过最终调用rootRetain(false, false);使得引用计数值++,是通过SideTable进行管理。

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

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

可以看到有一个自旋锁,一个引用计数map。从这里我们就可以比较清楚的看到了引用计数的底层实现,存在一个全局的map,以地址作为key,引用计数的值为value。

release的实现相似,不过多了一个计数值为零的dealloc释放代码

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    isa_t oldisa;
    isa_t newisa;

    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        
        if(xx) sidetable_unlock();
        
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  
        // rc计数值--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;   //underflow进行lock,dealloc判定等
        }
    } while (xx);
    
underflow:
    //省略大部分代码实现
    if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
    }
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    //发送dealloc消息
    
    return true;
}

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

<2>autorelease pool

我们提到了,使用autorelease可以将对象放入autorelease pool中,等到pool drain时候,会对池中的每一个对象发送release消息。我们自己建立的release pool在作用域结束后release所有池中对象,主线程runloop的autorelease pool会在runloop更新时drain,向对象发送release消息。

先看autorelease方法实现

static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }
static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }
id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }
void releaseAll() 
    {
        releaseUntil(begin());
    }
void releaseUntil(id *stop) 
    {
        //省略部分代码实现
        if (obj != POOL_BOUNDARY) {
                objc_release(obj);
        }
}

可以看到autorelease会把对象存储到AutoreleasePoolPage的链表里。等到auto release pool被释放的时候,把链表内存储的对象release掉。所以,AutoreleasePoolPage就是自动释放池的内部实现。

class AutoreleasePoolPage 
{
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    
    id * begin();
    id * end();
    id *add(id obj);
    void releaseAll() ;
    //省略一堆方法实现
}

其中parentchild指针用来构造双链表,magic校验AutoreleasePoolPage完整性,thread保存所在线程。

参考:

自动释放池的前世今生

黑幕背后的 Autorelease

NSAutoreleasePool

Objective-C Autorelease Pool 的实现原理

runtime2.0

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

推荐阅读更多精彩内容

  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 1,928评论 1 16
  • 1.1 什么是自动引用计数 概念:在 LLVM 编译器中设置 ARC(Automaitc Reference Co...
    __silhouette阅读 5,042评论 1 17
  • 11.看下面的程序,第一个NSLog会输出什么?这时str的retainCount是多少?第二个和第三个呢? 为什...
    AlanGe阅读 714评论 1 4
  • 29.理解引用计数 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数...
    Code_Ninja阅读 1,457评论 1 3
  • 颐阳集团隆重推出婚庆酒优惠政策 52°幸福威海(1*6):出厂价198.00元/瓶,享受20%的折扣,每箱再赠75...
    虚谷轩主阅读 1,160评论 0 0