OC底层探究 - 内存管理

752 阅读16分钟

内存管理

iOS程序的内存布局

内存布局

  • 代码段:编译之后的代码

  • 数据段

    • 字符串常量:比如 NSString *str = @"123"
    • 已初始化数据:已初始化的全局变量、静态变量等
    • 未初始化数据:未初始化的全局变量、静态变量等
  • :函数调用开销,比如局部变量。分配的内存空间地址越来越小

  • :通过 alloc、malloc、calloc 等动态分配的空间,分配的内存空间地址越来越大

int a = 10;
int b;

int main(int argc, char * argv[]) {
    @autoreleasepool {
        static int c = 20;
        
        static int d;
        
        int e;
        int f = 20;

        NSString *str = @"123";
        
        NSObject *obj = [[NSObject alloc] init];
        
        NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
              &a, &b, &c, &d, &e, &f, str, obj);
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

/*
 字符串常量
 str=0x10dfa0068
 
 已初始化的全局变量、静态变量
 &a =0x10dfa0db8
 &c =0x10dfa0dbc
 
 未初始化的全局变量、静态变量
 &d =0x10dfa0e80
 &b =0x10dfa0e84
 
 堆
 obj=0x608000012210
 
 栈
 &f =0x7ffee1c60fe0
 &e =0x7ffee1c60fe4
 */

Tagged Pointer

  • 从 64bit 开始,iOS引入了 Tagged Pointer 技术,用于优化 NSNumber、NSDate、NSString 等小对象的存储。

  • 在没有使用 Tagged Pointer 之前, NSNumber 等对象需要动态分配内存、维护引用计数等,NSNumber 指针存储的是堆中 NSNumber 对象的地址值。

  • 使用 Tagged Pointer 之后,NSNumber 指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。

  • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。

  • objc_msgSend 能识别 Tagged Pointer,比如 NSNumber 的 intValue 方法,直接从指针提取数据,节省了以前的调用开销。

  • 如何判断一个指针是否为 Tagged Pointer?

    • iOS平台,最高有效位是1(第64bit)
    • Mac平台,最低有效位是1

Objc 源码判断一个指针是否是 Tagged Pointer:

#if TARGET_OS_OSX && __x86_64__
    // 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
    // Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1
#endif

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

思考以下2段代码能发生什么事?有什么区别?

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
		dispatch_async(queue, ^{
				self.name = [NSString stringWithFormat:@"abcdefghijk"];
		});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
		dispatch_async(queue, ^{
				self.name = [NSString stringWithFormat:@"abc"];
		});
}

第一段代码会崩溃,报 EXC_BAD_ACCESS 坏内存访问,因为属性在修改的时候,内部是要先调用 release,然后在调用 copy 或者 retain,如果多个线程同时去修改,就可能会同时执行 release 方法,如果这时候该属性已经被释放掉了,再调用 release 就会导致坏内存访问。

第二段代码不会崩溃的原因是它是 Tagged Pointer,给它赋值时相当于直接修改它的指针的值。

OC对象的内存管理

OC内存管理遵循”谁创建,谁释放,谁引用,谁管理“的机制。

在ObjC中内存的管理是依赖对象引用计数器来进行的:在ObjC中每个对象内部都有一个与之对应的整数(retainCount),叫“引用计数器”。

  • 当程序调用方法名以 alloc、new、copy、mutableCopy 开头的方法来创建对象时,该对象的引用计数加1。

  • 当调用这个对象的 retain 方法之后它的引用计数加1,

  • 当调用这个对象的 release 方法之后它的引用计数减1,

  • 如果一个对象的引用计数为0,则系统会自动调用这个对象的 dealloc 方法来销毁这个对象。

在MRC下,注意最后一定要调用父类的 dealloc 方法,两个目的:

  • 一是:父类可能有其他引用对象需要释放;
  • 二是:当前对象真正的释放操作是在 super 的 dealloc 中完成的

我们可以通过 dealloc 方法来查看是否一个对象已经被回收,如果没有被回收则有可能造成内存泄露。

在MRC下,我们需要在适当的位置插入 release 和 autorelease,保证对象在使用完后会被释放。

