iOS-MRC内存管理


OC中的内存管理,也就是引用计数。
对对象的操作有:
生成并持有对象—alloc/new/copy/mutableCopy方法
持有对象 ---------retain方法
释放对象----------release方法
废弃对象----------delloc方法
内存管理方法不在OC语言中,而是在官方框架Cocoa中用于OS X、iOS开发中。

retainCount并不精准

  • 这个方法并未考虑系统自动释放池。
  • retainCount永远不可能返回0, 当retainCount = 0的时候,这个对象就被
    回收了我们无法打印出来它的计数了,
    在这里插入图片描述在这里插入图片描述

内存管理的思考方式:

  • 自己生成并持有对象。
id obj = [[NSobject alloc] init];
  • 取得非自己生成的对象
id obj = [NSMutableArray array];

NSMutableArray类对象被赋值给obj,但obj并不持有该对象,可以用retain方法持有该对象。

[obj retain];

调用这个方法后,打印retainCount方法显示为2;

如何理解无法释放非自己持有的对象

这个问题困扰了我很久,为什么无法释放呢,通过我的理解
此书是日文翻译来的,可能原文作者想要强调的是不能释放,就是不是你持有的对象你如果释放了会崩溃,因此不能释放。
为什么非自己持有的对象无法释放呢?

获得非自己持有的对象方法

 - (id)object {
 		id obj = [[NSObject alloc] init];
 		[obj autorelease];
 		return obj
 }

加入的autorelease后,我们在使用obj的时候就应该心里有数,这个对象会在某时刻自动释放,因此obj就不在持有该对象。
在这里插入图片描述
这就代表着该对象的引用计数即将为0,加入自动释放池,我们仍然可以打印出他的信息。
为了更好理解
我们可以这样写
在这里插入图片描述
手动释放了自动释放池。
会报如下错误
在这里插入图片描述
所以非自己持有的对象并不是无法释放,而是不能释放。
任何指向该对象的指针,都可以释放这个对象,但是如果只是单纯的指向它,而没有retain,释放会报错。这种指向在ARC下也叫弱引用。
在我们现在的ARC下,所有的变量默认都是强引用的
关于强弱引用可以看看这篇文章
iOS强引用与弱引用
系统默认的自动释放池的释放时机并不确定,因此autorelease后,对象在程序结束前不一定会收到release消息,也就不会调用delloc方法,delloc方法在程序结束后也不一定会被调用,系统可能在程序结束时已经清理了进程。

alloc/retain/release/dealloc的GNU实现

GNUstep的记录引用计数的方法:
是作为NSObject的字段来记录
高级编程这本书对比了GNUstep源码,目前苹果最新的实现方式,是利用散列表记录每个对象的引用计数,不在作为NSObject类的字段,减小了对象的内存)。
alloc调用栈
在这里插入图片描述

栈顶的函数来分配存放对象所需的内存空间,之后将空间置为0,最后返回作为对象而使用的指针。
此时的引用计数为0;
因为开始都初始化为0了,此时逻辑上引用计数应该是1.

- (NSUInteger)retainCount {
	return NSExtraRefCount(self)+1; //这里手动加了1.
}

再release的时候

- (void)release {
    if (NSDecrementExtraRefCountWasZero(self)) {
        [self dealloc];
    }
}

//这是一个C语言的函数
BOOL NSDecrementExtraRefCountWasZero(id anObject) {
    if (((struct obj_layout *)anObject)[-1].retained == 0) {
        return YES;
    } else {
        ((struct obj_layout *)anObject)[-1].retained--;
        return NO;
    }
}

可以看到,发送delloc消息是对象的引用计数为0的时候而不是为1的时候。有初始化的代码可知,当引用计数为1的时候实际上对象被持有了俩次,此时打印retainCout的值为2.
这就是为什么一般不说引用计数的具体值是多少。

来看看苹果的实现

昨天查看了最新的源码,在注释中,allocwithzone等方法都被ObjectAlloc替代。

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

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
   return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
//alloc的基类实现。
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 __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

新版的代码不太好懂。在调试的时候发现很多问题,
callAlloc被调用了不止一次,第一次调用时,allocWithZone是false,也就是说不是从alloc方法开始的,因为从alloc方法开始调用的话,allocWithZone会被赋值为true.
打了断点,发现第一次调用的方法是
在这里插入图片描述
先放一放,看看老版代码。
我们从老版的代码摸索
在这里插入图片描述
可以看到,再老版的callAlloc中,调用了createInstance这个方法

