内存管理-操作系统、iOS

背景

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探索

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

☆MOON

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值