如果一个对象被释放之后,那么最后引用它的变量我们需要手动设置为 nil,否则可能造成野指针错误,而且需要注意在 ObjC 中给空对象发送消息是不会引起错误的

MRC下的内存管理

属性参数

@property的参数分为三类,也就是说参数最多可以有三个,中间用逗号分隔,每类参数可以从上表三类参数中任选一个。

如果不进行设置或者只设置其中一类参数,程序会使用三类中的各个默认参数。

MRC下默认参数:(atomic, readwrite, assign)

原子性
  • atomic 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁

  • 可以参考源码 objc4 的 objc-accessors.mm

  • 它并不能保证使用属性的过程是线程安全的,因为我们可以绕过 setter 方法来进行赋值

atomic 在iOS中基本不会使用,因为太耗性能,一般用于MacOS。

内存管理属性

假设我们定义一个属性a,这里列出三种方式的生成代码:

  • assign

    • 直接赋值,引用计数不改变,适用于简单数据类型,例如:NSIngeter、CGFloat、int、char等。
    - (void)setA:(int)a {
        _a = a;
    }
    
  • retain

    • 指针的拷贝,使用的是原来的内存空间。
    • 对象的引用计数加1。
    • 此属性只能用于 Objective-C 对象类型,而不能用于 Core Foundation 对象。(原因很明显,retain 会增加对象的引用计数,而基本数据类型或者 Core Foundation 对象都没有引用计数)。
    - (void)setA:(Car *)a {
        if(_a != a){
            [_a release];
            _a = [a retain];
        }
    }
    
  • copy

    • 对象的拷贝,新申请一块内存空间,并把原始内容复制到那片空间。
    • 新对象的引用计数为1。
    • 此属性只对那些实行了 NSCopying 协议的对象类型有效。
    • 通常用于字符串对象、Block、NSArray、NSDictionary
    - (void)setA:(NSString *)a {
        if(_a != a){
            [_a release];
            _a = [a copy];
        }
    }
    

深浅拷贝

不管是集合类对象,还是非集合类对象,接收到 copy 和 mutableCopy 消息时,都遵循以下准则:

  • copy 返回 immutable 对象;所以,如果对 copy 返回值使用 mutable 对象接收就会 crash;
  • mutableCopy 返回 mutable 对象;

系统非集合类对象指的是 NSString,NSNumber ... 之类的对象

不管是集合对象,还是非集合对象。

  • immutable 对象
    • copy,是指针复制,
    • mutableCopy,是内容复制
  • mutable 对象
    • copy 和 mutableCopy 都是内容复制

但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制,被称为单层深复制。

因此,网上有人对浅复制、深复制、单层深复制做了概念区分:

  • 浅复制(shallow copy):在浅复制操作时,对于被复制对象的每一层都是指针复制。
  • 单层深复制(one-level-deep copy):在深复制操作时,对于被复制对象,至少有一层是深复制。
  • 完全深复制(real-deep copy):在完全复制操作时,对于被复制对象的每一层都是对象复制。

如何实现集合的完全深复制呢?

集合的深复制有两种方法。

  • 第一可以用 initWithArray:copyItems: 将第二个参数设置为YES即可:

    NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];
    

    如果你用这种方法深复制,集合里的每个对象都会收到 copyWithZone: 消息。如果集合里的对象遵循 NSCopying 协议,那么对象就会被深复制到新的集合。如果对象没有遵循 NSCopying 协议,而尝试用这种方法进行深复制,会在运行时出错。copyWithZone: 这种拷贝方式只能够提供一层内存拷贝(one-level-deep copy),而非真正的深复制。

  • 第二个方法是将集合进行归档(archive),然后解档(unarchive),如:

    NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
    

ARC下内存管理

ARC下默认的属性参数
  • 基本数据类型为:(atomic, readwrite, assign)

  • 对象类型为:(atomic, readwrite, strong)

所有权修饰符

