背景
iOS开发中我们因为内存的影响产生了各种问题,那么内存中都有什么东西呢?我们又该怎么管理内存呢?
准备
iOS是基于BSD演进来的,内存管理一般包括:操作系统内存管理、iOS内存管理
-
操作系统内存管理:要注意冯·诺依曼结构,以及冯·诺依曼的瓶颈,存储器的层次结构,CPU寻址方式,虚拟内存,内存分页等
-
iOS内存管理包括:iOS系统内存管理、App内存管理、内存管理不当导致的OOM崩溃
- iOS系统内存管理:clean memory 、 dirty memory、compressed memory 以及内存占用组成
- App内存管理:app地址空间、引用计数、循环引用、weak弱引用和unowned无主引用、不会导致循环引用的情况
- OOM崩溃:jetsam机制,那如何检测OOM崩溃,OOM常见的情况
- 内存分析检测
操作系统内存机制
冯·诺依曼结构
操作系统的内存机制依赖于冯·诺依曼结构,它第一次将存储器和运算器分离
- 存储器:存放程序指令和数据,程序运行时,根据需要把数据和指令提供给CPU,CPU去作计算和根据存储器指令去控制存储器,输入设备和输出设备。
- 冯·诺依曼瓶颈:目前的科技水平,存储器的读写速率远小于CPU处理的效率,CPU快,而存储器读写速度跟不上,造成了CPU性能的浪费,为了尽量突破这个瓶颈,现在采用的是多级分层存储,平衡读写速度、容量、和价格。
存储器多级存储、层次结构
- 易失存储器:断电后数据会丢失,读写速度快,容量小,造价高
- 非易失存储器:断电不会丢失数据,读写相对慢一些,容量大,造价低
- 下图(源于计算机组成原理)为分级存储器结构:
使用缓存能提升效率:被使用过的存储器内容在未来可能还会被多次使用,以及它附近的内容大概率也会被使用,放到高速缓存里面,部分情况下可以节约访问存储器的时间。
CPU寻址方式
- 由于每个进程系统会提供一个独立私有并且连续的地址空间,我们可以把内存看成一个数组,一个字节大小的空间是数组元素,而索引是对应的物理地址,CPU直接索引物理地址去访问内存地址,这叫物理寻址。
- 由于会有一些内存访问频率不是很高,内存全部加载到物理地址上,造成物理地址的浪费,后物理寻址扩展到分段机制,通过在CPU中增加段寄存器,将物理地址变成段地址,在段内通过 段内偏移量的形式,增加物理寻址,不过由于暴露了物理地址,地址空间缺乏保护,进程可以访问到任何物理地址,非常危险。
- 而经过计算机的演进,目前业界处理器采用虚拟寻址的方式:CPU通过访问虚拟地址,经过CPU内存管理单元MMU,翻译获得物理地址,然后访问内存
在往物理内存页表添加目标地址时原则:会先插空,后覆盖使用率低的内存。
虚拟内存及意义
保护每个进程的地址空间,简化内存管理,利用磁盘空间拓展内存空间。
1,保护进程地址空间,使进程不能越权相互干扰
2,虚拟一个连续的地址空间,便于内存空间管理
3,虚拟内存可以映射到物理内存以及磁盘的区域,
4,内存交换机制:当物理内存空间不足会把内存数据交换到硬盘去存储,利用磁盘空间拓展了内存空间。
内存分页
定义:把虚拟内存以一定大小的单位进行相同分割为页
意义:支持了物理内存的的离散使用,由于映射关系,虚拟内存对应的物理内存可以任意存放,方便对物理内存管理,最大化利用物理内存
场景:通过对一些页面的调度,将被使用的内存地址加到TLB或Page Table中,提高翻译效率,在iOS开发中性能优化中的启动优化的二进制重排,就是利用这个原理
iOS内存管理
iOS系统内存
- iOS系统是使用的虚拟内存机制
- 内存有限,但单应用可用内存大,系统为每个进程都提供高达4GB的可寻址空间
- 没有内存交换机制,由于虚拟内存远大于物理内存,一般不会出现内存不足,还有由于移动端的存储器通常为闪存,读写速度相交于PC端略慢,寿命有限,影响性能,并且存储器容量有限经常短缺,做交换过于奢侈。
- 内存警告,当内存不够时,会发出内存警告( didReceiveMemoryWarning() ),告知进程去清理自己的内存,app去清理一些不必要的内存来释放一定空间。
- OOM崩溃,app发生内存警告做了清理后,物理内存还是不够用,就会发生OOM崩溃,当app内存达到分配内存的55%超出这个临界值,会发生OOM崩溃,由于单个app可用物理内存实际上很大,还能发生OOM崩溃,大多数是程序设计的问题了。
clean memory & dirty memory
- clean memory:能够被重新创建的内存,
主要包括:- app的二进制可执行文件,
- framework中 _DATA_CONST段:(注意:当app使用到framework时,会变成dirty memory)
- 文件映射的内存:系统将文件映射加载到内存,如果文件只读的这部分内存属于clean memory。
- 没写入数据的内存,如下代码段:
int *array = malloc(10 * sizeof(int));
array[0] = 3
array[9] = 6
创建一个长度为10的内存空间的数组,第一个和最后一个为dirty memory会始终占据物理内存,中间的8个为clean memory,当物理内存不够用时,系统会开始清理clean memory。
Compressed memory
定义:物理内存不够用时,iOS会将部分物理内存压缩,在需要读写时再解压,来解决内存不够用的目的。
意义:内存紧张时,将目标内存压缩至原有的一半以下,压缩和解压消耗CPU资源,但之前谈到过CPU算力本就过剩,物理内存的空间相较于CPU算力更为重要,使用compressed memory更一步优化内存的使用。
- 如上图:作为clean memory 系统会帮助我们去作内存优化处理,但对于一个dirty memory,Dictionary处于未压缩状态时并没有减少物理内存。
- 在作缓存处理时,推荐使用NSCache而不是NSDictionary,是因为NSCache不仅线程安全,内部会根据LRC算法主动去处理对象的释放,而且对存在compressed的情况下,内存警告做了优化,可由系统自动释放内存。
App内存管理
定义:对于iOS,一个app就是一个进程,开发者经常讨论的内存管理实际上时进程内部的内存管理,在OC的内存管理中,其实就是对引用计数的管理,在程序需要时我们给他分配一段内存空间,使用完后将它释放。如果我们对内存资源使用不当,不仅会造成内存资源的浪费,甚至会导致程序crash。
内容包括App地址空间,引用计数,循环引用,weak&unowned(无主引用),
app地址空间
- 栈区一般存放局部变量、临时变量、由编译器自动分配和释放,速度比较快
- 而堆区用于动态内存的申请,需要开发者分配和释放,比较灵活
- 对于Swift来说,值类型存于栈区,引用类型存于堆区。
值类型:struct、enum、tuple(元组)、int、double、array、dictionary
引用类型:class、closure(闭包)
对于Swift多注意引用类型的内存管理。
引用计数
定义:由于堆区需要开发者去进行管理,iOS采用引用记数的方式进行内存管理、记录和回收,将资源被引用的次数保存,当引用次数为零时对占用空间释放回收。
意义:iOS早期通过手动管理引用计数(MRC),管理对象的生命周期,维护成本比较高;后来2011的WWDC提出了自动管理引用计数(ARC),通过编译器的静态分析,自动插入引用计数的管理逻辑,解决了手动管理的麻烦。
循环引用问题:引用计数是垃圾回收中的一种,垃圾回收还有标记-清除算法(Mark Sweep GC),可达性算法(Tracing GC),相比,引用计数只记录对象被引用的次数,实质是一个局部的信息,而非全局信息,因此可能产生循环引用的问题。
为何要采用引用计数:使用引用计数当对象的生命周期结束,可立即被回收,而不用等全局遍历之后,其次,iOS整体内存偏小,使用可达性和标记-清除方式有算法延迟影响效率。因此引用计数是更为合理的选择。
循环引用
本质:多个对象相互之间有强引用,不能释放让系统回收
iOS引起循环引用的情况:block、delegate、Timer、通知。
由于没能释放不能使用的内存,导致内存泄漏,浪费大量内存,同时也很可能导致App崩溃。
class viewController: UIViewController {
let a = 10
var sClosure: (() -> Int)?
override func viewDidLoad() {
super.viewDidLoad()
sClosure = {
return self.a
}
sClosure?()
}
}
上面代码段,vc中会持有sClosure,而sClosure需要self.a,就持有了vc,这就导致了循环引用。
解决方法:
利用Swift提供的闭包捕获列表,将强引用关系改为弱引用即可。
sClosure = { [weak self] in
return self.a
}
weak弱引用和unowned无主引用
weak:弱引用,一般将循环引用中的强引用替换为弱引用,来解决循环引用问题。
unowed:无主引用,循环引用也可通过无主引用来解决循环引用。
两者区别:
- 弱引用对象可用为nil,而无主引用对象不能为nil,否则会发生运行时错误。
- 使用weak需要使用guard let解包,而unowed可用省略解包。
- weak底层做了个附加层,间接把unowed引用包装于一个可选容器,影响了一些性能,unowed会相对快一些。
使用场景:weak是一个可选类型,需要考虑手动解包,使用相对安全,使用场景比较广泛,对于闭包,如果知道对象必然不会为nil的可用unowed,在闭包和捕获实例对象时总是相互引用并且同时销毁时,也用将闭包捕获定义为unowed(懒加载的闭包)
不会导致循环引用的情况
对于Swift来说,也有些闭包并不会引起循环引用的情况,比如使用
- DispatchQueue并不会被持有:
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.execute()
}
- static标识的静态 functions
class class1 {
static func getData(params: String, completion:@escaping (String) -> Void) {
request(method: .get, parameters: params) { (response) in
completion(response)
}
}
}
vc调用:
getData(params: self.params) { (value) in
self.value = value
}
因为self并不会持有static class
OOM崩溃
定义:内存使用过多,导致的App闪退。
OOM常见问题
- 内存泄漏
- UIWebView缺陷:在打开网页或执行js代码,UIWebView都会占用大量内存,最好替换使用WKWebView。
- 大图片、大视图:绘制大图,以及渲染过大的视图,都会占用大量内存,轻者卡顿,重者在解析、渲染的过程发生OOM
Jetsam机制
iOS是从BSD桌面操作系统衍生来的,其XNU内核是Mach,其中内存警告以及OOM崩溃处理机制就是Jetsam机制,Jetsam会始终监控内存整体使用情况,内存不足时会根据优先级、内存占用大小来kill一些进程,并记录成JetsamEvent。
Jetsam维护着一个优先级队列:
static const char *
memorystatus_priority_band_name(int32_t priority)
{
switch (priority) {
case JETSAM_PRIORITY_FOREGROUND:
return "FOREGROUND";
case JETSAM_PRIORITY_AUDIO_AND_ACCESSORY:
return "AUDIO_AND_ACCESSORY";
case JETSAM_PRIORITY_CONDUCTOR:
return "CONDUCTOR";
case JETSAM_PRIORITY_HOME:
return "HOME";
case JETSAM_PRIORITY_EXECUTIVE:
return "EXECUTIVE";
case JETSAM_PRIORITY_IMPORTANT:
return "IMPORTANT";
case JETSAM_PRIORITY_CRITICAL:
return "CRITICAL";
}
return ("?");
}
监控内存警告和处理Jetsam:
- 内核会调起一个内核分配的优先级最高的线程*95 /* MAXPRI_KERNEL /
// 同样在 bsd/kern/kern_memorystatus.c 文件中
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);
线程会维护两个列表,一个是基于优先级的进程列表,另一个是每个进程消耗的内存页的列表,同时会监听内核pageout线程对整体内存的使用情况的通知,内存不够时会向每个进程转发内存警告,触发 didReceiveMemoryWarning方法
- kill掉app,会触发OOM,主要是通过memorystatus_kill_on_VM_page_shortage,有同步和异步两种方式,
- 同步方式会立刻进入kill线程的逻辑,会根据优先级,kill优先级低的进程,如果同一优先级的会根据内存大小,kill内存占用大的进程。
- 异步方式会标记当前进程,再通过内存管理线程去杀死。
检测OOM
OOM分两类,FOOM(app在前台耗内存过大被kill),BOOM(app在后台耗内存过大被kill),直接表现为crash。
业界的几种方式:
- Facebook开源的FBAllocationTracker,原理是hook了 malloc/free等方法,在运行时记录所有实例的分配信息,来发现内存异常的情况,类似于在app内运行性能更好的Allocation,但这个库只监控OC对象,同时没法拿到对象的堆栈信息,有一定局限性,不好定位OOM的具体原因
- 腾讯的OOMDetector,通过malloc/free更底层的接口 malloc_logger_t记录当前存活对象的内存分配信息,并根据系统的backtrace_symbols回溯堆栈信息,后根据Splay Tree(伸展树)来作数据存储分析。
内存分析
内存分析检测分两种:线下和线上。
线下的情况:使用Xcode自带的内存分析工具,在Debug navigator中可用查看内存占用情况
线上的情况:我了解了一些工具,大致的原理是在一个时间点拿到弱引用指针,然后一段时间后再去检测。通过采样率控制整体的性能消耗
- Instrument - Allocations:可以查看虚拟内存占用、堆信息、对象信息、调用栈信息,VM Regions 信息等。可以利用这个工具分析内存,并针对地进行优化。
- Instrument - Leaks:用于检测内存泄漏。
- MLeaksFinder:通过判断 UIViewController 被销毁后其子 view 是否也都被销毁,可以在不入侵代码的情况下检测内存泄漏。
- Instrument - VM Tracker:可以查看内存占用信息,查看各类型内存的占用情况,比如 dirty memory 的大小等等,可以辅助分析内存过大、内存泄漏等原因。
- Instrument - Virtual Memory Trace:有内存分页的具体信息,具体可以参考 WWDC 2016 - Syetem Trace in Depth。
- Memory Resource Exceptions:从 Xcode 10 开始,内存占用过大时,调试器能捕获到 EXC_RESOURCE RESOURCE_TYPE_MEMORY 异常,并断点在触发异常抛出的地方。
- Xcode Memory Debugger:Xcode 中可以直接查看所有对象间的相互依赖关系,可以非常方便的查找循环引用的问题。同时,还可以将这些信息导出为 memgraph 文件。
- memgraph + 命令行指令:结合上一步输出的 memgraph 文件,可以通过一些指令来分析内存情况。vmmap 可以打印出进程信息,以及 VMRegions 的信息等,结合 grep 可以查看指定 VMRegion 的信息。leaks 可追踪堆中的对象,从而查看内存泄漏、堆栈信息等。heap 会打印出堆中所有信息,方便追踪内存占用较大的对象。malloc_history 可以查看 heap 指令得到的对象的堆栈信息,从而方便地发现问题。总结:malloc_history ===> Creation;leaks ===> Reference;heap & vmmap ===> Size。
内存泄漏
泄漏内存主要有两种:
- Leak Memory:这种忘记release操作造成的内存泄漏
- Abandon Memory:这种是循环引用,无法释放掉的内存
内存崩溃
内存崩溃主要分:
- double release: 内存释放free时,走了double free的逻辑
- 指针指向了一块儿别的地址空间
EXC_BAD_ACCESS:指向了一段被释放的内存.
App内存管理认识
内存管理中都分哪些呢:
- 内存布局
- 内存管理方案
- ARC&MRC
- 引用计数
- 弱引用
- 自动释放池
实践
内存布局
认识
- 栈区:函数,方法,成员变量,常量,block,通过SP寄存器定位,⼀般为:0x7开头
- 堆区:对象,通过alloc分配的对象,block copy ,通过指针地址寻找相应堆内存地址,⼀般为:0x6开头
- BSS段:未初始化的全局变量,静态变量,⼀般为:0x1开头
- 数据段:初始化的全局变量,静态变量,⼀般为:0x1开头
- text:程序代码,加载到内存中
- 保留地地址空间,nil
- static 修饰的静态变量在全局堆区,不占用当前结构体的内存空间
内存管理方案
tp代表taggedpoint。
- TaggedPointer:⼩对象-NSNumber,NSDate (pointer + tagged = 对小对象处理的指针)
- NONPOINTER_ISA:⾮指针型isa
- 散列表(sideTable):引⽤计数表,弱引⽤表
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return (void *)ptr;
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
return (void *)value;
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
return value;
}
taggedpointer
//多线程读写,底层对同一片地址新值的retain,对旧值的release,过渡释放
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"好好工作,好好生活,不急不躁"];
NSLog(@"%@",self.nameStr);
});
}
nonpointer
- nonpointer:表示是否对 isa 指针开启指针优化
0:纯isa指针,
1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等 - has_assoc:关联对象标志位,0没有,1存在
- has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
- shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。
- magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间
- weakly_referenced:志对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。
- deallocating:标志对象是否正在释放内存
- has_sidetable_rc:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位
- extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,
例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。
ARC&MRC
- ARC是LLVM和Runtime配合的结果
- ARC中禁⽌⼿动调⽤retain/release/retainCount/dealloc
- ARC新加了weak、strong属性关键字
alloc, retain,release
retainCount,autorelease,dealloc
retain,release
- Retain底层源码探索:
- 首先 isTaggedPointer(判断是否为TaggedPointer)->
- 然后 nonpointer -> isa 走位->散列表
Retain (nonpointer):
- 1: 判断是否为 nonpointer (isa走位)
- 2: 操作 引用计数
a: 如果不是 nonpointer -> 散列表
spinlock_t slock; 开解锁
RefcountMap refcnts; 引用计数表
weak_table_t weak_table; 弱引用表
散列表 在内存里面有多张 + 最多能够多少张
一张 对象 开锁解锁
b: 是否正在释放
c: extra_rc + 1 满了 - 散列表
d: carry 满 extra_rc 满/2 -> extra_rc 满/2 -> 散列表 (开锁关锁)
alloc -> retain -> release -> dealloc
针对相应引⽤计数位加1
如果引⽤计数出现上溢出,那么我们开始分开存储,⼀半存到散列表
针对相应引⽤计数位减1
如果引⽤计数出现下溢出,就去散列表借来的引⽤计数 - 1 存到extra_rc
release 就算借散列表的引⽤计数过来,还是下溢出,那么就调⽤dealloc
dealloc
dealloc:
- 1:根据当前对象的状态是否直接调⽤free()释放
- 2:是否存在C++的析构函数、移除这个对象的关联属性
- 3:将指向该对象的弱引⽤指针置为nil
- 4:从弱引⽤表中擦除对该对象的引⽤计数
散列表(sideTable)-哈希表
认识
我们通过objc源码,看到散列表底层源码,SideTable
是对引用计数表,和弱引用表的处理
弱引用(weak)、引用计数(RetainCount)
弱引用方式:__weak typof(id) weakSelf = self;
发现使用弱引用,不影响objc的引用计数,
但是打印weakObjc时却是2,为什么?
为什么建立个zm_objc指针指向objc,这个时候objc却打印为2?
解答:
1,首先objc指针指向alloc创建的对象内存,引用计数为1
2,weakObjc弱引用,没有强持有,走了objc_loadWeakRetained
-> rootRetain
,把对象做了一次retain,1+1,但它是个临时作用空间,走完流程的时候会release
3,这个时候objc的对象内存retainCount数为2
3,这时候zm_objc 也指向这块内存,引用计数1+1,强引用的假象
4,__weak声明的指针指向的内存空间没有的时候当前声明的指针还在,但指向了nil,内存没了,(弱引用表的影响)
通过看底层源码:
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
然后进入其中:storeWeak,栈的弱引用表
如果有旧表就移除旧表,没有的话,直接到weak_register_no_lock
,往散列表中的弱引用表添加。
strong&unsafe_unretain
retain新值,release旧值
小结:
根据流程我们看到:
1:⾸先我们知道有⼀个-sideTable(散列表)
2:得到sideTable的weakTable 弱引⽤表
3:创建⼀个weak_entry_t
4:把referent加⼊到weak_entry_t的数组inline_referrers
5:如果需要扩容,把weak_table扩容⼀下
5:把new_entry加⼊到weak_table中
NStimer探索
方案一:借助Runloop
- timer与runloop配合,但在对象释放的时候,如果在delloc中是不行的,强持有关系导致循环引用,需要在:
didMoveToParentViewController
中释放timer,然后才会走delloc
// weakSelf : 没有对内存加1,弱引用
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
//timer持有了weakSelf(强持有target),会对weakSelf指向的内存加1
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
//runloop 持有了timer
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
方案二:Block方式
使用block方式,由于没有对timer强持有,timer可以在delloc中释放
- (void)blockTimer{
//没有对timer强持有
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
方案三:抽离中介者
把timer,以及objc抽离,runtime运行时动态添加,把vc的action传到封装里面执行,最后在vc的delloc释放
- (instancetype)zm_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
if (self == [super init]) {
self.target = aTarget; // vc
self.aSelector = aSelector; // 方法 -- vc 释放
if ([self.target respondsToSelector:self.aSelector]) {
//runtime运行时动态添加
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
// 一直跑 runloop
void fireHomeWapper(LGTimerWapper *warpper){
if (warpper.target) { // vc - dealloc
void (*zm_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
zm_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
}else{ // warpper.target
[warpper.timer invalidate];
warpper.timer = nil;
}
}
- (void)zm_invalidate{
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
vc的实现 和 delloc
self.timerWapper = [[ZMTimerWapper alloc] zm_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
- (void)dealloc{
[self.timerWapper zm_invalidate];
NSLog(@"%s",__func__);
}
方案四:NSProxy 虚基类
@interface ZMProxy()
@property (nonatomic, weak) id object;
@end
@implementation ZMProxy
+ (instancetype)proxyWithTransformObject:(id)object{
ZMProxy *proxy = [ZMProxy alloc];
proxy.object = object;
return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// 转移
// 强引用 -> 消息转发-快速
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
// 慢速消息转发
// sel - imp -
// 消息转发 self.object
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
if (self.object) {
}else{
NSLog(@"麻烦收集 stack111");
}
return [self.object methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
if (self.object) {
[invocation invokeWithTarget:self.object];
}else{
NSLog(@"麻烦收集 stack");
}
}
@end
VC的实现和delloc
// 思路四: proxy 虚基类的方式,self.proxy的释放在delloc里面跟着timer、self一起销毁
self.proxy = [ZMProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
- (void)dealloc{
// VC -> proxy <- runloop
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
自动释放池(@autoreleasepool)
认识
Clang分析
通过Clang我们看到@autoreleasepool的底层实现方式
__AtAutoreleasePool __autoreleasepool;
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}//构造函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}//析构函数(释放函数)
void * atautoreleasepoolobj;
};
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
- 我们通过汇编调试,看到进入方法
objc_autoreleasePoolPush
:
下符号断点:
自动释放池底层结构
- AutoreleasePoolPage:
1,线程的自动释放池是一堆指针。
2,每个指针要么是要释放的对象,要么是自动释放池边界。
3,池标记是指向该池的pool\u边界的指针。什么时候池被弹出,每一个比哨兵更热的物体被释放。
4,堆栈被划分为一个双链接的页面列表(双向链表)。已添加页面并根据需要删除。
5,线程本地存储指向最新自动释放的热页存储对象。
双向链表:(类的加载,父类子类),autoreleasepool
AutoreleasePoolPage
继承的结构体AutoreleasePoolPageData
:
• magic ⽤来校验 AutoreleasePoolPage 的结构是否完整;
• next 指向最新添加的 autoreleased 对象的下⼀个位置,初始化时指向begin() ;
• thread 指向当前线程;
• parent 指向⽗结点,第⼀个结点的 parent 值为 nil ;
• child 指向⼦结点,最后⼀个结点的 child 值为 nil ;
• depth 代表深度,从 0 开始,往后递增 1;
• hiwat 代表 high water mark 最⼤⼊栈数量标记
然后我们看到push的结构:autoreleaseNewPage,autoreleaseFast(创建)
自动释放池的创建和压栈
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
autoreleaseFast:如果有page并且没有满则add压栈,如果满了 ->
则autoreleaseFullPage:先找子page,如果子page没有就创建new AutoreleasePoolPage,-> 否则 autoreleaseNoPage
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
自动释放池底层结构我们看到其size大小,总56
双向链表
由于分页,内存操作压栈出栈过程
分页结构(双向链表)。
自动释放池满页临界和压栈出栈
- 因为是通过autoreleasepool创建的,为什么第一页为504个对象?
- PAGE_MIN_SIZE每页的最小为12,<<12(左移12) = 2的12次方 : 4096,即4K,因此第一页504个对象
对于出栈:
- 当自动释放池收到pop指令时,一层一层把对象释放。由
哨兵对象
来标记是否完成,哨兵对象是否等于哨兵边界
扩展
_objc_autoreleasePoolPrint(void)
打印自动释放池内容- 1,自动释放池是否可以嵌套使用?
自动释放池可以嵌套使用,相互不影响
注意⚠️:
1,MRC中autorelease的对象会加到自动释放池由POOL哨兵管理
2,ARC中[ZMPerson person]对象的创建都会加到自动释放池中(person类方法)。
3,但是ARC/MRC中[[NSObject alloc] init]的对象都不会加到释放池中
4,由于用 alloc,new, copy, mutablecopy创建的对象都不会加到自动释放池中
LLVM查看原理 - 2, 临时变量什么时候释放?
临时变量作用域的范围结束时释放。
总结
- 内存管理内容很多,实际使用需要了解原理,深究的话看个人爱好,虽然很强大,但都是为了高级语言更好的使用。
参考文献:OOM探索