MJiOS底层笔记--内存管理

1,477 阅读6分钟

本文属笔记性质,主要针对自己理解不太透彻的地方进行记录。

推荐系统直接学习小码哥iOS底层原理班---MJ老师的课确实不错,强推一波。


CADisplayLink与NSTimer

CADisplayLink(保证调用频率和屏幕的刷帧频率一致,60FPS(60次/s))、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

target导致循环引用

如下代码是释放不掉的

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 保证调用频率和屏幕的刷帧频率一致,60FPS
    self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}


- (void)linkTest
{
    NSLog(@"%s", __func__);
}

__weak为什么解决target的强引用

block是捕获变量,timer是传递参数。

  1. block在捕获变量时根据变量类型自行进行若引用处理。
  2. timer作为参数传递时,内部接收到的都是对象的地址值,无法获取引用类型。

不过如果是NSTimer的block版本用__weak是可以的

中间代理

  1. 用代理隔离self与timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
  1. 用消息转发将selector发送回self
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}

其他方式释放timer

比如removeSuperView时之类吧

NSObject与NSProxy

NSProxy专门用来做消息转发

  1. 消息转发速度快

NSProxy在本类没有该方法的情况下会直接进入消息转发(methodSignatureForSelector:) 与 (forwardInvocation:),并不会去查找父类,动态方法解析等等。

  1. 大部分方法都能正确转发

原生方法如果未主动实现,内部直接进入消息转发。比如class,isKindOfClass等等

GCD定时器

GCD的定时器会更加准时

NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时

而GCD定时器依赖于系统内核,并不依赖Runloop


内存布局


Tagged Pointer

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

  1. 在没有使用Tagged Pointer之前,NSNumber与正常的OC对象一样:

    需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值。

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

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

  2. objc_msgSend能识别Tagged

  3. Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

  4. 如何判断一个指针是否为Tagged Pointer? class

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

    Mac平台,最低有效位是1(16进制下为7)

    通常来讲,判断最后一位不是0即可

NSLog(@"Person实例的内存地址=%p---指针变量p的内存地址=%p---指针变量p保存的内存地址=%p", p, &p, p);

MRC的内存管理

  1. 在iOS中,使用引用计数来管理OC对象的内存
  2. 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  3. 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结

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

MRC

使用return关键字只会管理setget方法中的内存,dealloc中仍然需要自己释放。


copy和mutableCopy


引用计数

  1. 在64bit中,引用计数可以直接存储在优化过的isa指针中
  2. 如果引用计数过大,isa中改为1并且将计数存储到SideTable中

SideTable

一个全局table

refcnts是一个存放着对象引用计数的散列表 weak_table存放着若引用的指针与对象


weak

当一个对象A被若引用指针持有,将会以[&A,weak指针表]的形式添加进SideTable中

当对象A被释放,可以根据&A查找到所有指向他的weak指针并进行释放

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


_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

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

    if (fastpath(isa.nonpointer  &&  //新isa指针
                 !isa.weakly_referenced  &&   //查看该对象是否被若引用了
                 !isa.has_assoc  &&   //关联对象
                 !isa.has_cxx_dtor  &&  //c++析构器
                 !isa.has_sidetable_rc)) //大额引用计数
    {
        assert(!sidetable_present());
        free(this); //直接释放
    } 
    else {
        object_dispose((id)this); 
    }
}


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


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

    SideTable& table = SideTables()[this]; //获得全局的SideTable
    table.lock();
    if (isa.weakly_referenced) {
        //从表中根据对象地址,释放所有指向他的弱引用指针
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}


AutoreleasePool

@autoreleasepool {
    for (int i = 0; i < 1000; i++) {
        MJPerson *person = [[[MJPerson alloc] init] autorelease];
    }
}

cpp中

{ 
    __AtAutoreleasePool __autoreleasepool; //结构体变量
    MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}


 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };

所以本质上就等于

atautoreleasepoolobj = objc_autoreleasePoolPush(); //创建释放池

MJPerson *person = [[[MJPerson alloc] init] autorelease];

objc_autoreleasePoolPop(atautoreleasepoolobj); //释放释放池

AutoreleasePool的结构

每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址

push,pop,autorelease

  1. 在调用objc_autoreleasePoolPush()时,插入POLL_BOUNDARY并返回地址0x1038

  2. 每个调用autorelease的对象都会被插入到AutoreleasePoolPage中

  3. 在调用objc_autoreleasePoolPop(0x1038)时,从当前位置到0x1038所有的对象都会被执行release操作。

可以通过以下私有函数来查看自动释放池的情况

extern void _objc_autoreleasePoolPrint(void);

AutoreleasePool的维护

  1. 始终有一个被标记hotPage的活跃AutoreleasePoolPage被系统持有
  2. page之间通过双向链表链接
  3. 如果push/autorelease操作时当前page已满,将会创建一个page或跳转到下一个page

runloop与AutoreleasePool

iOS在主线程的Runloop中注册了2个Observer,监听了三个状态。并适时操作AutoreleasePool

  1. 第1个Observer监听了kCFRunLoopEntry事件

    在进入runloop时,会调用objc_autoreleasePoolPush()

  2. 第2个Observer监听了kCFRunLoopBeforeExit事件

    在退出runloop时,会调用objc_autoreleasePoolPop()

  3. 第2个Observer还监听了kCFRunLoopBeforeWaiting事件

    在当前循环结束,准备休眠时时,会调用objc_autoreleasePoolPop()随后再调用一次objc_autoreleasePoolPush()


autorelease对象

借用群里一位大佬的解释

一般除了init其他(alloc/new/copy/mutableCopy)基本上都是autorelease的,包括C函数返回对象

也就是说init方法放回的对象,默认是会被retain/release,而其他的对象默认会autorelease

很显然的,二者的释放时机不同,所以才会有如下情况发生。


自动释放池的创建和释放,销毁的时机如下所示

kCFRunLoopEntry; // 进入runloop之前,创建一个自动释放池

kCFRunLoopBeforeWaiting; // 休眠之前,销毁自动释放池,创建一个新的自动释放池

kCFRunLoopExit; // 退出runloop之前,销毁自动释放池


关于子线程的自动释放池

有两个创建的时机:

  1. 开启了runloop
  2. 创建了autorelase对象

针对情况2,释放池会在线程销毁是进行pop 参考