iOS 内存管理

文章目录
   一、NSProxy
      1、作用: 用于消息转发
      2、定时器的细节
   二、内存布局
   三、标记指针(Tagged pointer)
   四、OC 对象的内存管理 
      1、内存管理
      2、copy、mutableCopy 关键字
      3、引用计数
      4、weak 指针
      5、autorelease  

一、 NSProxy

1、作用 : 用于消息转发

NSProxy没有父类,和 NSObject 算是同一级别的存在,都遵循 NSObject 协议,分别定义如下:

@interface NSProxy <NSObject> {
    Class	isa;
}

@interface NSObject <NSObject> {
    Class isa ;
}

那么两者有什么区别?
首先,NSProxy 是没有 init方法的,直接 alloc就产生实例
其次,NSProxy是专门用来处理消息转发的,也就是说,对 NSProxy 实例对象发送任何消息,直接进入消息转发环节。

举一个实际例子,以前用定时器,用方法scheduledTimerWithTimeInterval:target: selector: userInfo: repeats:创建一个定时器,该方法会对 target产生强引用。无论 target传的是弱引用进去,NSTimter里面都有一个成员对 target进行强引用。这样就会造成 viewcontroller 强引用 timer,timer 强引用 self,也就是 target 。造成循环引用问题,导致内存泄露。
在这里插入图片描述一般来说,我们解决方法有三种
1、第一种, 使用 scheduledTimerWithTimeInterval: repeats: block: API 去创建定时器,这样就没有 NSTimertarget产生强引用问题,NSTimer只对 block产生强引用,只要在block内部使用弱引用的话,就不会造成内存泄露。

2、第二种,使用中间对象和消息转发
在这里插入图片描述截取部分代码如下

中间对象的代码

#import "TargetObject.h"

@interface TargetObject : NSObject

+ (id)generalWithTarget:(id)target;

@property (nonatomic,weak) id target;

@end

@implementation TargetObject

+ (id)generalWithTarget:(id)target
{
    TargetObject *anyObject = [[TargetObject alloc]init];
    anyObject.target = target;
    return  anyObject;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}

@end

第三种 : 使用 NSProxy + 消息转发

@interface ProxyObject : NSProxy
@property (nonatomic,weak) id target;

+ (id)generalWithTarget:(id)target;

@end

@implementation ProxyObject

+ (id)generalWithTarget:(id)target
{
    ProxyObject  *anyObject = [ProxyObject alloc];
    anyObject.target = target;
    return  anyObject;
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}


@end

关于定时器

- (void)viewDidLoad {
    [super viewDidLoad];
    //self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TargetObject generalWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[ProxyObject generalWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
}

那么 方案二 和 方案三 有什么区别呢?
区别主要是在性能上,具体指方法查找上的性能。
方案二,TargetObject : NSObject 需要从TargetObject类对象的 cache、methods查找方法到NSObject,最后触发消息转发机制。
方案三: ProxyObject : NSProxy ,并不会去父类查找方法, 直接进入消息转发.

要深刻理解 专门用于消息转发 的功用

ProxyObject *proxy = [ProxyObject generalWithTarget:self];
NSLog(@"%d",[proxy isKindOfClass:[self class]]);  //结果为 1 

2、定时器的细节
NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能导致NSTimer不准确。
比较通俗易懂的话就是说: RunLoop每跑一圈计算下时间,如果遇到 NSTimer,发现时间还没到,继续跑圈。但是 RunLoop每跑一圈的时间是不固定的。如果中间插入一些比较重的任务,就会导致要执行定时器的时候被延迟了。

那如何保证定时器准确呢? 使用 GCD的定时器,因为 GCD 定时器直接跟系统内核挂钩,不依赖于 RunLoop

    //创建下定时器(注意,这里需要强引用)
   self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,  dispatch_get_main_queue());
    //设置时间(什么时候开始,间隔几秒,误差多少) 立即执行,1s 执行一次
    dispatch_source_set_timer(self.timer ,
                              dispatch_time(DISPATCH_TIME_NOW, 0*NSEC_PER_SEC),
                              1 * NSEC_PER_SEC,
                              0);
    //设置回调
    dispatch_source_set_event_handler(self.timer , ^{
        NSLog(@"TO DO");
    });
    //启动定时器
    dispatch_resume(self.timer);

二、内存布局

任何一个程序在运行的时候时机上是运行在内存中的,这个内存也就是我们通常说的主存,也叫运行内存,是可以直接与 CPU进行交换数据的内部存储器。内存读取速度很快,所以作为操作系统运行程序的区域。通常我们将内存划分为以下几大块:栈区、堆区、全局区、常量区、代码区

