您现在的位置是:首页 >学无止境 >iOS - 内存管理网站首页学无止境

iOS - 内存管理

Cross-D 2023-06-15 04:00:02
简介iOS - 内存管理

一、App 内存分布

在这里插入图片描述

二、OC对象的内存管理

iOS 中,使用引用计数来管理 OC 对象的内存,新创建的 OC 对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。调用 retain 会让 OC 对象的引用计数 +1,调用 release 会让 OC 对象的引用计数-1。

// 引用计数散列表(在64bit中,引用计数可以直接存储在优化过的isa指针中,也可以存储在SideTable类中)
struct SideTable{
	spinlock_t slock;//锁
	RefcoutnMap refcnts;//存放着对象引用计数的散列表
	weak_table_t weak_table;
}

当调用 allocnewcopymutableCopy 方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease释放它,想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1。

@property (nonatomic, assign)int age;
- (void)setAge:(int)age
{
    _age = age;
}
- (int)age
{
    return _age;
}

@property (nonatomic, retain)NSString *name;
- (void)setName:(int *)name
{
    if (_name != name) {
    	// 释放之前的指向资源
        [_name release];
        // 持有现在的指向资源
        _name = [name retain]
    }
}

仅堆区的数据才会使用引用计数管理内存。

2.1 TaggedPointer 内存优化

1、从64bit开始,iOS引入了 Tagged Pointer 技术,用于优化NSStringNSDateNSNumber 等小对象存储。
2、在没有使用 Tagged Pointer 之前,NSNumber 等对象需要动态分配内存、维护引用计数等,NSNumber 指针存储的是堆中 NSNumber 对象的地址值。
3、在使用 Tagged Pointer 之后,NSNumber 指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在指针中。
4、当对象真正的最高有效位数是1(iOS),最低有效位数是1(MAC),则该指针为 Tagged Pointer

//iOS平台
#define _OBJC_TAG_MASK (1UL<<63)
//Mac平台
#define _OBJC_TAG_MASK 1UL

