前言
我们借助几道面试题,来探究一下iOS的内存管理
一、使用CADisplayLink、NSTimer有什么注意点?
需要注意两个地方:小心循环引用、不准时
1. 小心循环引用
- (1).
CADisplayLink、NSTimer
会对target
产生强引用,如果target
又对它们产生强引用,那么就会引用循环引用,例如下面的代码,就会产生循环引用
class ViewController: UIViewController {
var displayLink: CADisplayLink!
var timer: Timer!
override func viewDidLoad() {
super.viewDidLoad()
displayLink = CADisplayLink(target: self, selector: #selector(testDisplayLink))
displayLink.add(to: RunLoop.current, forMode: RunLoop.Mode.default)
timer = Timer.init(timeInterval: 1, target: self, selector: #selector(testTimer), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: RunLoop.Mode.default)
}
}
-
(2).
CADisplayLink
调用次数跟屏幕刷新频率一致,当掉帧时,会响应减少;CADisplayLink
需要加到RunLoop
中才能生效;CADisplayLink
会对target
产生强引用,所以需要通过代理类规避循环引用,代理类的方法有两种,如下所示:- 第一种,通过
forwardingTarget
将消息转发给target
,VC控制器
对CADisplay对象
产生了强引用,CADisplay对像
对proxy代理
产生了强引用,proxy代理
对VC控制器
产生了弱引用,无法形成闭环,也就没有循环引用的问题了
- 第一种,通过
class TFTProxy1: NSObject{
weak var target: AnyObject?
override init() {
super.init()
}
convenience init(target: AnyObject?) {
self.init()
self.target = target
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}
使用的时候,这样写:
let proxy = TFTProxy1(target: self)
displayLink = CADisplayLink(target: proxy, selector: #selector(testDisplayLink))
displayLink.add(to: RunLoop.current, forMode: RunLoop.Mode.default)
- 第二种,通过
methodSignatureForSelector
方法签名,将消息转发给proxy
,同样也是proxy代理
对VC控制器
产生了弱引用,以避免循环引用
@interface TFTProxy2 ()
@property (nonatomic,weak) id target;
@end
@implementation TFTProxy2
+ (instancetype)proxyWithTarget:(id)target{
TFTProxy2 * proxy = [TFTProxy2 alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
- (3).
Timer
需要添加到RunLoop
中才能执行,Timer.scheduledTimer()
会自动加入到Runloop
中,不需要我们手动加了,而Timer.init()
就需要我们手动加到Runloop
了;Timer
也会对target
产生强引用,所以需要通过代理类
或者Block
来规避,如下所示:
let proxy = TFTProxy(target: self)
//let proxy = TFTProxy2(target: self)
timer1 = Timer.init(timeInterval: 1, target: proxy, selector: #selector(testTimer), userInfo: nil, repeats: true)
RunLoop.current.add(timer1, forMode: RunLoop.Mode.default)
timer2 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in
self?.testTimer()
})
2. 小心不准时
CADisplayLink
和Timer
都需要依赖RunLoop
,如果RunLoop
的任务过于繁重,那么就会造成不准时,所以想准时的话,我们应该使用GCD的定时器,如下所示,我们对GCD的定时器做了封装:
class TFTimer: NSObject{
static var timers_ = Dictionary()
static let semaphore_ = DispatchSemaphore(value: 1)
/*
描述:执行某个定时任务
参数:task-任务闭包
start-任务开始时间,单位是秒
intercval-定时器间隔,单位是秒
isReapeat-是否重复执行任务
isAsync-任务是否异步执行,true在全局队列中,false在主队列中
*/
class func executeTask(task: @escaping () -> Void, start: DispatchTimeInterval, interval: DispatchTimeInterval, isRepeat: Bool, isAsync: Bool) -> String?{
let queue = isAsync ? DispatchQueue.global() : DispatchQueue.main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.setEventHandler {
task()
}
let deadline = DispatchTime.now() + start
if isRepeat {
timer.schedule(deadline: deadline, repeating: interval)
}else{
timer.schedule(deadline: deadline, repeating: .never)
}
timer.resume()
semaphore_.wait()
let taskID = "\(timers_.count)"
timers_[taskID] = timer
semaphore_.signal()
return taskID
}
class func executeTask(target: NSObject?, selector: Selector?, start: DispatchTimeInterval, interval: DispatchTimeInterval, isRepeat: Bool, isAsync: Bool) -> String?{
guard let target = target, let selector = selector else { return nil}
let taskID = self.executeTask(task: {
if target.responds(to: selector){
target.perform(selector)
}
}, start: start, interval: interval, isRepeat: isRepeat, isAsync: isAsync)
return taskID
}
/*
取消某个定时任务
*/
class func cancelTask(taskID: String?){
guard let taskID = taskID , taskID.count > 0 else {
return
}
semaphore_.wait()
let timer = timers_[taskID]
if let timer = timer{
timer.cancel()
timers_.removeValue(forKey: taskID)
}
semaphore_.signal()
}
}
二、介绍下内存的几大区域?
iOS的虚拟内存地址由低至高,可分为:代码段、数据段、堆、栈、内核区
,如下所示:
代码段存放:编译之后的代码
-
数据段存放:
字符串常量,例如:NSString * str = @"123"
已初始化的全局变量、已初始化的静态变量等
未初始化的全部变量、未初始化的静态变量等
栈:函数调用开销,比如局部变量,分配的内存空间地址越来越小
堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
三、讲一下你对iOS内存管理的理解
-
- iOS中使用引用计数来管理OC对象的内存,新建的
OC对象
引用计数默认是1,当引用计数减为0时,OC对象就会被销毁,其内存就会被释放掉
- iOS中使用引用计数来管理OC对象的内存,新建的
-
- 调用
retain
会让OC对象的引用计数+1,调用release
会让OC对象的引用计数-1
- 调用
-
-
MRC
时,当调用alloc、new、copy、mutableCopy
方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease
来释放它
-
-
- 可以通过一下私有函数来查看自动释放池的情况
先声明此私有函数
extern void _objc_autoreleasePoolPrint(void);
然后就可以调用了,系统会自动帮我们找到此函数的实现
_objc_autoreleasePoolPrint();
四、ARC都帮我们做了什么?
ARC是LLVM编译器和RunLoop相互协作的一个结果,帮我们自动进行内存管理(如下所示),并且处理了弱引用(对象销毁时,指向这个对象的弱引用,都会被置为nil)
- 使用
assign
修饰普通数据类型时,ARC会帮我们自动生成get、set
方法,如下所示
@property (nonatomic,assign) NSInteger age;
- (void)setAge:(NSInteger)age{
_age = age;
}
- (NSInteger)age{
return _age;
}
- 使用
retain、strong
修饰对象类型,ARC会帮我们自动生成get、set
方法,并且在set方法里帮我们retain、release
对象,如下所示:
@property (nonatomic,retain) NSObject *status;
- (void)setStatus:(NSObject *)status{
if (_status != status){
//如果新对象和旧对象不相同,就先让旧对象的引用计数-1
[_status release];
//然后把新对象的引用计数+1,在赋值给_status
_status = [status retain];
}
}
- (NSObject *)status{
return _status;
}
- 使用
copy
修饰对象类型,ARC会帮我们自动生成get、set
方法,并且在set方法里帮我们copy、release
对象,如下所示:
@property (nonatomic,copy) NSString *name;
- (void)setName:(NSString *)name{
if (_name != name){
//如果新对象和旧对象不相同,就先让旧对象的引用计数-1
[_name release];
//然后把新对象的引用计数+1,在赋值给_status
此处会使用copy产生一个不可变的对象,这就是为什么NSMutableArray等可变类型不能使用copy的根本原因!!!
_name = [name copy];
}
}
- (NSString *)name{
return _name;
}
五、思考下面两段代码能发生什么事情?有什么区别
-
- 第一段代码会崩溃报,并且报下面这个错误;第二段代码不会崩溃
Thread 3: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
-
- 因为第一段代码字符串过长,
name
存储的是真正的指针,开启多个线程对self.name
进行赋值时,本质上就是多个线程同时调用下面的代码,也就是说同一个对象可能会被release
多次,从而导致EXC_BAD_INSTRUCTION
;
- 因为第一段代码字符串过长,
- (void)setName:(NSString *)name{
if (_name != name){
[_name release];
_name = [name copy];
}
}
-
- 第二段代码不会崩溃,是因为第二段字符串比较短,苹果对这种小对象的存储,专门采用了
TaggedPoint
技术做了优化,name的指针
中存储的是具体的数据,也就是字符串abc
的ASCII码值
,只有当指针不够存储数据的时候,才会使用动态分配内存的方式存储数据。
- 第二段代码不会崩溃,是因为第二段字符串比较短,苹果对这种小对象的存储,专门采用了
-
- 优化方法:改成串行队列、加锁
- 改成串行队列
let queue = DispatchQueue(label: "串行队列")
for i in 0..<10000{
queue.async {
self.name = "ajselfjalskfja;sfja;skjf;lasjfl;a\(i)"
}
}
- 加锁,例如加上信号量,如下所示
semaphore = DispatchSemaphore(value: 1)
for i in 0..<10000{
DispatchQueue.global().async {
self.semaphore.wait()
self.name = "ajselfjalskfja;sfja;skjf;lasjfl;a\(i)"
self.semaphore.signal()
}
}
-
-
objc_msgSend
能识别Tagged Pointer
,如果发现消息接受者是Tagged Pointer
类型,就会从指针中提取数据,节省了以前调用开销,例如:NSNumber
的intValue
方法
-
-
- 如何判断一个指针是否是
Tagged Pointer
呢?在iOS平台,最高有效位是1(也就是第64bit)就是Tagged Pointer;在Mac平台,最低有效位是1,就是Tagged Pointer,如下所示:
- 如何判断一个指针是否是
判断是否为Tagged Pointer
#if TARGET_OS_OSX &&__x86_64__
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
六、weak指针的实现原理
为了处理weak指针
,Runtime
专门维护了一个hash表
,用于存储指向某个对象的所有weak指针
,这个hash表的Key
是所指对象的地址,Value
是指向这个对象的weak指针组成的数组;
当这个对象销毁时,就会从hash表中取出指向这个对象的弱引用置为nil,并且从hash表中删除
七、autorelease对象在什么时机会被调用release?
在RunLoop休眠之前或者离开的时候调用release
iOS在RunLoop
注册了两个Observer
观察者:
第1个Observer监听了
kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
-
第2个Observer
监听了
kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
监听了
kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
八、方法里有局部对象,出了方法以后会立即释放吗?
一般情况下ARC是通过release管理内存的,所以出了作用域会立即释放;
但是,如果ARC是通过autorelease管理内存的,就是在RunLoop休眠之前或者RunLoop退出的时候进行的释放