iOS内存管理-引用计数及autorelease

iOS内存管理-引用计数及autorelease

学习了好久的iOS内存管理,一直是断断续续的,现在有时间找了个机会总结了一下,有时候时间久了好多知识点就会遗忘,希望能将这些点记下来,多看几次。原文链接

前言:虚拟内存

移动设备的内存资源是有限的,当App运行时占用的内存大小超过了限制后,就会被强杀掉,从而导致用户体验被降低。所以,为了提升App质量,开发者要非常重视应用的内存管理问题。

移动端的内存管理技术,主要有两种:

  • GC(Garbage Collection,垃圾回收)的标记清除算法;

  • Apple使用的引用计数方法。

相比较于 GC 标记清除算法,引用计数法可以及时地回收引用计数为0的对象,减少查找次数。但是,引用 计数会带来循环引用的问题,比如当外部的变量强引用 Block时,Block 也会强引用外部的变量,就会出现 循环引用。我们需要通过弱引用,来解除循环引用的问题。

另外,在 ARC(自动引用计数)之前,一直都是通过 MRC(手动引用计数)这种手写大量内存管理代码的 方式来管理内存,因此苹果公司开发了 ARC 技术,由编译器来完成这部分代码管理工作。但是,ARC依然 需要注意循环引用的问题。

内存管理的演进过程:在最开始的时候,程序是直接访问物理内存,但后来有了多程序多任务同时运 行,就出现了很多问题。比如,同时运行的程序占用的总内存必须要小于实际物理内存大小。再比如,程序 能够直接访问和修改物理内存,也就能够直接访问和修改其他程序所使用的物理内存,程序运行时的安全就 无法保障。

虚拟内存:

由于要解决多程序多任务同时运行的这些问题,所以增加了一个 中间层 来间接访问物理内存,这个中间层就是虚拟内存虚拟内存通过映射,可以将虚拟地址转化成物理地址

虚拟内存会给每个程序创建一个单独的执行环境,也就是一个独立的虚拟空间,这样每个程序就只能访问自 己的地址空间(Address Space),程序与程序间也就能被安全地隔离开了。

32位的地址空间是 2^32 = 4294967296 个字节,共 4GB,如果内存没有达到 4GB 时,虚拟内存比实际的物 理内存要大,这会让程序感觉自己能够支配更多的内存。如同虚拟内存只供当前程序使用,操作起来和物理 内存一样高效。

有了虚拟内存这样一个中间层,极大地节省了物理内存。iOS的共享库就是利用了这一点,只占用一份物理 内存,却能够在不同应用的多份虚拟内存中,去使用同一份共享库的物理内存。

1、 引用计数 RetainCount

在iOS开发中,使用 引用计数 来管理OC对象的内存:

  • 一个新创建的OC对象,它的引用计数是1,当引用计数减为0 ,OC对象就会被销毁,释放其占用的内存空间;
  • 调用retain会让对象的引用计数+1;调用release会让对象的引用计数-1;
  • 内存管理总结经验:
    • 当调用alloc,new,copy,mutablecopy等返回一个对象,在不需要这个对象的时候,需要对这个对象进行release或者autorelease操作;
    • 想拥有某个对象,就让它的引用计数+1;不想拥有某个对象,就让它的引用计数-1;

1.1 引用计数存放的位置

从64bit开始,对象的引用计数存放在优化过的isa指针中,也可能存放在sideTable中:

isa结构体详情.png

  • extra_rc:存放的是对象的引用计数值减1;
  • has_sidetable_rc: 如果引用计数值过大,extra_rc中存放不下,这时候此值为1,对象的引用计数存放在sidetable中;

isa中存放的值.png

1.2 引用计数存放的位置sidetable和retainCount、release

1.2.1 SideTables与SideTable

当优化过的isa指针中,引用计数过大存放不下时,就会将引用计数存放到SideTable中;

SideTables其实是一个哈希表,可以通过对象的指针找到对象内容具体存放在哪个SideTable中:

SideTables

通过指针找到对象引用计数存放的SideTable:

image.png

SideTable对应结构如下图:

SideTable结构

在runtime源码中,NSObje.mm可以看到SideTable结构体源码:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; //referanceCount:引用计数表
    weak_table_t weak_table;//弱引用表:存放的对象的弱引用指针
};

问题:为什么不是一个SideTable而是多个SideTable?或者(为什么不将所有的对象放到一个table里面,而是放到不同的side-table里面?)

查找或者修改引用计数的时候是要加锁的,如果有多个对象同时查找引用计数:

  • 只有一张表的话,查询肯定是需要加锁,同步有先后顺序的;
  • 如果是有多张表,就可以异步进行查询,不同的表之间查询是没有影响的;–> 效率更高
1.2.2 retainCount

