内存管理

内存管理是什么?内存泄漏是啥?


内存管理大致布局

代码都在代码段中,已初始化的变量在已初始化数据区,未初始化变量在未初始化数据区,定义的方法或函数在栈上,栈由高地址向地地址拓展的,所以说栈是向下拓展。我们定义的block经过copy之后,会被放在堆区上面,堆是向上增长的。

内存布局

  • stack :栈区,方法调用。
  • heap:堆区,通过alloc等分配的对象。
  • bss:未初始化的全局变量等。
  • data:已初始化的全局变量等。
  • text:程序代码

内存管理方案

  • TaggedPointer
  • NONPOINTER_ISA 64位架构下,剩余的ISA存储其他数据
  • 散列表

一下关于内存管理的讨论基于objc-runtime-680版本讲解。

NONPOINTER_ISA

arm64架构

  • 1indexed0代表是指针型isa,单纯代表类对象地址。1代表是非指针型isa,即不仅仅是对象地址,还存储了其他内存管理的数据。
  • 2has_assoc : 代表当前对象是否有关联对象,0是没有,1是有。
  • 3has_cxx_dtor :代表当前对象是否用到C++相关。
  • 4-35shiftcls:代表当前对象的类对象的指针地址,一共32bit位。
  • 36-41magic
  • 42weakly_referenced:标识这个对象是否有弱引用指针。
  • 43deallocating:标识这个对象是否在进行dealloc
  • 44has_sidetable_rc:当前isa指针当中,如果存储的引用计数已达到上限的话,需要外挂一个sidetable类型的数据结构,去存储相关的引用计数内容。表示是否存在外挂sidetable(散列表)
  • 45-47extra_rc:额外的引用计数,当引用计数很小的时候,就会存在isaextra_rc 中。

散列表方式

SideTables()结构

是哈希表

SideTable结构

为什么是由多个SideTable组成SideTables()

系统中存在多个线程多个对象对散列表进行操作,如果是同一张表,就会存在效率问题。
因此采用 分离锁方案
多个对象进行引用计数操作的时候,可以进行并发操作。

怎么实现快速分流?

SideTables的本质是一张Hash表。

Hash查找

例:给定值是对象内存地址,目标值是数组下标索引。

通过对对象内存地址与数组个数的取余,获得余数就是目标数组的索引。

数据结构

  • Spinlock_t 自旋锁
  • RefcountMap 引用计数表
  • weak_table_t 弱引用计数表

自旋锁

Spinlock_t

  • Spinlock_t 是 ”忙等”的锁
  • 使用于轻量访问

    引用计数表

    RefcountMap

    是用哈希表实现的 size_t代表对应对象的引用计数值
    获取和插入等操作都是通过同一个哈希函数进行的,避免遍历提高效率。

size_t

  • 0weakly_referenced:标识是否有弱引用
  • 1deallocating:标识当前对象是否在dealloc
  • 2-63RC:存储的是对象的实际引用计数值
  • 实际计算引用计数值时,需要向右偏移两位

弱引用表

weak_table_t

MRC

手动引用计数

红色方法如果在ARC下调用会导致编译报错

ARC

自动引用计数

  • ARCLLVMRuntime协作的结果
  • ARC中禁止手动调用retain/release/retainCount/dealloc
  • ARC中新增weakstrong属性关键字。

引用计数管理

实现原理分析

  • alloc
  • retain
  • release
  • retainCount
  • dealloc

alloc实现

经过一系列调用,最终调用了C函数calloc。
此时并没有设置引用计数为1。

retain实现

  • 首先通过当前对象的指针地址,经过哈希运算从 SideTables() 中获取到对应的 SideTable
  • 再通过当前对象的指针地址,通过哈希查找从 SideTable 中的 refcnts 中找到对应的引用计数。refcnts 就是 RefcountMap/引用计数表。size_t 就是个无符号long型的值。
  • 然后进行加一操作,但 SIDE_TABLE_RC_ONE 并不是 1,因为size_t的前两位分别weakly_referenced(标识是否有弱引用)和deallocating(标识当前对象是否在dealloc),所以是(1UL<<2),即向左偏移2位得到二进制的 100即十进制的4

release实现

