AutoRelease 全面解析

385 阅读7分钟

-1.什么是引用计数

内存中的对象在不断地生成和释放

我们需要去管理内存中的对象

//// 手动引用计数

// 创建对象
var a = Something()
// 赋值
b = a
// a的引用计数 + 1 防止 a 被释放
[a retain]
// 
b = c
// a的引用计数 - 1 如果此时a的引用计数为0 则a被释放
[a release]


//// 自动引用计数

// 创建对象
var a = Something()
// 引用 a 防止 a 被释放
var b = a
// 时机合适 a 会被自动释放

A使用对象时 引用计数为 1

当多一个人需要这个对象 如B 则引用计数 +1

当A不需要对象 A释放对象 引用计数 -1

当最后一个持有对象的人都不要这个对象了 则引用计数变为 0 丢弃对象

一些例如weak unowned等细节就不赘述了...

0. Retain & Release

GNU中retain的实现

- (id)retain {
  NSIncrementExtraRefCount(self);
}

inline void
NSIncrementExtraRefCount(id anObject) {
  // [-1]为寻址到该对象的头部
  // 判断retained这个变量的值是否已经大于了系统最大值
  if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1) {
    [NSException raise: NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"];
    // 计数 ++
    ((struct obj_layout *) anObject) [-1].retained++;
  }
} 

GNU中release的实现

- (void)release {
  if (NSDecrementExtraRefCountWasZero(self)) {
    // 析构函数
    // 对象释放
    [self delloc];
  }
}

BOOL
NSDecrementExtraRefCountWasZero(id anObject) {
  if (((struct obj_layout *) anObject)[-1].retained == 0) {
    return YES;
  } else {
    ((struct obj_layout *) anObject)[-1].retained--;
    return NO;
  }
}

Apple的实现

// CF前缀表示代码在Core Foundation框架中

- retain
__CFDoExternRefOperation
CFBasicHashAddValue;

- release
__CFDoExternRefOperation
CFBasicHashRemoveValue

从前缀中我们不难看出

Apple 在内部通过哈希表的方式管理引用计数

我认为是相比于GNU是更加解耦的实现

  • 分配对象时不需要考虑内存的头部
  • 在对象内存损坏时仍然可以通过表来寻址到对象所在的内存区域
  • 更加方便在后期进行调试和检测

1. ARC & MRC

MRC (Manual Reference Counting)

ARC (Automatic Reference Counting)

ARC 即代替手动添加内存管理函数进行人工的内存管理

在编译的阶段自动的在需要持有对象时插入retain函数

需要释放时插入release函数


2. AutoRelease

ARC 和 AutoRelease 并没有直接的联系

向一个对象发送延迟释放信息 在对象超出作用域时自动向对象发送release的消息

AutoRelease的具体使用方法

NSAutoreleasePool pool [[NSAutoreleasePool alloc] init];    
id obj = [[NSObject alloc] init];    
[obj autorelease];    

// TODO
// 当整个pool被释放的时候
[pool drain];  => 等价于 [obj release];

但是在平时我们并不需要显示的调用pool对象 因为在Runloop中会自动进行NSAutoreleasePool对象的生成

但是有时我们也需要显示的使用AutoRelease 比如for循环10000次 每次循环都创建一个对象

在Runloop还没有结束的时候是不会释放内存的 所以导致了内存疯长的现象

// before

func loop() {
  for (int i = 0; i<10000; i++) {
     UIImage *img = [UIImage imageNamed:@"1.png"];
  }
}


// after
func loop() {
  for (int i = 0; i<10000; i++) {
     @autoreleasepool {
        UIImage *img = [UIImage imageNamed:@"1.png"];
     }
  }
}

GNU中的autorelease方法

[obj autorelease];

// 表面上的实现方法
- (id)autorelease {
  [NSAutoreleasePool addObject:self];
}

/**
实际上, autorelease 内部是用 Runtime 的 IMP Caching 方法实现的
在进行方法调用时 为了获取函数指针 要在框架初始化时进行缓存
*/
id autorelease_class = [NSAutoreleasePool class];
// SEL 方法的指针
SEL autorelease_sel = @selector(addObject:);
// IMP 方法的实现
IMP autorelease_imp = [autorelease_class methodForSelector:autorelease_sel];

// 实际的方法调用时使用缓存的结果值
- (id)autorelease {
  (*autorelease_imp)(autorelease_class, autorelease_sel, self);
}

再看 NSAutoreleasePooladdObject 类方法实现

+ (void)addObject:(id)obj {
  NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 对象;
  if (pool) {
    [pool addObject:anObj];
  } else {
    NSLog("不存在正在使用的 NSAutoreleasePool 对象");
  }
}

PS:当多个 NSAutoreleasePool 对象嵌套使用时,理所当然会调用最里层的 NSAutoreleasePool 对象


Apple 中的 AutoRelease

id *objc_autorelease(id obj)
{
	return AutoreleasePoolPage::autorelease(obj);
}

