原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、内存布局
- 二、内存管理方案:Tagged Pointer
- 1、引入原因
- 2、特点
- 3、原理
- 4、面试题
- 三:内存管理方案:SideTables 散列表中的引用计数表
- 1、简介
- 2、MRC
- 3、ARC
- 四:内存管理方案:SideTables 散列表中的弱引用表 weak
- 1、简介
- 2、常见的循环引用场景:NSTimer
- 3、探索NSTimer循环引用的解决方案
- 4、weak的源代码解析
- 三、自动释放池 Autoreleasepool
- 1、简介
- 2、手动调用autoreleasepool
- 3、子线程AutoRelease对象何时释放
- Demo
- 参考文献
一、内存布局
内存布局栈区
- 创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
- 在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。
- 和堆一样,用户栈在程序执行期间可以动态地扩展和收缩,向下增长。
堆区
- 通过
new
、alloc
等分配的对象、block
经过copy
后,c通过malloc
。 - 它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会回销毁该内存区域对象)。
- 一般一个
new
就要对应一个release
。在ARC
下编译器会自动在合适位置为OC
对象添加release
操作,会在当前线程Runloop
退出或休眠时销毁这些对象。MRC
则需程序员手动释放。 - 堆可以动态地扩展和收缩,向上增长。
NSString
的对象就是stack
中的对象,NSMutableString
的对象就是heap
中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,适用于计数管理内存管理模式。两类对象的创建方法也不同,前者直接创建NSString * str1=@"welcome";
,而后者需要先分配再初始化NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"];
。
bss(静态区)
未初始化的全局变量和静态变量,程序运行过程内存的数据一直存在,程序结束后由系统释放。
data(常量区)
已初始化的全局变量、静态变量、常量,在程序结束后由系统释放。
text(代码区)
用于存放程序运行时的代码,是被编译成二进制的程序代码。
二、内存管理方案:Tagged Pointer
1、引入原因
通常我们创建对象,对象存储在堆中,对象的指针存储在栈中,如果我们要找到这个对象,就需要先在栈中,找到指针地址,然后根据指针地址找到在堆中的对象。
这个过程比较繁琐,当存储的对象只是一个很小的东西,比如一个字符串,一个数字。去走这么一个繁琐的过程,无非是耗费性能的,所以苹果就搞出了TaggedPointer
这么一个东西。
2、特点
-
TaggedPointer
是苹果为了解决32位CPU
到64位CPU
的转变带来的内存占用和效率问题,专门用来存储小的对象,针对NSNumber
、NSDate
以及部分NSString
的内存优化方案。 -
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮囊的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
。 -
Tagged Pointer
指针中包含了当前对象的地址、类型、具体数值。因此Tagged Pointer
指针在内存读取上有着3倍的效率,创建时比普通需要malloc
跟free
的类型快106倍。
3、原理
苹果将Tagged Pointer
引入,给 64 位系统带来了内存的节省和运行效率的提高。Tagged Pointer
通过在其最后一个 bit
位设置一个特殊标记,用于将数据直接保存在指针本身中。因为Tagged Pointer
并不是真正的对象,我们在使用时需要注意不要直接访问其isa
变量。
如果这个整数只是一个 NSInteger
、NSNumber
、NSDate
的普通变量,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的,占用的内存会翻倍。
为了存储和访问一个 NSNumber
对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失
int main(int argc, char * argv[])
{
@autoreleasepool {
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberffff pointer is %p", numberFFFF);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
//输出结果
number1 pointer is 0xb000000000000012
number2 pointer is 0xb000000000000022
number3 pointer is 0xb000000000000032
numberFFFF pointer is 0xb0000000000ffff2
苹果确实是将值直接存储到了指针本身里面。我们还可以猜测,数字最末尾的 2
以及最开头的 0xb
是否就是苹果对于Tagged Pointer
的特殊标记呢?
我们尝试放一个 8 字节的长的整数到NSNumber
实例中,对于这样的实例,由于Tagged Pointer
无法将其按上面的压缩方式来保存,那么应该就会以普通对象的方式来保存,我们的实验代码如下:
NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSLog(@"bigNumber pointer is %p", bigNumber);
// 输出
bigNumber pointer is 0x10921ecc0
验证了我们的猜测,bigNumber
的地址更像是一个普通的指针地址。可见,当 8 字节可以承载用于表示的数值时,系统就会以Tagged Pointer
的方式生成指针,如果 8 字节承载不了时,则又用以前的方式来生成普通的指针。
4、面试题
为什么第二个for
会崩溃?答:taggedpointer
。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0 ; i<1000000; i++) {
self.str = @"abcd";
}
});
dispatch_async(queue, ^{
for (int i = 0 ; i<1000000; i++) {
self.str = [NSString stringWithFormat:@"adfalkdjfldkasjflakjsdkflasf-- %d",I];
}
});
执行了objc_release(id obj)
,由于大量的循环,导致了线程问题,使引用计数<=-1
。但是由于第一个循环中的obj
是taggedpointer
类型的string
,会直接return obj
,并不会release
。
这里release
,那retain
的时候咋办呢,引用计数是一直往上增吗?并不是,在objc_retain(id obj)
中,同样判断了obj->isTaggedPointer
,如果是true
,就直接return obj
。
三、内存管理方案:SideTables 散列表中的引用计数表
1、简介
SideTables
是一个64个元素长度的hash
数组,里面存储了SideTable
。SideTables
的hash
键值就是一个对象obj
的address
。因此可以说,一个obj
对应了一个SideTable
,但是一个SideTable
,会对应多个obj
。因为SideTable
的数量只有64个,所以会有很多obj
共用同一个SideTable
。
a、什么是Spinlock_t 自旋锁?
自旋锁若已被其他线程获取,则当前线程会不断探索该锁是否被释放,如果释放则第一时间获取,适用于轻量访问的情况。
b、为什么不是一个SideTable?
一张表的引用计数或者弱引用需要顺序操作,这就需要等前一个解开锁才能继续工作,存在效率问题。所以通过分离锁打散成多张表(共8个),支持并发操作,提高访问效率。
c、怎样实现快速分流,即通过指针快速定位到属于哪一张SideTable表?
SideTable本质是一个哈希表,给定值是对象内存地址,目标值是数组下标索引,哈希查找通过取余存储值和取值,内存地址均匀分布。哈希函数是一个均匀散列函数,不需要通过遍历操作,效率很高。
2、MRC
a、什么是MRC?
MRC自己负责管理对象生命周期,负责对象的创建和销毁。
b、MRC举例分析
NSNumber *i = [NSNumber numberWithInteger:2];
[i retainCount];//1
NSMutableArray *array = [NSMutableArray array];
[array addObject:i];
[i retainCount];//2
NSNumber *j = i;//不获得i的所有权
[i retainCount];//2
NSNumber *j = [i retain];//获得
[i retainCount];//3
[i release];//2
[array removeObjectAtIndex:0];
[i retainCount];//1
[i release];//销毁
//释放动态分配的内存和关闭文件
- (void)dealloc{[super dealloc];}
c、MRC通过引用计数如何管理对象生命周期?
alloc
retain +1
release -1
retainCount 引用计数值
autorelease 结束时候调用release释放
dealloc
alloc实现:经过一系列调用,最终调用了C函数的calloc
,但此时并没有将引用计数+1
。
retainCount实现:刚alloc
对象引用计数值为0,之所以 retainCount
能获取到1,是因为添加了局部变量size_t refcnt_result
令其值为1,再让其实现相加操作。
SideTable &table = sideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find(this);
refcnt_resount+= it->second>>SIDE_TABLE_RC_SHIFT
retain实现:经过两次哈希查找,第一次从SideTables
中查找到对象所在的SideTable
,第二次从引用计数表中查找到该对象的引用计数值实现+1
(实际是通过地址偏移量来实现的)。
SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage += SIZE_TABLE_RC_ONE
release实现:经过两次哈希查找,第一次从SideTables
中查找到对象所在的SideTable
,第二次从引用计数表中查找到该对象的引用计数值实现-1
(实际是通过地址偏移量来实现的)。
SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage -= SIZE_TABLE_RC_ONE;
d、MRC经常会出现的内存泄漏问题
内存泄漏:指一个对象或变量在使用完成后没有释放掉。
容易导致内存泄漏的使用方式:
- 以
string
开头的方法,它是静态工厂方法,通过类直接调用 。 对于 使用该方法创建的对象, 其所有权非调用者所有 , 调用者无权释放它,否则就会因过度释放而“僵尸化"。 - 采用
alloc
、new
、copy
和mutableCopy
所创建的对象,所有权属于调用者,它的生命周期由调用者管理,调用者负责通过release
或autorelease
方法释放对象。
如何查找泄漏点?
Analyze
(product-->Analyze
)是静态分析工具 , Instruments
(product-->profile
)是动态分析工具( Leaks
和Allocations
)。
静态分析方法能发现大部分的问题,但是仅仅使用静态内存泄漏分析得到的结果并不是非常可靠,因为有的内存泄露是在运行时,用户操作时才产生的,所以我们需要将对项目进行更为完善的内存泄漏分析和排查。那就需要用到我们下面要介绍的动态内存泄漏分析方法Instruments
中的Leaks
方法进行排查。
点击左上角的红色圆点,这时项目开始启动了,由于
leaks
是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。如图所示,橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
Leaks
选中
Leaks Checks
,在Details
所在栏中选择CallTree
,并且在右下角勾选Invert Call Tree
和Hide System Libraries
,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。Leaks
- 监控内存分布情况,随着时间的变化内存使用的折线图,有红色菱形图标 O 出现 , 则有内存泄漏
- 显示泄漏的对象,列出了它们的内存地址 、 占用字节 、 所属框架和响应方法等信息
-
RetCt
是引用计数列,最后的引用计数不为零 ,这说明该对象内存没有释放 - 点击右边的跟踪栈信息按钮,可以定位到泄露点在项目中的代码位置
- 调用者没有这个对象的所有权而释放它 ,都会造成过度释放,从而产生僵尸对象(
Zombies
分析模板),试图调用僵尸对象 ,则 会崩溃(应用直接跳出),并抛出EXC_BAD_ACCESS
异常,僵尸对象的引用计数变化是 : l (创建) ----->0 (释放)一 (僵尸化)
3、ARC
a、什么是ARC?
ARC
是LLVM
和Runtime
协同作用的结果,在这种模式下,程序员不需要清楚地了解获得/放弃对象所有权的时机,ARC 会自动添加被注释掉的行,即在适当的位置插入retain
、release
或autorelease
函数,这样程序员能将更多的精力用于开发程序的核心功能。
b、ARC有什么好处?
- 不需要再在意对象的所有权
- 可以删除程序中内存管理部分的大部分代码,使程序看起来更清爽
- 可以避免手动内存管理时的错误(内存泄漏等)
- 可以使多线程环境下的编程更简单。例如:不用担心不同的线程之间可能出现的所有权冲突
c、ARC有什么特点?
- ARC会禁止调用引用计数的相关函数
retain
、release
、autorelease
、retainCount
。 - 可以重写
dealloc
方法,但是不能显示调用super.dealloc
。 - ARC中新增
weak
、strong
属性关键字。
四、内存管理方案:SideTables 散列表中的弱引用表 weak
1、简介
a、什么是循环引用?
当双方都在等待对方释放的时候,就形成了循环引用,结果两个对象都不会被释放,只有打破循环引用关系才能够释放内存。
ARC代码中的内存泄漏多半是由于强引用循环引起的 ,从而导致一些内存无法释放,这就会导致dealloc()
方法无法被调用, Leaks
模板提供了查看引用循环视图,选择Cycles & Roots
菜单项即可查看。
b、循环引用的解决方案有哪些?
- 通过使用弱引用既可以防止生成循环引用,实例对象被释放后自动变成nil,又可以防止对象被释放后形成野指针。
- 或者通过手动给一方赋值为nil 来打破循环引用关系
2、常见的循环引用场景:NSTimer
a、NSTimer的创建
第一中方法需要手动加入Runloop
。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1
target:weakSelf
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
forMode:NSDefaultRunLoopMode];
第二中方法自动加入Runloop
。
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:weakSelf
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
调用的方法为:
- (void)fireHome {
num++;
NSLog(@"hello word - %d",num);
}
b、NSTimer循环引用分析
发现VC pop之后,定时器并没有停止输出,deinit(swift)
方法也没有执行,这就是循环引用了。这个Timer
的addTarget
和UIButton
的addTarget
有什么不同呢?button
对target
是弱引用,Timer
对target
是强引用。self
强持有timer
我们都能直接看出来,target
方法中,timer
对self
对象进行了强持有,因此造成了循环引用。
3、探索NSTimer循环引用的解决方案
a、weakSelf
按照惯例用weakSelf
去打破强引用的时候,发现weakSelf
没有打破循环引用,timer
仍然在运行。
变成了self -> timer -> weakSelf -> self
,timer
之所以无法打破循环关系是因为timer
创建时target
是对weakSelf
的对象强持有操作,而weakSelf
和self
虽然是不同的指针但是指向的对象是相同的,也就相当于间接的强持有了self
,所以weakSelf
并没有打破循环引用关系。
那么block
使用weakSelf
为什么可以打破循环引用呢?
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object; // 实际上是指针赋值
在.cpp
文件中我们可以看到如上代码段,虽然weakself
对象传入进来,但是内部实际操作的是对象的指针,也就是weakself
的指针,我们知道weakself
和self
虽然内存地址相同,但指针地址是不一样的,也就是block
中并没有直接持有self
,而是通过weakSelf
指针操作,所以就打破了self -> block -> weakSelf -> self
中self
这一层的循环引用,变成了self -> block -> weakSelf
(临时变量的指针地址)来打破循环引用。
b、使用weak关键字修饰
一说到循环引用很容易就想到weak
,但是这里用weak
不行。这要从weak
修饰的对象的释放时机说起,用了weak
关键字修饰之后,系统会在一个hash
表中增加一对key value
,key
就是这个对象的hash
值,value
就是这个对象的指针地址。
我们都知道每一个runloop
都有自己的一个autorelease pool
,在一次runloop
结束之后会销毁这个autorelease pool
,在释放这个autorelease pool
的时候,也会到hash
表中找到weak
的对象把它和它的指针都释放掉,同时,那么问题来了,我这个timer
是在runloop
里面重复执行的,换而言之,这个runloop
是一直在执行的,所以这个池子根本不会释放啊有木有,所以用它没什么卵用啊。
c、使用invalidate结束timer运行
我们第一时间肯定想到的是[self.timer invalidate]
不就可以了吗,当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear
还是viewDidDisAppear
?实际上在我们实际操作中,如果当前页面有push
操作的话,而当前页面并没有pop
即还在栈里面,这时候我们释放timer肯定是错误的,所以这时候我们可以用到下面的方法:
- (void)didMoveToParentViewController:(UIViewController *)parent {
// 无论 push进来 还是 pop出去 都能正常运行
// 就算继续push到下一层 pop回去还是能够继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
为什么不在deinit(swift)
方法里面释放?因为denint
方法都不执行了,写了也没用。
d、中介者模式
中介者模式换个思路,timer
会造成循环引用是因为target
强持有了self
,造成的循环引用,那我们是否可以包装一下target
,使得timer
绑定另外一个不是self
的target
对象来打破这层强持有关系。
根据打印结果我们发现在dealloc
的时候也可以实现timer
的释放,打破了循环引用。class_addMethod
的作用看着是给target
增加了一个方法,但是实际上timer
的执行是在fireHomeObjc
里面执行的,而不是应该执行的fireHome
函数。
优化下代码,既然class_addMethod
中需要一个函数的IMP
,那么我们直接获取fireHome
的IMP
就可以了:
self.target = [[NSObject alloc] init];
Method method = class_getInstanceMethod([self class], @selector(fireHome));
class_addMethod([self.target class], @selector(fireHome), method_getImplementation(method), "v@:");
self.timer = [NSTimer
scheduledTimerWithTimeInterval:1
target:self.target
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
e、NSProxy虚基类的方式
NSProxy
是一个虚基类,它的地位等同于NSObject
。我们不用self
来响应timer
方法的target
,而是用NSProxy
来响应。虚基类方法是用proxy
打破self
这一块的循环。
// XJPProxy.h
@interface XJPProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
//XJPProxy.m
@interface XJPProxy()
@property (nonatomic, weak) id object;
@end
@implementation XJPProxy
+ (instancetype)proxyWithTransformObject:(id)object {
XJPProxy *proxy = [[XJPProxy alloc] init];
proxy.object = object; // 我们拿到外边的self,weak弱引用持有
return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// proxy虚基类并没有持有vc,而是消息的转发,又给了vc
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
//VC.m
- (void)viewDidLoad {
[super viewDidLoad];
self.proxy = [DZProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
4、weak的源代码解析
a、weak_entry_t
- 查找
weak_entry_t
- 判断
weak_table_t
是否需要扩容 - 插入新的
weak_entry_t
- 移除
weak_entry_t
查找weak_entry_t:找到对应对象的存放位置,需要处理hash
冲突,如果存在hash
冲突具体会往下一个查找,查找到返回对应weak_entry_t
,当超过了最大的冲突处理次数后,说明没有查找到,就会返回nil
,源代码如下:
static weak_entry_t *
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;
///hash算法
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
///找到对应对象的存放位置,需要处理hash冲突,如果存在hash冲突具体会往下一个查找,直到找到空位,这个就是开放地址法处理hash冲突
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++;///冲突处理次数加1
///当超过了最大的冲突处理次数后,说明没有查找到,就会返回nil。
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
///查找到返回对应weak_entry_t
return &weak_table->weak_entries[index];
}
判断weak_table_t是否需要扩容:默认容量大小初始值是64,超过最大容量的3/4,就在原来的容量基础上*2,源代码如下:
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
///超过最大容量的3/4
if (weak_table->num_entries >= old_size * 3 / 4) {
///在原来的容量基础上*2,默认初始值是64
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
插入新的weak_entry_t:通过hash
算法找到为nil
的位置,存储weak_entry_t
,源代码如下:
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
assert(weak_entries != nil);
///跟查找一样的hash算法
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
///找到为nil的位置
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;///当前的最大冲突次数
}
weak_entries[index] = *new_entry;///存储weak_entry_t
weak_table->num_entries++;///已存储位置数+1
if (hash_displacement > weak_table->max_hash_displacement) {
///当前冲突次数比max_hash_displacement大,则赋值给max_hash_displacement
weak_table->max_hash_displacement = hash_displacement;
}
}
移除weak_entry_t:分别从静态和动态数组中移除即可,代码量挺大,如下:
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
///从静态数组中移除
if (! entry->out_of_line()) {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == old_referrer) {
entry->inline_referrers[i] = nil;
return;
}
}
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
///从动态数组中移除
size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (entry->referrers[index] != old_referrer) {
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
hash_displacement++;
if (hash_displacement > entry->max_hash_displacement) {
_objc_inform("Attempted to unregister unknown __weak variable "
"at %p. This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
old_referrer);
objc_weak_error();
return;
}
}
entry->referrers[index] = nil;
entry->num_refs--;
}
b、添加weak变量
添加的位置是通过哈希算法进行添加的位置查找,如果查找到的位置已经有了当前对象的弱引用数组,就把新的weak
变量添加到该数组,如果没有创建新数组,添加weak
变量到第一个,其余初始化为nil
。
- 调用
objc_initWeak()
- 调用
storeWeak()
- 调用
weak_register_no_lock()
步骤一:objc_initWeak
方法中的两个参数location
是指weak
指针,newObj
是 weak
指针将要指向的对象,源代码如下:
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
步骤二:这里要先进行haveOld
判断,也就是如果该指针有指向的旧值,先要weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
,处理旧值即移除weak
指针。然后通过 weak_register_no_lock(&newTable->weak_table, (id)newObj, location,crashIfDeallocating);
进行赋值操作即插入weak
指针,源代码如下:
///如果有旧指向,从旧表中移除weak地址值
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
if (haveNew) {
///在新表中插入weak指针的地址,成功返回newObj,否则返回nil
newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
///Tagged Pointer是苹果在64位系统之后,用来优化内存的
if (newObj && !newObj->isTaggedPointer()) {
///在引用计数表中标记有弱引用指向,当引用计数为0时,触发移除相对应的weak指针。
newObj->setWeaklyReferenced_nolock();
}
*location = (id)newObj;///将weak指针指向新的对象
}
步骤三:weak_register_no_lock()
的功能是判断是否存在弱引用weak_entry_t
,有,则在对应的数组中插入当前需要插入的weak
指针,没有,新建一个weak_entry_t
数据插入到弱引用表中,源代码如下:
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (!referent || referent->isTaggedPointer()) return referent_id;///运用TaggedPointer计数,无需维护weak_table_t弱引用表
weak_entry_t *entry;///弱引用
if ((entry = weak_entry_for_referent(weak_table, referent))) {///在弱引用表中查找weak_entry_t,存在
append_referrer(entry, referrer);///插入weak指针
}
else {///没有weak_entry_t
weak_entry_t new_entry(referent, referrer);///new一份weak_entry_t
weak_grow_maybe(weak_table);///判断weak_table是否需要扩容
weak_entry_insert(weak_table, &new_entry);///插入weak指针,
}
return referent_id;
}
c、清除weak变量
❶ dealloc的流程图
举个例子:
__weak Person *weakPerson = person;
假如我们的person
对象已经被释放掉了,那么就需要告诉对象weakPerson
一声,防止野指针,导致程序崩溃,而clearDeallocating
函数所做的事情就是把很多类似weakPerson
这样的弱引用全部都置为nil
。具体weak
是如何实现的,这里给了一个表:
❶ 从图中知道有个全局表叫SideTables
,它里面维护了所有对象的引用计数,还有弱引用对象。当RefcountMap
表中某个对象的引用计数为0时,系统会调用此类的dealloc
方法,再调用其父类的dealloc
,沿着继承链一直调用到NSObject
的dealloc
方法,最终走到objc_destructInstance
函数里,主要操作四个:释放实例变量、移除关联属性、把弱引用置为nil
、释放自己self
。
在调用dealloc
之后,会经过 _objc_rootDealloc()
到rootDealloc()
,然后判断是否可以释放?判断释放的条件比较关键:
-
nonpointer_isa
:判断这个isa
指针类型,是否为非指针型的isa
-
weakly_referenced
:是否有弱引用 -
has_assoc
:是否有关联对象 -
has_cxx_dtor
:判断是否有C++
实现或者ARC
的实现 -
has_sideTable_rc
:引用计数是否在sideTable
中有存储
如果有一个满足条件,调用 object_disponse()
,再开始释放,objc_dispose()
的实现如下:
先判断是否有C++
实现,然后判断是否有关联对象,如果没有c++
,也没有关联对象,则直接调用clearDeallocating()
,否则分别调用object_cxxDestruct()
和_object_remove_assocaations
来释放。在这里系统内部实现清除了分类里定义的关联对象实例。
接下来在调用 clearDeallocating
的方法中,会调用sidetable_calearDeallocationg()
和weak_clear_no_lock()
两个方法,作用是指向该对象的弱引用指针置为nil
,这就是为什么我们不需要在dealloc
中将指向他的弱引用指针置为nil
的原因,接下来会调用table.refcnts.erase()
,来进行引用计数的擦除操作,然后结束流程。
❷ dealloc的源码解析
销毁对象这块最终需要调用到NSObject
类中的dealloc
方法:
//释放对象
- (void)dealloc {
_objc_rootDealloc(self);
}
沿着调用函数流程dealloc
—>_objc_rootDealloc
—>rootDealloc object_dispose
—>objc_destructInstance
走最终会发现:
void *objc_destructInstance(id obj)
{
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//---释放 C++ 的实例变量
if (cxx) object_cxxDestruct(obj);
//---移除关联属性
if (assoc) _object_remove_assocations(obj);
//---将弱引用置为nil
obj->clearDeallocating();
}
return obj;
}
❸ 清除weak变量
在weak_table
里有个数组weak_entries
,数组里元素都是结构体weak_entry_t
,这结构体里面有被引用的对象referent
,还有引用数组referrers
和inline_referrers
。需要注意的是,不是所有的对象都具有结构体weak_entry_t
的,只有当某个对象具有弱引用时,才会给这对象创建一个weak_entry_t
,并把它添加到数组weak_entries
中去。同理当一个对象变得没有弱引用时,会从数组weak_entries
中删去它对应的weak_entry_t
。我们知道一个对象可能有多个若引用,比如:
__weak Person *weakPerson1 = person;
__weak Person *weakPerson2 = person;
此时weakPerson1
和weakPerson2
都会放到weak_entry_t
中的referrers
数组或者inline_referrers
数组中去,二者区别主要是看数组长度大小超过4了没有,超过4,则放到referrers
中,否则放到inline_referrers
中,此时对应的referent
是person
。
这里说个非常重要的头文件objc-weak.h
,它专门处理OC中对象的弱引用问题,里面有几个核心方法:
-
weak_register_no_lock:给指定对象添加一个弱引用,当我们执行
_weak ClassA *objB = objA
类似代码时,会触发NSObject
的objc_initWeak
方法,最终会调用到weak_register_no_lock
方法。 - weak_unregister_no_lock:移除指定对象的一个弱引用。
- weak_is_registered_no_lock:判断指定对象是否存在弱引用。
-
weak_clear_no_lock:清除指定对象所有弱引用,上述
clearDeallocating
里最终调用的就是此方法。
对象dealloc
后,会调用弱引用清除函数weak_unregister_no_lock
,根据该对象查找弱引用表,遍历弱引用数组所有指针,分别置为nil
,源代码如下:
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{
//weak 指针指向的对象
objc_object *referent = (objc_object *)referent_id;
//referrer_id是 weak 指针, 操作时需要用到这个指针的地址
objc_object **referrer = (objc_object **)referrer_id;
weak_entry_t *entry;
if (!referent) return;
//查找 referent 对象对应的 entry
if ((entry = weak_entry_for_referent(weak_table, referent))) {
//从 referent 对应的 entry 中删除地址为 referrer 的 weak 指针
remove_referrer(entry, referrer);
//如果 entry 中的数组容量大于 4 并且数组中还有元素
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false; //entry 非空
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
//否则循环查找 entry 数组, 如果 4 个位置中有一个非空
if (entry->inline_referrers[i]) {
empty = false; //entry 非空
break;
}
}
}
if (empty) {
//从 weak_table 中移除该条 entry
weak_entry_remove(weak_table, entry);
}
}
}
其中从 weak_table
中移除该条 entry
的weak_entry_remove()
方法源代码如下:
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
///如果是静态数组,直接释放静态数组
if (entry->out_of_line()) free(entry->referrers);
///释放entry
bzero(entry, sizeof(*entry));
///对应的已存数量-1
weak_table->num_entries--;
///这里会查看下weakTable的容量是否需要减容
weak_compact_maybe(weak_table);
}
上面讲到过扩容,相对应的这里使用到的减容的方法 weak_compact_maybe()
源代码如下:
static void weak_compact_maybe(weak_table_t *weak_table)
{
size_t old_size = TABLE_SIZE(weak_table);
//当就容量超过1024且是利用率不及1/16的时候,减容
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {
///在原有的基础上除以8
weak_resize(weak_table, old_size / 8);
}
}
三、自动释放池 Autoreleasepool
1、简介
a、什么是@autoreleasepool?
用来管理自动释放池。release
消息马上将引用计数减1,而使用autorelease
对象的引用计数并不变化,而是向内存释放池中添加 一 条记录,会延迟到内存释放池周期到后,向池中所有对象发送 release消息,引用计数减 1。 当引用计数为 0时,对象所占用的内存才被释放 。
应用程序入口文件 main.m
,代码被包裹在@autoreleasepool {... }
之间,这是池的作用范围,默认是整个应用,释放默认在程序结束。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
编译的时候,这段代码会被转换成:
{
__AtAutoreleasePool __autoreleasepool;
}
其中,出现的结构体__AtAutoreleasePool
为:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这表明,我们的main
函数实际执行了:
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
@autoreleasepool
只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。
b、Autorelease Pool的主要结构
每一个autorelease pool
都是由一系列的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;
};
可见,自动释放池的AutoreleasePoolPage
是以双向链表的结构连接起来的,并且和线程是一一对应的关系。而在自动释放池的内存中,AutoreleasePoolPage
被以栈结构存储起来。
c、什么对象会加入Autoreleasepool中
- 使用
alloc
、new
、copy
、mutableCopy
的方法进行初始化时,由系统管理对象,在适当的位置release
。 - 使用
array
会自动将返回值的对象注册到Autoreleasepool
。 -
__weak
修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool
中。 -
id
的指针或对象的指针,在没有显示指定时会被注册到Autoleasepool
中。
d、Autorelease对象的释放时机
autorelease
释放对象的依据是Runloop
,简单说,runloop
就是iOS中的消息循环机制,当一个runloop
结束时系统才会一次性清理掉被autorelease
处理过的对象,其实本质上说是在本次runloop
迭代结束时清理掉被本次迭代期间被放到autorelease pool
中的对象的。
2、手动调用autoreleasepool
既然由runloop
来决定对象释放时机而不是作用域,那么,在一个{}内使用循环大量创建对象就有可能带来内存上的问题,大量对象会被创建而没有及时释放,这时候就需要靠我们人工的干预autorelease
的释放了。手动添加的是大括号结束的时候释放。
上文有提到autorelease pool
,一旦一个对象被autorelease
,则该对象会被放到iOS的一个池:autorelease pool
,其实这个pool
本质上是一个stack
,扔到pool
中的对象等价于入栈。
objc_autoreleasePoolPush()
的入栈流程图如下:
-
push
就是在page
中插入一个哨兵对象,代表这些属于要一起release
的对象。 - 如果
page
满了,则创建新的page
,和老的page
关联起来,将对象指针压栈。
多层嵌套就是多次插入哨兵对象。
同样,objc_autoreleasePoolPop()
的出栈流程图如下:
- 根据传入的哨兵对象找到对应位置。
- 给上次
push
操作之后添加的对象依次发送release
消息 - 回退
next
指针到正确位置。
我们把需要及时释放掉的代码块放入我们生成的autorelease pool
中,结束后清空这个自定义的pool
,主动地让pool
清空掉,从而达到及时释放内存的目的。以上述图片处理的例子为例,优化如下:
for (int i = 0; i <= 1000; i ++) {
//创建一个自动释放池
NSAutoreleasePool *pool = [NSAutoreleasePool new];//也可以使用@autoreleasePool{domeSomething}的方式
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"PNG"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];
UIImage *image2 = [image imageByScalingAndCroppingForSize:CGSizeMake(480, 320)];
[image release];
//将自动释放池内存释放,它会同时释放掉上面代码中产生的临时变量image2
[pool drain];
}
这样,每次循环结束时,可以及时的释放临时对象的内存,其中,对自动释放池的操作可以用上文提到的方法来替代:
@autoreleasePool{
//domeSomeThing;
}
3、子线程AutoRelease对象何时释放
在AutoreleasePoolPage pop
的时候释放,在主线程的runloop
中,有两个oberserver
负责创建和清空autoreleasepool
,详情可以看我的文章IOS的RunLoop,看这一篇文章就够了。那么子线程呢?子线程的runloop
都需要手动开启,那么子线程中使用autorelease
对象会内存泄漏吗,如果不会又是什么时候释放呢。
a、子线程的autoreleasepool如何被创建?
在MRC
下,使用@autoreleasing
修饰符等同于MRC
下调用autorelease
方法,所以在NSObject
源码中找到-(id)autorelese
方法开始看(简化版的):
static inline id autorelease(id obj)
{
id *dest __unused = autoreleaseFast(obj);
return obj;
}
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);
}
}
id *autoreleaseNoPage(id obj)
{
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push the requested object or pool.
return page->add(obj);
}
得知如果当前线程没有AutorelesepoolPage
的话,代码执行顺序为autorelease
-> autoreleaseFast
-> autoreleaseNoPage
。在autoreleaseNoPage
方法中,会创建一个hotPage
,然后调用page->add(obj)
。
也就是说即使这个线程没有AutorelesepoolPage
,使用了autorelease
对象时也会new
一个AutoreleasepoolPage
出来管理autorelese
对象,不用担心内存泄漏。
b、子线程的autoreleasepool何时被释放?
明确了何时创建autoreleasepool
以后就自然而然的有下一个问题,这个autoreleasepool
何时清空?对于这个问题,这里使用watchpoint set variable
命令来观察。首先是一个最简单的场景,创建一个子线程:
__weak id obj;
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread) toTarget:self withObject:nil];
使用一个weak
指针观察子线程中的autorelease
对象,子线程中执行的任务。
- (void)createAndConfigObserverInSecondaryThread{
__autoreleasing id test = [NSObject new];
NSLog(@"obj = %@", test);
obj = test;
[[NSThread currentThread] setName:@"test runloop thread"];
NSLog(@"thread ending");
}
在obj = test
处设置断点使用watchpoint set variable obj
命令观察obj
,可以看到obj
在释放时的方法调用栈是这样的。
通过这个调用栈可以看到释放的时机在_pthread_exit
。然后执行到AutorelepoolPage
的tls_dealloc
方法。这个方法如下(简化版的):
static void tls_dealloc(void *p)
{
// pop all of the pools
if (!page->empty()) pop(page->begin());
// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}
在这找到了if (!page->empty()) pop(page->begin());
这句关键代码。再往上看一点,在_pthread_exit
时会执行下面这个函数:
void _pthread_tsd_cleanup(pthread_t self)
{
// clean up dynamic keys first
for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
pthread_key_t k;
for (k = __pthread_tsd_start; k <= self->max_tsd_key; k++) {
_pthread_tsd_cleanup_key(self, k);
}
}
}
也就是说thread
在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool
,在tls_delloc
中,执行了pop
操作。
c、子线程加入runloop的效果
上述这个例子中的线程并没有加入runloop
,只是一个一次性的线程。现在给这个线程加入runloop
来看看效果会是怎么样的。
对于runloop
,我们知道runloop
一定要有source
才能保证run
起来以后不立即结束,而source
有三种,custom source
,port source
,timer
。
先加一个timer
尝试下:
- (void)createAndConfigObserverInSecondaryThread{
[[NSThread currentThread] setName:@"test runloop thread"];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
// 用于监控runloop的状态
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopAllActivities,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopRef cfrunloop = [loop getCFRunLoop];
if (observer) {
CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(testAction) userInfo:nil repeats:YES];
[loop run];
NSLog(@"thread ending");
}
相应测试代码为:
- (void)testAction{
__autoreleasing id test = [NSObject new];
obj = test;
NSLog(@"obj = %@", obj);
}
在testAction()
中加上watchpoint
断点,观察obj
的释放时机:
可以看到释放的时机在CFRunloopRunSpecific
中,也就是runloop
切换状态的时候。
用自己实现的source
尝试下:
- (void)createAndConfigObserverInSecondaryThread{
__autoreleasing id test = [NSObject new];
NSLog(@"obj = %@", test);
obj = test;
[[NSThread currentThread] setName:@"test runloop thread"];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopAllActivities,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopRef cfrunloop = [loop getCFRunLoop];
if (observer) {
CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
// 自定义source
CFRunLoopSourceRef source;
CFRunLoopSourceContext sourceContext = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &runLoopSourcePerformRoutine};
source = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
CFRunLoopAddSource(cfrunloop, source, kCFRunLoopDefaultMode);
runLoopSource = source;
runLoop = cfrunloop;
[loop run];
NSLog(@"thread ending");
}
-(void)wakeupSource{
//通知InputSource
CFRunLoopSourceSignal(runLoopSource);
//唤醒runLoop
CFRunLoopWakeUp(runLoop);
}
这里wakeupSource()
是一个按钮的点击事件,用于唤醒runloop
。runloop
唤醒之后将执行runLoopSourcePerformRoutine
函数:
void runLoopSourcePerformRoutine (void *info)
{
__autoreleasing id test = [NSObject new];
obj = test;
// 如果不对obj赋值,obj会一直持有createAndConfigObserverInSecondaryThread函数入口的那个object,那个object不受这里面的autoreleasepool影响。
NSLog(@"obj is %@" , obj);
NSLog(@"回调方法%@",[NSThread currentThread]);
}
在runLoopSourcePerformRoutine()
中观察obj
的释放时机,发现是在[NSRunloop run:beforeDate:]
中。所以即使是我们自定义的source
,执行函数中没有释放autoreleasepool
的操作也不用担心,系统在各个关键入口都给我们加了这些操作。
d、总结
- 子线程在使用
autorelease
对象时,如果没有autoreleasepool
会在autoreleaseNoPage
中懒加载一个出来。 - 在
runloop
的run:beforeDate
,以及一些source
的callback
中,有autoreleasepool
的push
和pop
操作,总结就是系统在很多地方都有autorelease
的管理操作。 - 就算插入后没有
pop
也没关系,在线程exit
的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc
,在这里面会清空autoreleasepool
。
Demo
Demo在我的Github上,欢迎下载。
MemoryManagementDemo
参考文献
深入理解 Tagged Pointer
iOS weak底层原理及源码解析
iOS weak源码详解
iOS Runtime探索之旅
网友评论