同retain类似

  • 首先通过当前对象的指针地址,经过哈希运算从 SideTables() 中获取到对应的 SideTable
  • 再根据指针找到对应的引用计数表
  • 然后引用计数减一

retainCount实现

  • 首先找到对应的散列表 SideTable
  • 然后声明一个局部变量 size_t类型,值为 1
  • 然后通过当前对象,到引用计数表中查找对应的引用计数表
  • 然后将查找到的size_t类型的结果向右偏移2位,然后加上局部变量 refcnts_result,最后返回给调用方。(这里向右偏移的原因上面已经说过)
  • 因此在alloc的时候,虽然没有引用计数(it->second = 0),但是有个加 refcnts_result 的操作,所以返回的结果是1

dealloc实现

  • nonpointer_isa判断当前对象是否使用了非指针对象的isa
  • weakly_referenced判断当前对象是否有weak指针指向它
  • has_assoc判断当前对象是否有关联对象
  • has_cxx_dtor判断当前对象的实现是否有C++相关的内容或者是否使用了ARC,满足其一都为YES
  • has_sidetable_rc判断当前对象是否使用了SideTable来存储引用计数。(因为如果是非指针isa,会在isa中存储一部分引用计数,超出上限再使用SideTable
    以上条件都满足,才能调用C函数 free() 直接释放,否则调用 object_dispose() 对象清除的函数

object_dispose() 实现

objc_destructInstance()

clearDeallocating() 实现

弱引用管理

添加weak 变量

storeWeak
参数:

  • loacation :弱指针的地址
  • newObj:您希望弱指针现在指向的新对象
    返回:
    The value stored in location (that is, obj).
    存储在location中的值(即obj)。 weak_register_no_lock
    参数:
  • 当前对象的SideTable中的weak_table
  • 被弱引用指向的原对象地址
  • 弱引用指针地址
  • 在废弃的过程中,crash的标志位 判断这个新对象是有值的,并且不是taggedPointer这种内存管理方式
    就给对象设置有弱引用的标记位
1
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)

参数:

  • 弱引用表
  • 原对象指针

首先根据 weak_table(弱引用表)找到 weak_entries(弱引用结构体数组)
然后通过referent(原对象地址)经过 hash_pointer (哈希算法)计算获取到这个对象在弱引用表中的索引位置 index
然后进行一个哈希冲突的算法
如果这个index的对象不是当前对象,就移动index,直到找到真正的索引位置
然后将当前对象对应的弱引用数组返回给调用方

Q:添加弱引用相关问题
A:可以通过弱引用对象,进行哈希算法的计算查找它对应的位置

如果获取到 weak_entry_t这个结构entry
就直接把新产生的弱引用指针添加进去
如果没获取到,就重新创建一个weak_entry_t 数组
最后插入弱引用表

清除weak变量,同时设置指向为nil

weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
参数:

  • weak_table:对应的弱引用表
  • referent_iddealloc的对象 首先声明一个局部变量
    然后用这个局部变量去查找对应的弱引用数组
    如果为空,则无操作,返回 如果弱引用变量个数小于4,就取inline_referrers这个数组
    如果大于4,就取referrers这个数组
    最终取到当前对象的所有弱引用指针列表 然后for循环遍历弱引用指针
    如果存在,并且弱引用指针所代表的地址是当前被废弃对象的地址,就置为nil

Q:清除weak变量,同时设置指向为nil,具体流程?
A:当一个对象被 dealloc 后,在dealloc的内部实现中,会调用弱引用清除的相关函数,然后在这个函数当中,根据当前对象指针,查找弱引用表,把当前对象对应的弱引用作为数组拿出,然后遍历数组中的所有弱引用指针,分别置为nil

自动释放池

objc_autoreleasePoolPush 内部实现

1
void* objc_autorelaeasePoolPush(void)

调用C++中的一个方法 AutoreleasePoolPage这个数据结构中的push方法

objc_autoreleasePoolPop 内部实现

数据结构

  • 是以为结点通过双向链表的形式组合而成。
  • 是和线程一一对应的。

双向链表

-w1287



-w906
特点:后入先出

AutoreleasePoolPage

-w959

  • next:指向栈当中下一个可填充的位置
  • parent:双向链表中的父指针
  • child:孩子指针
  • thread:对应的线程对象

