美文网首页
IOS基础:内存管理

IOS基础:内存管理

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-22 14:49 被阅读0次

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、内存布局
  • 二、内存管理方案:Tagged Pointer
    • 1、引入原因
    • 2、特点
    • 3、原理
    • 4、面试题
  • 三:内存管理方案:SideTables 散列表中的引用计数表
    • 1、简介
    • 2、MRC
    • 3、ARC
  • 四:内存管理方案:SideTables 散列表中的弱引用表 weak
    • 1、简介
    • 2、常见的循环引用场景:NSTimer
    • 3、探索NSTimer循环引用的解决方案
    • 4、weak的源代码解析
  • 三、自动释放池 Autoreleasepool
    • 1、简介
    • 2、手动调用autoreleasepool
    • 3、子线程AutoRelease对象何时释放
  • Demo
  • 参考文献

一、内存布局

内存布局

栈区

  • 创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
  • 在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。
  • 和堆一样,用户栈在程序执行期间可以动态地扩展和收缩,向下增长。

堆区

  • 通过newalloc等分配的对象、block经过copy后,c通过malloc
  • 它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会回销毁该内存区域对象)。
  • 一般一个 new 就要对应一个release。在ARC下编译器会自动在合适位置为OC对象添加release操作,会在当前线程Runloop退出或休眠时销毁这些对象。MRC则需程序员手动释放。
  • 堆可以动态地扩展和收缩,向上增长。

NSString的对象就是stack 中的对象,NSMutableString 的对象就是heap 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,适用于计数管理内存管理模式。两类对象的创建方法也不同,前者直接创建NSString * str1=@"welcome";,而后者需要先分配再初始化NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];

bss(静态区)

未初始化的全局变量和静态变量,程序运行过程内存的数据一直存在,程序结束后由系统释放。

data(常量区)

已初始化的全局变量、静态变量、常量,在程序结束后由系统释放。

text(代码区)

用于存放程序运行时的代码,是被编译成二进制的程序代码。

二、内存管理方案:Tagged Pointer

1、引入原因

通常我们创建对象,对象存储在堆中,对象的指针存储在栈中,如果我们要找到这个对象,就需要先在栈中,找到指针地址,然后根据指针地址找到在堆中的对象。

这个过程比较繁琐,当存储的对象只是一个很小的东西,比如一个字符串,一个数字。去走这么一个繁琐的过程,无非是耗费性能的,所以苹果就搞出了TaggedPointer这么一个东西。

2、特点

  • TaggedPointer是苹果为了解决32位CPU64位CPU的转变带来的内存占用和效率问题,专门用来存储小的对象,针对NSNumberNSDate以及部分NSString的内存优化方案。
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮囊的普通变量而已。所以,它的内存并不存储在堆中,也不需要mallocfree
  • Tagged Pointer指针中包含了当前对象的地址、类型、具体数值。因此Tagged Pointer指针在内存读取上有着3倍的效率,创建时比普通需要mallocfree的类型快106倍。

3、原理

苹果将Tagged Pointer引入,给 64 位系统带来了内存的节省和运行效率的提高。Tagged Pointer通过在其最后一个 bit位设置一个特殊标记,用于将数据直接保存在指针本身中。因为Tagged Pointer并不是真正的对象,我们在使用时需要注意不要直接访问其isa变量。

如果这个整数只是一个 NSIntegerNSNumberNSDate的普通变量,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的,占用的内存会翻倍。

为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失

int main(int argc, char * argv[])
{
    @autoreleasepool {
        NSNumber *number1 = @1;
        NSNumber *number2 = @2;
        NSNumber *number3 = @3;
        NSNumber *numberFFFF = @(0xFFFF);

        NSLog(@"number1 pointer is %p", number1);
        NSLog(@"number2 pointer is %p", number2);
        NSLog(@"number3 pointer is %p", number3);
        NSLog(@"numberffff pointer is %p", numberFFFF);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

//输出结果
number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberFFFF pointer is 0xb0000000000ffff2

苹果确实是将值直接存储到了指针本身里面。我们还可以猜测,数字最末尾的 2 以及最开头的 0xb 是否就是苹果对于Tagged Pointer的特殊标记呢?

我们尝试放一个 8 字节的长的整数到NSNumber实例中,对于这样的实例,由于Tagged Pointer无法将其按上面的压缩方式来保存,那么应该就会以普通对象的方式来保存,我们的实验代码如下:

NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);

// 输出
bigNumber pointer is 0x10921ecc0

验证了我们的猜测,bigNumber的地址更像是一个普通的指针地址。可见,当 8 字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果 8 字节承载不了时,则又用以前的方式来生成普通的指针。

4、面试题

为什么第二个for会崩溃?答:taggedpointer

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (int i = 0 ; i<1000000; i++) {
            self.str = @"abcd";
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0 ; i<1000000; i++) {
            self.str = [NSString stringWithFormat:@"adfalkdjfldkasjflakjsdkflasf-- %d",I];
        }
    });

执行了objc_release(id obj),由于大量的循环,导致了线程问题,使引用计数<=-1。但是由于第一个循环中的objtaggedpointer类型的string,会直接return obj,并不会release