如图所示(哈哈,这张图是扒别人博客的)
在这里插入图片描述下面来详细说明下每个分区的功能:
栈区(stack): 栈区是有系统来自动分配释放,是一个栈的数据结构,存储函数的参数、局部变量、引用。高地址向低地址扩展。

堆区(heap) : 堆区是由开发者"手动管理"或程序结束时由系统全部回收。是一种树状的数据结构,一般用于存储 alloc、new等方式创建的对象。在 iOS 开发中,大多数内存管理方面的问题也多出于此,开发者没有及时回收内存、内存溢出、内存泄露等。低地址向高地址扩展。

全局区 : 用于存放全局变量和静态变量。存储方式是 未经初始化的全局变量和静态变量放在一块区域,初始化后的全局变量和静态变量在另外一个区域。回收也是等进程结束后由系统回收。

常量区 : 字符串常量和基本数据类型值。回收也是进程结束后系统自动回收。

代码区 : 存储编译后的代码

三、标记指针(Tagged pointer)

64bit开始,iOS 引入了 Tagged Pointer技术,用于优化NSString、NSNumber、NSDate等小对象存储。在没有使用 Tagged Pointer 之前,NSString、NSNumber、NSDate等对象需要动态分配内存,维护引用计数,对应的指针存储堆上对象的地址值。使用Tagged Pointer之后,指针存储的数据变成:Tag+Data将数据直接存储在指针中,当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。对于 objc_msgSend或者objc_msgSendSuper能够识别 Tagged Pointer,直接从指针中提取数据,节省了调用的开销。

如何判断一个指针是否是标记指针?在 runtime源码objc-internal.h中有给出一个方法用于判断一个指针是否为标记指针。

#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;
}

从上面可以看出,在 iOS 中,指针的最高有效位为 1 (1<<63)是标记指针。

标记指针有什么用?
节省内存 (只有在 8 字节无法存储的条件下才需要在堆上动态分配内存),
提高访问速度 (objc_msgSend能够识别 标记指针,无法去进行方法查找,直接从指针取值)。

看下具体的应用

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

上面,运行 crash,访问坏内存。分析: 多个线程同时访问 self.name 的 setter 方法。可能对同一块内存释放两次。

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

上面,运行正常。因为,"abc"这个字符串的值被存储在标记指针中,取值的时候被objc_msgSend识别,直接从指针地址直接读取。不存在访问堆上对象。

为验证想法,进一步写了个 demo,验证

bool isToggedPointer(id obj)
{
  return (long)(__bridge void *)obj & 1;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  NSString *name = [NSString stringWithFormat:@"fnawggjoowrgnwg4y34y"];
  NSString *toggedStr = [NSString stringWithFormat:@"abc"];
  NSLog(@"%p----%p",name,toggedStr);
  NSLog(@"%@----%@",[name class],[toggedStr class]);

}

运行结果:

0x60400024e9d0----0xa000000006362613
__NSCFString----NSTaggedPointerString

iOS 内存是 小端模式,即 数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。第一位 : 0x61,第二位: ox62,第三位 ,ox63。6*16+1 = 97,依次类推。另一方面,0x a000 0000 0636 2613指针最高有效位为 a, 转成二进制 : 0b1010,最高位为 1,满足条件。

四、OC 对象的内存管理

1、内存管理
在 iOS 中,使用引用计数来管理 OC 对象的内存。
一个新创建的对象的引用计数(retainCount)默认为 1,当引用计数为 0, 对象就会销毁,释放其占用的空间。
调用 retain会让对象的引用计数 +1,调用release回让引用计算 -1

当调用 alloc、new、copy、mutableCopy方法返回一个对象,在不需要这个对象时,要调用release、autorelease来释放他
想拥有某个对象,就让它引用对象 + 1;不想再拥有某个对象,就让它引用计算 -1

内存管理解决的问题:
内存泄露(该释放的对象没有释放) 和 野指针( 坏内存访问 )

2、copy 关键字

拷贝的目的: 产生一个副本对象,跟源对象互不影响

iOS 提供了两个拷贝方法
copy : 不可变拷贝,产生不可变副本
mutableCopy : 可变拷贝,产生可变副本,

深拷贝 :内容拷贝,产生新的对象
浅拷贝: 指针拷贝,没有产生新的对象

