-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);
}
再看 NSAutoreleasePool 的 addObject 类方法实现
+ (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与线程间的一一对应的关系