这里release,那retain的时候咋办呢,引用计数是一直往上增吗?并不是,在objc_retain(id obj)中,同样判断了obj->isTaggedPointer,如果是true,就直接return obj

三、内存管理方案:SideTables 散列表中的引用计数表

1、简介

SideTables是一个64个元素长度的hash数组,里面存储了SideTableSideTableshash键值就是一个对象objaddress。因此可以说,一个obj对应了一个SideTable,但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable

SideTables 散列表 SideTable结构
a、什么是Spinlock_t 自旋锁?

自旋锁若已被其他线程获取,则当前线程会不断探索该锁是否被释放,如果释放则第一时间获取,适用于轻量访问的情况。

b、为什么不是一个SideTable?

一张表的引用计数或者弱引用需要顺序操作,这就需要等前一个解开锁才能继续工作,存在效率问题。所以通过分离锁打散成多张表(共8个),支持并发操作,提高访问效率。

c、怎样实现快速分流,即通过指针快速定位到属于哪一张SideTable表?

SideTable本质是一个哈希表,给定值是对象内存地址,目标值是数组下标索引,哈希查找通过取余存储值和取值,内存地址均匀分布。哈希函数是一个均匀散列函数,不需要通过遍历操作,效率很高。

2、MRC

a、什么是MRC?

MRC自己负责管理对象生命周期,负责对象的创建和销毁。

b、MRC举例分析
    NSNumber *i = [NSNumber numberWithInteger:2];
    [i retainCount];//1

    NSMutableArray *array = [NSMutableArray array];
    [array addObject:i];
    [i retainCount];//2

    NSNumber *j = i;//不获得i的所有权
    [i retainCount];//2

    NSNumber *j = [i retain];//获得
    [i retainCount];//3

    [i release];//2

    [array removeObjectAtIndex:0];
    [i retainCount];//1

    [i release];//销毁
    
    //释放动态分配的内存和关闭文件
    - (void)dealloc{[super dealloc];}
c、MRC通过引用计数如何管理对象生命周期?
alloc
retain +1
release -1
retainCount 引用计数值
autorelease 结束时候调用release释放
dealloc

alloc实现:经过一系列调用,最终调用了C函数的calloc,但此时并没有将引用计数+1

retainCount实现:alloc对象引用计数值为0,之所以 retainCount能获取到1,是因为添加了局部变量size_t refcnt_result令其值为1,再让其实现相加操作。

SideTable &table = sideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find(this);
refcnt_resount+= it->second>>SIDE_TABLE_RC_SHIFT

retain实现:经过两次哈希查找,第一次从SideTables中查找到对象所在的SideTable,第二次从引用计数表中查找到该对象的引用计数值实现+1(实际是通过地址偏移量来实现的)。

SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage += SIZE_TABLE_RC_ONE

release实现:经过两次哈希查找,第一次从SideTables中查找到对象所在的SideTable,第二次从引用计数表中查找到该对象的引用计数值实现-1(实际是通过地址偏移量来实现的)。

SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage -= SIZE_TABLE_RC_ONE;
d、MRC经常会出现的内存泄漏问题

内存泄漏:指一个对象或变量在使用完成后没有释放掉。

容易导致内存泄漏的使用方式:

  • string开头的方法,它是静态工厂方法,通过类直接调用 。 对于 使用该方法创建的对象, 其所有权非调用者所有 , 调用者无权释放它,否则就会因过度释放而“僵尸化"。
  • 采用allocnewcopymutableCopy所创建的对象,所有权属于调用者,它的生命周期由调用者管理,调用者负责通过releaseautorelease方法释放对象。

如何查找泄漏点?
Analyzeproduct-->Analyze)是静态分析工具 , Instrumentsproduct-->profile)是动态分析工具( LeaksAllocations)。

静态分析方法能发现大部分的问题,但是仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,因为有的内存泄露是在运行时,用户操作时才产生的,所以我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments中的Leaks方法进行排查。

Leaks
点击左上角的红色圆点,这时项目开始启动了,由于leaks是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。 Leaks
选中Leaks Checks,在Details所在栏中选择CallTree,并且在右下角勾选Invert Call TreeHide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。
Leaks
  • 监控内存分布情况,随着时间的变化内存使用的折线图,有红色菱形图标 O 出现 , 则有内存泄漏
  • 显示泄漏的对象,列出了它们的内存地址 、 占用字节 、 所属框架和响应方法等信息
  • RetCt是引用计数列,最后的引用计数不为零 ,这说明该对象内存没有释放
  • 点击右边的跟踪栈信息按钮,可以定位到泄露点在项目中的代码位置
  • 调用者没有这个对象的所有权而释放它 ,都会造成过度释放,从而产生僵尸对象(Zombies分析模板),试图调用僵尸对象 ,则 会崩溃(应用直接跳出),并抛出EXC_BAD_ACCESS异常,僵尸对象的引用计数变化是 : l (创建) ----->0 (释放)一 (僵尸化)

3、ARC

a、什么是ARC?

