OC对象的内存管理
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
copy和mutableCopy区别
- copy 产生不可变对象
- mutableCopy产生可变对象
注意点
可变数组copy后会变成不可变数组 , 添加元素 会报错 变成了不可变对象
引用计数的存储
在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
refcnts是一个存放着对象引用计数的散列表
RetainCount
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;//指针
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {//非指针类型 ,是否优化过
uintptr_t rc = 1 + bits.extra_rc;//引用计数
if (bits.has_sidetable_rc) {// 1不是存储在isa中,而是存储在sideTable中
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
dealloc
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());
}
__weak指针原理
runtime维护着一张弱引用表,用于存储指向某个对象的所有Weak指针,弱引用以散列表的方式存储到弱引用表里,释放时调⽤用clearDeallocating函数,通过对象地址值作为key & 掩码获取索引 ,取出当前对象的弱引用表 ,将里面的弱引用都清除掉,并将所有Weak指针的值设为nil
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();
}
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
weak_entry_remove(weak_table, entry);
}
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;
//地址值 & 掩码 获取 一个索引
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
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++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
ARC帮我们做了什么
利用LLVM 编译器自动帮我们添加retain,release
利用Runtime 监听对象销毁的时候,找到弱引用并清除
自动释放池
/*
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
{
__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"));
}
atautoreleasepoolobj = objc_autoreleasePoolPush();
MJPerson *person = [[[MJPerson alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
*/
- 自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
- 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
AutoreleasePoolPage 结构
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
- 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
- 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
- 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
autorelease什么时候释放
iOS在主线程的Runloop中注册了2个Observer
- 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
- 第2个Observer监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
// MJPerson *person = [[[MJPerson alloc] init] autorelease];
MJPerson *person = [[MJPerson alloc] init];
NSLog(@"%s", __func__);
}
/*
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
};
*/
/*
kCFRunLoopEntry push
{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
kCFRunLoopBeforeWaiting | kCFRunLoopExit
kCFRunLoopBeforeWaiting pop、push
kCFRunLoopExit pop
{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
*/
@end
方法里有局部变量 ,出了 方法会立即释放吗
分情况讨论,如果生成的是autorelease 代码 不会立马释放,会等到runloop休眠的时候会释放
如果是生成release 会立马释放
使用CADisplayLink、NSTimer有什么注意点?
CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用
解决方案
- 使用block
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- 使用中间对象
@interface MJProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
MJProxy *proxy = [[MJProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation MJProxy
NSProxy 专门用来消息转发,少了去父类里找方法的过程,比NSObject效率要高
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
MJProxy *proxy = [MJProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
Tagged Pointer
从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
// 加锁
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 解锁
});
}
//没有采用 Tagged Point 技术,多线程重复释放崩溃
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
//采用 Tagged Point 技术,不会访问setter方法
/**
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release]; //多线程重复释放
_name = [name retain];
}
}
*/