【IOS开发进阶系列】Objective-c内存管理学习总结

weak的生命周期:具体实现方法

http://www.cocoachina.com/ios/20150605/11990.html

(试读)Objective-C高级编程:iOS与OS X多线程和内存管理

http://book.2cto.com/201305/23841.html


1 内存管理概论

1.1 内存管理思考方式

1、自己生成的对象,自己持有,以alloc、new、copy、mutablecopy开头的方法;

2、非自己生成的对象,自己也能持有,通过retain方法;

3、不再需要自己持有的对象时,将其释放,用release方法;

4、非自己持有的对象,无法释放;


1.2 Autorelease方法

        Autoreleasepool的作用是:避免频繁申请/释放内存。

        调用autorelease方法,可使取得的对象存在,但自己不持有。在对象超出指定生存范围时能够自动并正确地释放——通过将对象注册到autoreleasepool中实现,在最近的pool结束时,自动调用release方法释放对象。

        特别说明:在函数返回值时,调用autorelease方法后,返回值对象的释放机制——在每一次事件触发时(即一个运行循环runloop),系统会自动生成一个autoreleasepool,在事件响应结束时,通过释放此pool,来释放那些所调用的函数产生的对象。

       网友解释:在Iphone项目中,大家会看到一个默认的Autoreleasepool,程序开始时创建,程序退出时销毁,按照对Autorelease的理解,岂不是所有autoreleasepool里的对象在程序退出时才release, 这样跟内存泄露有什么区别?

        答案是,对于每一个Runloop, 系统会隐式创建一个Autoreleasepool, 这样所有的releasepool会构成一个象CallStack一样的一个栈式结构,在每一个Runloop结束时,当前栈顶的Autoreleasepool会被销毁,这样这个pool里的每个Object会被release。

        那什么是一个Runloop呢? 一个UI事件,Timercall,delegate call, 都会是一个新的Runloop。

2 具体实现

2.1 alloc/retain/release/dealloc实现

        接下来,以Objective-c 内存管理中使用的alloc/retain/release/dealloc 方法为基础,通过实际操作来理解内存管理。

        OS X、iOS 中的大部分作为开源软件公开在Apple Open Source上。虽然想让大家参考NSObject 类的源代码,但是很遗憾,包含NSObject 类的Foundation 框架并没有公开。不过,Foundation 框架使用的Core  Foundation 框架的源代码,以及通过调用NSObject 类进行内存管理部分的源代码是公开的。但是,没有NSObject 类的源代码,就很难了解NSObject 类的内部实现细节。为此,我们首先使用开源软件GNUstep来说明。

        GNUstep 是Cocoa 框架的互换框架。也就是说,GNUstep 的源代码虽不能说与苹果的Cocoa实现完全相同,但是从使用者角度来看,两者的行为和实现方式是一样的,或者说非常相似。理解了GNUstep 源代码也就相当于理解了苹果的Cocoa 实现。

        我们来看看GNUstep 源代码中NSObject 类的alloc 类方法。为明确重点,有的地方对引用的源代码进行了摘录或在不改变意思的范围内进行了修改。

id obj = [NSObject alloc];

        上述调用NSObject 类的alloc 类方法在NSObject.m 源代码中的实现如下。

GNUstep/modules/core/base/Source/NSObject.m

+(id)alloc

{

    return [self allocWithZone: NSDefaultMallocZone()];

}

+ (id) allocWithZone: (NSZone*) z

{

    return NSAllocateObject(self, 0, z);

}

        通过allocWithZone :类方法调用NSAllocateObject 函数分配了对象。下面我们来看看NSAllocateObject 函数。

GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject   ▼

struct obj_layout {

    NSUInteger retained;

};

inline id

NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)

{

    int size =计算容纳对象所需内存大小;

    id new = NSZoneMalloc(zone,size);

    memset(new, 0, size);

    new =(id)&((struct obj_layout *)new)[1];

}

        NSAllocateObject 函数通过调用NSZoneMalloc 函数来分配存放对象所需的内存空间,之后将该内存空间置0,最后返回作为对象而使用的指针。

        专栏区域NSDefaultMallocZone、NSZoneMalloc等名称中包含的NSZone是什么呢?它是为防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。

        但是,如同苹果官方文档《Programming With ARC Release Notes》中所说,现在的运行时系统只是简单地忽略了区域的概念。运行时系统中的内存管理本身已极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。

    以下是去掉NSZone 后简化了的源代码:

GNUstep/modules/core/base/Source/NSObject.m alloc简化版   ▼

struct obj_layout {

    NSUInteger retained;

};

+ (id)alloc

{

    int size = sizeof(struct obj_layout) + 对象大小;

    struct obj_layout *p =(struct obj_layout *)calloc(1,size);

    return(id)(p+1);

}

        alloc 类方法用struct obj_layout 中的retained 整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0 后返回。以下用图示来展示有关GNUstep 的实现,alloc 类方法返回的对象。

        对象的引用计数可通过retainCount 实例方法取得。

id obj = [[NSObject alloc] init];

NSLog(@"retainCount=%d", [obj retainCount]);

