iOS底层原理(五):内存管理

前言
我们借助几道面试题,来探究一下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将消息转发给targetVC控制器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. 小心不准时

CADisplayLinkTimer都需要依赖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的虚拟内存地址由低至高,可分为:代码段、数据段、堆、栈、内核区,如下所示:

iOS内存的几大区域

  • 代码段存放:编译之后的代码

  • 数据段存放:

    • 字符串常量,例如:NSString * str = @"123"

    • 已初始化的全局变量、已初始化的静态变量

    • 未初始化的全部变量、未初始化的静态变量

  • :函数调用开销,比如局部变量,分配的内存空间地址越来越小

  • :通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

三、讲一下你对iOS内存管理的理解
    1. iOS中使用引用计数来管理OC对象的内存,新建的OC对象引用计数默认是1,当引用计数减为0时,OC对象就会被销毁,其内存就会被释放掉
    1. 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
    1. MRC时,当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    1. 可以通过一下私有函数来查看自动释放池的情况
先声明此私有函数
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;
}
五、思考下面两段代码能发生什么事情?有什么区别
    1. 第一段代码会崩溃报,并且报下面这个错误;第二段代码不会崩溃
Thread 3: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    1. 因为第一段代码字符串过长,name存储的是真正的指针,开启多个线程对self.name进行赋值时,本质上就是多个线程同时调用下面的代码,也就是说同一个对象可能会被release多次,从而导致EXC_BAD_INSTRUCTION
- (void)setName:(NSString *)name{
    if (_name != name){
        [_name release];
        _name = [name copy];
    }
}
    1. 第二段代码不会崩溃,是因为第二段字符串比较短,苹果对这种小对象的存储,专门采用了TaggedPoint技术做了优化,name的指针中存储的是具体的数据,也就是字符串abcASCII码值,只有当指针不够存储数据的时候,才会使用动态分配内存的方式存储数据。
    1. 优化方法:改成串行队列、加锁
    • 改成串行队列
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()
     }
}
    1. objc_msgSend能识别Tagged Pointer,如果发现消息接受者是Tagged Pointer类型,就会从指针中提取数据,节省了以前调用开销,例如:NSNumberintValue方法
    1. 如何判断一个指针是否是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退出的时候进行的释放

你可能感兴趣的:(iOS底层原理(五):内存管理)