在ARC中,id类型和对象类型必须附加所有权修饰符,所有权修饰符有四种:

  • __strong

    • id类型和对象类型默认的所有权修饰符
    • __strong 修饰的指针指向的对象,会在赋值时调用被指向对象的 retain 方法,导致其引用计数加1
    • __strong 修饰的指针变量指向对象时,表示对其强引用(owner),只要还有某个 __strong 修饰的指针变量指向该对象,该对象就无法释放;当指针指向新值,或者指针不再存在时,相关联的对象就会释放(也就是说现在已经没有指针对其进行强引用了,所以才能释放)
    • 使用 __strong 修饰符可能会导致循环引用
  • __weak

    • 提供弱引用(reference),而不是 owner,即引用计数不会加1。
    • __weak 修饰的指针变量指向的对象被释放后,这个指针变量会被置为nil,不会产生野指针
    • 使用 __weak 修饰符来解决循环引用
  • __unsafe_unretained

    • 提供弱引用(reference),而不是 owner,即引用计数不会加1。
    • __unsafe_unretained 修饰指针变量指向的对象被释放后,指针不会置空,而是变成一个野指针,若该指针指向的内存后来被重新写入了数据,那么此时如果访问这个对象的话,程序就会 Crash,抛出 BAD_ACCESS 的异常。
  • __autoreleasing

    • 在ARC环境下,将对象赋值给附有 __autoreleasing 修饰符的指针变量等价于在MRC环境下调用对象的 autorelease 方法,即将对象注册到 autoreleasepool。
    • 但是,显式地附加 __autoreleasing 修饰符同显式地使用 __strong 修饰符一样罕见,因为编译器会帮我们添加,所以这个修饰符很少用到。
    • 作为 alloc/new/copy/mutableCopy 方法返回值取得的对象是自己生成并持有的,其他情况下便是取得非自己生成的对象并持有,这些非 alloc/new/copy/mutableCopy 方法返回的对象,编译器会自动将他们注册到 autoreleasepool。
内存管理属性
属性声明所有权修饰符
assign__unsafe_unretained
copy__strong
retain__strong
strong__strong
unsafe_unretained__unsafe_unretained
weak__weak
  • assign

    • 只可以用来修饰基本数据类型,该方式会对象直接赋值而不会进行retain操作。
    • 如果用assign修饰对象,当对象释放后(因为不存在强引用,离开作用域对象内存可能被回收),指针的地址还是存在的,也就是说指针并没有被置为nil,下次再访问该对象就会造成野指针异常。对象是分配在堆上的,堆上的内存由程序员手动释放。
    • assign修饰基本数据类型或OC数据类型,因为基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针。
  • copy

    • 是owner,不是reference(引用),对复制出来的对象强引用。
    • 新的对象引用计数为1,与原始对象引用计数无关,且原始对象引用计数不会改变。
    • 使用copy创建的新对象也是强引用,使用完成后需要负责释放该对象。
    • copy特性可以减少对象对上下文的依赖。新对象、原始对象中任一对象的值改变不会影响另一对象的值。
    • 要想设置该对象的特性为copy,该对象必须遵守NSCopying协议,Foundation类默认实现了NSCopying协议,所以只需要为自定义的类实现该协议即可。
  • strong

    • 创建一个强引用的指针,引用对象引用计数加1。
  • retain

    • 创建一个强引用的指针,引用对象引用计数加1。
    • iOS引入ARC后,用strong替代了retain
    • 与 strong 的区别主要体现再修饰 block的时候,strong 修饰 block,相当于 copy,此时 block 是放在堆上的,生命周期不回随函数作用域结束而结束;但是 retain 修饰的 block 是存放在栈上,block 会有被提前释放的风险。
  • weak

    • 只是reference,不是owner。即引用计数不会加1。
    • 当 weak 指向的对象被释放后,这个指针变量会被置为 nil。

引用计数的存储

在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在 SideTable 结构体中

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; 			// 一个存放着对象引用计数的散列表
    weak_table_t weak_table;	// 弱引用表,也是散列表
};

我们通过源码看看是如何取出对象的引用计数的:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

