1, 面试题
1, 使用NSDisplayLink、NSTimer有什么注意的地方?
2, 介绍下内存的几大区域?
3, 你对iOS内存管理是怎样理解的?
4, ARC都帮我们做了什么?
5, weakSelf的原理是怎样的?
6, autorelease对象会在什么时机调用release?
7, 方法里有局部变量, 会在方法执行完之后马上释放吗?
2, timer的探究
我们先从NSTimer开始讲起
2.1 NSTimer
使用NSTimer最常见的问题有两个: 一个在后面说, 另一个就是循环引用, 引起循环引用的原因是: NSTimer中有一个强引用的target属性, 持有VC本身, 所以会形成指针环. 解决的方案有:
1,调用带block的方法且配合weakSelf来达到弱引用的效果;
2, 使用一个第三方的中间体来达到弱引用的效果(proxy).
接下来我们先简单说一下方法1, 然后重点说方法2.
调用带block的方法且配合weakSelf来达到弱引用的效果
使用一个第三方的中间体来达到弱引用的效果(proxy)
这个方法的原理是这样的: 创建一个中间体来弱引用持有viewController, 从而达到破坏循环引用的目的
这个中间体我们一般称为proxy, 实现的方法有两种: 一种是继承自NSObject, 另一种方法是继承自NSProxy. 下面直接po出这两种方法的实现, 然后比对效果.
新建一个工程, 然后分别封装两个proxy类, 并实现调用, 代码截图如下
封装的代码如下, 注意截图中的提示
上面的代码就是实现的全部了, 可能也会有同学有疑问: 为什么不直接在GQProxy00或GQProxy01中实现test方法呢? 因为我们设计这个proxy中间体就是是为了考虑到以后别的类或者taget也会使用这个proxy做中间体, 所以proxy内部不写最终要调用的方法, 而是返回消息发送者本身.
接下来我们说一下继承自NSObject和NSProxy这两种不同方案的区别:
1, 继承自NSObject的中间体使用的是消息发送的整套机制, 也就是会遍历本身和父类的消息列表, 最终找到方法本身来进行调用;
2, 继承自NSProxy的中间体则直接使用消息转发机制, 也就是消息发送机制的最后一步, 直接将消息转发出去, 这样效率更高.
上面的第二点就是NSProxy类的最大用处. 关于NSProxy可能我们了解的很少, 下面展开说一下:
NSProxy是啥
NSProxy是一个跟NSObject同级的类, 遵守<NSProxy>协议但不继承自NSObject; 其本身并没有init方法; 当使用NSProxy的对象进行方法调用时, 并不会去遍历父类的方法列表, 而是只遍历本身的方法列表, 如果没找到方法实现, 则会直接将消息转发; 是一个专门设计用来进行消息转发的类, 相比于一般继承自NSObject的类, 其转发消息非常的高效.
刚刚没说的关于NStimer的一个问题就是: NStimer定时器并不能保证时间精准. 因为NStimer的底层是基于runloop来实现的. 实现的过程是这样的: 假如设置了NStimer时间间隔为1.0秒, runloop底层有一段计算时间的代码, 当runloop每进行一次循环都会判断累计的时间, 如果累计时间>=1.0s, 就会调用NStimer的方法. 但其实runloop每次循环的时间都是根据任务的不同而有所不同的, 这就是导致不能保证每次累计的时间间隔都能精确的控制在1.0s. 所以如果需要精准的时间间隔, 应该使用GCD, 因为GCD是不依赖runloop来实现的时间间隔, 而是依赖内核来实现的.
2.2 NSDisplayLink
相比于NStimer, NSDisplayLink也可以实现定时器功能, 但其可以精准的实现60fps帧率的水平, 也就是屏幕刷的帧率. 但其跟NSTimer一样, 也会对taget形成强引用, 引起循环引用, 而解决的方法跟NStimer一样, 所以这里就不展开说了.
接下来我们讲内存管理的另一块知识点: 内存布局.
3. 内存布局
4, tagged pointer
4.1 简介
看下面的介绍在使用tagged pointer之前, 对象的存储是通过指针来指向的, 而在tagged pointer之后, 直接把对象存储在指针中, 示意图如下
下面我们来看一个面试题.
4.2面试题
下面我们直接说结果吧:
第一个for循环会直接报错崩溃, 而且是地址错误. 为什么呢? 主要原因有两个:
1,第一个跟tagged pointer相关. 在这里, 由于字符串比较长, 所以第一个name会被runtime设置成非tagged pointer, 开启多线程频繁访问name的时候, 在底层不停的[_name release]和_name=[name copy]的时候, 由于没有加锁, 所以可能会导致不同线程间销毁原对象导致野指针错误;
1, 第二个跟线程相关, 就是上面提到的多线程读写操作.
如果要解决这个问题, 就需要我们在name属性前加atomic修饰, 而不是nonatomic. 或者对dispatch_queue进行信号量限制为1.
第二个for循环不会报错, 因为第二个name的字符串很短, runtime会将其设置成tagged pointer, 就不会存在上面的问题, 读写值都非常高效.
5, 对象的内存管理
5.1MRC
5.2 copy
一般调用copy的目的是为了获得一个副本对象, 当修改副本对象或者修改原对象的时候, 相互之间不影响. copy可分为深拷贝和浅拷贝, 深拷贝是产生了新的对象并分配了新的内存地址, 浅拷贝则只是拷贝了指针指向原对象地址, 原对象和地址都没有改变. 下面表格是拷贝的总结:其规律就是对象和拷贝返回的对象不一致时候, 就需要深拷贝另外需要注意的两点是:
1, 用copy修饰的属性, 编译器编译后会在底层的setter方法内部调用copy方法, 所以一般NSMutabelArray、NSDictionary等可变对象都不建议使用copy来修饰, 否则很容易在后续代码中修改值的时候引起崩溃, 而且xcode也没有提示警告;
2, 自定义的类如果想要调用copy方法, 则必须遵守<NSCopy>协议并且实现initWithZone:方法, 并且在方法中写明需要copy的对象属性等.
5.3weak指针的原理
直接总结如下:
1,当一个对象被weak指针指向的时候, runtime会以这个对象的地址值作为key保存到sideTable类中的weak_table散列表中对应的weak指针数组里;
2, 当对象调用dealloc方法时候, 就会以对象地址作为key, 从sideTable的weak_table散列表中的weak数组遍历逐个把weak设置为nil.