BOOL isTaggedPointer(id pointer)
{
    return ((uintptr_t)pointer * _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

5、当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
6、objc_msgSend 能识别到 Tagged Pointer,比如 NSNumberintValue 方法,直接从指针提取数据,节省了调用开销

2.2 对象的释放流程

当一个对象要释放时,会自动调用dealloc,调用轨迹如下:

  1. dealloc
  2. _objc_rootDealloc
  3. rootDealloc
  4. object_dispose
  5. objc_destructInstance、free
void *objc_destructInstance(id obj)
{
	if(obj){
		bool cxx = obj->hasCxxDtor();
		bool assoc = obj->hasAssociatedObjects();

		if(cxx)object_cxxDestruct(obj);//清除成员变量
		if(assoc)_object_remove_assocations(obj);//清除关联对象
		objc->clearDeallocating();//将指向当前对象的弱指针置为nil
		
	}
}

2.3 AutoreleasePool

自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
在这里插入图片描述

//每个AutoreleasePoolPage对象占用4096字节内存,处理用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
//所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
//调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
//调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
struct AutoreleasePoolPage
{
	magic_t const magic;
	id *next; //指向下一个能存放autorelease对象地址的区域
	pthread_t const thread;
	AutoreleasePoolPage *const parent;
	AutoreleasePoolPage *child;
	uint32_t const depth;
	uint32_t hiwat;
}


struct __AtAutoreleasePool {
    __AtAutoreleasePool(){ //构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    ~__AtAutoreleasePool(){ //析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    
    void * atautoreleasepoolobj;
};

App 下的自动释放池

iOS在主线程的Runloop中注册了2个Observer
第一个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()。
第二个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()。
监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()。

可以通过 extern void _objc_autoreleasePoolPrint(void); 私有函数来查看自动释放池的情况

三、内存问题

3.1 内存溢出

App 在使用过程中,使用的内存已超过系统分配的内存,导致不够用,故称为内存溢出。

3.2 内存泄漏

3.2.1 内存类型

系统在运行的时候,都会给进程(App)分配一块内存空间(限制大小),当 App 在运行的过程中使用的内存大小大于系统分配的内存空间,将会出现 OOM(Out Of Memory) 崩溃。

App 内影响内存泄漏主要有三种类型:

  • Leaked Memory:内存没有被引用,并且也不能被重复使用或者释放掉。
  • Abandoned Memory:内存有被引用,但是不能重复使用。
  • Cached Memory:内存有被引用到,并且能被重复使用。

Clean & Dirty 内存

系统内存一般以页为单位来划分(iOS 每一页包含 16kb),一般一段数据会占用多页内存,所占用页总数乘以每页空间得到的就是总使用内存。
内存页依照占用和非占用状态将内存分为 clean 和 Dirty。

// 此时 arr 所指向的是 clean 内存(未存储数据)
int *arr = malloc(16); 
// 此时 arr[0] 所指向的是 Dirty 内存(存储数据), arr[1-3] 依然是 clean 内存(未存储数据)
arr[0] = 1;

Compressed 内存
内存不足时,系统会依据策略将优先级比较低的内存挪到磁盘上,此过程称为 Page Out。当 App 再次访问时,系统会将磁盘上的数据加载到内存空间,此过程称为 Page In。
由于频繁的的 IO 操作会降低存储设备的寿命,故后面系统都采用 Compressed 来压缩内存空间。

内存类型
clean MemoryApp 未使用的内存空间,包含能够 Page Out 的内存(类似于 frameworks 的 _DATA_CONST 段)
dirty MemoryApp 已使用的内存空间(堆区的对象,缓冲区),比如 frameworks 的 _DATA _DATA_DIRTY 段。
compressed Memory内存吃紧时,系统会把非活跃的内存压缩,当访问该内存时会先进行解压缩(一种CPU 时间换系统 IO 时间的折中方案)

内存告警处理方案:
方案一:在 -didReceiveMemoryWarning 方法内部通过手动设置策略来清理占用内存
方案二:使用 NSCache 对象替代其他对象,将内存交由系统来管理

3.2.2 内存泄漏场景

  • Block 循环引用
// 循环引用(block 在堆区)
self.block = ^(){
	NSLog(@"%@", self.name);
}

self 持有 block 属性,当 block 被拷贝到堆区时同时也强持有 self,故会产生引用环,导致 self 不能被系统正常的释放。

【解决方法】
__weak 修饰 self 对象

  • 3.2.1 Delegate 循环引用
@interface JHProxyObject : NSObject
@property (nonatomic, strong)JHObject *object;
@end

- (instancetype)init{
	...
	// 设置代理
	self.object.delegate = self;
	...
}


@interface JHObject : NSObject
@property (nonatomic, strong)id delegate;
@end

JHProxyObject 持有 JHObject 属性,JHObject 也由于 delegate 强引用 JHProxyObject,故会产生引用环,导致 JHProxyObjectJHObject不能被系统正常的释放。

【解决方法】
weak 修饰 delegate 属性

  • 3.2.3 NSTimer 循环引用
@interface JHTimer : NSObject
@property (nonatomic, strong)NSTimer *timer;
@end

- (instancetype)init{
	...
	// 设置计时器
	self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(do) userInfo:nil repeats:YES];
	...
}

JHTimer 持有 timer 属性,timer 内部会维护一个 Runloop 循环,timer 通过 target 强持用 self, 故会产生引用环,导致 JHTimer 不能被系统正常的释放。

【解决方法】
1、timer通过 Block API 创建实例,使用 __weak 修饰 self 对象,打破引用环。
2、创建 用 JHProxyTimer 类,弱引用 self

  • 3.2.4 非 OC 对象内存处理

//GPU优化
EAGLContext * eaglContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
eaglContext.multiThreaded = YES;

// 设置上下文
CIContext *context = [CIContext contextWithEAGLContext:eaglContext];
[EAGLContext setCurrentContext:eaglContext];

CGImage *outputImage = nil;
CGImageRef ref = [context createCGImage:outputImage fromRect:outputImage.extent];

UIImage *img = [UIImage imageWithCGImage:ref];
CGImageRelease(ref);

非 OC 对象 CGImageRef 通过 create- 的方式创建,需要调用 CGImageRelease(ref) 释放占用资源,否则内存得不到释放(Core Foundation 框架的一些对象或变量也需要手动释放,C/C++ 中一些通过开辟内存空间方法生成的指针,在使用完成后需要调用 free 函数释放掉占用资源)。

  • 3.2.5 循环创建对象
for (int i = 0; i < 100000; i ++) {
	NSString *str = @"123";
	str = [str stringByAppendingString:@"xyz"];
}

多次循环创建临时变量,由于临时变量只会在出了作用域后才开始释放占用内存资源,故如大量创建会导致在作用域内内存资源被大量占用。

【解决方法】

for (int i = 0; i < 100000; i ++) {
	@autoreleasepool {
		NSString *str = @"123";
		str = [str stringByAppendingString:@"xyz"];
	}
}

添加 @autoreleasepool, 将生成的临时变量放入自动释放池,当系统发现内存不够时会自动回收内存。

3.2.3 内存泄漏检测工具

  • FBRetainCycleDetector 工具:通过收集对象的强引用构成的有向图,并且检测有向图中是否产生环。
    • 成员变量强弱引用检测
    • NSArray / NSDictory / NSSet / NSMapTable 强弱引用检测
    • AssociationObjc 对象检测(通过 fishhook 替换函数方法)
  • MLeaksFinder 工具:检测 ViewController 在 pop or dismiss 后限定时间内本身或者承载的子视图是否被释放来判定当前是否有其他内存被占用。

MLeaksFinder 注意点:
1、全局单例对象
2、控制器释放时机问题,比如右滑返回中途等

3.2.4 iOS OOM 执行流程

iOS 通过 Jetsam 机制开启进程来监控系统出现的 OOM ,它是通过 Signal 捕获等 Crash 监控方案无法捕获到的 OOM 事件(流程如下)。
第一步:Jetsam 机制初始化完毕,从外部接收到内存压力
第二步:接收到内存压力是当前物理内存达到限制时,同步触发 per-process-limit 类型的 OOM ,退出流程。
第三步:接收到内存压力是其他类型时,唤醒 Jetsam 线程,判断可用内存是否小于阀值,进入 OOM
第四步:遍历优先级最低的每个进程,判断当前进程是否高于阀值,直到找到触发内存 high-water 类型的 OOM
第五步:回收触发内存 high-water 类型的 OOM后,继续第四步操作。
第六步:所有低优先级的进程被回收后,再判断当前内存是否小于阀值,如果依然大于,则继续杀掉后台进程,每杀掉一个进程,判断一下当前内存是否小于阀值,如果小于则挂起线程。
第七步:当所有后台进程被杀掉后,继续杀掉前台的进程,挂起线程,等待唤醒。
第八步:如果前七步未杀掉任何线程,就通过 LRU 杀掉 Jetsam 队列中的第一个进程,挂起线程,等待唤醒。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。