OC的内存管理方式

作者: taobingzhi | 来源:发表于2019-03-08 16:00 被阅读28次

MRC

手动内存管理,每一次的retain,new,alloc都对应一次release,autorelease;

ARC

自动引用计数,由编译器在编译期间用更底层的C接口实现retain/release/autorelease,不需要手动释放。(底层的原理和接口api待会讲解)

ARC的规则

当对象创建时,引用计数为1;当一个强指针(strong或者retain)指向该对象时,引用计数+1;当强指针不在指向该对象时,引用计数-1;当对象的引用计数为0时,说明这个对象不被任何指针指向,可以销毁,回收内存。

ARC的内部实现

ARC背后的引用计数主要依赖于三个方法:

  • retain:增加引用计数
  • release:降低引用计数,引用计数为0的时候,释放对象
  • autorelease:在当前的autorelease pool结束后,降低引用计数

下面我们来看看runtime的源码。

- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}
id objc_object::sidetable_retain()
{
    //获取table
    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;
}

我们可以看到底层的C语言,retain方法实际是调用sidetable_retain();方法,在该方法中,有一个SideTable的结构体来存储引用计数,下面来看一下这个结构体的组成。

typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
struct SideTable {
       spinlock_t slock;
       RefcountMap refcnts;
       weak_table_t weak_table;
       //省略
}

可以看到,这个数据结构就是存储了一个自旋锁,一个引用计数map。这个引用计数的map以对象的地址为key,引用计数作为value。到这里,retain的底层实现就很清楚了。
再来看看release的实现:

SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    table.lock();
    //找到对应地址的
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) { //找不到的话,执行dellloc
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用计数小于阈值,dealloc
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
    //引用计数减去1
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        //执行dealloc
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;

很简单的逻辑:查找map,对引用计数减1,如果引用计数小于阈值或者找不到引用计数,则调用dealloc。

我们知道非autorelease对象在超出作用域的时候就会被release掉,而autorelease对象在什么情况下会被释放呢?

  • 手动干预释放:指定autorelease pool,在autorelease pool执行完毕时,对对象进行释放;
  • 自动释放:当runloop迭代退出的时候,自动对对象进行释放。
    所以autorelease会延迟对象的release。

下面介绍一下autorelease pool。

Autorelease Pool

事实上在iOS 程序启动之后,主线程会启动一个Runloop,这个Runloop在每一次循环是被自动释放池包裹的,在合适的时候对池子进行清空。

也就是说AutoreleasePool创建是在一个RunLoop事件开始之前(push),AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时。

autorelease pool的底层实现:
Autorelease Pool主要是三个方法实现:
objc_autoreleasePush();->objc_autorelease();->objc_autoreleasePop();
研究源码可以发现,一个线程的autorelease pool是一个指针栈,栈中存放的指针指向需要release的对象或者POOL_SENTINEL(哨兵对象,用于分隔autorelease pool,现在叫POOL_BOUNDARY)。
栈中指向POOL_SENTINEL(即POOL_BOUNDARY)的指针就是autorelease pool的一个标记。当autorelease pool进行出栈操作,每一个比这个哨兵对象晚进栈的对象,都会release。

下面我们来看一下源码:
在终端中使用clang -rewrite-objc 命令将OC的main函数重写成C++的实现:

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
    }

    return 0;
}

可以看到一个__AtAutoreleasePool类型的局部变量__autoreleasepool,__AtAutoreleasePool的定义如下:

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

这里一个是构造函数一个是析构函数(局部变量的构造函数是在程序执行到声明对象的位置时调用,而析构函数是在程序执行到离开这个对象的作用域时调用)。

因此,自动释放池的执行过程就是objc_autoreleasePoolPush() —> [object autorelease] —> objc_autoreleasePoolPop(void )。

来看一下push和pop两个方法的实现:

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

下面介绍一下AutoreleasePoolPage的实现。

AutoreleasePoolPage实现

AutoreleasePoolPage介绍

是c++中的一个类,它在NSObject.mm中的定义如下:

class AutoreleasePoolPage {
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)

#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    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;
};

介绍一下参数:

  • magic 检查校验完整性的变量
  • next 指向新加入的autorelease对象
  • thread page 当前所在的线程,AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • parent 父节点 指向前一个page
  • child 子节点 指向下一个page
  • depth 链表的深度,节点个数
  • hiwat 数据容纳的上限(high water mark)
  • EMPTY_POOL_PLACEHOLDER 空池占位
  • POOL_BOUNDARY 是一个边界对象nil,之前变量名是POOL_SENTINEL哨兵对象,用来区分每个page即每个autoreleasepoolpage边界
  • PAGE_MAX_SIZE = 4096
  • COUNT 一个page里对象数

双向链表

自动释放池并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成的栈结构(对应parent指针和child指针)
parent指向前一个page,child指向下一个page。
当一个page的空间被占满时,会新建一个page对象,连接链表,后来的autorelease对象在新的page中加入。