-w708

AutoreleasePoolPage::push

-w1190

当执行push操作时,会将next指针指向的位置置为nil,称它为哨兵对象,然后将next指针指向下一个可入栈的位置。

[obj autorelease]

-w917

AutoreleasePoolPage::pop

  • 根据传入的哨兵对象找到对应位置。
  • 给上次push操作之后添加的对象依次发送release消息。
  • 回退next指针到正确位置。

自动释放池总结

  • 在当次runloop将要结束的时候调用AutoreleasePoolPage::pop()
  • 多层嵌套就是多次插入哨兵对象。
  • for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool

循环引用

三种循环引用

  • 自循环引用
  • 相互循环引用
  • 多循环引用

自循环引用

如果给一个对象的成员变量,赋值成这个对象自身,就会造成自循环引用

-w538

相互循环引用

对象A的属性a指向对象B,对象B的属性b执行对象A

-w934

多循环引用

-w1166

循环引用考点

  • 代理(一般涉及相互循环引用)
  • Block
  • NSTimer
  • 大环引用

如何破除循环引用

  • 避免产生循环引用
  • 在合适的时机手动断环

具体的解决方案

  • __weak
  • __block
  • __unsafe_unretained

__weak

__block

  • MRC下,__block修饰对象不会增加其引用计数,避免了循环引用。
  • ARC下,__block修饰对象会被强引用,无法避免循环引用,需手动解环。

__unsafe_unretained

  • 修饰对象不会增加其引用计数,避免了循环引用。
  • 如果被修饰对象在某一时机被释放,会产生悬垂指针

所以一般不使用__unsafe_unretained来接触循环引用,因为可能引发不可预知的问题。
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称悬垂指针

循环引用示例

  • Block的使用示例
  • NSTimer使用示例

NSTimer的循环引用问题

一个VC上有一个轮播图,需要每秒切换图片,当使用NSTimer进行实现时,在NSTimer每秒的回调中,会对这个对象进行强引用。造成循环引用问题。
这时候,如果让对象弱引用NSTimer并不能解决问题。
因为如果是在主线程创建的NSTimer,那么主线程的Runloop也强引用了这个NSTimer,间接强引用了这个对象,所以光当VC释放后,对象依旧被Runloop间接强引用而导致内存泄漏。

-w1229
NSTimer有重复定时器和非重复定时器之分,
如果是非重复的定时器,一般是在定时器回调当中,执行invalidate方法然后将NSTimer置为nil
这样就将RunloopNSTimer的强引用解除,同时NSTimer对对象的强引用也解除。

-w1295
如果是重复定时器,那可以创建一个中间对象,NSTimer对中间对象强引用,中间对象对NSTimer和对象弱引用。
这样当VC释放后,对象就会被释放,当NSTimer回调中间对象时,只需要对中间对象的对象做判断,是否为nil,如果是nil,在去执行invalidate方法然后将NSTimer置为nil
代码实现,创建NSTimer的一个分类
NSTimer+WeakTimer.h

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

@interface NSTimer (WeakTimer)

+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo;

@end

NSTimer+WeakTimer.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#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)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{
TimerWeakObject* obj = [[TimerWeakObject alloc]init];
obj.target = aTarget;
obj.selector = aSelector;
obj.timer = [NSTimer scheduledWeakTimerWithTimeInterval:ti target:obj selector:@selector(fire:) userInfo:userInfo repeats:yesOrNo];

return obj.timer;
}
@end

内存管理面试总结

什么是ARC
ARC是由LLVM编译器和runtime共同协作,来实现的引用计数自动的管理。
runtimeARC的作用等等

为什么weak指针指向的对象在废弃之后会被自动置为nil?
当对象被废弃之后,dealloc方法中会调用一个清除弱引用的方法,在这个方法中,会通过哈希算法查找到被废弃对象在弱引用表中的位置,然后提取到弱引用指针的列表数组,然后进行for循环遍历,将每个指针置为nil

AutoreleasePool是如何实现的?
AutoreleasePool是以栈为节点,由双向链表形式合成的一个数据结构。

什么是循环引用?你遇到过哪些循环引用,是怎么解决的?
上面NSTimer的例子。