inline uintptr_t 
objc_object::rootRetainCount()
{
  	// 如果是TaggedPointer类型,直接返回它自己
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) { // 如果是优化过的isa指针
      	// 从isa中取出引用计数
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
          	// 如果has_sidetable_rc为1,则证明isa已经存不下了,
          	// 超出的引用计数被存储在了sidetable中
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
  	// 拿到sidetable
    SideTable& table = SideTables()[this];
  	// 从sidetable的refcnts中取出对象的引用计数
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

dealloc

当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

  • dealloc

  • _objc_rootDealloc

  • rootDealloc

    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;  // fixme necessary?
    
        if (fastpath(isa.nonpointer  &&  
                     !isa.weakly_referenced  &&  
                     !isa.has_assoc  &&  
                     !isa.has_cxx_dtor  &&  
                     !isa.has_sidetable_rc))
        {
            assert(!sidetable_present());
            free(this);
        } 
        else {
            object_dispose((id)this);
        }
    }
    
  • object_dispose

    id 
    object_dispose(id obj)
    {
        if (!obj) return nil;
    
        objc_destructInstance(obj);    
        free(obj);
    
        return nil;
    }
    
  • objc_destructInstance、free

    void *objc_destructInstance(id obj) 
    {
        if (obj) {
            // Read all of the flags at once for performance.
            bool cxx = obj->hasCxxDtor();
            bool assoc = obj->hasAssociatedObjects();
    
            // This order is important.
            if (cxx) object_cxxDestruct(obj);	// 清楚当前的成员变量
            if (assoc) _object_remove_assocations(obj);
            obj->clearDeallocating();	// 将指向当前对象的弱指针置为nil
        }
    
        return obj;
    }
    
  • clearDeallocating

    1. 从weak表中获取废弃对象的地址为键值的记录

    2. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil

    3. 将weak表中该记录删除

    4. 从引用计数表中删除废弃对象的地址为键值的记录

weak指针的实现原理

对对象的弱引用会被存储在 SideTable 中的 weak_table 里,如果对象被释放掉,那么在调用对象的 dealloc 方法的时候,就会把 weak_table 里面的内容清空。

自动释放池

