内存管理

5/19/2021

# 内存区域

  • BSS段:未初始化的全局变量,静态变量,一旦初始化就回收,并转存到数据段中
  • 代码段:代码,程序结束的时候系统自动回收存储在代码段中的数据,内存区域较小
  • 数据段:已经初始化的全局变量,静态变量,直到程序结束时才会回收
  • 堆:动态分配内存,alloc出来的对象,需要程序员自己进行内存管理
  • 栈:局部变量,自动分配内存,当局部变量的作用域执行完毕之后就会被系统立即收回。

# 引用计数

一、TaggedPointer存储引用计数

为了优化NSNumber、NSDate、NSString等小对象的存储,TaggedPointer指针的值不再是地址来,而是真正的值。所以实际上它不再是一个对象了,只是一个披着对象皮的普通变量而已。所以它的内存并不存储在堆中,当指针不够存储数据时,才会使用动态分配内存的方式存储数据。

二、isa指针存储引用计数

nonpointer 针对arm64,优化过的指针,使用extra_rc + sidetable存储引用计数
extra_rc 19位,保存引用计数-1的值,在溢出后,减小一半,放入sidetable中,继续使用extra_rc(既扩大了引用计数的存储,也减少了计算)
sidetable 引用计数散列表

nonpointer也就是之前说过的TaggedPointer技术
如果isa非nonpointer,即 arm64 架构之前的isa指针。由于它只是一个普通的指针,存储着Class、Meta-Class对象的内存地址,所以它本身不能存储引用计数,所以以前对象的引用计数都存储在一个叫SideTable结构体的RefCountMap(引用计数表)散列表中。
如果isa是nonpointer,则它本身可以存储一些引用计数。从以上union isa_t的定义中我们可以得知,isa_t中存储了两个引用计数相关的东西:extra_rc和has_sidetable_rc。
extra_rc:里面存储的值是对象本身之外的引用计数的数量,这 19 位如果不够存储,has_sidetable_rc的值就会变为 1;
has_sidetable_rc:如果为 1,代表引用计数过大无法存储在isa中,那么超出的引用计数会存储SideTable的RefCountMap中。
所以,如果isa是nonpointer,则对象的引用计数存储在它的isa_t的extra_rc中以及SideTable的RefCountMap中。

# ARC和MRC的区别

# MRC

手动管理内存(retain,release,autorelease),retain引用计数+1,release引用计数-1,当引用计数为0时,会自动释放内存。autorelease对象管理放到autoreleasePool中,当pool drain时回收内存。

缺点:

  1. 释放一个堆内存时,首先要确定指向这个堆空间的指针都被release了。(避免提前释放)
  2. 释放指针指向的堆空间,首先要要确定哪些指向同一个堆,这些指针只能释放一次(避免多次释放,造成内存泄漏)
  3. 模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁释放,多线程操作时,不确定哪个线程最后使用完毕。

# ARC

ARC的规则就是只要对象没有被强指针引用,就会被释放,换言之,只要有强指针变量指向对象,那么对象就会存在于内存中。弱指针变量指向的对象释放时,弱指针变量会自动被设置为nil

优点:

引用计数方式的内存管理方式没有变,只是自动地帮我们去处理引用计数。strong变量在超出变量作用域时,会自动释放其所retain的对象,即自动调用release(编译器自动插入release,用clang看程序汇编输出可以看出来,插入了objc_release)

缺点:

  1. 可能出现内存泄漏,比如循环引用
  2. ARC会造成额外的retain/release,造成不必要的消耗,所以会比MRC慢一点。ARC只对OC对象进行内存管理,对于CoreFundation的api使用,他的对象所有权没有移交给OC对象管理,需要手动释放。

# MRC下写setter方法

@synthesize 表示如果属性没有手动实现setter和getter方法,编译器会自动加上这两个方法
@dynamic告诉编译器,属性的setter和getter方法由用户手动实现

// assign属性

-(void)setName:(NSString *)name {

  _name = name;

}

// retain 属性

-(void)setName:(NSString *)name {

 if(_name != name) {

    [_name release];

    _name = [name retain];

  }

  return _name;

}

// copy

-(void)setName:(NSString *)name

 {

 if(_name != name) {

    [_name release];

    _name = [name copy];

  }

  return _name;

}
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
35
36
37
38
39
40
41

# 循环引用

引用计数这种管理内存的方式虽然简单,但是有一个比较大的瑕疵,它不能很好的解决循环引用问题。