在OC工程的main.m文件中 我们可以看到

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

首先我们可以使用 clang 的 rewrite-objc 命令将 main.m 文件转换为 main.cpp 文件

从而查看 swift 在转换为 cpp 后的实现

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ {
        __AtAutoreleasePool __autoreleasepool;
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

所以 @autorelease 的本质是声明一个 _AtAutoReleasePool 结构体

然后我们再分析 _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 *" 可以指向任意类型的数据
  void * atautoreleasepoolobj;
};

所以main函数可以大致转换为这样

// 创建自动释放池
__AtAutoreleasePool __autoreleasepool = objc_autoreleasePoolPush();

// TODO 将对象加入自动释放池
// say:
[Object_A autorelease];

//释放自动释放池
objc_autoreleasePoolPop(__autoreleasepool)

我们继续向下探索 去源码中寻找Push和Pop的具体实现

opensource.apple.com/source/objc…

大致实现如下

void* objc_autoreleasePoolPush(void)
{
    if (InARC) return NULL; //如果使用垃圾回收机制
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    if (InARC) return;

    // fixme rdar://9167170
    if (!ctxt) return;

    AutoreleasePoolPage::pop(ctxt);
}

所以C++类AutoreleasePoolPage 是 实际的实现所在

来 让我们找到 AutoReleasePoolPage

class AutoreleasePoolPage 
{
		#define POOL_SENTINEL 0
  
    static size_t const SIZE = 4096
    //用于数据校验
    magic_t const magic; 
    //栈顶地址
    id *next;
    //所在的线程
    pthread_t const thread; 
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
  	...
}

我们不难发现 这是个链表节点 每个page的大小为4096byte

所以AutoReleasePool实质上是一个节点为AutoReleasePoolPage的双向节点

让我们继续去找pop和push的实现


Push & 创建

static inline void *push() 
{
    if (!hotPage()) {
        setHotPage(new AutoreleasePoolPage(NULL));
    } 
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

PS: hotPage()找出当前使用的page

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
  
    // 当前使用的Page存在且不为full状态
    if (page && !page->full()) {
      	// 直接将对象添加到Page中
        return page->add(obj);
    } else {
        return autoreleaseSlow(obj);
    }
}

id *add(id obj)
{
    *next++ = obj;
    return next-1;
}

slow 相比于 fast 多了Page的创建部分

id *autoreleaseSlow(id obj)
{
    AutoreleasePoolPage *page;
    page = hotPage();
  
    //如果没有page,则新建一个自动释放池,并添加obj对象进释放池
    if (!page) {
        objc_autoreleaseNoPool(obj);
        return NULL;
    }
  
    //如果当前hotPage已经满了,则以链表的形式新增一个page并添加到当前page的后面,然后将此设置为hotPage;
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

看到这里 我们知道

在Push的调用过程中进行了 Page的创建 创建Page后会将 POOL_SENTINEL (哨兵) 压栈并将栈顶的地址返回

方便之后对对象进行 [obj autorelease] 操作将对象压入栈中

下面我们介绍 Pop方法 即 AutoReleasePool的销毁过程


Pop & 析构

// -parameter: token 即先前push返回的地址 即Page栈中的哨兵地址
static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;

    if (token) {
        // 找到POOL_SENTINEL所在的Page地址
        page = pageForPointer(token);
      	// 方便之后从栈顶一直release对象到stop位置
        stop = (id *)token;
    } else {
        // hotPage代表当前使用的Page coldPage代表最初的Page
        page = coldPage();
        // begin() 返回Page的默认栈底部
      	// 相反 end() 可以找到Page装满时的栈顶 也就是大小为4096byte的Page的尽头
        stop = page->begin();
    }

  	// 对自动释放池中对象调用objc_release()进行释放 即引用计数减一操作
    page->releaseUntil(stop); 

    if (!token) {
        // special case: 传入0删除全部Page对象
        // Token 0 is top-level pool
        page->kill();
        setHotPage(NULL);
    } else if (page->child) {
        if (page->lessThanHalfFull()) {
          	// 如果当前Page装了不到一半 就只留下当前Page把子Page都删除
            page->child->kill();
        }else if (page->child->child) {
         		// 如果Page此时装了一半多 就必须留下一个空child
            page->child->child->kill();
        }
    }
}

当前Page装了一半多时一定要留下一个 empty child 是节省需要新建page的开销


AutoRelease 与 线程

static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    // EMPTY_POOL_PLACEHOLDER 表示没有 page
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

static inline void setHotPage(AutoreleasePoolPage *page) 
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}

我们不妨看看hotPage是如何设计的 常规方法一定是定义一个全局变量 / 静态变量去存储它

可Apple却用了 TLS(thread local stroage) 这样可以避免用额外的空间去记录hotPage

也验证了 AutoRelease与线程间的一一对应的关系


Reference

iOS - AutoreleasePool底层详解

Objective-C高级编程(一) 自动引用计数,看我就够了