iOS内存管理及部分Runtime复习
【iOS】内存管理及部分Runtime复习
1.继承链
关于继承链存在两个指针 类的superclass指向父类 父类的sp指向根类 根类的sp指向空 元类的sp指向父类的元类 最终指向根元类 而根元类的sp指向根类 而关于isa指针 对象的isa指针指向它所属的类 类的isa指针指向元类 元类的isa指针指向根元类 根元类的isa指针指向自己
2.MRC和ARC的实现 ARC在编译器和运行期都做了什么
关于MRC,主要涉及到四个方法retain、release、retainCount和dealloc,retain在底层会给对象的引用计数加一,如果对象的isa指针是nonpointer_isa是则直接在散列表的引用计数表中获取引用计数并操作,如果对象是一般的对象,引用计数就可能会有一半保存在sidetable中,另一半放在isa指针位域中的extra_rc这一部分 retainCount就是返回对象的引用计数 dealloc就是引用计数为0时就释放掉当前对象的内存 在alloc时会默认给对象的引用计数加一 但是没有调用retain或者release
关于ARC,主要涉及到strong修饰符、weak修饰符、unsafe_retained修饰符、autoreleasing修饰符,关于strong修饰符,每当**strong修饰符开始引用一个对象时,对象引用计数加一,当变量超出作用域或者被赋值为nil时,它所持有的引用计数会被释放,对象引用计数减一,关于weak修饰符,内存中存在一个散列表weak,用于存放当前所有的弱引用,当一个对象被声明为weak时,运行时系统会自动将其添加到相应的weak表中,当对象引用计数为零时,对象内存被释放,运行时系统会遍历weak表,把其中所有的弱引用设置为nil,避免弱引用指向已经释放的对象,关于unsafe_retained修饰符,与**_weak修饰符一样不持有对象,不增加引用计数,但是它在底层不存在散列表并且他修饰的自动变量不会被初始化为nil,关于__autoreleasing,会把变量添加到自动释放池
**__**weak修饰符的变量所引用的对象被废弃,就将nil赋值给变量,使用附有__weak的变量,就是在使用注册到自动释放池中的对象
ARC模式下,如果方法名以alloc、new、copy、mutableCopy开头,则方法的调用者需要保留和释放使用该方法的对象,否则不用调用者保留和释放,因为方法内部会自动执行autorelease方法
在编译期,ARC会分析代码,消除不必要的retain、release、autorelease操作,在适当的位置插入retain、release、autorelease等方法,检查代码中出现的循环引用等问题;在运行期,ARC包含的运行时组件会对weak变量进行处理,当引用计数为0时,将变量设置为nil,避免野指针错误,还会在运行时优化返回值,检测将对象注册到自动释放池的操作,对注册自动释放池的操作进行优化 例如,当以非alloc/new/copy/mutableCopy开头的方法返回一个自动释放对象,并且调用方需要保留该对象时,ARC 会使用 objc_autoreleaseReturnValue
和 objc_retainAutoreleasedReturnValue
优化,绕过将对象注册到自动释放池的操作
除此之外,ARC在运行期还会进行动态行为管理,比如:
动态方法解析: 当调用未实现的方法时,通过 resolveInstanceMethod/resolveClassMethod 动态添加方法实现。 ARC 确保动态添加的方法正确管理引用计数。 消息转发: 在消息转发流程中(如 forwardInvocation:),ARC 维护临时对象的生命周期,避免内存泄漏。 Runtime API 交互: 当使用 objc_msgSend、class_addMethod 等动态 API 时,运行期确保引用计数正确更新。
对MRC和ARC的理解
MRC和ARC是两种内存管理技术,主要区别在于对象的引用计数是由开发者手动管理还是编译器自动管理
MRC:开发者需要手动管理对象的引用计数。当一个对象被引用时,需要调用retain
方法增加其引用计数;当一个对象不再需要时,需要调用release
方法减少其引用计数;当引用计数变为0时,对象将被销毁。
ARC:ARC引入了编译器的自动内存管理技术,它在编译时根据代码的上下文自动插入retain、release和autorelease等方法调用,开发者不需要显式地调用这些方法来管理内存,ARC会在对象不再被引用时自动将其销毁,从而减少了内存泄漏和野指针的风险,ARC默认开启。
对自动释放池的理解
自动释放池是一种用于管理内存的机制,它允许开发者将对象的释放操作推迟到稍后的时候,从而更好地管理内存的使用。主要目的是延迟释放对象,避免在创建大量临时对象时频繁地手动释放,对象被添加到自动释放池中时,引用计数不会立即减少,而是等到自动释放池被销毁时才进行释放操作(release方法)
常见的使用场景有:
- 循环中创建临时对象:当在循环中创建大量临时对象时,可以将这些对象添加到自动释放池中,以确保及时释放内存
- 在多线程中处理对象:当在多线程环境中使用对象时,可以为每个线程创建独立的自动释放池,以避免线程之间的干扰
当有多层自动释放池时,对象的释放是在最内层的自动释放池呗销毁时进行的,当最内层的自动释放池结束时,池中的对象会被释放
自动释放池在MRC和ARC的区别
对于MRC:
开发者需要手动管理内存,包括手动增加和减少对象的引用计数
自动释放池需要手动创建和释放,使用NSAutoreleasePool类来管理
对象可以通过调用autorelease方法将其添加到当前自动释放池中,当自动释放池被销毁时,池中的对象会被发送一次release消息,从而释放对象的内存
对于ARC:
开发者不用手动管理内存,编译器会自动插入内存管理代码
自动释放池由编译器隐式地创建和释放,开发者通过@autoreleasepool语法块来指明自动释放池的生命周期
autorelease方法不再使用,编译器会根据代码的变量所有权修饰符、方法返回类型等代码的上下文在编译时自动、精确地在适当的位置插入retain、release、autorelease等操作的调用,如果要显式地调用autorelease,可以使用__autorelease修饰符(很少这样)
很多情况下,即使不显式地使用修饰符,对象也可以注册到释放池中:比如当方法名不以alloc/new/copy/mutableCopy开头,就会自动将返回对象注册到autoreleasepool中(未优化的情况);访问**__**weak修饰符的变量时,必定要访问注册到autoreleasepool的对象;id的指针或对象的指针在没有显式指定时会被赋加上__autoreleasing修饰符
注意⚠️:**strong修饰符和自动释放池的变量虽然都是在块结束时释放,但是二者的release不相同,**strong修饰的是因为变量生命周期结束,编译器自动加上的release,而自动释放池里变量的是通过autorelease释放的,是随着释放池的生命周期结束而释放的
对于block在MRC和ARC下的区别
在MRC(Manual Reference Counting)下:
- 在MRC中,使用Block需要手动管理其内存。
- 当一个Block被创建时,它会在栈上分配内存,如果需要在Block中引用外部的对象,需要手动将这些对象进行retain和release操作,以确保对象在Block执行期间不会被提前释放或销毁。
- 当Block需要在长期存储或在异步操作中使用时,需要将Block进行copy操作,将其移动到堆上分配内存,以确保Block及其引用的对象能够正确地存活。
在ARC(Automatic Reference Counting)下:
- 在ARC中,编译器会自动处理Block的内存管理,无需手动管理retain和release操作。
- ARC会自动根据Block对外部对象的引用情况来决定是否在Block创建时将外部对象进行retain操作,并在Block销毁时自动进行release操作。
- 不需要手动执行copy操作,因为ARC会根据需要自动将Block从栈上移动到堆上。
使用注意事项:
- 在MRC中使用Block时,需要注意对外部对象的手动内存管理,特别是避免在Block中持有强引用循环,即循环引用的情况。
- 在ARC中,由于自动进行内存管理,循环引用的问题有所缓解,但仍需要注意Block对外部对象的强引用,特别是在Block可能长期存储、在异步操作中使用或作为属性的情况下。
- 当需要在Block内部修改外部的局部变量时,在MRC下需要使用
__block
修饰符来修饰变量,在ARC下不需要。 - 在使用Block时,要考虑到Block可能会被延迟执行,因此需要注意解决对于外部对象的强引用问题,避免出现潜在的内存泄漏。
3.消息传递和消息转发
消息传递分为快速查找和慢速查找两个过程:
消息传递
关于快速查找,就是在类的cache方法缓存中查找,没找到的话就开始慢速查找
关于慢速查找,还是会在cache中先找一次,目的是防止多线程操作时,刚好调用慢速查找函数lookUpImpOrForward时方法缓存进来了,然后进入for循环 首先判断类是否实现以及初始化,没有的话就要实现并初始化,接着开始在类自己的方法列表中二分查找,找到的话就返回imp并开始写入cache流程,没找到就继续 将当前类赋为父类,如果父类为nil,imp = 消息转发
,并终止递归,开始判断是否执行过动态方法解析;如果父类链中存在循环就报错,在父类查找时先找缓存再找方法列表。都没找到的话就判断是否进行过动态方法解析,没有就进行动态方法解析,进行过的话就直接消息转发
在找到方法时,要排除分类方法
消息转发
关于消息转发,会有三次拯救三个阶段,分别是动态方法决议和快速慢速转发
关于动态方法决议,可以使用resolveInstanceMethod或者resolveClassMethod对没实现的方法进行处理,为消息添加方法,先查找resolveInstanceMethod和resolveClassMethod方法,如果找到就发送消息,没找到就直接返回。在发送消息后成功添加方法后会再进行对原本方法的慢速查找
关于快速转发,是可以用forwardingTargetForSelector将方法更换一个接收对象
关于慢速转发,会首先用methodSignatureForSelector生成一个方法签名,以给forwardInvocation中的参数NSInvocation调用,生成方法签名时可以改变消息内容,比如追加参数或者更换选择子等等,对象会创建一个NSInvocation用来表示消息,把尚未处理的消息有关的细节封装在anInvocation中,我们可以在forwardInvocation方法中选择将消息转发给其他对象
在快速转发慢速转发之间还会再进行一次动态方法决议
4.对象的底层结构,isa指针,class
对象的本质就是一个有isa指针的结构体,这个结构体中会存放isa指针和对象的成员变量和属性
关于isa_t
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#if ISA_HAS_INLINE_RC
bool isDeallocating() const {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif // ISA_HAS_INLINE_RC
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated) const;
Class getDecodedClass(bool authenticated) const;
};
Isa_t是一个联合体,其中有两个成员,cls和bits,这两个成员互斥(因为联合体的定义),也就意味着,初始化isa存在两种方式:
- 通过cls初始化,bits无默认值
- 通过bits初始化,cls有默认值
除此之外,还有一个结构体定义的位域,用来存储类信息及其他信息。结构体中有一个成员ISA_BITFIELD,是一个宏定义,有两个版本**arm64(对应iOS)和**x86_64(对应macOS)
define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
这个位域中有一些标志位:
-
nonpointer:表示自定义的类,有两个值
- 0:纯isa指针
- 1:不只是类对象地址,isa中包含类信息、对象的引用计数等
-
has_assoc:表示关联对象位
- 0:无关联对象
- 1:有关联对象
-
has_cxx_dtor表示该对象是否有C++/OC的析构器
-
shiftcls表示存储类的指针的值
-
magic用于判断当前对象是真的对象还是没有初始化的空间
-
weakly_refrenced表示对象是否被指向或者曾经指向一个ARC的弱变量
-
extra_rc值为引用计数值减一
class是一个指向内存中objc_class结构体的指针,objc_class继承自objc_object,因此其实也是一个对象,也具有isa指针,关于objc_class结构体的结构
bit中有一个方法data()会返回class_rw_t,class_rw_t是在运行时生成的,它在realizeClass中生成,它包含了class_ro_t。它在_objc_init方法中关于dyld的回调的map_images中最终将分类的方法与协议都插入到自己的方法列表、协议列表中。它不包含成员变量列表,因为成员变量列表是在编译期就确定好的,它只保存在class_ro_t的ivars中。ro是在编译期生成的,class_rw_t中包含了一个指向class_ro_t的指针(ro保存了类在编译期的所有静态信息,包括方法属性等等)
5.isMemberOfClass和isKindOfClass
对于isMerberOfClass,方法会对比元类或类本身与某个类或实例所属的类(元类还是类取决于类方法还是实例方法,总之就是取isa指针指向的东西)
而对于isKindOfClass,走以下逻辑:
6.TaggedPointer和内存对齐
关于TaggedPointer(只有在64位的环境下才会有小对象)
小对象是xcode对较小的对象进行的一种优化,一般对象在调用alloc方法后在堆区分配内存单元,而小对象由于比较小,可以直接由指针来体现对象的值,因此不在堆区分配内存单元,而是保存在常量区
以NSString为例:
- 一般的
NSString
对象指针,都是string值
+指针地址
,两者是分开的
- 对于
Tagged Pointer
指针,其指针 + 值
,都能在小对象中体现
.所以Tagged Pointer
既包含指针,也包含值
在底层,在类的加载时,_read_images
中有方法会对小对象进行处理,编码后的小对象最高位为0xa、0xb等等,这个最高位主要是用来判断是否是小对象。
比如:0xa转换为二进制是1010,第64位为1,表示是小对象,6361位表示对象类型,从06七个枚举值,010为2,表示NSString类型
关于内存对齐
就对象整体而言,苹果系统采用16字节对齐开辟内存大小,提高系统存取性能,关于内部结构体,结构体内存对齐规则 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数).程序员可以通过预编译命令#pragma pack(n)
,n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”.在iOS
中,Xcode
默认为#pragma pack(8)
,即8字节对齐(他表示的是内存对齐的最大对齐值)
【三条内存对齐规则】:
- 数据成员的对齐规则可以理解为
min(m, n)
的公式, 其中m
表示当前成员的开始位置,n
表示当前成员所需位数.如果满足条件m
整除n
(即m % n == 0
),n
从m
位置开始存储, 反之继续检查m+1
能否整除n
, 直到可以整除, 从而就确定了当前成员的开始位置. - 数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小(即在确定复合类型成员的偏移位置时则是将复合类型作为整体看待),且结构体成员要从其内部最大元素大小的整数倍地址开始存储.比如结构体a嵌套结构体b,b中有char、int、double等,那b应该从8的整数倍开始存储.
- 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐
7.编译和链接的过程
预处理:处理macro 宏(如#define), import 头文件替换及处理其他的预编译指令,产生.i文件。(都是以#号开头) 编译:把预处理完的一系列文件进行一系列词法、语法、语义分析,并且优化后生成相应的汇编代码,产生.s文件。
- 词法分析:将源代码字符串分解成一个个独立的词法单元(token),例如关键字、标识符、运算符、常量等。
- 语法分析:根据语言的语法规则,将token序列组织成语法树(通常是抽象语法树,AST),以表示代码的结构。
- 语义分析:检查代码的含义和一致性,例如类型检查、作用域检查、变量是否已声明等。
汇编:汇编器将汇编代码生成机器指令,输出目标文件,产生.o文件。(根据汇编指令和机器指令的对照表一一翻译就可以了) 链接:在一个文件中可能会用到其他文件,因此,还需要将编译生成的目标文件和系统提供的文件组合到一起,这个过程就是链接。经过链接,最后生成可执行文件。
链接过程做了这些:把一个或者多个文件(目标文件)与所需要的库链接形成可执行文件mach-O文件
链接分为两个类别:静态链接、动态链接
8.DYLD2和DYLD3的区别
相对于DYLD2,DYLD3将部分工作(比如分析依赖的动态库、查找需要rebase和bind的符号)在进程外完成并在每次应用程序安装或更新时将操作的结果缓存成launch closure,而对于系统程序的launch closure直接内置在shared cache中,后续启动时直接使用缓存,减少了重复工作
Mach-O文件的结构:Mach-O头部
、Load Command
、section
、Other Data
dyld3的两个过程:
out-process会做:
- 分析Mach-O Headers
- 分析依赖的动态库
- 查找需要的Rebase和Bind的符号
- 将上面的分析结果写入缓存。
in-process会做:
- 读取缓存的分析结果
- 验证分析结果
- 加载Mach-O文件
- Rebase&Bind
- Initializers
关于“符号缺失问题”:
dyld 2默认采取的是lazy symbol的符号加载方式,但在dyld 3中,在app启动之前,符号解析的结果已经在”lauch closure“内了,所以“lazy symbol”就不再需要。这时,如果有符号缺失的情况,APP的行为会有不同:在dyld 2中,首次调用缺失符号时APP会crash;而dyld 3中,缺失符号会导致APP一启动就会crash。
9.动态库静态库的区别
Foundation
和UIKit
这种可以共享代码、实现代码的复用统称为库
——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为静态库
和动态库
静态库:静态库是一种将代码编译后封装起来的二进制文件,在程序编译链接阶段被打包进最终的可执行文件中,运行时不再依赖外部库文件。但同时由于需要将库复制进最终程序,会使最终可执行文件体积变大。如.a、.lib都是静态库
动态库:动态库(Dynamic Library),也称为 共享库,是指在程序运行时动态加载的代码模块,不会在编译时被打包进可执行文件中,而是以共享形式存在,运行时由操作系统加载。多个程序可以共享同一个动态库的实例,系统只需加载一次动态库,可以节省内存。如.dylib、.framework都是动态库
10.内存分配区域
在 iOS 应用程序的内存管理中,常见的几大区域包括:
-
栈(Stack):
- 栈是一块内存区域,用于存储局部变量、方法参数、函数调用等信息。
- 栈上的内存分配和释放是由系统自动管理的,遵循后进先出(LIFO)的原则。
-
堆(Heap):
- 堆是一块动态分配的内存区域,用于存储动态分配的对象、数据结构等。
- 堆上的内存分配和释放需要手动管理,通过调用
alloc
、init
、new
、malloc
等方法来分配内存,需要free
-
全局区(Global Area):
- 全局区是用于存储全局变量、静态变量、常量等的内存区域。
- 全局区的内存分配和释放由系统自动管理,这些变量在整个应用程序生命周期内都是有效的。
-
常量区(Constant Area):
- 常量区用于存储字符串常量、静态常量等的内存区域。
- 常量区的内存分配和释放由系统自动管理,这些常量在整个应用程序生命周期内都是有效的。
-
代码区(Code Area):
- 代码区存储应用程序的可执行代码,包括方法、函数等的指令。
- 代码区的内存是只读的,不可修改。
这些内存区域的管理方式和生命周期是不同的。栈、常量和全局区的内存管理由系统自动处理,而堆区的内存需要手动进行管理。