可以通过以下私有函数来查看自动释放池的情况 extern void _objc_autoreleasePoolPrint(void);

  • 自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage

  • 调用了 autorelease 的对象最终都是通过 AutoreleasePoolPage 对象来管理的

  • 源码分析

    • clang重写 @autoreleasepool,底层相当于:

      @autoreleasepool {
      		// atautoreleasepoolobj = objc_autoreleasePoolPush();
      		MJPerson *person = [[[MJPerson alloc] init] autorelease];
      		// objc_autoreleasePoolPop(atautoreleasepoolobj);
      }
      
    • objc4源码:NSObject.mm

      • objc_autoreleasePoolPush

        void *
        objc_autoreleasePoolPush(void)
        {
            return AutoreleasePoolPage::push();
        }
        
        static inline void *push() 
        {
            id *dest;
            if (DebugPoolAllocation) {
                // Each autorelease pool starts on a new pool page.
              	// 每个autorelease pool开始都创建一个新的page
                dest = autoreleaseNewPage(POOL_BOUNDARY);
            } else {
              	// page已经存在,快速添加
                dest = autoreleaseFast(POOL_BOUNDARY);
            }
            assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
            return dest;
        }
        
        id *autoreleaseNewPage(id obj)
        {
            AutoreleasePoolPage *page = hotPage();
          	// 如果已经存在一个页,就当这个页已经存满了,创建一个新的页,添加到child
          	// 然后在新的页里添加obj
            if (page) return autoreleaseFullPage(obj, page);
          	// 否则,创建一个新页
            else return autoreleaseNoPage(obj);
        }
        
        static inline id *autoreleaseFast(id obj)
        {
            AutoreleasePoolPage *page = hotPage();
          	// 如果存在一个页,且这个页还没满,直接添加obj
            if (page && !page->full()) {
                return page->add(obj);
            } else if (page) {
              	// 否则,就创建一个新的页,添加到child
                return autoreleaseFullPage(obj, page);
            } else {
              	// 若不存在当前页,创建一个新页
                return autoreleaseNoPage(obj);
            }
        }
        
        id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
        {
            // The hot page is full. 
            // Step to the next non-full page, adding a new page if necessary.
            // Then add the object to that page.
            assert(page == hotPage());
            assert(page->full()  ||  DebugPoolAllocation);
        
            do {
                if (page->child) page = page->child;
                else page = new AutoreleasePoolPage(page);
            } while (page->full());
        
            setHotPage(page);
            return page->add(obj);
        }
        
        id *autoreleaseNoPage(id obj)
        {
            // "No page" could mean no pool has been pushed
            // or an empty placeholder pool has been pushed and has no contents yet
            assert(!hotPage());
        
            bool pushExtraBoundary = false;
            if (haveEmptyPoolPlaceholder()) {
                // We are pushing a second pool over the empty placeholder pool
                // or pushing the first object into the empty placeholder pool.
                // Before doing that, push a pool boundary on behalf of the pool 
                // that is currently represented by the empty placeholder.
                pushExtraBoundary = true;
            }
            else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
                // We are pushing an object with no pool in place, 
                // and no-pool debugging was requested by environment.
                _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                             "autoreleased with no pool in place - "
                             "just leaking - break on "
                             "objc_autoreleaseNoPool() to debug", 
                             pthread_self(), (void*)obj, object_getClassName(obj));
                objc_autoreleaseNoPool(obj);
                return nil;
            }
            else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
                // We are pushing a pool with no pool in place,
                // and alloc-per-pool debugging was not requested.
                // Install and return the empty pool placeholder.
                return setEmptyPoolPlaceholder();
            }
        
            // We are pushing an object or a non-placeholder'd pool.
        
            // Install the first page.
            AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
            setHotPage(page);
            
            // Push a boundary on behalf of the previously-placeholder'd pool.
            if (pushExtraBoundary) {
                page->add(POOL_BOUNDARY);
            }
            
            // Push the requested object or pool.
            return page->add(obj);
        }
        
      • objc_autoreleasePoolPop

        static inline void pop(void *token) 
        {
            AutoreleasePoolPage *page;
            id *stop;
        
            if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
                // Popping the top-level placeholder pool.
                if (hotPage()) {
                    // Pool was used. Pop its contents normally.
                    // Pool pages remain allocated for re-use as usual.
                    pop(coldPage()->begin());
                } else {
                    // Pool was never used. Clear the placeholder.
                    setHotPage(nil);
                }
                return;
            }
        
            page = pageForPointer(token);
            stop = (id *)token;
            if (*stop != POOL_BOUNDARY) {
                if (stop == page->begin()  &&  !page->parent) {
                    // Start of coldest page may correctly not be POOL_BOUNDARY:
                    // 1. top-level pool is popped, leaving the cold page in place
                    // 2. an object is autoreleased with no pool
                } else {
                    // Error. For bincompat purposes this is not 
                    // fatal in executables built with old SDKs.
                    return badPop(token);
                }
            }
        
            if (PrintPoolHiwat) printHiwat();
        		// 核心在这里,释放直到遇到stop
            page->releaseUntil(stop);
        
            // 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();
                }
            }
        }
        
        void releaseUntil(id *stop) 
        {
            // Not recursive: we don't want to blow out the stack 
            // if a thread accumulates a stupendous amount of garbage
            // 循环调用next,释放资源,直到遇到stop
            while (this->next != stop) {
                // Restart from hotPage() every time, in case -release 
                // autoreleased more objects
                AutoreleasePoolPage *page = hotPage();
        
                // fixme I think this `while` can be `if`, but I can't prove it
              	// 如果当前页空了还没有遇到stop,那么找到当前页的父页,设置父页为当前页,继续释放
                while (page->empty()) {
                    page = page->parent;
                    setHotPage(page);
                }
        
                page->unprotect();
                id obj = *--page->next;
                memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
                page->protect();
        
                if (obj != POOL_BOUNDARY) {
                    objc_release(obj);
                }
            }
        
            setHotPage(this);
        }
        
AutoreleasePoolPage
class AutoreleasePoolPage 
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}
  • 每个 AutoreleasePoolPage 对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象的地址
  • 所有的 AutoreleasePoolPage 对象通过双向链表的形式连接在一起