不可变可变
copy不可变副本(不产生新对象,被拷贝对象引用计数+1)不可变副本(产生新对象)
mutableCopy可变副本 (产生新对象)可变副本(产生新对象)

总结: 不可变对象调用copy 是浅拷贝,其他都是深拷贝

3、引用计数

64 bit 中,引用计数存储在 isa 指针中, 有可能存在 sideTable类中

union isa_t 
{
    Class cls;
    uintptr_t bits;
    //此结构体仅为了增加可读性,纯属摆设
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; 
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;  //当 19 位不够存储的时候,为1 ,存在sideTable_t 结构中
        uintptr_t extra_rc          : 19;  //存储引用计数  19 位
    };

其中 SideTable 定义如下

struct SideTable {
    spinlock_t slock;      //自旋锁
    RefcountMap refcnts;   // 引用计数 散列表
    weak_table_t weak_table; //弱引用 
}; 

weak_table_t定义如下

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

看下 获取引用计数的底层实现

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

继续看 RootRetainCount函数实现

inline uintptr_t objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    //获取 bits
    ClearExclusive(&isa.bits);   
    //判断是不是优化过的指针
    if (bits.nonpointer) {
        // extra_rc 本来存储就是引用计算 -1 ,直接加上 1 上去
        uintptr_t rc = 1 + bits.extra_rc;
        // 如果存在 sideTable_t
        if (bits.has_sidetable_rc) {
            //就继续去 sideTable_t结构中去找
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

4、 weak 指针

__weak 和 _unsafe_unretain 共同点都不会产生强引用,指向对象如销毁了,__weak 会将指向对象的指针置为 nil,而 _unsafe_unretain 不会。
看下 weak修饰的指针如果在对象销毁的时候(即调用 dealloc)如何将指针设置成 nil
还是查找 runtime源码

dealloc 源码

- (void)dealloc {
    _objc_rootDealloc(self);
}

_objc_rootDealloc源码

void _objc_rootDealloc(id obj)
{
    assert(obj);
    obj->rootDealloc();
}

rootDealloc源码

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    /**
     如果是优化过的 isa,
     且没有弱引用
     且没有关联对象
     且没有 c++ 析构函数
     且没有 sizdeTable_t
     直接 free
*/
    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 函数
*/
        object_dispose((id)this);
    }
}

object_dispose 函数

id object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);  
    // 在对象销毁之前,调用了  objc_destructInstance 函数
    free(obj);

    return nil;
}

看下,在释放对象之前,调用了 objc_destructInstance函数

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.
        /**
         如果存在 c++ 析构函数、存在关联对象
         先进行释放
         后,继续调用  clearDeallocating 函数
        */
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

clearDeallocating函数的调用

inline void objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    /**
     如果存在弱引用 和 has_sidetable_rc不为空
    */
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

clearDeallocating_slow函数

void objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    //将散列表取出来,并 weak_clear 清除 weak 修饰的指针
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

继续看下 weak_clear_no_lock的实现,结合 weak_table_t的结构

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;  //这个 mask 应该是容量 -1 
    uintptr_t max_hash_displacement;
};
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;
    
    //取出来 entry 
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    /**
    这里可以证明,entry 里面有个类似数组的数据结构,存放这被 weak 修饰的指针
    遍历,然后设置成 nil 
*/
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    // 从 弱引用表中删除
    weak_entry_remove(weak_table, entry);
}

看下如何取 entry

static weak_entry_t *weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;
    //看到这里,也是进行 & 操作 得到 index
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

总上所述,可以得出如下结果: 当出现不存在 关联对象、弱引用、析构函数,对象是直接调用 free()函数直接释放。如果存在,需要对这些进行额外的释放工作。其中,对于弱引用,需要调用 clearDeallocating函数对齐进行释放,需要根据当前对象找到 sideTable,进入通过 sideTabl->weakTable,最后,根据当前对象的地址去 weak_table_t这张散列表表中根据 &obj & mask取出weak_entry_t,先把 weak_entry_t 中的指针数组取出来,遍历设置为 nil,然后从 weakTableremove 这个weak_entry_t。其中这个 weak_entry_t里面有个 弱引用指针数组。这个过程跟runtime消息发送过程中,查找类对象方法缓存cache非常类似。

5、 autorelease

对一个对象发送 autorelease消息后,这个对象会被放进 autoreleasepool(自动释放池)。

为了,看清楚 autoreleasepool的结构,创建了命令行工程,并对其进行重写 生成 c++代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

经过命令行 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m得到 main.cpp

struct __AtAutoreleasePool {
    
    void * atautoreleasepoolobj;
    