SideTable中的引用计数表中RefcountMap,存放的就是对象的引用计数:retainCount
runtime源码如下:


_objc_rootRetainCount(id obj){
    return obj->rootRetainCount();
}

objc_object::rootRetainCount()
{
    //如果是taggerPointer,直接返回指针;
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    
    //判断是否是优化过的isa指针
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;//extra_rc+1 isa指针中存储的引用计数值;
        if (bits.has_sidetable_rc) {
            // has_sidetable_rc值如果为1:表示引用计数存放在sidetable中
            rc += sidetable_getExtraRC_nolock();//细节:注意+=
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    
    //不是优化过的isa指针直接查找SideTable
    return sidetable_retainCount();
}

//优化过的isa指针在SideTable中查找引用计数
size_t objc_object::sidetable_getExtraRC_nolock()
{
    ASSERT(isa.nonpointer);
    
    //找到对应的table
    SideTable& table = SideTables()[this];
    
    //table.refcnts(引用计数表)中找到对应计数it
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

//没有优化过的isa指针在SideTable中查找引用计数
objc_object::sidetable_retainCount()
{
    //从sideTables中找出当前对象的sideTable
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    
    //table.refcnts(引用计数表)中找到对应计数it
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}


步骤:

  • 判断是否是TaggerPointer,如果是,直接返回指针
  • 判断是否是优化过的isa指针:
    • 如果是优化过的isa指针,先读取isa指针中存放的引用计数extra_rc
      • 如果has_sidetable_rc为1,代表引用计数存放在SideTable中;
      • 注意:rc = 1 + bits.extra_rc;–>rc += sidetable_getExtraRC_nolock();二者之和;
    • 如果不是优化过的isa指针,直接去SideTable中去查找引用计数值;
1.2.3 release

当调用alloc,new,copy,mutablecopy等返回一个对象,在不需要这个对象的时候,需要对这个对象进行release或者autorelease操作,让对象的引用计数-1;

  • 在ARC中,LLVM编译器会自动帮我们生成对应的[xxx release];
  • 在MRC中,需要我们手动添加[xxx release];

当对象的引用计数减为0时,就会调用dealloc()函数;

- (oneway void)release {
    _objc_rootRelease(self);
}

_objc_rootRelease(id obj){
    obj->rootRelease();
}

objc_object::rootRelease(bool performDealloc, bool handleUnderflow){
    if (isTaggedPointer()) return false;
    
    if (slowpath(!newisa.nonpointer)) {
        return sidetable_release(performDealloc);
    }
   
    //has_sidetable_rc为1,引用计数存放在sidetable中
    if (slowpath(newisa.has_sidetable_rc)) {
     
        // 从sidetable中读取对象引用计数值        
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
        if (borrowed > 0) {
            newisa.extra_rc = borrowed - 1;  
            // 存储修改过的引用计数值
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            return false;
        }
    }

    // Really deallocate.
    //如果对象的引用计数减为0,使用objc_msgSend()发送消息调用dealloc()方法;
    if (performDealloc) {
        objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

  • 注意在引用计数减为0时,会发送消息调用对象的dealloc()方法销毁对象,并释放对象占用的内存空间;

1.3 weak指针实现原理

示例如下图:
YYPerson只是重写了dealloc方法,添加打印对象释放时机;

  • 当在作用域{}中,LLVM编译器会自动添加release操作,使得引用计数-1;

    • __strong YYPerson * person1; // 强指针
    • __weak YYPerson * person2; // 弱指针
    • __unsafe_unretained YYPerson * person3; // 弱指针

1、当没有任何指针指向[YYPerson alloc]创建出来的对象时,出了{}作用域,直接释放:

image.png

2、 强引用:__strong:person1
当有强引用指针指向[YYPerson alloc]创建出来的对象时,引用计数+1;

image.png

3、弱引用

  • __weak:person2
  • __unsafe_unretained:person3

当有弱引用指针指向[YYPerson alloc]创建出来的对象时,引用计数不变;现象如下:

image.png

1.3.1 __weak 和 __unsafe-unretained区别

在对象释放以后调用weak指针: 打印显示指针为空;

weak打印.png

在对象释放以后调用:_unsafe-unretained指针,直接崩溃,显示野指针错误(指针指向地址的对象已经被释放)

__unsafe_unretained打印.png

区别

  • weak指针在对象销毁时,dealloc()方法会调用rootDealloc方法,最终会判断,如果该对象有弱引用,会将存放弱引用的散列表weakTable清空,所以最终weak指针会置为nil

  • __unsafe_unretained不会有将对应指针清空的操作,所以不太安全,在对象销毁释放以后,再去调用指针就回造成坏内存访问:EXC-BAD-ACCESS;

  • 注意: weak指针置为nil后,再调用方法,在objc_msgSend()中,会先判断,如果消息接收者为空,则直接return,所以不会调用方法,也不会crash;

1.3.2 objc_initWeak()

在进行编译过程前,clang 其实对 __weak 做了转换,调用objc_initWeak,将对应的弱引用指针存储到SideTable中:

NSObject objc_initWeak(&p, 对象指针);

id objc_initWeak(id *location, id newObj) {
    // 查看对象实例是否有效
    // 无效对象直接导致指针释放
    if (!newObj) {
        *location = nil;
        return nil;
    }

    // 这里传递了三个 bool 数值
    // 使用 template 进行常量参数传递是为了优化性能
    // storeWeak:将弱引用指针存放到SideTable中
    return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
        (location, (objc_object*)newObj);
}

这一块知识点参考了瓜神详细讲解weak实现原理的博文:
瓜神-弱引用的实现方式

1.4 dealloc()方法

当一个对象的引用计数减为0时,就会调用dealloc()方法释放对象并清理对应的内存空间

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

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

objc_object::rootDealloc(){
    if (isTaggedPointer()) return;  // 如果是isTaggedPointer直接return

    /*
      isa.nonpointer :0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
                       1,代表优化过,使用位域存储更多的信息
    
      isa.weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快

      
      isa.has_assoc:是否有设置过关联对象,如果没有,释放时会更快
      
    
      isa.has_sidetable_rc:引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
    **/

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        free(this);//上述条件都不满足,直接free(),释放当前对象
    } else {
        object_dispose((id)this);//清理其他相关和引用
    }
}


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

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // 判断是否有c++析构函数和关联对象
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // 处理c++析构函数相关:清除成员变量
        if (cxx) object_cxxDestruct(obj);
        //移除关联对象
        if (assoc) _object_remove_assocations(obj);
        //将指向当前对象的弱引用指针置为nil
        obj->clearDeallocating();
    }

    return obj;
}

objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // 普通的isa指针
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // 优化过的isa指针
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

objc_object::clearDeallocating_slow(){
    
    //从 SideTables中取出指针对应存放的SideTable
    SideTable& table = SideTables()[this];
    
    //如果有弱引用,清除弱引用指针
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    
    //如果引用计数不是存放在isa中,而是存放在SideTable中,清除SideTable中refcnts(引用计数表)中的引用计数
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
}

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) {
    objc_object *referent = (objc_object *)referent_id;

    //通过弱引用表weak_table和弱引用指针referent,找出weak_table中弱引用对象的索引entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        return;
    }

    // zero out references
    
    //DisguisedPtr<objc_object *> weak_referrer_t; 
    //weak_referrer_t是一个集合类型

    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;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            //遍历如果referrer == 传进来的referent,置为nil
            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_table对应索引entry中的内容
    weak_entry_remove(weak_table, entry);
}


