内存布局
- stack(栈区): 方法调用
- heap(堆区):通过
alloc
等分配的对象 - bss:未初始化的全局变量或静态变量等。
- data:已初始化的全局变量等。
- text:程序的代码段
内存管理方案
iOS是如何对内存进行管理的?
TaggedPointer:对一些小对象如
NSNumber
等-
NONPOINTER_ISA: 对于64位架构下的应用程序
在64位架构下,isa指针占用64位bit,实际有32位或者40位就够用了,剩余的实际上是浪费的,苹果为了提高内存利用率,在这些剩余的bit位当中,存储了一些关于内存管理的相关数据内容,所以称为非指针型的isa
-
散列表
散列表是一个复杂的数据结构,其中包含了应用计数表和弱引用计数表。
NONPOINTER_ISA结构
arm64架构
第0号位是
indexed
的标志位,如果这个位置是0,代表的是我们使用的isa指针只是一个纯的isa指针,它里面的内容就直接代表了当前对象的类对象的地址;如果这个位置是1,就代表这个isa指针里面存储的不仅是他的类对象的地址,而且还有一些内存管理方面的数据。第1号位
has_assoc
是表示当前对象是否有关联对象,0没有,1有。第2位
has_cxx_dtor
,表示的是当前对象是否有使用到C++
相关的一些代码,或者C++
语言方面的一些内容。在ARC中也可以通过这个标志位,来表示有些对象是通过ARC来进行内存管理的。后面的3-35位
shiftcls
,表示当前对象的类对象指针地址。后面的6位是一个
magic
字段后面是一位
weakly_referenced
,标识这个对象是否有相应的弱引用指针。deallocating
,表示的是当前对象是否在进行dealloc
操作has_sidetable_rc
,表示的是当前这个isa指针当中,如果所存储的引用计数已经达到了上限的话,需要外挂一个sidetable
数据结构,去存储相关的引用计数内容(也就是散列表)extra_rc
额外的引用计数,当我们引用计数在一个很小的值得范围之内就会存到isa指针当中,而不是由单独的引用计数表去存他的引用计数。
散列表方式
SideTables()
源码
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
SideTables()
结构
side tables
实际上是一个hash表,通过一个对象指针,找到他对应的引用计数表,或弱引用表。
Side Table
SideTable源码
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
SideTable结构
为什么不是一个side table
?
假如说只有一张side table
,相当于我们在内存当中分配的所有对象的引用计数或者说弱引用存储都放在一张大表当中,这个时候如果我们要操作某一个对象的引用计数值进行修改,比如说进行加1或减1的操作的话,由于所有的对象可能是在不同的线程当中去分配创建的,包括调用他们的release
,retain
等方法,也可能是在不同的线程当中进行操作;这个时候对一种表进行操作的时候,需要进行加锁处理,才能保证对于数据的访问安全,在这个过程中就存在了一个效率问题。比如说用户的内存空间一共有4GB,那么可能分配出成千上百万个内存对象,如果说每一个对象在对他进行内存引用计数的改变的时候,都操作这张表很显然就会有效率的问题。如果说已经又一个对象在操作这张表,下一个对象就要等他操作完,把锁释放之后再进行操作,这效率就会太低了。
系统为了解决效率问题,引入了分离锁的技术方案。我们可以把内存对象所对应的引用计数表,可以分拆成多个部分。比如说分拆成8个,需要对8个表分别加锁。当A和B同时进行引用计数操作的话可以进行并发操作,如果是一张表他们需要进行顺序操作。很明显分离锁可以提高访问效率。
怎样实现快速分流?
快速分流指通过一个对象的指针如何快速定位到它属于那张side table
表当中。
side tables
的本质是一张Hash表。这张hash表当中,可能有64张具体的side table
存储不同对象的引用计数表和弱引用表。
自旋锁 Spinlock_t
-
Spinlock_t是"忙等"的锁。
如果当前锁已被其他线程获取,那么当前线程会不断的探测这个锁是否有被释放,如果释放掉,自己第一时间去获取这个锁。所以说自旋锁是一种忙等的锁。获取不到锁的时候,他会他自己的线程阻塞休眠,然后等到其他线程释放这个锁的时候来唤醒当前线程。
适用于轻量访问。
引用计数表RefcountMap
引用计数表实际上是一个hash表,我们可以通过指针来找到对应对象的引用天计数,这一过程实际上也是hash查找(使用hash查找是为了提高查找效率)。
插入和获取是通过同一个hash函数完成,避免了递归查找和for循环遍历
size_t内存分配
- 第一个二进制位表示的是
weakly_referenced
,对象是否有弱引用,0没有,1有。 - 第二位
deallocating
表示当前对象是否处于dealloc
中 - 后面(RC)存储的是对象的实际引用计数值,在实际计算这个引用计数值,需要向右偏移两位,因为后面两位需要去掉。
弱引用表weak_table_t
weak_table_t实际上也是一个hash表.
weak_entry_t
实际上是一个结构体数组。结构体数组存储的是每一个的弱引用指针,也就是代码当中定义的__weak id obj
,obj内存地址即指针就存储在weak_entry_t
MRC & ARC
MRC 手动引用计数
-
alloc
: 用来分配一个对象的内存空间。 -
retain
:对一个对象的引用计数加1; -
release
:对一个对象的引用计数减1; -
retainCount
:获取当前对象的引用计数值 -
autorelease
:如果调用了一个对象的autorelease方法,当前这个对象会在autoreleasepool结束的时候,调用他的release操作进行引用计数减1. -
dealloc
:在MRC当中调用dealloc
方法需要显式调用[super dealloc]
来释放或废弃父类的相关成员变量。
ARC 自动引用计数
- ARC是LLVM和Runtime协作来进行自动引用计数管理;
- ARC中禁止手动调用
retain
,release
,retainCount
,dealloc
,并且在ARC中可以重写某个对象的dealloc
方法,但是不能再dealloc
方法当中,显示调用[super dealloc]
; - ARC中新增了
weak
,strong
属性关键字。
ARC实际是由编译期自动为我们插入
retain
和release
操作之外,还需要runtime的功能进行支持,然后由编译器和Runtime共同协作才能组成ARC的全部功能。
引用计数管理
实现原理分析
- alloc
- retain
- release
- retainCount
- dealloc
alloc实现
- 经过一系列调用,最终调用了C函数
calloc
- 此时并没有设置引用计数为1
retain实现
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
//hash查找SideTable
SideTable& table = SideTables()[this];
//SideTable加锁
table.lock();
//hash查找引用计数值
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//引用计数加1
//#define SIDE_TABLE_RC_ONE (1UL<<2)
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
- 通过当前对象的指针
this
,经过hash函数的计算,可以快速的SideTables
当中找到(hash查找)它对应的SideTable
。 - 然后在
SideTable
当中获取应用计数map这个成员变量,通过对象的指针this
,在SideTable
的引用计数表中获取(hash查找)当前当前对象的引用计数值。 - 经过一定的条件判断之后,引用计数加1。
引用计数加1,实际是加上了偏移量对应的操作,这个偏移量是4,反应出来的结果是加1,因为
size_t
64位,前两位不是存储引用计数,所以需要向左偏移两位操作1UL<<2
release实现
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
//hash查找SideTable
SideTable& table = SideTables()[this];
bool do_dealloc = false;
//加锁
table.lock();
//根据当前对象指针,访问table的应用计数表
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减1操作
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
- 通过当前对象的指针
this
,经过hash函数的计算,可以快速的SideTables
当中找到(hash查找)它对应的SideTable
。 - 根据当前对象指针,访问table的应用计数表
- 找到对应的值进行引用计数减1操作
retainCount实现
uintptr_t
objc_object::sidetable_retainCount()
{
//hash查找SideTable
SideTable& table = SideTables()[this];
//声明局部变量赋值为1
size_t refcnt_result = 1;
//加锁
table.lock();
//根据当前对象指针,访问table的应用计数表
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
//查找结果向右位移2位,再加上局部变量的值
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
通过当前对象的指针
this
,经过hash函数的计算,可以快速的SideTables
当中找到(hash查找)它对应的SideTable
。根据当前对象指针,访问table的应用计数表
-
找到对应的值向右位移2位,再加上局部变量的值1
这就是alloc操作之后,引用计数没有变化,但retainCount获取的值是1的原因
dealloc实现源码
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
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);
}
}
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();
}
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());
}
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
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();
}
void
objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
dealloc实现流程图
object_dispose()实现
objc_destructInstance()实现
clearDeallocating()实现
弱引用管理
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
添加weak变量的弱引用实现
当一个对象被释放或者废弃之后,weak变量怎样处理的?
/**
* Called by dealloc; nils out all weak pointers that point to the
* provided object so that they can no longer be used.
*
* @param weak_table
* @param referent The object being deallocated.
*/
void
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;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
//referrers取到弱引用指针的所有对应的数组列表
objc_object **referrer = referrers[I];
if (referrer) {//如果referrer即弱引用指针存在
if (*referrer == referent) { //如果弱引用指针对应的是被废弃的对象的话,就将指针置为nil
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
当一个对象被dealloc之后,内部实现当中会去调用
weak_clear_no_lock()
函数,函数实现内部会根据弱引用指针查找弱引用表把当前对象相对应的弱引用拿出来,然后遍历数组的所有弱引用指针,分别置为nil
自动释放池
编译期会将代码块@autoreleasepool{}
改写为:
void *ctx = objc_autoreleasePoolPush();
{}中的代码
-
objc_autoreleasePoolPop(ctx);
一次pop实际上相当于一次批量的pop操作
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
自动释放池的数据结构
- 是以栈为节点通过双向链表的形式组合而成。(什么是自动释放池/自动释放池的数据结构是怎样的?)
- 是和线程一一对应的
双向链表
栈
AutoreleasePoolPage类源码
class AutoreleasePoolPage
{
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;//指向当前栈中下一个可填充的位置
pthread_t const thread; //线程
AutoreleasePoolPage * const parent;//双向链表的父指针
AutoreleasePoolPage *child;//双向链表的子指针
uint32_t const depth;
uint32_t hiwat;
// SIZE-sizeof(*this) bytes of contents follow
static void * operator new(size_t size) {
return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}
static void operator delete(void * p) {
return free(p);
}
.....
}
AutoreleasePoolPage结构
AutoreleasePoolPage::push
[obj autorelease]
AutoreleasePoolPage::pop
- 根据传入的哨兵对象找到对应位置。
- 给上次push操作之后添加的对象依次发送release消息。
- 回退next指针到正确位置。
总结
- 当每次runloop将要结束的时候调用
AutoreleasePoolPage::pop()
- 多层嵌套就是多次插入哨兵对象。
- 在for循环中alloc图片数据等内存消耗较大的场景手动插入
autoreleasePool
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
NSLog(@"%@",array);
}
上面array的内存在什么时候释放的?
当每次runloop将要结束的时候,都会对前一次创建的AutoreleasePool调用
AutoreleasePoolPage::pop()
操作,同时会push进来一个AutoreleasePool。所以array
对象会在当前runloop就要结束的时候调用AutoreleasePoolPage::pop()
方法,把对应的array
对象,调用其release函数对其进行释放
AutoreleasePool的实现原理是怎样的?
是以栈为节点通过双向链表的形式组合而成的数据结构
AutoreleasePool为何可以嵌套使用?
多层嵌套就是多次插入哨兵对象。在我们每次创建代码块
@autoreleasepool{}
,系统就会为我们进行哨兵对象的插入,完成新的AutoreleasePool的创建,实际上也是创建了一个AutoreleasePoolPage,假如当前AutoreleasePoolPage没有满的话,就不用创建AutoreleasePoolPage。所以新创建的AutoreleasePool底层就是插入一个哨兵对象,所以可以多层嵌套。
循环引用
三种循环引用
- 自循环引用
- 相互循环引用
- 多循环引用
自循环引用
相互循环引用
多循环引用
循环引用考点
- 代理
- Block
- NSTimer
- 大环引用
如何破除循环引用?
- 避免产生循环引用
- 在合适的时机手动断环
破除循环引用具体的解决方案都有哪些?
-
__weak
破解 -
__block
破解 -
__unsafe_unretained
破解
__weak
破解
__block
破解
- MRC下,
__block
修饰对象不会增加其引用计数,避免了循环引用。 - ARC下,
__block
修饰对象会被强引用,无法避免循环引用,需手动解环。
__unsafe_unretained
破解
- 修饰对象不会增加其引用计数,避免了循环引用。
- 如果被修饰对象在某一时机被释放,会产生悬垂指针!
循环引用示例
Block的使用示例。(参看Block的讲解)
NSTimer使用示例。
#import "NSTimer+WeakTimer.h"
@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
- (void)fire:(NSTimer *)timer;
@end
@implementation TimerWeakObject
- (void)fire:(NSTimer *)timer
{
if (self.target) {
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timer.userInfo];
}
}
else{
[self.timer invalidate];
}
}
@end
@implementation NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats
{
TimerWeakObject *object = [[TimerWeakObject alloc] init];
object.target = aTarget;
object.selector = aSelector;
object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
return object.timer;
}
@end
内存管理面试总结
- 什么是ARC?
- 为什么weak指针指向的对象在废弃之后会被自动置为nil?
- 苹果是如何实现AutoreleasePool的?
- 什么是循环引用?你遇到过哪些循环引用,是怎样解决的?