美文网首页
iOS内存管理

iOS内存管理

作者: 6ffd6634d577 | 来源:发表于2020-03-31 00:29 被阅读0次
1.weak的实现原理?SideTable的结构是什么样的

weak:其实是runtime全局维护的一个hash表结构,其中的key是所指对象的地址,value是weak的指针数组,weak表示的是弱引用,不会对对象引用计数+1,当引用的对象被释放的时候,其值被自动设置为nil,一般用于解决循环引用的。

weak的实现原理

1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak()函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

SideTable的结构如下
struct SideTable {
// 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

参考这篇文章

2.关联对象的应用?系统如何实现关联对象的
应用:
  • 可以在不改变类的源码的情况下,为类添加实例变量(注意:这里指的实例变量,并不是真正的属于类的实例变量,而是一个关联值变量
  • 结合category使用,为类扩展存储属性。
关联对象实现原理:

系统通过管理一个全局哈希表,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter传入的参数协议,来管理对象的生命周期。
关联对象的值实际上是通过AssociationsManager对象负责管理的,这个对象里有个AssociationsHashMap静态表,用来存储对象的关联值的,关于AssociationsHashMap存储的数据结构如下:

AssociationsHashMap:
------添加属性对象的指针地址(key):ObjectAssociationMap(value:所有关联值对象)
ObjectAssociationMap:
------关联值的key:关联值的value


image.png

所以:关联对象的值它不是存储在自己的实例对象的结构中,而是维护了一个全局的结构AssociationManager

具体runtime的方法实现请参考这篇文章

3.关联对象的如何进行内存管理的?关联对象如何实现weak属性

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 指定一个弱引用相关联的对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   

内存管理方面是通过在赋值的时候设置一个policy,根据这个policy的类型对设置的对象进行retain/copy等操作。
当policy为OBJC_ASSOCIATION_ASSIGN的时候,设置的关联值将是以弱引用的方式进行内存管理的。

具体的可以看这篇

4.Autoreleasepool的原理?所使用的的数据结构是什么
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);

///而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。
void *
objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePool的是通过AutoreleasePoolPage类实现的

image.png
    magic_t const magic; //用来校验 AutoreleasePoolPage 的结构是否完整;
    id *next; //指向栈顶,也就是最新入栈的autorelease对象的下一个位置;
    pthread_t const thread; //指向当前线程
    AutoreleasePoolPage * const parent; //指向父节点
    AutoreleasePoolPage *child; //指向子节点
    uint32_t const depth; //表示链表的深度,也就是链表节点的个数
    uint32_t hiwat;
  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage栈为节点的双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
AutoreleasePool的释放有如下两种情况:
  1. 一种是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
  2. 手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool或者@autoreleasepool{}执行完释放
AutoreleasePool 和 RunLoop 有什么联系?

因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。这个Observer的order是-2147483647优先级最高,确保发生在所有回调操作之前
第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态

当runloop即将休眠的时候会把之前的自动释放池释放,然后重新创建一个新的释放池
主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建AutoreleasePool否则一般不需要自己创建)。

参考这篇文章

5.ARC的实现原理?ARC下对retain & release做了哪些优化

参考这篇文章

6.ARC下哪些情况会造成内存泄漏
  • block中的循环引用
  • NSTimer的循环引用
  • addObserver的循环引用
  • delegate的强引用
  • 大次数循环内存爆涨
  • 非OC对象的内存处理(需手动释放)
image.png

有人可能有疑问,为什么都同样是target-action方式button就不会出现循环引用的问题,有去研究的同学应该都知道UIControl的内部做了weak操作,即真正持有的时候是weak的并没有导致retain加1,而NSTimer由于runloop的原因并没有做weak操作。

NSTimer
  • 它会被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
  • 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
  • runloop会对timer有强引用,timer会对目标对象target进行强引用(是否隐约的感觉到坑了。。。)
  • timer的执行时间并不准确,系统繁忙的话,还会被跳过去
  • invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????
解决循环引用的方法
  1. invalidate方法
    invalidate方法有2个功能:
    一是将timer从runloop中移除
    二是timer本身也会释放它持有资源,比如target

  2. 引入中间者, 借助runtime给对象添加消息处理的能力

    _target = [[NSObject alloc] init];
    class_addMethod([_target class], @selector(fire), class_getMethodImplementation([self class], @selector(fire)), "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_target selector:@selector(fire) userInfo:nil repeats:YES];
  1. 通过消息转发的方法的方式
    创建一个集成自NSProxy的类PHJProxy 声明一个target
    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>

    @interface PHJProxy : NSProxy
    @property (nonatomic, weak) id target;
    @end

PHJProxy的实现

@implementation PHJProxy
// 发送给target
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
// 给target注册一个方法签名
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

@end

PHJProxy 和 NSTimer的使用

   self.proxy = [PHJProxy alloc];
   self.proxy.target = self;
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy 
   selector:@selector(fire) userInfo:nil repeats:YES];