/*

 *显示retainCount=1

*/

        执行alloc 后对象的retainCount 是“1”。下面通过GNUstep 的源代码来确认。

GNUstep/modules/core/base/Source/NSObject.m retainCount ▼

- (NSUInteger) retainCount

{

    return NSExtraRefCount(self) + 1;

}

inline NSUInteger

NSExtraRefCount (id anObject)

{

    return ((struct obj_layout *) anObject)[-1].retained;

}

        由对象寻址找到对象内存头部,从而访问其中的retained 变量。因为分配时全部置0,所以retained 为0。由NSExtraRefCount(self) + 1 得出,retainCount 为1。

        可以推测出,retain 方法使retained 变量加1,而release 方法使retained 变量减1。

[obj retain];

       下面来看一下像上面那样调用出的retain 实例方法。

GNUstep/modules/core/base/Source/NSObject.m retain   ▼

-  (id) retain

{

    NSIncrementExtraRefCount(self);

    return self;

}

inline void

NSIncrementExtraRefCount (id anObject)

{

    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++;

}

        虽然写入了当retained 变量超出最大值时发生异常的代码,但实际上只运行了使retained 变量加1 的retained++ 代码。同样地,release 实例方法进行retained-- 并在该引用计数变量为0 时做出处理。下面通过源代码来确认。

[obj release];

    以下为此release 实例方法的实现。

GNUstep/modules/core/base/Source/NSObject.m  release   ▼

- (void) release

{

    if (NSDecrementExtraRefCountWasZero(self))

        [self dealloc];

}

BOOL NSDecrementExtraRefCountWasZero (idanObject)

{

    if (((struct obj_layout *)anObject)[-1].retained== 0) {

        return YES;

    } else {

        ((struct obj_layout *)anObject)[-1].retained--;

       return NO;

    }

}

        同预想的一样,当retained 变量大于0 时减1,等于0 时调用dealloc 实例方法,废弃对象。以下是废弃对象时所调用的dealloc 实例方法的实现。

GNUstep/modules/core/base/Source/NSObject.m  dealloc   ▼

- (void) dealloc

{

    NSDeallocateObject (self);

}

inline void NSDeallocateObject (id anObject)

{

    struct obj_layout *o = &((structobj_layout *)anObject)[-1];

    free(o);

}

        上述代码仅废弃由alloc 分配的内存块。

        以上就是alloc/retain/release/dealloc 在GNUstep 中的实现。具体总结如下:

1、在Objective-C 的对象中存有引用计数这一整数值。

2、调用alloc 或是retain 方法后,引用计数值加1。

3、调用release 后,引用计数值减1。

4、引用计数值为0 时,调用dealloc 方法废弃对象。


2.2 Weak实现

        我们以下面这行代码为例:

代码清单1:示例代码

{

    id__weak obj1 = obj;

}

        当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在Clang中的声明如下:

id objc_initWeak(id *object, id value);

        其具体实现如下:

id objc_initWeak(id *object, id value)

{

    *object= 0;

    return objc_storeWeak(object, value);

}

       示例代码轮换成编译器的模拟代码如下:

id obj1;

objc_initWeak(&obj1, obj);

        因此,这里所做的事是先将obj1初始化为0(nil),然后将obj1的地址及obj作为参数传递给objc_storeWeak函数。

        objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,或者指向一个有效的对象。

        如果value是一个空指针或者其指向的对象已经被释放了,则object是zero-initialized的。否则,object将被注册为一个指向value的__weak对象。而这事应该是objc_storeWeak函数干的。objc_storeWeak的函数声明如下:

id objc_storeWeak(id *location, id value);

        其具体实现如下:

id objc_storeWeak(id *location, id newObj)

{

    id oldObj;

    SideTable *oldTable;

    SideTable *newTable;

    ......

    // Acquire locks for old and new values.

    // Order by lock address to prevent lock ordering problems.

    //Retry if the old value changes underneath us.

    retry:

       oldObj = *location;

       oldTable = SideTable::tableForPointer(oldObj);

       newTable = SideTable::tableForPointer(newObj);

       ......

       if(*location != oldObj) {

           OSSpinLockUnlock(lock1);

       #if SIDE_TABLE_STRIPE > 1

         if(lock1 != lock2)OSSpinLockUnlock(lock2);

#endif

          goto retry;

    }

    if(oldObj) {

       weak_unregister_no_lock (&oldTable->weak_table, oldObj, location);

    }

    if(newObj) {

       newObj = weak_register_no_lock (&newTable->weak_table, newObj,location);

       // weak_register_no_lock returns NULL if weak store should be rejected

    }

    // Do not set

*location anywhere else. That would introduce a race.

    *location= newObj;

    ......

    return newObj;

}

        我们撇开源码中各种锁操作,来看看这段代码都做了些什么。在此之前,我们先来了解下weak表和SideTable。

        weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了某个对象相关的的所有的弱引用信息。其定义如下(具体定义在objc-weak.h中):

struct weak_table_t{

    weak_entry_t *weak_entries;

    size_t num_entries;

    ......

};

        其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下:

struct weak_entry_t {

    DisguisedPtr referent;

    union{

       struct{

