欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
- iOS探索 内存管理篇
写在前面
一个优秀的app必然是对内存"精打细算"的,本文就来探索一下内存管理中的一些门道。如果你看到了这篇文章,请仔细看下去,尤其是NSTimer部分的内容介绍了NSTimer和Block中的循环引用存在的差异
一、内存布局
1. 五大区
接下来我从内存中的低地址往高地址依次介绍五大区
- 代码段(.text)
- 存放着程序代码,直接加载到内存中
- 初始化区域(.data)
- 存放着初始化的全局变量、静态变量
- 一般以 0x1 开头
- 未初始化区域(.bss)
- bss段存放着未初始化的全局变量、静态变量
- 一般以 0x1 开头
- 堆区(.heap)
- 堆区存放着通过alloc分配的对象、block copy后的对象
- 堆区速度比较慢
- 一般以 0x6 开头
- 栈区(.stack)
- 栈区存储着函数、方法以及局部变量
- 栈区比较小,但是速度比较快
- 一般以 0x7 开头
在这里提一句关于函数在内存中的分布:函数指针存在栈区,函数实现存在堆区
除了五大区之外,内存中还有保留字段和内核区
- 内核区:在4gb内存中只用到了3gb,还有1gb给内核区处理
- 保留字段:保留一定的区域给保留字段,进行一些存储
平时在使用App过程中,栈区就会向下增长,堆区就会向上增长
接下来就用LLDB来打印一下堆区和栈区中的一些内容
- 堆区访问对象的顺序是先拿到栈区的指针,再拿到指针指向的对象,才能获取到对象的isa、属性方法等
- 栈区访问对象的顺序是直接通过寄存器访问到对象的内存空间,因此访问速度快
2. 内存布局面试题
- 全部变量和局部变量在内存中是否有区别?
- 全局变量存放在相应的全局储存区,而局部变量存放在栈区
- 两者访问的权限不一样
-
block是否可以修改全局变量 可以,因为全局变量的作用域很大(可以理解为公共区域),block可以访问到
-
关于全局静态变量的误区
- 全局静态变量是可变的
- 全局静态变量的值只针对文件而言,不同文件的全局静态变量的内存地址是不一样的
- 假设有个
全局静态变量num=10 - vc中修改
num=20并在vc中打印会输出20 - view中修改
num=num+1并在view中会输出11 - model中修改
num=0并在model中会输出0 - 在model的分类中修改
num=1000并在model分类中会输出1000
- 假设有个
总结: 全局静态变量只针对文件而言,无论别的文件怎么修改,本文件使用时都拿原有值/本文件修改后的值
二、内存管理方案
1. taggedPointer
1.1 taggedPointer初探
分别调用下面两种方法,哪个会崩溃?为什么?
@interface ViewController ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.queue = dispatch_queue_create("com.felix", DISPATCH_QUEUE_CONCURRENT);
[self testNormal];
[self testTaggedPointer];
}
- (void)testNormal {
for (int i = 0; i < 10000; i++) {
dispatch_async(self.queue, ^{
self.name = [NSString stringWithFormat:@"1234567890-"];
NSLog(@"%@", self.name);
});
}
}
- (void)testTaggedPointer {
for (int i = 0; i < 10000; i++) {
dispatch_async(self.queue, ^{
self.name = [NSString stringWithFormat:@"F"];
NSLog(@"%@", self.name);
});
}
}
经过运行测试之后,会发现testNormal会崩溃,而testTaggedPointer方法正常运行
首先来分析下为什么会崩溃的原因?其实是多线程和setter、getter操作造成的
- 调用
setter方法会objc_retain(newValue)+objc_release(oldValue) - 但是加上多线程就不一样了——在某个时刻
线程1对旧值进行relese(没有relese完毕),同时线程2对旧值进行relese操作,即同一时刻对同一片内存空间释放多次,会造成野指针问题(访问坏的地址)
但是为什么testNormal会崩溃,而testTaggedPointer方法正常运行?
-
testNormal中的对象为NSCFString类型 -
testTaggedPointer中的对象为NSTaggedPointerString类型
其实之前在objc源码的方法中有看到过类似的身影——objc_retain和objc_release的对象如果是isTaggedPointer类型就直接返回(不操作)
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
其实
taggedPointer的优化不仅仅如此,在read_images阶段就对taggedPointer进行了处理
1.2 taggedPointer深入
在推出iPhone 5s(iPhone首个采用64位架构)的时候,为了节省内存和提高执行效率,同时也提出了taggedPointer
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
底层也做了对objc_debug_taggedpointer_obfuscator进行异或的操作(两次异或同一个数相当于编码解码)
extern uintptr_t objc_debug_taggedpointer_obfuscator;
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
我们也可以通过类似的方法对taggedPointer进行解码
extern uintptr_t objc_debug_taggedpointer_obfuscator;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str1 = @"F";
NSString *str2 = [NSString stringWithFormat:@"F"];
NSString *str3 = [NSString stringWithFormat:@"F"];
NSNumber *number1 = @10;
NSNumber *number2 = [NSNumber numberWithInt:10];
NSNumber *number3 = [NSNumber numberWithFloat:10];
NSNumber *number4 = [NSNumber numberWithDouble:10];
NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:0];
NSLog(@"%@ %p %lx", str1, str1, _objc_decodeTaggedPointer_(str1));
NSLog(@"%@ %p %lx", str2, str2, _objc_decodeTaggedPointer_(str2));
NSLog(@"%@ %p %lx", str3, str3, _objc_decodeTaggedPointer_(str3));
NSLog(@"%@ %p %lx", number1, number1, _objc_decodeTaggedPointer_(number1));
NSLog(@"%@ %p %lx", number2, number2, _objc_decodeTaggedPointer_(number2));
NSLog(@"%@ %p %lx", number3, number3, _objc_decodeTaggedPointer_(number3));
NSLog(@"%@ %p %lx", number4, number4, _objc_decodeTaggedPointer_(number4));
NSLog(@"%@ %p %lx", indexPath, indexPath, _objc_decodeTaggedPointer_(indexPath));
}
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
--------------------输出结果:-------------------
F 0x1065591c0 7527f27d4e64a912
F 0xd527f27c48313cb3 a000000000000461
F 0xd527f27c48313cb3 a000000000000461
10 0xc527f27c48313870 b0000000000000a2
10 0xc527f27c48313870 b0000000000000a2
10 0xc527f27c48313876 b0000000000000a4
10 0xc527f27c48313877 b0000000000000a5
<NSIndexPath: 0x8dec1fd169f9f89d> {length = 1, path = 0} 0x8dec1fd169f9f89d c00000000000000e
--------------------输出结果:-------------------
这里还是有细节点,读者可以自行去尝试一下:
taggedPointer的指针是由:标志位+值+值类型三种组成的- 从
number1就可以看出:a其实是10的16进制 - 最后一位的规律是:NSString为1、Int为2、long为3、float为4、double为5...
- 从
NSString用字面量初始化会是__NSCFString而不是NSTaggedPointerStringNSString不能超过9位,否则会使用__NSCFConstantString存储
最后再介绍一下taggedPointer中的标志位
enum objc_tag_index_t : uint16_t
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
可是明明NSString的标志位是a呀,这里怎么显示的是2呢?
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
上面这段代码针对不同tag进行了不同的位运算最后将标志位+值+值类型格式的taggedPointer呈现出来,但是调用此处的代码并未开源
1.3 taggedPointer总结
taggedPointer专门用来存储小的对象,例如NSNumber、NSDatetaggedPointer指针的值不再是地址了,而是真正的值。所以他不再是一个对象了,他只是一个"披着对象外皮"的普通变量而已——它的内存并不在堆中,也不需要malloc和freetaggedPointer不像地址指针一样,直接从指针中拿到值——编译读取的时候更加直接了- 在内存读取上有着3倍的效率,创建时比以前快106倍
关于taggedPointer并没有深入下去,苹果提出的这层内存优化还是有很多神秘之处等待着被发现(部分未开源)网上也有很多大佬进行了更深层次的研究
2. nonpointer_isa
nonpointer_isa在isa章节已经有提到过了,这是苹果优化内存的一种方案:
isa是个8字节(64位)的指针,仅用来isa指向比较浪费,所以isa中就掺杂了一些其他数据来节省内存
3. SideTable
3.1 为什么有多张散列表
- 安全性能低——若所有对象全存在一张散列表中,某个对象需要处理时就对散列表进行unlock,表中其他对象的安全性无法得到保障
- 优化加锁、解锁速度——对于散列表的操作频率较高,分为多表可以提高性能
3.2 散列表结构
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
...
}
每一张SideTable主要是由三部分组成:自旋锁、引用计数表、弱引用表
spinlock_t:自旋锁,用于加锁/解锁散列表RefcountMap:用来存储OC对象的引用计数的哈希表- 仅在未开启isa优化或在isa优化情况下isa的引用计数溢出时才会用到
weak_table_t:存储对象弱应用指针的哈希表
3.3 散列表的数据结构
散列表本质上是一张哈希表,综合了数组和链表的优势
- 数组:通过下标查询数据快(查询快);增删改慢
- 链表:需要通过一个个节点才能找到目标(查询慢);增删改快
三、ARC&MRC
面试中常常会问到ARC和MRC,其实这两者在内存管理中才是核心所在
ARC是LLVM和Runtime配合的结果ARC中禁止手动调用retain/release/retainCount/deallocARC新加了weak、strong关键字
1. alloc
之前已经对alloc流程有了一个详细的介绍
2. retain
retain会在底层调用objc_retain
objc_retain先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->retain()objc_object::retain通过fastpath大概率调用rootRetain(),小概率通过消息发送调用对外提供的SEL_retainrootRetain调用rootRetain(false, false)
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
rootRetain内部实现其实是个do-while循环:- 先判断是否为
nonpointer_isa(小概率事件)不是的话则对散列表中的引用技术表进行处理- 找到对应的散列表进行
+=SIDE_TABLE_RC_ONE,其中SIDE_TABLE_RC_ONE是左移两位找到引用计数表
- 找到对应的散列表进行
- 正常情况下为
nonpointer_isa会调用addc函数进行引用计数的处理,并用carry记录引用计数是否超负荷- 对
isa中的第45位(RC_ONE在arm64中为45)extra_rc进行操作处理
- 对
- 超负荷情况下,将
extra_rc的一半引用计数存入引用计数表中,并标记isa->has_sidetable_rc为true- 这里为什么优先考虑使用isa进行引用计数存储是因为对isa操作的性能强,操作引用计数表需要进行加锁解锁操作
- 先判断是否为
3. release
release与retain相似,会在底层调用objc_release
objc_release先判断是否为isTaggedPointer,是就直接返回不需要处理,不是在调用obj->release()objc_object::release通过fastpath大概率调用rootRelease(),小概率通过消息发送调用对外提供的SEL_releaserootRelease调用rootRelease(true, false)rootRelease内部实现也有个do-while循环- 先判断是否为
nonpointer_isa(小概率事件)不是的话则对散列表中的引用技术表进行处理 - 正常情况下为
nonpointer_isa会调用subc函数进行引用计数的处理,并用carry记录引用计数是否超负荷 - 超负荷情况下会来到
underflow分支- 如果
isa中的has_sidetable_rc为true时就开始着手处理引用计数,否则就将isa中的deallocating标记为true准备释放 - 如果
isa中的extra_rc减到只剩一半时会清空存放在引用计数表中的值,重新放回到extra_rc中,返回retry的do-while循环
- 如果
- 先判断是否为
4. retainCount
前面说了这么多引用计数,那么我们来看看retainCount和引用计数有什么关系呢?
NSObject *objc = [[NSObject alloc] init];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));
上述代码会输出1,然而在alloc流程中并没有看到任何与retainCount相关的内容,这又是怎么一回事呢?接下来就来看看retainCount的底层实现
retainCount会调用rootRetainCountrootRetainCount的具体实现
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) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
- 先判断是否为
isTaggedPointer - 再判断是否为
nonpointer,如果是的话,当前引用计数=1+extrac_rc- 这行代码就能说明
alloc出来的对象引用计数为0,是苹果人员为了不给开发人员造成引用计数为0时就销毁造成错觉才默认加一
- 这行代码就能说明
- 接着判断
has_sidetable_rc是否有额外的散列表- 有的话引用计数再加上引用计数表中的数量
- 所以
引用计数=1 + extrac_rc + sidetable_getExtraRC_nolock
5. autorealese
在后续讲到
6. dealloc
在第4小节已经提到了dealloc——在release底层会判断当前extra_rc和引用计数表是否都为0,如果满足条件就会通过消息发送调用dealloc
dealloc在底层会调用_objc_rootDealloc
_objc_rootDealloc调用rootDeallocrootDealloc- 判断是否为
isTaggedPointer,是的话直接返回,不是的话继续往下走 - 判断isa标识位中是否有弱引用、关联对象、c++析构函数、额外的散列表,有的话调用
object_dispose,否则直接free
- 判断是否为
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
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);
}
}
object_dispose中- 先判空处理
- 接着调用
objc_destructInstance(核心部分) - 最后再
free释放对象
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
objc_destructInstance- 判断是否有
c++析构函数和关联对象,有的话分别调用object_cxxDestruct、_object_remove_assocations进行处理 - 然后再调用
clearDeallocating
- 判断是否有
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();
}
return obj;
}
objc_destructInstance中- 判断是否是
nonpointer,是的话调用sidetable_clearDeallocating清空散列表 - 判断是否有
弱引用和额外的引用计数表has_sidetable_rc,是的话调用clearDeallocating_slow进行弱引用表和引用计数表的处理
- 判断是否是
最后附上一张dealloc流程图
四、弱引用
1. weak原理
笔者在之前的iOS探索 runtime面试题分析中已经讲过了
2. NSTimer中的循环引用
众所周知使用NSTimer容易出现循环引用,那么我们就来分析并解决一下
static int num = 0;
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"TimerViewController";
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome {
num++;
NSLog(@"current - %d",num);
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s", __FUNCTION__);
}
这段代码运行起来所发生的问题就是当前VCpop到前一页时不会触发dealloc函数
刚才我们已经看到了release在引用计数为0时会调用dealloc消息发送,此时没有触发dealloc函数必然是出现了循环引用,那么循环引用出现在哪个环节?其实是NSTimer的API是被强持有的,直到Timer invalidated
那么能不能像block一样使用弱引用来解决循环引用呢?答案是不能的!
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
之前在Block篇章说的是使用弱引用
__weak typeof(self) weakSelf = self; 不处理引用计数,使用弱引用表管理,怎么在这里就不好使了呢?
关键在于
weakSelf和self指向的都是VC对象,但是weakSelf和self的指针并不相同——两者并不是一个东东,只是指向同一个VC对象
block持有的是weakSelf的指针地址;timer持有的是weakSelf的指针指向的对象,这里间接持有了self,所以仍然存在循环引用导致释放不掉
3. 解决NSTimer的循环引用
-
方案一:提前invalidate
- 既然dealloc不能来,就在dealloc函数调用前解决掉这层强引用
- 可以在
viewWillDisappear、viewDidDisappear中处理NSTimer,但这样处理效果并不好,因为跳转到下一页定时器也会停止工作,与业务不符 - 使用
didMoveToParentViewController可以很好地解决这层强引用
-(void)didMoveToParentViewController:(UIViewController *)parent { if (parent == nil) { [self.timer invalidate]; self.timer = nil; } } -
方案二:中介者模式
- 使用其他全局变量,此时
timer持有全局变量,self也持有全局变量,只要页面pop,self因为没有被持有就能正常走dealloc,在dealloc中再去处理timer - 此时的持有链分别是runloop->timer->target->timer、self->target、self->timer
-(void)intermediary { self.target = [[NSObject alloc] init]; class_addMethod([NSObject class], @selector(timerFire), (IMP)fireFunc, "v@:"); self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(timerFire) userInfo:nil repeats:YES]; } void fireFunc() { num++; NSLog(@"current - %d",num); } -(void)dealloc { [self.timer invalidate]; self.timer = nil; NSLog(@"%s", __FUNCTION__); } - 使用其他全局变量,此时
-
方案三:使用包装者
- 类似于
方案二,但是在使用更便捷 - 如果传入的
响应者target能响应传入的响应事件selector,就使用runtime动态添加方法并开启计时器 fireWapper中如果如果wrapper.target,就让wrapper.target(外界响应者)调用wrapper.aSelector(外界响应事件)fireWapper中没有了wrapper.target,意味着响应者释放了(无法响应了),此时定时器也就可以休息了(停止并释放)- 持有链分别是runloop->timer->FXProxy、vc->FXProxy-->vc
- 类似于
-
方案四:使用虚基类——
NSProxy有着NSObject同等的地位,多用于消息转发- 使用
NSProxy打破NSTimer的对vc的强持有,但是强持有依然存在,需要手动关闭定时器 - 持有链分别是runloop->timer->FXTimerWrapper->timer、vc->FXTimerWrapper-->vc
- 使用
第一种较为简便,第二种合理使用中介者但是很拉胯,第三种适合装逼,第四种更适合大型项目(定时器用的较多) 详细代码
五、自动释放池
1. autoreleasepool结构
通过clang命令对空白的main.m输出一份main.cpp文件来查看@autoreleasepool的底层结构
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
在objc源码中有一段对AutoreleasePool的注释
/***********************************************************************
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
**********************************************************************/
从中可以得出几点:
自动释放池是一个以栈为节点的结构,拥有栈的特性——先进后出自动释放池的节点可以是对象(可以被释放)也可以是POOL_BOUNDARY(边界/哨兵对象)自动释放池的数据结构是双向链表自动释放池跟tls/线程是有关系的
自动释放池中有个相关的对象AutoreleasePoolPage是继承于AutoreleasePoolPageData
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
magic用来检验AutoreleasePoolPage的结构是否完整next指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()thread指向的的当前线程parent指向父节点,第一个节点的parent值为nilchild指向子节点,最后一个节点的child值为nildepth代表深度,从0开始,往后递增1hiwat代表high water mark——最大入栈数量标记
parent和child两个属性的存在就能证明双向链表的结构,而begin()又是什么呢?
在AutoreleasePoolPage中有个属性begin
这个指针地址为什么要加上
56呢?这个56是哪里来的呢?其实就是AutoreleasePoolPage中的固有属性
分析:
AutoreleasePoolPageData中的指针和对象都占8字节,uint占4字节,只有magic_t未知(因为不是个指针,所以需要看具体类型);magic_t是个指针,由于静态变量的存储区域在全局段,所以magic_t占用4*4=16字节
接着使用_objc_autoreleasePoolPrint函数来打印一下自动释放池的相关信息
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] autorelease];
_objc_autoreleasePoolPrint();
}
return 0;
}
objc[18648]: ##############
objc[18648]: AUTORELEASE POOLS for thread 0x1000d3dc0
objc[18648]: 2 releases pending.
objc[18648]: [0x10081c000] ................ PAGE (hot) (cold)
objc[18648]: [0x10081c038] ################ POOL 0x10081c038
objc[18648]: [0x10081c040] 0x100641de0 NSObject
objc[18648]: ##############
POOL与PAGE的地址相差38(16进制),相当于56(10进制)
那么是否可以无限往AutoreleasePool添加对象呢?答案是不能!
在AutoreleasePoolPage中有个SIZE=4096字节=56字节+4040字节=56字节+8*505个对象
#define PAGE_MIN_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096
将i的上限改为505会输出
objc[18750]: ##############
objc[18750]: AUTORELEASE POOLS for thread 0x1000d3dc0
objc[18750]: 506 releases pending.
objc[18750]: [0x103809000] ................ PAGE (full) (cold)
objc[18750]: [0x103809038] ################ POOL 0x103809038
objc[18750]: [0x103809040] 0x102937230 NSObject
...
objc[18750]: [0x10380e000] ................ PAGE (hot)
objc[18750]: [0x10380e038] 0x10293c1c0 NSObject
objc[18750]: ##############
将i的上限改为505+505会输出
objc[19082]: ##############
objc[19082]: AUTORELEASE POOLS for thread 0x1000d3dc0
objc[19082]: 1011 releases pending.
objc[19082]: [0x10480c000] ................ PAGE (full) (cold)
objc[19082]: [0x10480c038] ################ POOL 0x10480c038
objc[19082]: [0x10480c040] 0x101c3e9d0 NSObject
...
objc[19082]: [0x10480e000] ................ PAGE (full)
objc[19082]: [0x10480e038] 0x101c43960 NSObject
...
objc[19082]: [0x104810000] ................ PAGE (hot)
objc[19082]: [0x104810038] 0x101c458f0 NSObject
由于自动释放池在初始化时会讲POOL_BOUNDARY哨兵对象push到栈顶,所以第一页只能存放504个对象,接下来每一页都能存放505个对象
2.哨兵对象
# define POOL_BOUNDARY nil
哨兵对象本质上是个nil,它的作用主要在调用objc_autoreleasePoolPop时体现:
- 根据传入的
哨兵对象地址找到哨兵对象所在的page - 在当前page中,将晚于
哨兵对象插入的所有autorelese对象都发送一次release消息,并移动next指针到正确位置 - 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到
哨兵对象所在的page
3.进栈/出栈
objc_autoreleasePoolPush和objc_autoreleasePoolPop分别代表进栈和出栈
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
接下来看看进栈时的操作:push->autoreleaseFast
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
autoreleaseFast中分为三条分支:(hotPage可以获取当前的AutoreleasePoolPage)
- 无当前页(刚创建,意味着池子尚未被push)
- 调用
autoreleaseNoPage创建一个hotPage - 调用
page->add(obj)将对象添加至AutoreleasePoolPage的栈中
- 调用
- 有
hotPage但page没有满(当前页尚未存满)- 调用
page->add(obj)将对象添加至AutoreleasePoolPage的栈中
- 调用
- 有
hotPage且page已满(当前页已存满)- 调用
autoreleaseFullPage初始化一个新page - 调用
page->add(obj)将对象添加至AutoreleasePoolPage的栈中
- 调用
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
autoreleaseFullPage通过遍历当前页的子页,如果存在继续遍历,如果不存在就开辟新的AutoreleasePoolPage并设为HotPage
接下来看看出栈操作
objc_autoreleasePoolPop传入的ctxt是一个指针,回顾上文中的clang文件就可以得知传入的是哨兵对象atautoreleasepoolobj
AutoreleasePoolPage::pop(ctxt)->pop->popPage最终会调用到popPage
popPage其中
- 通过
page->releaseUntil(stop)通过一个while循环和next指针来不断遍历调用objc_release(obj)释放对象,直到next指针指向栈顶才停止循环 - 然后开始
page->kill()、page->child->kill()等,最后setHotPage(nil)
4.提出疑问
- 临时变量什么时候释放?
- 每一次运行循环执行后,也就是每当事件被触发时都会创建自动释放池。在程序执行的过程中,所有autorelease的对象在出了作用域之后会被添加到最近创建的自动释放池中。运行循环结束前会释放自动释放池,还有池子满了也会销毁
- 每一次运行循环执行后,也就是每当事件被触发时都会创建自动释放池。在程序执行的过程中,所有autorelease的对象在出了作用域之后会被添加到最近创建的自动释放池中。运行循环结束前会释放自动释放池,还有池子满了也会销毁
- 自动释放池原理
- 自动释放池被销毁或耗尽时会向池中的所有对象发送release消息,释放所有autorelease对象
- 自动释放池能否嵌套使用?
MRC下autoreleasepool需要手动添加@autoreleasepool与线程关联,不同的线程的autoreleasepool是不一样的@autoreleasepool嵌套时只会创建一个page,但是有两个哨兵- 为什么只有
一个page?因为page的创建和线程有关,一个线程对应一个autoreleasepool的空间 - 为什么有
两个哨兵?因为有两个作用域
写在后面
最近的打算是先将iOS探索系列完结,然后开始着手整理其他系列,本系列后续还有Runloop、启动优化、性能优化、页面优化、组件化、项目架构,敬请期待!
喜欢作者的文章可以点赞+收藏iOS探索系列支持一下