iOS-底层原理(27)-内存管理之copy+weak+autorease原理

1.下面代码执行结果如何
// Person.h
@interface Person : NSObject
@property (copy, nonatomic) NSMutableArray *data;
@end

// 调用
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Person *p = [[Person alloc] init];
    
    p.data = [NSMutableArray array];
    [p.data addObject:@"jack"];
    [p.data addObject:@"rose"];
    NSLog(@"end");
}

运行结果

image.png
- (void)setData:(NSArray *)data {
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}

分析:因为datacopy属性,所以在其set方法里先执行判断,然后执行release操作,最后执行copy操作,变成了一个不可变对象。

二 copy
  • 拷贝的目的:产生一个副本对象,跟源对象互不影响
  • 修改了源对象,不会影响副本对象
  • 修改了副本对象,不会影响源对象
iOS提供了2个拷贝方法
  • copy 不可变拷贝,产生不可变副本
  • mutableCopy可变拷贝,产生可变副本

深拷贝和浅拷贝

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

copy和mutableCopy 图解

image.png

1.copy都是不可变拷贝,产生不可变副本mutableCopy都是可变拷贝,产生可变副本
2.除了不可变对象copy浅拷贝,其他都是深拷贝

三 引用计数的存储

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

image.png
  • refcnts是一个存放着对象引用计数的散列表
四 weak实现原理 - dealloc
  • 当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

1.dealloc
2._objc_rootDealloc
3.rootDealloc
4.object_dispose
5.objc_destructInstancefree

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

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

  • __AtAutoreleasePool结构体

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

下面将代码进行转换

@autoreleasepool {
    Person *p4 = [[[MJPerson alloc] init] autorelease];
}

将上述代码转成C++代码

 {
    __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"));
 }

去除一些不必要的代码后变成下面这个样子

 {
    __AtAutoreleasePool __autoreleasepool;
   MJPerson *person = [[[MJPerson alloc] init] autorelease];
 }

又因为__AtAutoreleasePool是一个结构体,所以创建时会调用其构造函数__AtAutoreleasePool(),当离开其作用域后,会调用其析构函数~__AtAutoreleasePool(),所以上面的代码又可以转换成下面的代码

atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
5.0 自动释放池
  • 自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
  • 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

源码分析

  • clang重写@autoreleasepool
  • objc4源码:NSObject.mm
image.png

变量说明

  • magic 用来校验 AutoreleasePoolPage 的结构是否完整
  • next指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()
  • thread 指向当前线程
  • parent指向父结点,第一个结点的 parent 值为 nil
  • child 指向子结点,最后一个结点的 child 值为 nil
  • depth 代表深度,从 0 开始,往后递增 1
  • hiwat 代表 high water mark
5.1 AutoreleasePoolPage的结构
  • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
  • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
image.png
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);

上图的执行步骤说明

  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,即返回给atautoreleasepoolobj
  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
  • id *next指向了下一个能存放autorelease对象地址的区域

代码例子如下

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        
        Person *p1 = [[[Person alloc] init] autorelease];
        Person *p2 = [[[Person alloc] init] autorelease];
        
        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 5; i++) {
                Person *p3 = [[[Person alloc] init] autorelease];
            }
            
            @autoreleasepool { // r3 = push()
                Person *p4 = [[[Person alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)
        } // pop(r2)
    } // pop(r1)
    return 0;
}

执行结果

image.png
  1. 因为只打印了一个PAGE,所以说明他们是在同一个AutoreleasePoolPage,只是每次一个新的autoreleasepool,都会插入一个POOL_BOUNDARY
  2. 每次释放对象时,都是从后往前释放,直到遇到POOL_BOUNDARY为止。

代码例子二

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        @autoreleasepool {
            MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
}

执行结果

image.png

代码例子三

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        
        MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
        MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
        
        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 600; i++) {
                MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
            }

            @autoreleasepool { // r3 = push()
                MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)

        } // pop(r2)
        
    } // pop(r1)
    return 0;
}

执行结果

image.png
image.png
image.png
5.2 Runloop和Autorelease

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

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()

  • 第2个Observer

<1> 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
<2> 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

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

实践内容可以参考 你真的懂iOS的autorelease吗?

代码例子如下

  • MRC环境下
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 这个Person什么时候调用release,是由RunLoop来控制的
    // 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    NSLog(@"%s", __func__);
}

运行结果如下

image.png
  1. 得出结论,autorelease并不是根据对象的作用域来决定释放时机。
  2. 实际上,autorelease释放对象的依据是Runloop,简单说,runloop就是iOS中的消息循环机制,当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。至于何时runloop结束并没有固定的duration。
  3. 本次runloop迭代休眠之前调用了objc_autoreleasePoolPop()方法,然后调用release,从而释放Person对象。
5.4 方法里有局部对象, 出了方法后会立即释放吗
  • ARC环境下
- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    NSLog(@"%s", __func__);
}
image.png

通过打印结果可知,当person对象出了其作用域后就销毁,即系统会在它出作用域的时候,自动调用其release方法。

扩展

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

上文有提到autorelease pool,一旦一个对象被autorelease,则该对象会被放到iOS的一个池:autorelease pool,其实这个pool本质上是一个stack,扔到pool中的对象等价于入栈。我们把需要及时释放掉的代码块放入我们生成的autorelease pool中,结束后清空这个自定义的pool,主动地让pool清空掉,从而达到及时释放内存的目的。优化代码如下

@autoreleasePool{
    //domeSomeThing;
}
什么时候用@autoreleasepool

根据 Apple的文档 ,使用场景如下:

  • 写基于命令行的的程序时,就是没有UI框架,如AppKitCocoa框架时。
  • 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
  • 创建了新的线程。(非Cocoa程序创建线程时才需要)
  • 长时间在后台运行的任务。
  1. autorelease 机制基于 UI framework。因此写非UI framework的程序时,需要自己管理对象生存周期。
  2. autorelease 触发时机发生在下一次runloop的时候。因此如何在一个大的循环里不断创建autorelease对象,那么这些对象在下一次runloop回来之前将没有机会被释放,可能会耗尽内存。这种情况下,可以在循环内部显式使用@autoreleasepool {}autorelease对象释放。
  3. 自己创建的线程。Cocoa的应用都会维护自己autoreleasepool。因此,代码里spawn的线程,需要显式添加autoreleasepool。注意:如果是使用POSIX API 创建线程,而不是NSThread,那么不能使用Cocoa,因为Cocoa只能在多线程(multithreading)状态下工作。但可以使用NSThread创建一个马上销毁的线程,使得Cocoa进入multithreading状态。

上述结论来自 guijiewan 的CSDN 博客 ,什么时候应该使用Autorelease

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

本文参考MJ底层原理教程,非常感谢
本文参考Autorelease Pool学习笔记,非常感谢。
优秀文章推荐 - Objective-C Autorelease Pool 的实现原理


  • 多多点赞,打赏更好,您的支持是我写作的动力。

项目连接地址 - MemoryManage-CADisplayLink+Timer
项目连接地址 - MemoryManage-Copy
项目连接地址 - MemoryManager_autorelease1
项目连接地址 - MemoryManager_autorelease

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容