           weak_referrer_t *referrers;

           uintptr_t out_of_line : 1;

           ......

       };

       struct{

//out_of_line=0 is LSB of one of these (don't care which)

           weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];

       };

    };

};

        其中referent是被引用的对象,即示例代码中的obj对象。下面的union即存储了所有指向该对象的弱引用。由注释可以看到,当out_of_line等于0时,hash表被一个数组所代替。另外,所有的弱引用对象的地址都是存储在weak_referrer_t指针的地址中。其定义如下:

typedef objc_object **weak_referrer_t;


        SideTable是一个用C++实现的类,它的具体定义在NSObject.mm中,我们来看看它的一些成员变量的定义:

class SideTable{

private:

    static uint8_t table_buf[SIDE_TABLE_STRIPE *SIDE_TABLE_SIZE];

public:

    RefcountMap refcnts;

    weak_table_t weak_table;

    ......

}

        RefcountMap refcnts,大家应该能猜到这个做什么用的吧?看着像是引用计数什么的。哈哈,貌似就是啊,这东东存储了一个对象的引用计数的信息。当然,我们在这里不去探究它,我们关注的是weak_table。这个成员变量指向的就是一个对象的weak表。

        了解了weak表和SideTable,让我们再回过头来看看objc_storeWeak。首先是根据weak指针找到其指向的老的对象:

oldObj = *location;

        然后获取到与新旧对象相关的SideTable对象:

oldTable = SideTable::tableForPointer(oldObj);

newTable = SideTable::tableForPointer(newObj);

        下面要做的就是在老对象的weak表中移除指向信息,而在新对象的weak表中建立关联信息:

if(oldObj){

    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);

}

if(newObj){

    newObj= weak_register_no_lock (&newTable->weak_table, newObj, location);

    // weak_register_no_lock returns NULL if weak store should be rejected

}


    接下来让弱引用指针指向新的对象:

*location = newObj;

    最后会返回这个新对象:

return newObj;

       objc_storeWeak的基本实现就是这样。当然,在objc_initWeak中调用objc_storeWeak时,老对象是空的,所有不会执行weak_unregister_no_lock操作。

        而当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

    1、调用objc_release

    2、因为对象的引用计数为0,所以执行dealloc

    3、在dealloc中,调用了_objc_rootDealloc函数

    4、在_objc_rootDealloc中,调用了object_dispose函数

    5、调用objc_destructInstance

    6、最后调用objc_clear_deallocating

        我们重点关注一下最后一步,objc_clear_deallocating的具体实现如下:

void objc_clear_deallocating(id obj)

{

    ......

    SideTable*table = SideTable::tableForPointer(obj);

    // clear any weak table items

    // clear extra retain count and deallocating bit

    // (fixme warn or abort if extra retain count == 0 ?)

    OSSpinLockLock(&table->slock);

    if (seen_weak_refs){

        arr_clear_deallocating(&table->weak_table, obj);

    }

    ......

}

        我们可以看到,在这个函数中,首先取出对象对应的SideTable实例,如果这个对象有关联的弱引用,则调用arr_clear_deallocating来清除对象的弱引用信息。我们来看看arr_clear_deallocating具体实现:

PRIVATE_EXTERN void arr_clear_deallocating(weak_table_t*weak_table, id referent){

    {

       weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);

       if (entry == NULL){

           ......

           return;

       }

       // zero out references

       for (int i = 0; i < entry->referrers.num_allocated; ++i){

           id *referrer = entry->referrers.refs[i].referrer;

           if (referrer){

                if (*referrer ==referent){

                    *referrer = nil;

                }

                else if (*referrer){

                    _objc_inform ("__weak variable @ %p holds %p instead of %p\n", referrer, *referrer, referent);

                }

           }

       }

       weak_entry_remove_no_lock(weak_table, entry);

       weak_table->num_weak_refs--;

    }

}

        这个函数首先是找出对象对应的weak_entry_t链表,然后挨个将弱引用置为nil。最后清理对象的记录。

        通过上面的描述,我们基本能了解一个weak引用从生到死的过程。从这个流程可以看出,一个weak引用的处理涉及各种查表、添加与删除操作,还是有一定消耗的。所以如果大量使用__weak变量的话,会对性能造成一定的影响。那么,我们应该在什么时候去使用weak呢?《Objective-C高级编程》给我们的建议是只在避免循环引用的时候使用__weak修饰符。

        另外,在clang中,还提供了不少关于weak引用的处理函数。如objc_loadWeak, objc_destroyWeak, objc_moveWeak等,我们可以在苹果的开源代码中找到相关的实现。等有时间,我再好好研究研究。

3 参考链接

ARC下需要注意的内存管理

http://www.jianshu.com/p/556ba33fa498


iOS内功篇:内存管理

http://www.cocoachina.com/ios/20160411/15892.html?utm_source=tuicool&utm_medium=referral


《Objective-C高级编程》1.4: __weak修饰符


Clang 3.7 documentation – Objective-C Automatic Reference Counting (ARC)


apple opensource – NSObject.mm

你可能感兴趣的:(【IOS开发进阶系列】Objective-c内存管理学习总结)