对象A和对象B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。

memory1

不仅仅只在两个对象中存在循环引用问题,多个对象依次持有对方,形成一个环状,也会造成循环引用问题。

memory2

# 常见内存情况

  1. Delegate

代理协议是一个最典型的场景,需要你使用弱引用来避免循环引用。ARC时代,需要将代理声明为 weak 是一个即好又安全的做法

@property (nonatomic, weak) id <XXXDelegate> delegate;
1
  1. Block

Block 的循环引用,主要是发生在 ViewController 中持有了 block, 同时在对 callbackBlock 进行赋值的时候又调用了 ViewController 的方法. 就会发生循环引用,因为:ViewController-> 强引用了 callback -> 强引用了ViewController

@property (nonatomic, copy)LFCallbackBlock callbackBlock;

self.callbackBlock = ^{        
  [self doSomething];    
}];
1
2
3
4
5

解决方法也很简单:使用weakSelf

__weak __typeof(self) weakSelf= self;    
self.callbackBlock = ^{      
  [weakSelf doSomething];    
}];
1
2
3
4

使用 MRC 管理内存时,Block 的内存管理需要区分是 Global(全局)、Stack(栈)还是 Heap(堆)
而在使用了 ARC 之后,苹果自动会将所有原本应该放在栈中的 Block 全部放到堆中。
全局的 Block 比较简单,凡是没有引用到 Block 作用域外面的参数的 Block 都会放到全局内存块中,在全局内存块的 Block 不用考虑内存管理问题。(放在全局内存块是为了在之后再次调用该 Block 时能快速反应,当然没有调用外部参数的 Block 根本不会出现内存管理问题)。
所以 Block 的内存管理出现问题的,绝大部分都是在堆内存中的 Block 出现了问题。默认情况下,Block 初始化都是在栈上的,但可能随时被收回,通过将 Block 类型声明为 copy 类型,这样对 Block 赋值的时候,会进行 copy 操作,copy 到堆上,如果里面有对 self 的引用,则会有一个强引用的指针指向 self,就会发生循环引用,如果采用 weakSelf,内部不会有强类型的指针,所以可以解决循环引用问题。

  1. NSTimer

NSTimer 我们开发中会用到很多,比如下面一段代码:

 - (void)viewDidLoad {        
   [super viewDidLoad];        
   self.myTimer = [NSTimerscheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomeThing)userInfo:nil repeats:YES];    
 }    

 - (void)doSomeThing {    
 }    

 - (void)dealloc {         
   [self.timer invalidate];         
   self.timer = nil;    
 }
1
2
3
4
5
6
7
8
9
10
11
12

这是典型的循环引用,因为 timer 会强引用 self,而 self 又持有了timer,所有就造成了循环引用。那有人可能会说,我使用一个 weak 指针,比如:

__weak typeof(self) weakSelf = self;    
self.myTimer = [NSTimerscheduledTimerWithTimeInterval:1 target:weakSelfselector:@selector(doSomeThing) userInfo:nil repeats:YES];
1
2

但是其实并没有用,因为不管是 weakSelf 还是 strongSelf,最终在 NSTimer 内部都会重新生成一个新的指针指向 self,这是一个强引用的指针,结果就会导致循环引用。那怎么解决呢?主要有如下三种方式:

a. 使用中间类

创建一个继承 NSObject 的子类MyTimerTarget,并创建开启计时器的方法。

// MyTimerTarget.h   
#import <Foundation/Foundation.h>   
@interface MyTimerTarget : NSObject   
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)intervaltarget:(id)target selector:(SEL)selector userInfo:(id)userInforepeats:(BOOL)repeats;   
@end   

// MyTimerTarget.m   
#import "MyTimerTarget.h"   
@interface MyTimerTarget ()   
@property (assign, nonatomic) SEL outSelector;   
@property (weak, nonatomic) id outTarget;   
@end   
@implementation MyTimerTarget
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)intervaltarget:(id)target selector:(SEL)selector userInfo:(id)userInforepeats:(BOOL)repeats {       
  MyTimerTarget *timerTarget = [[MyTimerTarget alloc] init];       
  timerTarget.outTarget = target;       
  timerTarget.outSelector = selector;       
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:intervaltarget:timerTarget selector:@selector(timerSelector:) userInfo:userInforepeats:repeats];       
  return timer;   
}
   