id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
**********************************************************************/

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor(); // 获取 cls 及其父类是否有构造函数
    bool hasCxxDtor = cls->hasCxxDtor(); // 获取 cls 及其父类是否有析构函数
    bool fast = cls->canAllocNonpointer(); // 是对 isa 的类型的区分,如果一个类和它父类的实例不能使用 isa_t 类型的 isa 的话,返回值为 false,但是在 Objective-C 2.0 中,大部分类都是支持的

    size_t size = cls->instanceSize(extraBytes); // 获取需要申请的空间大小
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) { // 如果 zone 参数为空,且支持 fast,通过 calloc 申请空间并初始化 isa
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) { // 如果 zone 不为空,使用 malloc_zone_calloc 方法申请空间
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else { // 如果 zone 为空,使用 calloc 方法申请空间
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls); // 初始化 isa
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls); // 构建对象
    }

    return obj;
}

获取对象大小。

size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;     // 8位对齐   
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

老版代码基本解决了所有问题,对象的内存申请和初始化。
init方法仅仅是返回了对象本身。
用calloc完成了空间申请,用initIsa完成isa初始化
这张图是简化板
在这里插入图片描述
下面探究一下刚刚方法调用的问题
在这里插入图片描述
可以看到第一次调用callAlloc没干啥事,在第二次的时候才将对象初始化了.
至于为什么进来先调用其他方法,是一个符号绑定的东西干的,类似于方法交换.
这是我在objc-runtime-new中找到的,根据注释,这个绑定是在程序链接阶段完成的。
有个问题是:在第一遍执行callAlloc的时候如何执行alloc方法呢?此时的alloc不应该是objc_alloc吗?
在这里插入图片描述
这就是官方alloc的流程。

retain/release/autorelease

这里的源码较为复杂,因为retain和release存在多次调用,我尝试打断点,可是调用顺序太乱了。
按上面的图片,先调用的是这个方法
在这里插入图片描述
很明显他要调用retain();这个retain是objc-object.h的底层c代码

在这里插入图片描述
然后回到了NSObject.mm
在这里插入图片描述
这个rootRetain调用了底层c的rootRetain,里面的isa_t可以看看这篇https://www.jianshu.com/p/e90677b9c13e

objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

isa_t这个结构体

struct {
    uintptr_t nonpointer        : 1; //表示 isa_t 的类型, 是否是指针, 还是
    uintptr_t has_assoc         : 1; //表示对象是否有关联对象
    uintptr_t has_cxx_dtor      : 1; //表示对象是否有c++析构函数
    uintptr_t shiftcls          : 44;//存储Class,Meta-Class对象的内存地址
    uintptr_t magic             : 6;//被调试器用来从没初始化的东西中区分真是的对象
    uintptr_t weakly_referenced : 1;//对象是否有若引用
    uintptr_t deallocating      : 1;//对象是否在释放
    uintptr_t has_sidetable_rc  : 1;//对象是否引用计数太大,需要使用sidetable计数
    uintptr_t extra_rc          : 8;//对象的引用计数是extra_rc 值+1(举例,如果 extra_rc 是5,那么对象的真是引用计数是6)
};

我们还是以书为主。对比一下GNUstep和苹果官方应用技术存储区别。
GUNstep:

  • 少量代码可以完成
  • 能够统一管理引用计数用内存块与对象用内存块。
    通过引用计数表管理引用计数的好处如下:
  • 对象用内存块的分配无需考虑内存块头部。
  • 引用计数表各记录中存有内存地址,可从各个记录追溯到各对象的内存块。

autorelease

autorelease会像c语言的自动变量那样对待对象示例。当超过autorelease时,会调用对象的release方法。
我们可以创建自己的autorelease池。
1.生成并持有NSAutorelease对象。
2.调用已分配对象的autorelease实例方法。
3.废弃NSAutoreleasePool对象。

GNU实现
- (id)autorelease {
	[NSAutoreleasePool addobject:self];
}

本质就是调用addobject方法。
这个addObject的伪实现:

+  (void)addObject:(id)anobj {
	NSAutoreleasePool *pool = 取得正在使用的NSAutorelease对象;
	if (pool != nil) {
		[pool addObject:anobj]
	} else {
		NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
	}
}

调用drain方法会直接调用delloc方法

- (void)delloc {
	[self emptyPool];
	[array release]; //这个array就是pool
}
- (void)emptyPool {
	for (id obj in array) {
		[obj release];
	}
}
苹果实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以看到注释,每个自动释放池开启了一个新的池页。
创建了自动释放池,他会重复调用这俩个方法:pop和push。调用很多次
控制台会输出一下提示。
KCObjcTest was compiled with optimization - stepping may behave oddly; variables may not be available.
关于这一块源码的分析https://www.jianshu.com/p/e07ee9d2c4ce

参考文章

OC底层系列一
iOS内存管理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值