欢迎阅读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
而不是NSTaggedPointerString
NSString
不能超过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
、NSDate
taggedPointer
指针的值不再是地址了,而是真正的值。所以他不再是一个对象了,他只是一个"披着对象外皮"的普通变量而已——它的内存并不在堆中,也不需要malloc
和free
taggedPointer
不像地址指针一样,直接从指针中拿到值——编译读取的时候更加直接了- 在内存读取上有着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/dealloc
ARC
新加了weak
、strong
关键字
1. alloc
之前已经对alloc流程有了一个详细的介绍
2. retain
retain
会在底层调用objc_retain
objc_retain
先判断是否为isTaggedPointer
,是就直接返回不需要处理,不是在调用obj->retain()
objc_object::retain
通过fastpath
大概率调用rootRetain()
,小概率通过消息发送调用对外提供的SEL_retain
rootRetain
调用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_release
rootRelease
调用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
会调用rootRetainCount
rootRetainCount
的具体实现
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
调用rootDealloc
rootDealloc
- 判断是否为
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__);
}
这段代码运行起来所发生的问题就是当前VC
pop到前一页时不会触发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探索系列支持一下