- (void)timerSelector:(NSTimer *)timer {       
  if (self.outTarget && [self.outTargetrespondsToSelector:self.outSelector]) {            
    [self.outTargetperformSelector:self.outSelector withObject:timer.userInfo];       
  } else {            
    [timer invalidate];       
  }   
}   
@end   

// 调用方   
@property (strong, nonatomic) NSTimer *myTimer;   
- (void)viewDidLoad {       
  [super viewDidLoad];       
  self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1target:self selector:@selector(doSomething) userInfo:nil repeats:YES];    
}   

- (void)doSomeThing {   
}   

- (void)dealloc {       
  NSLog(@"MyViewController dealloc");
}
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
35
36
37
38
39
40
41
42
43

VC 强引用 timer,因为 timer 的 target 是MyTimerTarget 实例,所以 timer 强引用MyTimerTarget 实例,而 MyTimerTarget 实例弱引用 VC,解除循环引用。这种方案 VC 在退出时都不用管 timer,因为自己释放后自然会触发 timerSelector:中的[timer invalidate]逻辑,timer 也会被释放。

b. 使用类方法

我们还可以对 NSTimer 做一个category,通过 block 将 timer 的 target 和 selector 绑定到一个类方法上,来实现解除循环引用。

// NSTimer+MyUtil.h
#import <Foundation/Foundation.h>
@interface NSTimer (MyUtil)
+ (NSTimer*)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)intervalblock:(void(^)())block repeats:(BOOL)repeats;
@end
// NSTimer+MyUtil.m
#import "NSTimer+MyUtil.h"
@implementation NSTimer (MyUtil)
+ (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)intervalblock:(void(^)())block repeats:(BOOL)repeats {
  return [self scheduledTimerWithTimeInterval:interval target:selfselector:@selector(MyUtil_blockInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)MyUtil_blockInvoke:(NSTimer *)timer {
  void (^block)() = timer.userInfo;
  if (block) {
     block();
  }
}
@end

// 调用方
@property (strong, nonatomic) NSTimer *myTimer;
- (void)viewDidLoad {
  [super viewDidLoad];
  self.myTimer = [NSTimer MyUtil_scheduledTimerWithTimeInterval:1 block:^{
    NSLog(@"doSomething");
  } repeats:YES];
}

- (void)dealloc {
  if (_myTimer) {
    [_myTimer invalidate];
  }
  NSLog(@"MyViewController dealloc");
}
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
35

这种方案下,VC 强引用 timer,但是不会被 timer 强引用,但有个问题是 VC 退出被释放时,如果要停掉 timer 需要自己调用一下 timer 的 invalidate 方法。

c. 使用 weakProxy

创建一个继承 NSProxy 的子类 MyProxy,并实现消息转发的相关方法。NSProxy 是 iOS 开发中一个消息转发的基类,它不继承自 NSObject。因为他也是 Foundation 框架中的基类, 通常用来实现消息转发, 我们可以用它来包装 NSTimer 的 target, 达到弱引用的效果。

// MyProxy.h
#import <Foundation/Foundation.h>
@interface MyProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
// MyProxy.m
#import "MyProxy.h"
@interface MyProxy ()
@property (weak, readonly, nonatomic) idweakTarget;
@end
@implementation MyProxy
+ (instancetype)proxyWithTarget:(id)target{
    return [[MyProxy alloc]initWithTarget:target];
}
- (instancetype)initWithTarget:(id)target {
    _weakTarget = target;
    return self;
}
- (void)forwardInvocation:(NSInvocation*)invocation {
    SEL sel = [invocation selector];
    if (_weakTarget &&[self.weakTarget respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.weakTarget];
    }
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
    return [self.weakTarget methodSignatureForSelector:sel];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.weakTarget respondsToSelector:aSelector];
}
@end
// 调用方
@property (strong, nonatomic) NSTimer*myTimer;
- (void)viewDidLoad {
    [super viewDidLoad];
    self.myTimer = [NSTimerscheduledTimerWithTimeInterval:1 target:[MyProxy proxyWithTarget:self]selector:@selector(doSomething) userInfo:nil repeats:YES];
}
- (void)dealloc {
    if (_myTimer) {
        [_myTimer invalidate];
    }
    NSLog(@"MyViewControllerdealloc");
}
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
35
36
37
38
39
40
41
42
43

上面的代码中,了解一下消息转发的过程就可以知道-forwardInvocation: 是会有一个 NSInvocation 对象,这个 NSInvocation 对象保存了这个方法调用的所有信息,包括 Selector 名,参数和返回值类型,最重要的是有所有参数值,可以从这个 NSInvocation 对象里拿到调用的所有参数值。这时候我们把转发过来的消息和weakTarget 的 selector 信息做对比,然后转发过去即可。

这里需要注意的是,在调用方的 dealloc 中一定要调用 timer 的 invalidate 方法,因为如果这里不清理 timer,这个调用方 dealloc 被释放后,消息转发就找不到接收方了,就会 crash。

# Autoreleasepool

自动释放池是由一个或者多个AutoreleasePoolPage组成,page的size为4096 byte,他们通过parent和child指针组成一个双向链表。

hotPage: 是当前正在使用的page,操作都是在hotPage上完成,一般处于链表末端或者倒数第二个位置,存储在TLS,可以理解为每个线程共享一个自动释放池链表。
colPage: 位于链表头部的page,可能同时为hotPage
POOL_BOUNDARY: nil的宏定义,代替之前的哨兵对象POOL_SENTINEL, 在objc_autoreleasePoolPush中将其推入道自动释放池中,在调用objc_autoreleasePoolPop时,会将池中对象按顺序释放,直至遇到最近一个POOL_BOUNDARY时停止
EMPTY_POOL_PLACEHOLDER: 当自动释放池中没有推入过任何对象时,这个时候推入一个POOL_BOUNDARY,会先将EMPTY_POOL_PLACEHOLDER存储在TLS中作为标识符,并且此次不推入POOL_BOUNDARY。等再次由对象推入自动释放池时,检查在TLS中取出该标识符,这个时候再推入POOL_BOUNDARY
next: 指向AutoreleasePoolPage栈顶空位的指针,每次加入新的元素都会往上移动。

# objc_autoreleasePoolPush

objc_autoreleasePoolPush方法其实就是向自动释放池推入一个POOL_BOUNDARY,作为该autoreleasepool的起点

# autorelease

如果hotPage存在且尚未满,直接推入hotPage
如果hotpage存在且已满,则调用autoreleaseFullPage
如果hotPage不存在,则调用autoreleaseNoPage

一、autoreleaseFullPage该方法是在hotPage已满的情况下执行,

  1. 查看hotPage是否有后继节点,如果有则直接使用后继节点
  2. 如果没有后继节点,则新建一个AutoreleasePoolPage
  3. 将对象加入到获取到的page,并将其设置为hotPage,其实就是存入TLS中共享

二、autoreleaseNoPage该方法只有在自动释放池还没有page时调用。

  1. 如果当前自动释放池推入的是一个分割线POOL_BOUNDARY时,将EmptyPoolPlaceholder存入到TLS中
  2. 如果TLS存储了EmptyPoolPlaceholder时,在创建好page,设置为hotPage之后,会先推入一个POOL_BOUNDARY,然后再将加入到自动释放池的对象推入

# objc_autoreleasePoolPop

  1. 检查入参是否为空池占位符EMPTY_PLACEHOLDER,如果是则判断是否存在hotPage,如果hotPage存在,则将是否终点改为colPage()->begin(),如果hotPage不存在,则空置TLS存储中的hotPage
  2. 检查stop既不是POOL_BOUNDARY也不是colPage()->begin()的情况下报错。
  3. 清空自动释放池中stop之后的所有对象
  4. 判断当前page如果没有达到半满,则干掉后续所有page,如果超过半满,则只保留下一个page

# strong的实现

底层调用objc_storeStrong,本质是新值retain,旧值release

# weak的作用,场景和实现

# weak的作用

弱引用,在对象释放后置为nil,避免错误的内存访问。
通俗的讲:weak可以在不增加对象引用计数的同时,又使得指针的访问是安全的。

# weak的使用场景

  1. delegate
  2. block
  3. timer

以上三个均是为了避免循环引用所带来的内存泄露

  1. weak singleton
+(id)sharedInstance {

  static __weak ASingletonClass *instance;

   ASingletonClass *strongInstance = instance;

  @synchronized(self) {

    if(strongInstance == nil) {

      strongInstance = [[[self class] alloc] init];

      instance = strongInstance;

    }

  }

  return strongInstance;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. weak associate object

associate object本身并不支持添加具备weak特效的property,但可以通过小技巧来完成

-(void)setContext:(CDDContext *)object {

  id __weak weakObject = object;

  id(^block)() = ^{ return weakObject };

  objc_setAssociatedObject(self, @selector(context), block,  OBJC_ASSOCIATION_COPY);

}

- (CDDContext *)context {

  id(^block)() = objc_getAssociatedObject(self, @selector(context));

  id curContext = (block ? block() : nil);

  return curContext;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# weak的实现

objc_initWeak/objc_destroyWeak:

调用storeWeak来实现。

  1. initWeak,该地址没有值,正在赋予新值,如果正在释放则crash
  2. destroyWeak,该地址有值,没有赋予新值,如果正在释放则不crash

storeWeak:

storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj);

  1. 从全局的哈希表SideTables中,利用对象本身的地址进行位运算后得到下标,取得该对象的弱引用表。SideTables是一个64个元素长度的散列表,发生碰撞时,可能一个SideTable中存在多个对象共享一个弱引用表
  2. 如果有分配新值,则检查新值对应的类是否初始化过,如果没有,则就地初始化。
  3. 如果location有指向其他旧值,则将旧值对应的弱引用表进行注销
  4. 如果分配了新值,将新值注册到对应的弱引用表中。将isa.weakly_referenced设置位true,表示该对象是有弱引用变量,释放时要去清空弱引用表

weak_register_no_lock/weak_unregister_no_lock

对弱引用表进行注册和注销

referent_id 被引用的对象
referrer_id 弱应用变量

一、weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)

  1. 检查是否正在释放中,如果是,则根据crashIfDeallocating参数判断是否出触发crash
  2. 检查weak_table中是否有被引用对象的entry,如果有则直接将弱引用变量指针地址加入到该entry中
  3. 如果weak_table中没有找到对应的entry,则新建一个entry,并将弱应用变量指针地址加入到entry中,同时检查weak_table是否需要扩容。

二、weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)

  1. 从weak_table中根据找到被引用对象对应的entry,然后将弱引用变量指针referrer从entry中移除
  2. 移除弱引用变量指针referrer后,检查entry是否为空,如果为空,将其从weak_table中移除