ARCLLVMRuntime协同作用的结果,在这种模式下,程序员不需要清楚地了解获得/放弃对象所有权的时机,ARC 会自动添加被注释掉的行,即在适当的位置插入retainreleaseautorelease 函数,这样程序员能将更多的精力用于开发程序的核心功能。

b、ARC有什么好处?
  • 不需要再在意对象的所有权
  • 可以删除程序中内存管理部分的大部分代码,使程序看起来更清爽
  • 可以避免手动内存管理时的错误(内存泄漏等)
  • 可以使多线程环境下的编程更简单。例如:不用担心不同的线程之间可能出现的所有权冲突
c、ARC有什么特点?
  • ARC会禁止调用引用计数的相关函数retainreleaseautoreleaseretainCount
  • 可以重写dealloc方法,但是不能显示调用super.dealloc
  • ARC中新增weakstrong属性关键字。

四、内存管理方案:SideTables 散列表中的弱引用表 weak

1、简介

a、什么是循环引用?

当双方都在等待对方释放的时候,就形成了循环引用,结果两个对象都不会被释放,只有打破循环引用关系才能够释放内存。

ARC代码中的内存泄漏多半是由于强引用循环引起的 ,从而导致一些内存无法释放,这就会导致dealloc()方法无法被调用, Leaks模板提供了查看引用循环视图,选择Cycles & Roots菜单项即可查看。

b、循环引用的解决方案有哪些?
  • 通过使用弱引用既可以防止生成循环引用,实例对象被释放后自动变成nil,又可以防止对象被释放后形成野指针。
  • 或者通过手动给一方赋值为nil 来打破循环引用关系

2、常见的循环引用场景:NSTimer

a、NSTimer的创建

第一中方法需要手动加入Runloop

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];

第二中方法自动加入Runloop

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                              target:weakSelf
                                            selector:@selector(fireHome)
                                            userInfo:nil
                                             repeats:YES];

调用的方法为:

- (void)fireHome {
    num++;
    NSLog(@"hello word - %d",num);
}
b、NSTimer循环引用分析

发现VC pop之后,定时器并没有停止输出,deinit(swift)方法也没有执行,这就是循环引用了。这个TimeraddTargetUIButtonaddTarget有什么不同呢?buttontarget是弱引用,Timertarget是强引用。self 强持有timer我们都能直接看出来,target方法中,timerself对象进行了强持有,因此造成了循环引用。

3、探索NSTimer循环引用的解决方案

a、weakSelf

按照惯例用weakSelf去打破强引用的时候,发现weakSelf没有打破循环引用,timer仍然在运行。

self -> timer -> weakSelf -> self

变成了self -> timer -> weakSelf -> selftimer之所以无法打破循环关系是因为timer创建时target是对weakSelf的对象强持有操作,而weakSelfself虽然是不同的指针但是指向的对象是相同的,也就相当于间接的强持有了self,所以weakSelf并没有打破循环引用关系。

那么block使用weakSelf为什么可以打破循环引用呢?

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg; 
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object; // 实际上是指针赋值

.cpp文件中我们可以看到如上代码段,虽然weakself对象传入进来,但是内部实际操作的是对象的指针,也就是weakself的指针,我们知道weakselfself虽然内存地址相同,但指针地址是不一样的,也就是block中并没有直接持有self,而是通过weakSelf指针操作,所以就打破了self -> block -> weakSelf -> selfself这一层的循环引用,变成了self -> block -> weakSelf (临时变量的指针地址)来打破循环引用。

b、使用weak关键字修饰

一说到循环引用很容易就想到weak,但是这里用weak不行。这要从weak修饰的对象的释放时机说起,用了weak关键字修饰之后,系统会在一个hash表中增加一对key valuekey就是这个对象的hash值,value就是这个对象的指针地址。

我们都知道每一个runloop都有自己的一个autorelease pool,在一次runloop结束之后会销毁这个autorelease pool,在释放这个autorelease pool的时候,也会到hash表中找到weak的对象把它和它的指针都释放掉,同时,那么问题来了,我这个timer是在runloop里面重复执行的,换而言之,这个runloop是一直在执行的,所以这个池子根本不会释放啊有木有,所以用它没什么卵用啊。

c、使用invalidate结束timer运行

我们第一时间肯定想到的是[self.timer invalidate]不就可以了吗,当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear还是viewDidDisAppear?实际上在我们实际操作中,如果当前页面有push操作的话,而当前页面并没有pop即还在栈里面,这时候我们释放timer肯定是错误的,所以这时候我们可以用到下面的方法:

- (void)didMoveToParentViewController:(UIViewController *)parent {
    // 无论 push进来 还是 pop出去 都能正常运行
    // 就算继续push到下一层 pop回去还是能够继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

为什么不在deinit(swift)方法里面释放?因为denint方法都不执行了,写了也没用。

d、中介者模式
中介者模式

换个思路,timer会造成循环引用是因为target强持有了self,造成的循环引用,那我们是否可以包装一下target,使得timer绑定另外一个不是selftarget对象来打破这层强持有关系。

中介者模式

根据打印结果我们发现在dealloc的时候也可以实现timer的释放,打破了循环引用。class_addMethod的作用看着是给target增加了一个方法,但是实际上timer的执行是在fireHomeObjc里面执行的,而不是应该执行的fireHome函数。

优化下代码,既然class_addMethod中需要一个函数的IMP,那么我们直接获取fireHomeIMP就可以了:

self.target = [[NSObject alloc] init];
Method method = class_getInstanceMethod([self class], @selector(fireHome));
class_addMethod([self.target class], @selector(fireHome), method_getImplementation(method), "v@:");
self.timer = [NSTimer
              scheduledTimerWithTimeInterval:1
              target:self.target
              selector:@selector(fireHome)
              userInfo:nil
              repeats:YES];
e、NSProxy虚基类的方式

NSProxy是一个虚基类,它的地位等同于NSObject。我们不用self来响应timer方法的target,而是用NSProxy来响应。虚基类方法是用proxy打破self这一块的循环。

// XJPProxy.h
@interface XJPProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

//XJPProxy.m
@interface XJPProxy()
@property (nonatomic, weak) id object;
@end

@implementation XJPProxy
+ (instancetype)proxyWithTransformObject:(id)object {
    XJPProxy *proxy = [[XJPProxy alloc] init]; 
    proxy.object = object; // 我们拿到外边的self,weak弱引用持有
    return proxy;
}

// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// proxy虚基类并没有持有vc,而是消息的转发,又给了vc
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

//VC.m
- (void)viewDidLoad {
    [super viewDidLoad];
    self.proxy = [DZProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}

4、weak的源代码解析

a、weak_entry_t
  • 查找weak_entry_t
  • 判断weak_table_t是否需要扩容
  • 插入新的weak_entry_t
  • 移除weak_entry_t

查找weak_entry_t:找到对应对象的存放位置,需要处理hash冲突,如果存在hash冲突具体会往下一个查找,查找到返回对应weak_entry_t,当超过了最大的冲突处理次数后,说明没有查找到,就会返回nil,源代码如下:

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;
    
    ///hash算法
    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;

    ///找到对应对象的存放位置,需要处理hash冲突,如果存在hash冲突具体会往下一个查找,直到找到空位,这个就是开放地址法处理hash冲突
    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++;///冲突处理次数加1

        ///当超过了最大的冲突处理次数后,说明没有查找到,就会返回nil。
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    ///查找到返回对应weak_entry_t
    return &weak_table->weak_entries[index];
}

判断weak_table_t是否需要扩容:默认容量大小初始值是64,超过最大容量的3/4,就在原来的容量基础上*2,源代码如下:

static void weak_grow_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    ///超过最大容量的3/4
    if (weak_table->num_entries >= old_size * 3 / 4) {
        ///在原来的容量基础上*2,默认初始值是64
        weak_resize(weak_table, old_size ? old_size*2 : 64);
    }
}

插入新的weak_entry_t:通过hash算法找到为nil的位置,存储weak_entry_t,源代码如下:

static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
    weak_entry_t *weak_entries = weak_table->weak_entries;
    assert(weak_entries != nil);

    ///跟查找一样的hash算法
    size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t index = begin;
    size_t hash_displacement = 0;

    ///找到为nil的位置
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_entries);
        hash_displacement++;///当前的最大冲突次数
    }

    weak_entries[index] = *new_entry;///存储weak_entry_t
    weak_table->num_entries++;///已存储位置数+1

    if (hash_displacement > weak_table->max_hash_displacement) {
        ///当前冲突次数比max_hash_displacement大,则赋值给max_hash_displacement
        weak_table->max_hash_displacement = hash_displacement;
    }
}

移除weak_entry_t:分别从静态和动态数组中移除即可,代码量挺大,如下:

static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    ///从静态数组中移除
    if (! entry->out_of_line()) {
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == old_referrer) {
                entry->inline_referrers[i] = nil;
                return;
            }
        }
        _objc_inform("Attempted to unregister unknown __weak variable "
                     "at %p. This is probably incorrect use of "
                     "objc_storeWeak() and objc_loadWeak(). "
                     "Break on objc_weak_error to debug.\n", 
                     old_referrer);
        objc_weak_error();
        return;
    }

    ///从动态数组中移除
    size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != old_referrer) {
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
        hash_displacement++;
        if (hash_displacement > entry->max_hash_displacement) {
            _objc_inform("Attempted to unregister unknown __weak variable "
                         "at %p. This is probably incorrect use of "
                         "objc_storeWeak() and objc_loadWeak(). "
                         "Break on objc_weak_error to debug.\n", 
                         old_referrer);
            objc_weak_error();
            return;
        }
    }
    entry->referrers[index] = nil;
    entry->num_refs--;
}
b、添加weak变量

添加的位置是通过哈希算法进行添加的位置查找,如果查找到的位置已经有了当前对象的弱引用数组,就把新的weak变量添加到该数组,如果没有创建新数组,添加weak变量到第一个,其余初始化为nil

  1. 调用objc_initWeak()
  2. 调用storeWeak()
  3. 调用weak_register_no_lock()