objc_autoreleasePoolPush

autoreleasePoolPush.jpg

调用objc_autoreleasePoolPush方法时会把边界对象放进栈顶,然后返回边界对象,用于objc_autoreleasePoolPop。

atautoreleasepoolobj = objc_autoreleasePoolPush();
///atautoreleasepoolobj就是返回的边界对象(POOL_BOUNDARY)

push实现如下:

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

static inline void *push() {
   return autoreleaseFast(POOL_BOUNDARY);
}

在这里会进入一个比较关键的方法autoreleaseFast,并传入边界对象;

/*
有 hotPage 并且当前 page 不满,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
有 hotPage 并且当前 page 已满,调用 autoreleaseFullPage 初始化一个新的页,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
无 hotPage,调用 autoreleaseNoPage 创建一个 hotPage,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
*/
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);
   }
}

最后都会调用page->add(obj)将对象添加到自动释放池中。hotPage可以理解为当前正在使用的AutoreleasePoolPage。

AutoreleasePoolPage::autorelease(id obj)

inline id objc_object::rootAutorelease() {
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

__attribute__((noinline,used)) id objc_object::rootAutorelease2() {
    return AutoreleasePoolPage::autorelease((id)this);
}

static inline id autorelease(id obj) {
   id *dest __unused = autoreleaseFast(obj);
   return obj;
}

autorelease方法和push方法一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,不过push函数入栈的是一个边界对象,而autorelease函数入栈的是需要加入释放池的对象。

objc_autoreleasePoolPop

autoreleasePoolPop.jpg

自动释放池释放是传入push返回的边界对象,然后将边界对象指向的这一页AutoreleasePoolPage内的对象释放。

objc_autoreleasePoolPop(atautoreleasepoolobj);

AutoreleasePoolPage::pop()实现:

static inline void pop(void *token)   // token指针指向栈顶的地址
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);   // 通过栈顶的地址找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
        // This check is not valid with DebugPoolAllocation off
        // after an autorelease with a pool page but no pool in place.
        _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                    token);
    }

    if (PrintPoolHiwat) printHiwat();   // 记录最高水位标记

    page->releaseUntil(stop);   // 从栈顶开始操作出栈,并向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    } 
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

该过程主要分为两步:

  • page->releaseUntil(stop),对栈顶(page->next)到stop地址(POOL_SENTINEL)之间的所有对象调用objc_release(),进行引用计数减1
  • 清空page对象page->kill()

小结

  • 自动释放池是多个AutoreleasePoolPage以双向链表的形式组成的
  • push时会将边界对象加入AutoreleasePoolPage的栈中,作为一个标识,且返回该边界对象;
  • 当对象调用autorelease方法时,会将对象加入AutoreleasePoolPage的栈中;
  • pop时传入边界对象,然后对page中的对象发送release消息;

觉得有用,请帮忙点亮红心


Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

相关文章

  • iOS面试题(一)

    1、#import和#include的区别,@class代表什么? 2、OC的内存管理方式以及过程。 3、OC中有...

  • 内部管理初步,协议概述

    内存管理方式概述: OC2.0提供两种内存管理方式:垃圾回收机制(gc)和引用计数;gc用于mac系统开发,iOS...

  • iOS内存泄漏的场景和检测

    内存泄漏的相关定义OC当中内存管理方式:ARC/MRCARC:自动引用计数(系统自动管理内存),由开发人员开辟内存...

  • Objective-C的内存管理-引用计数

    OC的内存管理方式 垃圾回收(Garbage Collection) 手动引用计数管理(Manual Refere...

  • OC的内存管理方式

    MRC 手动内存管理,每一次的retain,new,alloc都对应一次release,autorelease; ...

  • 堆和栈

     按管理方式看:oc对象存放于堆里面(堆内存需要程序员手动回收) 非oc对象一般放在栈里面(栈内存会被系统自...

  • 读《Objective-C高级编程..》笔记

    第一章、自动引用计数 1、内存管理/引用计数 OC中通过引用计数来进行内存管理(其他的内存管理方式有垃圾回收等) ...

  • iOS 内存管理(一)

    前言 iOS开发中,内存管理是从来都不能忽视的问题,OC采用的是动态内存管理方式,跟踪每个对象被引用的次数,当对...

  • 操作系统之内存管理

    内存管理 包括内存管理和虚拟内存管理 内存管理包括内存管理概念、交换与覆盖、连续分配管理方式和非连续分配管理方式(...

  • Swift与OC内存管理方式比较

    从内存管理方面:主要比较对于强引用和弱引用的实现逻辑进行比较1,从引用计数讨论:oc的相对复杂,但是swift的管...

网友评论

    本文标题:OC的内存管理方式

    本文链接:https://www.haomeiwen.com/subject/mturpqtx.html