weak_table:

weak_table保存被引用对象的entry

一、static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)

weak_table查找entry的过程,也是哈希表寻址过程,使用线性探测的方法解决哈希冲突的问题(如果该下标存储的是其他对象,往下移,知道找到正确的下标)。

  1. 通过被引用对象地址计算获得哈希表下标
  2. 检查对应下标存储的是不是我们要找的地址,如果是则返回该地址
  3. 如果不是则继续往下找,直至找到。在下移过程中,下标不能超过weak_table的最大长度,同时hash_displacement不能超过记录的max_hash_displacement最大哈希位移。max_hash_displacement是所有插入操作时记录的最大哈希位移,如果超过了,那肯定是出错了。

二、static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry);

  1. 通过被引用对象地址计算获得哈希下标
  2. 检查对应下标是否为空,如果不为空继续往下找,直至找到空位
  3. 将弱应用变量指针存入空位,同时更新weak_table的当前成员变量num_entries和最大哈希位移max_hash_displacement

三、static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry);

  1. 释放entry和其中的弱引用变量,并设置为空指针
  2. 更新weak_table对象的数量,并检查是否可以缩减表容量

entry和referrer:

在弱引用表中entry对应着被引用的对象,而referrer代表着弱引用变量。
每次被弱引用时,都会将弱引用变量指针referrer加入到entry中,而当原对象被释放时,会将entry清空并移除。

一、static void append_referrer(weak_entry_t *entry, objc_object **new_referrer);

entry的结构和weak_table相似,都使用了哈希表,并使用线性探测法寻找对应位置,在此基础上有一点不同的地方:

  1. entry有一个标志位out_of_line,最初时刻该标记为false,entry使用的是一个有序数组inline_referrers的存储结构
  2. 当inline_referrers的成员数量超过了WEAK_INLINE_COUNT, out_of_line标志为被设置为true,开始使用哈希表存储结构。每当哈希表负载超过3/4时会进行扩容。

二、static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer);

  1. out_of_line为false时,从有序数组inline_referrer中查找并移除
  2. out_of_line为true时,从哈希表中查找并移除

dealloc:

在对象执行dealloc函数时,会检查isa_weakly_referenced标志位,然后判断是否要清空weak_table中的entry

Last Updated: 10/25/2024, 6:55:06 AM