2. autorelease 和 AutoReleasePool

在MRC环境下,有些对象的释放调用了[XXX autoerlease]来释放对象,调用autorelease的对象不会立即释放,也就是对象的引用计数不会立马 -1;

通过对autorelease方法的研究发现:

调用autorelease 的对象的生命周期是通过一个叫AutoreleasePoolPage的对象来管理的,调用autorelease的对象,其实在@autoreleasepool中执行了一下操作:

@autoreleasepool {
//        atautoreleasepoolobj = objc_autoreleasePoolPush();
       执行操作        
//        objc_autoreleasePoolPop(atautoreleasepoolobj);
}

  • objc_autoreleasePoolPush(): 入栈 --> 将对象加入到AutoreleasePoolPage表中;

  • objc_autoreleasePoolPop(): 出栈 --> 将对象从AutoreleasePoolPage表中移除;

2.1 AutoReleasePoolPage结构

调用autoreleasePoolPush()函数操作时,会将调用autorelease的对象加入到AutoReleasePoolPage中;

AutoReleasePoolPage其实是:以栈为节点通过双向链表的形式链接起来的数据结构

AutoReleasePoolPage结构体成员

其结构体成员中,有以下注意的地方

  • next : 指向的是下一个可以存放元素的位置;
  • thread : 线程,AutoReleasePoolPage和线程是对应关系
  • parent : 双向链表中的 prev指针,指向上一个AutoReleasePoolPage
  • child : 双向链表中的 next指针,指向下一个AutoReleasePoolPage

AutoReleasePoolPage结构

2.1.1 autoreleasePoolPush

AutoReleasePoolPage是一个 栈的结构,栈的特点是: 先进后出 :

如下图,入栈顺序为0-9,出栈顺序为9-0:

栈:先进后出

在执行push操作时,例如添加一个obj(3):

  • 先将哨兵对象指向的位置置为nil;

  • 将push进来的对象指针添加到

  • 再将next指针和哨兵对象向上移动;