在iOS中基本上90%的类都是集成NSObject,但是有一个类NSProxy就很特别,它就没有继承NSobject,但是这个类实现了协议
NSProxy是一个虚类,你可以通过继承它,并重写这两个方法以实现消息转发到另一个实例。说白了,NSProxy转为代理而生(负责将消息转发到真正的target的代理类)。从类名来看是代理类,专门负责代理对象转发消息的。相比NSObject类来说NSProxy更轻量级,通过NSProxy可以帮助Objective-C间接的实现多重继承的功能。

- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

思考:设置weak能解决循环引用吗?

不能,runloop是强持有timer的,声明为weak只是vc不持有

日常如何检查内存泄露?

  • 泄露的内存主要有以下两种:

Laek Memory 这种是忘记 Release 操作所泄露的内存。
Abandon Memory 这种是循环引用,无法释放掉的内存。

  • 目前我知道的方式有以下几种

Memory Leaks

Leaks 工具只负责检测 Leaked Memory,而不管 Abandoned Memory。
MRC 时代 Leaked memory 很常见,因为很容易忘了调用 release,但在 ARC时代更常见的内存泄露是循环引用导致的 Abandoned Memory,Leaks 工具查不出这类内存泄露,应用有限

Alloctions

对于 Abandoned memory,可以用 Instrument 的 Allocations 检测出来。
检测方法:每次点击 Mark Generation 时,Allocations 会生成当前 App 的内存快照,而且 Allocations 会记录从上回内存快照到这次内存快照这个时间段内,新分配的内存信息。

我们可以不断重复 push 和 pop 同一个 UIViewController,理论上来说,push 之前跟 pop 之后,app 会回到相同的状态。因此,在 push 过程中新分配的内存,在 pop 之后应该被 dealloc 掉,除了前几次 push 可能有预热数据和 cache 数据的情况。如果在数次 push 跟 pop 之后,内存还不断增长,则有内存泄露。
用这种方法来发现内存泄露还是很不方便的:
1、首先,你得打开 Allocations
2、其次,你得一个个场景去重复的操作
3、无法及时得知泄露,得专门做一遍上述操作,十分繁琐

Analyse

静态分析工具: 可以通过Product ->Analyze菜单项启动
Analyze主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、API调用错误:未包含使用的库和框架。
这里使用Analyze静态分析查找出来的泄漏点,称之为"可疑泄漏点"。之所以称之为"可疑泄漏点",是因为这些点未必一定泄露,确认这些点是否泄露, 还要通过Instruments动态分析工具的 Leaks和Allocations跟踪模板。 Analyze静态分析只是一个理论上的预测过程.

Debug Memory Graph

可以看看这篇文章

MLeaksFinder

MLeaksFinder 是腾讯WeRead团队开源的一款检测 iOS 内存泄漏的框架,其使用非常简单,只需将文件加入项目中,如果有内存泄漏,3秒后自动弹出 alert 来捕捉循环引用。具有无侵入性、
可构建泄漏堆栈、白名单机制等优点。
目前只检测ViewControllerView对象(可扩展,MLCheck())

总体思路:当一个 ViewController 被 pop 或 dismiss 之后,我们认为该 ViewController,包括它上面的子 ViewController,及它的 View,View 的 subView 等,都很快会被释放,如果某个 View 或者 ViewController 没释放,我们就认为该对象泄漏了。

具体的做法:为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在3秒后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接弹框提醒该对象可能存在内存泄漏。
UIViewController的分类中,使用 Method Swizzling,hook掉了
viewDidDisappear:viewWillAppear:dismissViewControllerAnimated:completion:等方法,让他们都执行willDealloc方法,这样,在不入侵开发代码的情况下,为UIViewController添加了检查内存泄露的功能(AOP

查找循环引用链:
Facebook 开源了一个循环引用检测工具 FBRetainCycleDetector。当传入内存中的任意一个 OC 对象,FBRetainCycleDetector 会递归遍历该对象的所有强引用的对象,以检测以该对象为根结点的强引用树有没有循环引用。
我们知道,很多循环引用是 block 的使用不当造成的。而 FBRetainCycleDetector 最大的技术亮点,正在于如何找出一个 block 的所有强引用对象
当 MLeaksFinder 与 FBRetainCycleDetector 结合使用时,正好能达到很好的效果。我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用即可。

wereadteam
参考这篇文章

相关文章

网友评论

      本文标题:iOS内存管理

      本文链接:https://www.haomeiwen.com/subject/zjybuhtx.html