    //构造函数(默认初始化方法)
    __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
        
    }
    //析构函数(相当于  dealloc )
    ~__AtAutoreleasePool() {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { 
       __AtAutoreleasePool __autoreleasepool;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_hm_w26qvm0x6ld5k2pfy0007h3r0000gn_T_main_8920aa_mi_0);
    }
    return 0;
}

可以知道,生成是个对象,相当于 大括号开始调用objc_autoreleasePoolPush,大括号结束之前调用 objc_autoreleasePoolPop()
这两个函数可在 runtime源码中,NSObject.mm找打对应是实现

void *objc_autoreleasePoolPush(void)
{   //调用  AutoreleasePoolPage 对象的 push()方法
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
   //调用  AutoreleasePoolPage 对象的 pop()方法
    AutoreleasePoolPage::pop(ctxt);
}

由此可见,自动释放池的主要底层数据结构为 : __AtAutoreleasePoolAutoreleasePoolPage。调用autorelease的对象最终都是通过 AutoreleasePoolPage 对象来管理的。

AutoreleasePoolPage定义如何,也可在 runtime中找到对应定义如下(仅摘取部分代码)

class AutoreleasePoolPage {
    magic_t const magic;
    id *next;  //返回下一个能存放 autorelase 对象的内存地址
    pthread_t const thread;
    AutoreleasePoolPage * const parent; // piror 指针,指向上一个 AutoreleasePoolPage 对象
    AutoreleasePoolPage *child;  // next 指针,指向下一个 AutoreleasePoolPage 对象
    uint32_t const depth;
    uint32_t hiwat;
    //返回开始存放 autorelease 对象的地址
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
    //返回整个对象的内存的结束地址
    id * end() {
        return (id *) ((uint8_t *)this+SIZE); // 这个size 为 4096
    }
    //添加 autorelease 对象
    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }
}

每个 AutoreleasePoolPage对象占用了 4096字节内存,除了用它来存放内部的成员变量(7*8 = 56 字节),剩下的空间(4096 - 56 = 4040 字节)用来存放 autorelease 对象的地址。
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
什么是双向链表 :双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
在这里插入图片描述在这里插入图片描述
接下来就需要搞懂,autorelease 对象如何存放

存放过程:
当调用 objc_autoreleasePoolPush()的时候,

会将 POOL_BOUNDARY存入 第一个 AutoreleasePoolPage 对象中从 第 56 字节内存并返回对应内存地址,赋值给 next 指针

当有 autorelease 对象,继续往下面存(每次存放都会更新 next指针的值,指向下一个能存放autorelease 对象地址的区域)

如果 第一个 AutoreleasePoolPage不够用了,会创建第二个 AutoreleasePoolPage 对象,并将该对象的 parent指针指向第一个AutoreleasePoolPage 对象,第一个AutoreleasePoolPage 对象的child指针指向第二个 AutoreleasePoolPage 对象。然后继续存放 autorelease 对象

当调用 objc_autoreleasePoolPop(POOL_BOUNDARY)
依次从栈顶取出 autorelease 对象 直到遇到 POOL_BOUNDARY,对其 发送 release 消息进行释放 。

注意 :POOL_BOUNDARY 从 调用 PUSH 返回,然后 调用 Pop中传入。对于 autoreleasepool嵌套那么就有 三个 POOL_BOUNDARY。嵌套的时候,每个POOL_BOUNDARY返回的内存地址都不同。当销毁的时候,暂停遇到的POOL_BOUNDARY 也不同。

如何嵌套是这么存储的?关键看个方法 _objc_autoreleasePoolPrint()。这个方法可以用来查看自动释放池的情况。

#import <Foundation/Foundation.h>

//在 buildsetting 搜索 automatic reference 将其改成 MRC 

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *object1 = [[[NSObject alloc]init] autorelease];
        
        @autoreleasepool{
            NSObject *object2 = [[[NSObject alloc]init] autorelease];
            NSObject *object3 = [[[NSObject alloc]init] autorelease];
    
            @autoreleasepool{
                NSObject *object4 = [[[NSObject alloc]init] autorelease];
                _objc_autoreleasePoolPrint();
            }
        }
    }
    return 0;
}

控制台输出