步骤一:objc_initWeak方法中的两个参数location是指weak指针,newObjweak指针将要指向的对象,源代码如下:

id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

步骤二:这里要先进行haveOld判断,也就是如果该指针有指向的旧值,先要weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);,处理旧值即移除weak指针。然后通过 weak_register_no_lock(&newTable->weak_table, (id)newObj, location,crashIfDeallocating);进行赋值操作即插入weak指针,源代码如下:

    ///如果有旧指向,从旧表中移除weak地址值
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    if (haveNew) {
        ///在新表中插入weak指针的地址,成功返回newObj,否则返回nil
        newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
        
        ///Tagged Pointer是苹果在64位系统之后,用来优化内存的 
        if (newObj  &&  !newObj->isTaggedPointer()) {
            ///在引用计数表中标记有弱引用指向,当引用计数为0时,触发移除相对应的weak指针。
            newObj->setWeaklyReferenced_nolock();
        }

        *location = (id)newObj;///将weak指针指向新的对象
    }

步骤三:weak_register_no_lock()的功能是判断是否存在弱引用weak_entry_t,有,则在对应的数组中插入当前需要插入的weak指针,没有,新建一个weak_entry_t数据插入到弱引用表中,源代码如下:

weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    if (!referent  ||  referent->isTaggedPointer()) return referent_id;///运用TaggedPointer计数,无需维护weak_table_t弱引用表

    weak_entry_t *entry;///弱引用 
    if ((entry = weak_entry_for_referent(weak_table, referent))) {///在弱引用表中查找weak_entry_t,存在
        append_referrer(entry, referrer);///插入weak指针 
    } 
    else {///没有weak_entry_t
        weak_entry_t new_entry(referent, referrer);///new一份weak_entry_t
        weak_grow_maybe(weak_table);///判断weak_table是否需要扩容 
        weak_entry_insert(weak_table, &new_entry);///插入weak指针,
    }
    return referent_id;
}
c、清除weak变量
❶ dealloc的流程图

举个例子:

__weak Person *weakPerson = person;

假如我们的person对象已经被释放掉了,那么就需要告诉对象weakPerson一声,防止野指针,导致程序崩溃,而clearDeallocating函数所做的事情就是把很多类似weakPerson这样的弱引用全部都置为nil。具体weak是如何实现的,这里给了一个表:

weak_table

❶ 从图中知道有个全局表叫SideTables,它里面维护了所有对象的引用计数,还有弱引用对象。当RefcountMap表中某个对象的引用计数为0时,系统会调用此类的dealloc方法,再调用其父类的dealloc,沿着继承链一直调用到NSObjectdealloc方法,最终走到objc_destructInstance函数里,主要操作四个:释放实例变量、移除关联属性、把弱引用置为nil、释放自己self

dealloc实现

在调用dealloc之后,会经过 _objc_rootDealloc()rootDealloc(),然后判断是否可以释放?判断释放的条件比较关键:

  • nonpointer_isa:判断这个isa指针类型,是否为非指针型的isa
  • weakly_referenced:是否有弱引用
  • has_assoc:是否有关联对象
  • has_cxx_dtor:判断是否有C++实现或者ARC的实现
  • has_sideTable_rc:引用计数是否在sideTable中有存储

如果有一个满足条件,调用 object_disponse(),再开始释放,objc_dispose()的实现如下:

objc_dispose()的实现

先判断是否有C++实现,然后判断是否有关联对象,如果没有c++,也没有关联对象,则直接调用clearDeallocating(),否则分别调用object_cxxDestruct()_object_remove_assocaations来释放。在这里系统内部实现清除了分类里定义的关联对象实例。

clearDeallocating实现

接下来在调用 clearDeallocating的方法中,会调用sidetable_calearDeallocationg()weak_clear_no_lock()两个方法,作用是指向该对象的弱引用指针置为nil,这就是为什么我们不需要在dealloc中将指向他的弱引用指针置为nil的原因,接下来会调用table.refcnts.erase(),来进行引用计数的擦除操作,然后结束流程。

❷ dealloc的源码解析

销毁对象这块最终需要调用到NSObject类中的dealloc方法:

//释放对象
- (void)dealloc {
    _objc_rootDealloc(self);
}

沿着调用函数流程dealloc—>_objc_rootDealloc—>rootDealloc object_dispose—>objc_destructInstance走最终会发现:

void *objc_destructInstance(id obj) 
{
    if (obj) {
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //---释放 C++ 的实例变量 
        if (cxx) object_cxxDestruct(obj);
        //---移除关联属性
        if (assoc) _object_remove_assocations(obj);
        //---将弱引用置为nil
        obj->clearDeallocating();
    }
    return obj;
}
❸ 清除weak变量

weak_table里有个数组weak_entries,数组里元素都是结构体weak_entry_t,这结构体里面有被引用的对象referent,还有引用数组referrersinline_referrers。需要注意的是,不是所有的对象都具有结构体weak_entry_t的,只有当某个对象具有弱引用时,才会给这对象创建一个weak_entry_t,并把它添加到数组weak_entries中去。同理当一个对象变得没有弱引用时,会从数组weak_entries中删去它对应的weak_entry_t。我们知道一个对象可能有多个若引用,比如:

__weak Person *weakPerson1 = person;
__weak Person *weakPerson2 = person;

此时weakPerson1weakPerson2都会放到weak_entry_t中的referrers数组或者inline_referrers数组中去,二者区别主要是看数组长度大小超过4了没有,超过4,则放到referrers中,否则放到inline_referrers中,此时对应的referentperson

这里说个非常重要的头文件objc-weak.h,它专门处理OC中对象的弱引用问题,里面有几个核心方法:

  • weak_register_no_lock:给指定对象添加一个弱引用,当我们执行_weak ClassA *objB = objA类似代码时,会触发NSObjectobjc_initWeak方法,最终会调用到weak_register_no_lock方法。
  • weak_unregister_no_lock:移除指定对象的一个弱引用。
  • weak_is_registered_no_lock:判断指定对象是否存在弱引用。
  • weak_clear_no_lock:清除指定对象所有弱引用,上述clearDeallocating里最终调用的就是此方法。

对象dealloc后,会调用弱引用清除函数weak_unregister_no_lock,根据该对象查找弱引用表,遍历弱引用数组所有指针,分别置为nil,源代码如下:

weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{
    //weak 指针指向的对象
    objc_object *referent = (objc_object *)referent_id; 
    //referrer_id是 weak 指针, 操作时需要用到这个指针的地址
    objc_object **referrer = (objc_object **)referrer_id; 

    weak_entry_t *entry;

    if (!referent) return;

    //查找 referent 对象对应的 entry
    if ((entry = weak_entry_for_referent(weak_table, referent))) {  
        //从 referent 对应的 entry 中删除地址为 referrer 的 weak 指针
        remove_referrer(entry, referrer); 

        //如果 entry 中的数组容量大于 4 并且数组中还有元素
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) { 
            empty = false; //entry 非空
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                //否则循环查找 entry 数组, 如果 4 个位置中有一个非空
                if (entry->inline_referrers[i]) { 
                    empty = false;  //entry 非空
                    break;
                }
            }
        }
        if (empty) {
            //从 weak_table 中移除该条 entry
            weak_entry_remove(weak_table, entry); 
        }
    }
}

其中从 weak_table 中移除该条 entryweak_entry_remove()方法源代码如下:

static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    ///如果是静态数组,直接释放静态数组
    if (entry->out_of_line()) free(entry->referrers);
    ///释放entry
    bzero(entry, sizeof(*entry));

    ///对应的已存数量-1
    weak_table->num_entries--;

    ///这里会查看下weakTable的容量是否需要减容
    weak_compact_maybe(weak_table);
}

上面讲到过扩容,相对应的这里使用到的减容的方法 weak_compact_maybe()源代码如下:

static void weak_compact_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    //当就容量超过1024且是利用率不及1/16的时候,减容
    if (old_size >= 1024  && old_size / 16 >= weak_table->num_entries) {
         ///在原有的基础上除以8
        weak_resize(weak_table, old_size / 8);
    }
}

三、自动释放池 Autoreleasepool

1、简介

a、什么是@autoreleasepool?

用来管理自动释放池。release消息马上将引用计数减1,而使用autorelease 对象的引用计数并不变化,而是向内存释放池中添加 一 条记录,会延迟到内存释放池周期到后,向池中所有对象发送 release消息,引用计数减 1。 当引用计数为 0时,对象所占用的内存才被释放 。

应用程序入口文件 main.m,代码被包裹在@autoreleasepool {... }之间,这是池的作用范围,默认是整个应用,释放默认在程序结束。

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

编译的时候,这段代码会被转换成:

{
    __AtAutoreleasePool __autoreleasepool;
}

其中,出现的结构体__AtAutoreleasePool为:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

这表明,我们的main函数实际执行了:

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        // do whatever you want
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

@autoreleasepool只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。

b、Autorelease Pool的主要结构

每一个autorelease pool都是由一系列的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是以双向链表的结构连接起来的,并且和线程是一一对应的关系。而在自动释放池的内存中,AutoreleasePoolPage被以栈结构存储起来。

c、什么对象会加入Autoreleasepool中
  • 使用allocnewcopymutableCopy的方法进行初始化时,由系统管理对象,在适当的位置release
  • 使用array会自动将返回值的对象注册到Autoreleasepool
  • __weak修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool中。
  • id的指针或对象的指针,在没有显示指定时会被注册到Autoleasepool中。
d、Autorelease对象的释放时机

autorelease释放对象的依据是Runloop,简单说,runloop就是iOS中的消息循环机制,当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。

2、手动调用autoreleasepool

既然由runloop来决定对象释放时机而不是作用域,那么,在一个{}内使用循环大量创建对象就有可能带来内存上的问题,大量对象会被创建而没有及时释放,这时候就需要靠我们人工的干预autorelease的释放了。手动添加的是大括号结束的时候释放。