如下图:哨兵对象其实是指一个值为POOL_BOUNDARY的值,这个标识了当前autoreleasePool池的起始位置;

push

autorelease流程:

首先判断next指针是否在栈顶,每个autorelpool都是4096字节:

  • 当next指针指向栈顶的时候,当前page已存满,重新创建一个page对象来存放push进来的对象
  • 当next不是栈顶时,直接执行入栈操作;

image.png

2.1.2 autoreleasePoolPop

执行pop操作有以下流程:

  • 根绝传入的哨兵对象找到对应的位置
  • 对上次执行push操作添加的所有对象依次发送release消息
  • 回退next指针到正确的位置(到另一个哨兵对象的位置,一个page对象只有一个next指针,但是哨兵对象可以有多个)

执行pop操作前:
执行pop操作

执行pop操作结束后:到下一个哨兵对象前的对象都被释放,并改变next指针的位置。

执行pop操作结束

所以,总结一下:

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

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

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

2.1.2 多个@autoreleasepool和嵌套使用

在有多个@autoreleasepool{}时,遵循3步:
1、 push入栈;
2、 执行代码;
3、 pop出栈;

多个平级的@autoreleasepool{};

image.png

多个嵌套的@autoreleasepool{};
image.png

3. autorelease与runloop的关系

经过前面的了解,调用autorelease的对象都是通过autoreleasePoolPage来管理的,而autoreleasePoolPage结构体对象中,有一个NSThread对象,说明autoreleasePoolPage来管理对象的入栈和出栈和线程有一定的关系,而线程处理任务离不开runloop,所以得仔细探究一下runLoop和autorelease之间的联系;

我们知道,在Runloop中,runloop有不同的模式,每种Mode下都会有observers,source0,source1,timers,_name,其中sources和timers是runloop需要处理的任务;

处理timer

在runloop每一次运行循环中,都会处理一遍所有的timers和blocks,然后进入休眠状态,如果有事件处理,就被唤醒,再次进行循环,处理一遍这些事情;

但是在runloop进行休眠之前,也就是在状态kCFRunLoopBeforeWaiting之前,会处理一些特殊的事情,比如刷新界面的UI:

问题: 在修改UI,背景色或添加一个subview等操作后,是立即生效执行的吗?

答案是:不会立即生效,而是在当前线程进入休眠,也就是kCFRunLoopBeforeWaiting之前进行刷新操作(刷新UI是在主线程,所以当前线程是指主线程);

所以猜测: 调用autorelease的对象也有可能是在当前线程休眠的时候出栈释放的;

以下进行验证:

3.1 添加observer监听runloop的状态

runloop的状态是一个枚举,其对应的各种状态值如下:

runloop状态

有以下两种方式添加一个observer监听Runloop的状态:(注意C语言创建的对象最后都需要release)

方式一: 添加一个C语言ObserverCallBack()函数回调:

void ObserverCallBack (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
        break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
        break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
        break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
        break;
        
        break;
        default:
            break;
    }
}

- (void)observeRunloopMode{
    CFRunLoopObserverRef observeRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0,  ObserverCallBack, NULL);
    
    CFRunLoopRef current = CFRunLoopGetCurrent();
    
    CFRunLoopAddObserver( current, observeRef, kCFRunLoopCommonModes);
    
    CFRelease(current);

    CFRelease(observeRef);

}

方式二: 直接使用block(这种方式更简单)

- (void)observeRunloopMode{

    CFRunLoopObserverRef observeRef =CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
        switch (activity) {
            case kCFRunLoopEntry:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry ----- %@",mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopBeforeWaiting:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopBeforeWaiting ----- %@-----------",mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopAfterWaiting:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopAfterWaiting ----- %@",mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopBeforeTimers:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopBeforeTimers ----- %@",mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopBeforeSources:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopBeforeSources ----- %@",mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopExit:{
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit ----- %@",mode);
                CFRelease(mode);
                break;
                break;
            }
            default:
                break;
        }        
    });
    
    CFRunLoopRef current = CFRunLoopGetCurrent();
    
    CFRunLoopAddObserver( current, observeRef, kCFRunLoopCommonModes);
    
    CFRelease(current);

    CFRelease(observeRef);
}

有了监听Runloop状态的方法,验证猜测结果如下:

释放时机

所以结论如下:

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

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush();(优先级最高,保证创建释放池发生在其他所有回调之前)

  • 第2个Observer

    • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush();

    • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

子线程 中:

  • 子线程创建的时候就会创建一个autoreleasepool,并且在线程退出的时候,清空autoreleasepool。

结语

笔记中提到的内容大部分总结自小码哥底层原理,仔细总结起来发现好多细节要自己实现了才能深刻的理解;
学无止境,关于内存管理这块还有很多需要查缺补漏的地方,这篇只是自己的学习笔记,有不对的地方请见谅。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值