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
类实现的
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的释放有如下两种情况:
- 一种是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
- 手动调用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对象的内存处理(需手动释放)
有人可能有疑问,为什么都同样是target-action
方式button就不会出现循环引用的问题,有去研究的同学应该都知道UIControl的内部做了weak操作
,即真正持有的时候是weak的并没有导致retain加1,而NSTimer
由于runloop
的原因并没有做weak操作。
NSTimer
- 它会被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
- 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
- runloop会对timer有强引用,timer会对
目标对象target
进行强引用(是否隐约的感觉到坑了。。。) - timer的执行时间并不准确,系统繁忙的话,还会被跳过去
- invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????
解决循环引用的方法
-
invalidate方法
invalidate
方法有2个功能:
一是将timer从runloop中移除
二是timer本身也会释放它持有资源,比如target -
引入中间者, 借助
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];
- 通过消息转发的方法的方式
创建一个集成自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 来捕捉循环引用。具有无侵入性、
可构建泄漏堆栈、白名单机制等优点。
目前只检测ViewController
和View
对象(可扩展,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 检测该对象有没有循环引用即可。
网友评论