上文有提到autorelease pool,一旦一个对象被autorelease,则该对象会被放到iOS的一个池:autorelease pool,其实这个pool本质上是一个stack,扔到pool中的对象等价于入栈。

objc_autoreleasePoolPush()的入栈流程图如下:

  1. push就是在page中插入一个哨兵对象,代表这些属于要一起release的对象。
  2. 如果page满了,则创建新的page,和老的page关联起来,将对象指针压栈。
objc_autoreleasePoolPush()

多层嵌套就是多次插入哨兵对象。

同样,objc_autoreleasePoolPop()的出栈流程图如下:

  1. 根据传入的哨兵对象找到对应位置。
  2. 给上次push操作之后添加的对象依次发送release消息
  3. 回退next指针到正确位置。
objc_autoreleasePoolPop()

我们把需要及时释放掉的代码块放入我们生成的autorelease pool中,结束后清空这个自定义的pool,主动地让pool清空掉,从而达到及时释放内存的目的。以上述图片处理的例子为例,优化如下:

for (int i = 0; i <= 1000; i ++) {
    //创建一个自动释放池
    NSAutoreleasePool *pool = [NSAutoreleasePool new];//也可以使用@autoreleasePool{domeSomething}的方式
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"PNG"];
    UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];
    UIImage *image2 = [image imageByScalingAndCroppingForSize:CGSizeMake(480, 320)];
    [image release];

    //将自动释放池内存释放,它会同时释放掉上面代码中产生的临时变量image2
    [pool drain];
}

这样,每次循环结束时,可以及时的释放临时对象的内存,其中,对自动释放池的操作可以用上文提到的方法来替代:

@autoreleasePool{
    //domeSomeThing;
}

3、子线程AutoRelease对象何时释放

AutoreleasePoolPage pop的时候释放,在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,详情可以看我的文章IOS的RunLoop,看这一篇文章就够了。那么子线程呢?子线程的runloop都需要手动开启,那么子线程中使用autorelease对象会内存泄漏吗,如果不会又是什么时候释放呢。

a、子线程的autoreleasepool如何被创建?

MRC下,使用@autoreleasing修饰符等同于MRC下调用autorelease方法,所以在NSObject源码中找到-(id)autorelese方法开始看(简化版的):

static inline id autorelease(id obj)
{
    id *dest __unused = autoreleaseFast(obj);
    return obj;
}

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

id *autoreleaseNoPage(id obj)
{
    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    // Push the requested object or pool.
    return page->add(obj);
}

得知如果当前线程没有AutorelesepoolPage的话,代码执行顺序为autorelease -> autoreleaseFast -> autoreleaseNoPage。在autoreleaseNoPage方法中,会创建一个hotPage,然后调用page->add(obj)

也就是说即使这个线程没有AutorelesepoolPage,使用了autorelease对象时也会new一个AutoreleasepoolPage出来管理autorelese对象,不用担心内存泄漏。

b、子线程的autoreleasepool何时被释放?

明确了何时创建autoreleasepool以后就自然而然的有下一个问题,这个autoreleasepool何时清空?对于这个问题,这里使用watchpoint set variable命令来观察。首先是一个最简单的场景,创建一个子线程:

__weak id obj;
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread) toTarget:self withObject:nil];

使用一个weak指针观察子线程中的autorelease对象,子线程中执行的任务。

- (void)createAndConfigObserverInSecondaryThread{
    __autoreleasing id test = [NSObject new];
    NSLog(@"obj = %@", test);
    obj = test;

    [[NSThread currentThread] setName:@"test runloop thread"];
    NSLog(@"thread ending");
}

obj = test处设置断点使用watchpoint set variable obj命令观察obj,可以看到obj在释放时的方法调用栈是这样的。

obj在释放时的方法调用栈

通过这个调用栈可以看到释放的时机在_pthread_exit。然后执行到AutorelepoolPagetls_dealloc方法。这个方法如下(简化版的):

static void tls_dealloc(void *p)
{
    // pop all of the pools
    if (!page->empty()) pop(page->begin());
    
    // clear TLS value so TLS destruction doesn't loop
    setHotPage(nil);
}

在这找到了if (!page->empty()) pop(page->begin());这句关键代码。再往上看一点,在_pthread_exit时会执行下面这个函数:

void _pthread_tsd_cleanup(pthread_t self)
{
    // clean up dynamic keys first
    for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
        pthread_key_t k;
        for (k = __pthread_tsd_start; k <= self->max_tsd_key; k++) {
            _pthread_tsd_cleanup_key(self, k);
        }
    }
}

也就是说thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_delloc中,执行了pop操作。

c、子线程加入runloop的效果

上述这个例子中的线程并没有加入runloop,只是一个一次性的线程。现在给这个线程加入runloop来看看效果会是怎么样的。

对于runloop,我们知道runloop一定要有source才能保证run起来以后不立即结束,而source有三种,custom sourceport sourcetimer

先加一个timer尝试下:

- (void)createAndConfigObserverInSecondaryThread{
    [[NSThread currentThread] setName:@"test runloop thread"];
    NSRunLoop *loop = [NSRunLoop currentRunLoop];

    // 用于监控runloop的状态
    CFRunLoopObserverRef observer;
    observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                       kCFRunLoopAllActivities,
                                       true,      // repeat
                                       0xFFFFFF,  // after CATransaction(2000000)
                                       YYRunLoopObserverCallBack, NULL);

    CFRunLoopRef cfrunloop = [loop getCFRunLoop];
    if (observer) {
        CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    }

    [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(testAction) userInfo:nil repeats:YES];
    [loop run];

    NSLog(@"thread ending");
}

相应测试代码为:

- (void)testAction{
    __autoreleasing id test = [NSObject new];
    obj = test;
    NSLog(@"obj = %@", obj);
}

testAction()中加上watchpoint断点,观察obj的释放时机:

obj的释放时机

可以看到释放的时机在CFRunloopRunSpecific中,也就是runloop切换状态的时候。

用自己实现的source尝试下:

- (void)createAndConfigObserverInSecondaryThread{
    __autoreleasing id test = [NSObject new];
    NSLog(@"obj = %@", test);
    obj = test;

    [[NSThread currentThread] setName:@"test runloop thread"];
    NSRunLoop *loop = [NSRunLoop currentRunLoop];

    CFRunLoopObserverRef observer;
    observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                       kCFRunLoopAllActivities,
                                       true,      // repeat
                                       0xFFFFFF,  // after CATransaction(2000000)
                                       YYRunLoopObserverCallBack, NULL);

    CFRunLoopRef cfrunloop = [loop getCFRunLoop];
    if (observer) {        
        CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    }
    
    // 自定义source
    CFRunLoopSourceRef source;
    CFRunLoopSourceContext sourceContext = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &runLoopSourcePerformRoutine};
    source = CFRunLoopSourceCreate(NULL, 0, &sourceContext);

    CFRunLoopAddSource(cfrunloop, source, kCFRunLoopDefaultMode);
    runLoopSource = source;
    runLoop = cfrunloop;
    [loop run];

    NSLog(@"thread ending");
}

-(void)wakeupSource{
    //通知InputSource
    CFRunLoopSourceSignal(runLoopSource);
    //唤醒runLoop
    CFRunLoopWakeUp(runLoop);
}

这里wakeupSource()是一个按钮的点击事件,用于唤醒runlooprunloop唤醒之后将执行runLoopSourcePerformRoutine函数:

void runLoopSourcePerformRoutine (void *info)
{
      __autoreleasing id test = [NSObject new];
    obj = test;
    // 如果不对obj赋值,obj会一直持有createAndConfigObserverInSecondaryThread函数入口的那个object,那个object不受这里面的autoreleasepool影响。
    NSLog(@"obj is %@" , obj);
    NSLog(@"回调方法%@",[NSThread currentThread]);
}

runLoopSourcePerformRoutine()中观察obj的释放时机,发现是在[NSRunloop run:beforeDate:]中。所以即使是我们自定义的source,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。

d、总结
  1. 子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。
  2. runlooprun:beforeDate,以及一些sourcecallback中,有autoreleasepoolpushpop操作,总结就是系统在很多地方都有autorelease的管理操作。
  3. 就算插入后没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool

Demo

Demo在我的Github上,欢迎下载。
MemoryManagementDemo

参考文献

深入理解 Tagged Pointer
iOS weak底层原理及源码解析
iOS weak源码详解
iOS Runtime探索之旅

相关文章

  • OC语法_IOS内存管理

    目录: 1、内存的定义 2、内存管理的基础概念 3、IOS系统中的内存管理 1、内存的定义 1.1. 内存是计算...

  • iOS 内存管理

    # 前言 反复地复习iOS基础知识和原理,打磨知识体系是非常重要的,本篇就是重新温习iOS的内存管理。 内存管理是...

  • iOS内功篇:内存管理

    iOS内功篇:内存管理 iOS内功篇:内存管理

  • iOS内存管理基础

    软件运行时会分配和使用设备的内存资源,因此,在软件开发的过程中,需要进行内存管理,以保证高效、快速的分配内存,并且...

  • iOS 内存管理基础

    一、Object C中内存管理的对象 在iOS开发中,内存中的对象有两类,一类是值类型,例如:init、float...

  • IOS基础:内存管理

    原创:知识点总结性文章创作不易,请珍惜,之后会持续更新,不断完善个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈...

  • iOS内存管理基础

    主要分析了iOS的内存区域、ARC机制、循环引用的解决方案 1 iOS内存与存储区域: 1.1 栈区,由编译器自动...

  • iOS性能调优之--内存管理

    iOS性能调优之--内存管理 iOS性能调优之--内存管理

  • iOS夯实:ARC时代的内存管理

    iOS夯实:ARC时代的内存管理 iOS夯实:ARC时代的内存管理

  • iOS全解1-1:基础/内存管理、Block、GCD与多线程

    面试系列: iOS面试全解1:基础/内存管理/Block/GCD[https://www.jianshu.com/...

网友评论

      本文标题:IOS基础:内存管理

      本文链接:https://www.haomeiwen.com/subject/djaumktx.html