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 图解

iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第1张图片
image.png

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

三 引用计数的存储

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

iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第2张图片
image.png
  • refcnts是一个存放着对象引用计数的散列表
四 weak实现原理 - dealloc
  • 当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

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

iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第3张图片
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
iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第4张图片
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对象通过双向链表的形式连接在一起
iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第5张图片
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;
}

执行结果

iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第6张图片
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;
    }
}

执行结果

iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第7张图片
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;
}

执行结果

iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第8张图片
image.png
iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第9张图片
image.png
iOS-底层原理(27)-内存管理之copy+weak+autorease原理_第10张图片
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

你可能感兴趣的:(iOS-底层原理(27)-内存管理之copy+weak+autorease原理)