内存管理(三)
OC对象的内存管理
在iOS中,使用引用计数来管理OC对象内存。
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1。
内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease释放它。
想拥有某个对象,就让他的引用计数+1;不想再拥有某个对象,就让他的引用计数-1。
MRC环境下
int类型的age,不需要做retain、release操作,直接赋值、取值即可。
MJDog类型的dog对象,才需要做release、retain操作。
release旧值,是一个人要换一条狗,要把之前拥有的那条狗释放。
retain新值,是要拥有新的狗。
if(_dog != dog){
}
才会执行换狗操作。
因为,如果不加判断条件,那么下列代码:
YZDog *dog = [[YZDog alloc] int];//dog:1
YZPerson *person = [[YZPerson alloc] init];
[person setDog:dog];//dog:2
[dog release];//dog:1
[person setDog:dog];//执行这句代码,先释放,再添加。在释放的时候,就是0,再添加,就是野指针错误。
以下代码,跟上图代码一起看
-(void)dealloc
{
[_dog release];
_dog = nil;
[super dealloc];
}
copy
copy顾名思义,就是复制。
copy的目的:产生一个副本对象,跟原对象互不影响。
修改了原对象,不会影响副本对象;
修改了副本对象,不会影响原对象。
NSString、NSDictionary、NSArray等Foundation框架中的某些对象具备了copy能力。
iOS提供了两种拷贝方法:
copy:不可变拷贝,产生不可变的副本;
mutableCopy:可变拷贝,产生可变的副本。
深拷贝和浅拷贝
深拷贝:内容拷贝,产生新的对象;
浅拷贝:指针拷贝,没有产生新的对象,只是引用计数器+1.
NSString本身就是不可变对象,使用copy,产生的还是不可变对象,既然两个都是不可变对象,那么,使用同一个内存地址,也就是指针copy(浅拷贝)就可以满足要求,就不需要再新建一块地址存储副本对象了。
在MRC环境下:
YZPerson *person = [[YZPerson alloc] init];
person.array = @[@"jack", @"rose"];
[person release];
@property (copy, nonatomic) NSArray *array;
相当于做了一下set方法:
- (void)setArray:(NSArray *)array
{
if (_array != array) {
[_array release];
_array = [array copy];
}
}
@property (retain, nonatomic) NSArray *array;
相当于做了一下set方法:
- (void)setArray:(NSArray *)array
{
if (_array != array) {
[_array release];
_array = [array retain];
}
}
在使用copy的时候,相当于对:
person.array = @[@"jack", @"rose"];
[person setArray:@[@"jack", @"rose"]];
进入set方法:
_array = [@[@"jack", @"rose"] copy];
等于
person.array = [@[@"jack", @"rose"] copy];
也就是,使用copy修饰属性,相当于对传入的值进行了copy操作,然后赋值给原属性。
面试题
问:以下代码会有什么问题?
@property (copy, nonatomic) NSMutableArray *array;
YZPerson *person = [[YZPerson alloc] init];
person.array = [NSMutableArray array];
[person.array addObject:@"jim"];
[person release];
答:
会崩溃,报错信息:
[__NSArray0 addObject:]: unrecognized selector sent to instance 0x100501190
NSArray没有addobject:方法。
原因:
array虽然定义为NSMutableArray,但使用的是copy修饰,那么,person.array创建出来的对象是:
person.array = [[NSMutableArray array] copy];
也就是
person.array创造出来的是一个不可变对象。
不可变对象array,没有addObject:方法。
不存在使用mutableCopy修饰的属性。
@property (mutableCopy, nonatomic) NSString *name;
对于一个普通自定义类,如果想使用copy,则需要遵守NSCopying协议
@interface YZPerson : NSObject<NSCopying>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) float weight;
@end
@implementation YZPerson
//实现copyWithZone:方法
- (id)copyWithZone:(NSZone *)zone
{
YZPerson *person = [YZPerson allocWithZone:zone];
return person;
}
@end
引用计数的存储
在64位中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中。
isa指针中存储的什么东西,具体可以参考Runtime的本质(一)
在isa里面,有一个extra_rc参数
其中:rc就是retainCount引用计数的意思。
引用计数器太大,extra_rc中存储不下,则has_sidetable_rc=1,引用计数器会存储在一个名为SideTable的类的属性中。
struct SideTable
{
spinlock_t slock;// 保证原子操作的自旋锁
RefcountMap refcnts;//引用计数器存储地,是一个哈希map表
weak_table_t weak_table;//弱引用表,也是哈希map存储
}
在源码中,可以找到retainCount的源码:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
点击进去:
sidetable_getExtraRC_nolock();
的实现源码
refcnts以对象的地址作为key,引用计数作为value
更多学习请看:
iOS管理对象内存的数据结构以及操做算法–SideTables、RefcountMap、weak_table_t
weak指针相关知识点
__strong会对对象产生强引用;
__weak会对对象产生弱引用,并且,在对象销毁的时候,将指针置为nil;
__unsafe_unretain会对对象产生弱引用,并且,在对象销毁的时候,什么也不做。如果再次使用该指针,则会报野指针错误。
weak指针的实现原理
也就是,weak指针指向对象销毁的时候,weak指针怎么做到变为nil的。
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个Hash(哈希)表,
Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
举个例子:
__weak NSObject *obj1 = [[NSObject alloc] init];
__weak NSObject *obj2 = obj1;
Key指的是[[NSObject alloc] init]
Value指的是[obj1, ibj2]
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
dealloc
当一个对象要释放时,会自动调用dealloc,接下来调用的是:
dealloc
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance、free
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
//weakly_referenced是弱指针引用
//强引用走yes,弱引用走no
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
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) {
// 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;
}
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
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();
}
一句话,在64位下,weak指针存储在一个weak_table哈希表中,在dealloc中进行判断,如果是weak指针,则置为nil。
更多关于weak学习:
[weak 弱引用的实现方式](https://www.desgard.com/iOS-Source-Probe/Objective-C/Runtime/weak 弱引用的实现方式.html)
iOS开发 - 底层解析weak的实现原理
笔记-更深层次的了解iOS内存管理
ARC都帮我们做了什么?
ARC主要是LLVM编译器+RunTime系统相互协作的结果;
通过LLVM编译器,帮我们自动做了retain、release、autorelease操作,也就是自动做release操作是编译器特性。
通过runtime,监控系统,在程序运行中,将weak指针销毁置为nil
autoreleasepool
在之前的学习中,我们有这样的说法:
autorelease是在@autoreleasepool{}大括号结束的时候,调用一下release操作。
从运行结果来看,确实是在17行结束的时候,调用了person的dealloc方法,也就是调用了release。本质是什么呢?
使用命令行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m将main.m文件转换为main.cpp文件,可以看到上图中的底层代码:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_64ff8e_mi_0);
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
YZPerson *person1 = ((YZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((YZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((YZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YZPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_64ff8e_mi_1);
}
简化下:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog(@"begin");
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}
NSLog(@"end");
}
也就是:
@autoreleasepool
{
YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}
转化为:
{
__AtAutoreleasePool __autoreleasepool;
YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}
在被转换的main.cpp文件中,可以看到__AtAutoreleasePool的定义:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {//构造函数,在创建结构体变量的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {//析构函数,在结构体变量销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
也就是,在执行__AtAutoreleasePool __autoreleasepool;
代码的时候,会调用构造函数:
atautoreleasepoolobj = objc_autoreleasePoolPush();
当
{
__AtAutoreleasePool __autoreleasepool;
YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
}
大括号结束的时候,会调用析构函数:objc_autoreleasePoolPop(atautoreleasepoolobj);
整合上面,也就是:
{
__AtAutoreleasePool __autoreleasepool;
atautoreleasepoolobj = objc_autoreleasePoolPush();
YZPerson *person1 = [[[YZPerson alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
autoreleasepool自动释放池的研究,转化为对objc_autoreleasePoolPush()和objc_autoreleasePoolPop()的研究。
objc_autoreleasePoolPush和objc_autoreleasePoolPop
在objc源码中,我们可以看到objc_autoreleasePoolPush和objc_autoreleasePoolPop的实现:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
两个定义里面都用到了AutoreleasePoolPage
自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage。
调用了autorelease的对象,最终都是通过AutoreleasePoolPage对象来管理的。
接下来,我们来研究下AutoreleasePoolPage对象
AutoreleasePoolPage
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;
}
从定义,大致可以猜出,自动释放池是与线程有关的。究竟是怎么回事,我们继续往下看。
1、每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址(例子中的person地址值也被存放在了AutoreleasePoolPage内部,也就是调用autorelease就会把地址存放在AutoreleasePoolPage内部)。
2、所有AutoreleasePoolPage对象通过双向链表的形式链接在一起。
3、调用push方法,会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,(POOL_BOUNDARY其实就是一个栈入口)
4、调用pop方法时,传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。(找到栈的入口,查找到最后入栈的对象。)
5、id *next指向了下一个能存放autorelease对象地址的区域
6、可以通过extern void _objc_autoreleasePoolPrint(void)私有函数来查看自动释放池的情况。
0x2000-0x1000 = 0x1000=D4096(十进制4096)
双向链表,通过parent、child指针来链接。
autorelease与runloop
使用autorelease修饰的对象,在什么时候调用release方法?
MRC环境下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
@autoreleasepool {
YZPerson *person = [[[YZPerson alloc] init] autorelease];
}
NSLog(@"3");
}
如果是将对象person加入到一个autoreleasepool中,那么person对象会在自动释放池}时调用release方法。原因上面已经讲过。
如果,没有明显的加入自动释放池中,又是什么时候调用release方法呢?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
YZPerson *person = [[[YZPerson alloc] init] autorelease];
NSLog(@"3");
}
结果:
1
3
-[YZPerson dealloc]
看着像是在viewDidLoad大括号结束的时候调用的release操作。
真的是这样吗?
首先,我们确定的是,肯定不是在main.m文件中的autoreleasepool中去管理person的release操作。因为,main.m文件中的autoreleasepool是整个程序生命周期延续的,只有在程序结束的时候,才会运行到},才会去销毁对象。如果person交给main.m文件中的autoreleasepool,也就意味着person在整个程序运行过程中,都不会被释放,这明显是不对的。因此:
main.m文件中的autoreleasepool不负责person对象的release操作。
从上面结果可以看出,[YZPerson dealloc]是在[ViewController viewWillAppear:]和[ViewController viewDidAppear:]中间被执行的。
也就是说,并不是在48行代码执行的dealloc。
直接打印[NSRunLoop mainRunLoop],我们在打印结果里面发现observers里面有这两个类型:_wrapRunLoopWithAutoreleasePoolHandler
拿取出来,可以看到:
observers = (
"<CFRunLoopObserver 0x6000006e41e0 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000039bc690 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fc00e802038>\n)}}",
"<CFRunLoopObserver 0x6000006e4280 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000039bc690 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fc00e802038>\n)}}"
)
其中,第一个activities = 0x1,第二个activities = 0xa0
结合
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//1
kCFRunLoopBeforeTimers = (1UL << 1),//2
kCFRunLoopBeforeSources = (1UL << 2),//4
kCFRunLoopBeforeWaiting = (1UL << 5),//32
kCFRunLoopAfterWaiting = (1UL << 6),//64
kCFRunLoopExit = (1UL << 7),//128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
可以看出:
第一个activities = 0x1,监听的是kCFRunLoopEntry
第二个activities = 0xa0换算成十进制是160,也就是32+128=160,监听的是kCFRunLoopBeforeWaiting | kCFRunLoopExit
换句话说,就是在runloop里面有两个跟AutoreleasePool相关的observer,一个observer监听kCFRunLoopEntry,一个observer监听kCFRunLoopBeforeWaiting | kCFRunLoopExit。
iOS在主线程的runloop中注册了2个observer
第一个observer监听了kCFRunLoopEntry,会调用objc_autoreleasePoolPush()
第二个observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
第二个observer也监听了kCFRunLoopExit事件,会调用objc_autoreleasePoolPop()
翻译成白话,也就是:
在线程中:
在runloop开始的时候,就开始调用入栈操作,将调用autorelease的对象存储在栈中。
在runloop休眠前,执行出栈(release)操作,再执行一次入栈操作。
在runloop销毁的时候,执行出栈(release)操作。