AutoreleasePoolPage的结构

  • 调用 push 方法会将一个 POOL_BOUNDARY 入栈,并且返回其存放的内存地址

  • 之后每当一个对象调用 autorelease 方法,就会将该对象的地址入栈

  • 调用 pop 方法时传入一个 POOL_BOUNDARY 的内存地址,会从最后一个入栈的对象开始发送 release 消息,直到遇到这个 POOL_BOUNDARY

  • id *next 指向了下一个能存放 autorelease 对象地址的区域

运行下面代码:

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
        MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
        
        @autoreleasepool {
            for (int i = 0; i < 6; i++) {
                MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
            }
            
            @autoreleasepool {
                MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
                
                _objc_autoreleasePoolPrint();
            }
        }
    }
    return 0;
}

结果输出:

objc[6215]: ##############
objc[6215]: AUTORELEASE POOLS for thread 0x1000d1dc0
objc[6215]: 12 releases pending.
objc[6215]: [0x10280c000]  ................  PAGE  (hot) (cold)
objc[6215]: [0x10280c038]  ################  POOL 0x10280c038
objc[6215]: [0x10280c040]       0x1007ab9f0  MJPerson
objc[6215]: [0x10280c048]       0x1007ac7e0  MJPerson
objc[6215]: [0x10280c050]  ################  POOL 0x10280c050
objc[6215]: [0x10280c058]       0x1007abbc0  MJPerson
objc[6215]: [0x10280c060]       0x1007ab730  MJPerson
objc[6215]: [0x10280c068]       0x1007aaec0  MJPerson
objc[6215]: [0x10280c070]       0x1007a53d0  MJPerson
objc[6215]: [0x10280c078]       0x1007a4530  MJPerson
objc[6215]: [0x10280c080]       0x1007a4410  MJPerson
objc[6215]: [0x10280c088]  ################  POOL 0x10280c088
objc[6215]: [0x10280c090]       0x1007a46a0  MJPerson
objc[6215]: ##############
Runloop与Autorelease

iOS在主线程的 Runloop 中注册了2个 Observer

  • 第1个Observer监听了 kCFRunLoopEntry 事件,会调用 objc_autoreleasePoolPush()
  • 第2个Observer
    • 监听了 kCFRunLoopBeforeWaiting 事件,会调用 objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
    • 监听了 kCFRunLoopBeforeExit 事件,会调用 objc_autoreleasePoolPop()
自动内存释放简单总结
  1. autorelease 方法不会改变对象的引用计数器,只是将这个对象放到自动释放池中;
  2. 自动释放池实质是当自动释放池销毁后调用对象的 release 方法,不一定就能销毁对象(例如如果一个对象的引用计数器 >1 则此时就无法销毁);
  3. 由于自动释放池最后统一销毁对象,因此如果一个操作比较占用内存(对象比较多或者对象占用资源比较多),最好不要放到自动释放池或者考虑放到多个自动释放池;
  4. ObjC类库中的静态方法一般都不需要手动释放,内部已经调用了 autorelease 方法;

面试题

  1. 使用CADisplayLink、NSTimer有什么注意点?

  2. 介绍下内存的几大区域

  3. 讲一下你对 iOS 内存管理的理解

  4. ARC 都帮我们做了什么?

    • ARC是LLVM编译器和Runtime系统相互协作的一个结果。

    • LLVM帮我们自动添加了retain、release和autoRelease。

    • Runtime帮我们在运行时做一些支持的操作,比如清空weak指针。

  5. weak指针的实现原理

  6. autorelease 对象在什么时机会被调用 release

    • 如果 autorelease 对象被一个 @autoreleasepool 包含,就会在大括号结束的地方调用 release。
    • 它可能是在当前对象所属的某次 runloop 循环,在 runloop 休眠之前调用了 release。
  7. 方法里有局部对象, 出了方法后会立即释放吗

    • 如果局部对象后面跟的是 autorelease,不会立即释放,而是等到当前 runloop 休眠之前释放
    • 如果局部对象后面跟的是 release,会立即释放