objc[27124]: ##############
objc[27124]: AUTORELEASE POOLS for thread 0x100392380
objc[27124]: 7 releases pending.   // 7 个对象准备 release 
objc[27124]: [0x103001000]  ................  PAGE  (hot) (cold) // page对象(hot 表示当前正在使用的 哪一页 )
objc[27124]: [0x103001038]  ################  POOL 0x103001038  // 这个是POOL_BOUNDRY
objc[27124]: [0x103001040]       0x100727ff0  NSObject
objc[27124]: [0x103001048]  ################  POOL 0x103001048 // 这个是POOL_BOUNDRY
objc[27124]: [0x103001050]       0x1007230f0  NSObject
objc[27124]: [0x103001058]       0x100721310  NSObject
objc[27124]: [0x103001060]  ################  POOL 0x103001060 // 这个是POOL_BOUNDRY
objc[27124]: [0x103001068]       0x10071dd60  NSObject
objc[27124]: ##############
Program ended with exit code: 0

调用 autorelease

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

函数 rootAutorelease

inline id 
objc_object::rootAutorelease()
{
 /**
 如果是标记指针,直接返回当期那对象
*/
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

调用rootAutorelease2()

id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

可以看到,调用 autorelease最后底层调用的是 AutoreleasePoolPage的公开方法,里面继续走了存放过程的流程。

  static inline id *autoreleaseFast(id obj)
    {
        //拿到当前页
        AutoreleasePoolPage *page = hotPage();
        /**
           如果当期页的还有存储空间,直接添加
           如果没有,创建新对象,然后再添加
        */
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            return autoreleaseFullPage(obj, page);
        } else {
            return autoreleaseNoPage(obj);
        }
    }

下面看下 pop流程, 也是调用AutoreleasePoolPage的公开方法, token参数为POOL_BOUNDARY,里面的核心方法为 page->releaseUntil(stop)

  
    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();

         /**
          核心代码: 从栈顶开始将 autorelease 对象释放
          
         */
        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)的实现

 void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        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
            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);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

那么,剩下最后一个问题,autorelease对象释放时机的问题。

我觉得回答这个问题,得分两种情况,
第一种,就是对象被 autoreleasepool包裹着,且有机会调用 AutoReleasePoolPage.pop(POOL_BOUNDARY),那么,对象就会立即被释放。

第二种,就是 下面这种情况

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

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

没机会调用 AutoReleasePoolPage.pop(POOL_BOUNDARY),因为这里会有 runloop,那是不是意味着在其他页面创建的 autorelease对象就没办法释放了?事实上,并不是。那就说明 main函数里的autoreleasepool并不管理其他地方创建的autorelease对象。事实上,这个跟 runloop有关系,iOS在主线程的RunLoop中注册了两个Observer

第 一 个 Observer监听了 RunLoopkCFRunLoopEntry 事件,会调用 objc_autoreleasePoolPush()
第 二 个 Observer 监听了 RunLoopkCFRunLoopBeforeWaiting事件,会调用 objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
第 二 个 Observer 还监听了 RunLoopkCFRunLoopExit事件,调用objc_autoreleasePoolPop()

这个如何验证,简单,打印下[NSRunLoop mainRunLoop]即可。


 activities = 0x1 = kCFRunLoopEntry
 // 第一个观察者
<CFRunLoopObserver 0x604000135720 [0x10bd9ec80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10bf4cd92), context = <CFArray 0x60400005a9a0 [0x10bd9ec80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8861003048>\n)}}",
 
 activities = 0xa0 = 160 ( = 128 + 32 ) = kCFRunLoopBeforeWaiting | kCFRunLoopExit
 
 // 第二个观察者
 <CFRunLoopObserver 0x604000135540 [0x10bd9ec80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10bf4cd92), context = <CFArray 0x60400005a9a0 [0x10bd9ec80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f8861003048>\n)}}"
 ),

再贴下 RunLoop的 activity

 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
 kCFRunLoopEntry = (1UL << 0),                 1
 kCFRunLoopBeforeTimers = (1UL << 1),          2
 kCFRunLoopBeforeSources = (1UL << 2),         4
 kCFRunLoopBeforeWaiting = (1UL << 5),         32
 kCFRunLoopAfterWaiting = (1UL << 6),          64
 kCFRunLoopExit = (1UL << 7),                  128
 kCFRunLoopAllActivities = 0x0FFFFFFFU
 };

讲了这么多,其实内管管理主要关注两个问题,也是主要解决的两大问题 : 内存泄露坏内存的访问(僵尸对象)。其中,内存泄露检查,可以使用 instrument 中的 leak工具检测,僵尸对象可以勾选 Zoombie object进行捕获。了解底层实现原理的只是帮助你更好的理解整个流程,我觉得还是取决于平常的编码习惯和经验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值