iOS底層原理(五):內(nèi)存管理

前言
我們借助幾道面試題,來探究一下iOS的內(nèi)存管理
一、使用CADisplayLink、NSTimer有什么注意點?

需要注意兩個地方:小心循環(huán)引用、不準時

1. 小心循環(huán)引用

  • (1). CADisplayLink、NSTimer會對target產(chǎn)生強引用,如果target又對它們產(chǎn)生強引用,那么就會引用循環(huán)引用,例如下面的代碼,就會產(chǎn)生循環(huán)引用
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調(diào)用次數(shù)跟屏幕刷新頻率一致,當?shù)魩瑫r,會響應(yīng)減少;CADisplayLink需要加到RunLoop中才能生效;CADisplayLink會對target產(chǎn)生強引用,所以需要通過代理類規(guī)避循環(huán)引用,代理類的方法有兩種,如下所示:

    • 第一種,通過forwardingTarget將消息轉(zhuǎn)發(fā)給targetVC控制器CADisplay對象產(chǎn)生了強引用,CADisplay對像proxy代理產(chǎn)生了強引用,proxy代理VC控制器產(chǎn)生了弱引用,無法形成閉環(huán),也就沒有循環(huán)引用的問題了
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方法簽名,將消息轉(zhuǎn)發(fā)給proxy,同樣也是proxy代理VC控制器產(chǎn)生了弱引用,以避免循環(huán)引用
@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中才能執(zhí)行,Timer.scheduledTimer()會自動加入到Runloop中,不需要我們手動加了,而Timer.init()就需要我們手動加到Runloop了;Timer也會對target產(chǎn)生強引用,所以需要通過代理類或者Block來規(guī)避,如下所示:
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的任務(wù)過于繁重,那么就會造成不準時,所以想準時的話,我們應(yīng)該使用GCD的定時器,如下所示,我們對GCD的定時器做了封裝:

class TFTimer: NSObject{
    
    static var timers_ =  Dictionary<String,DispatchSourceTimer>()
    static let semaphore_ = DispatchSemaphore(value: 1)
    /*
     描述:執(zhí)行某個定時任務(wù)
     參數(shù):task-任務(wù)閉包
          start-任務(wù)開始時間,單位是秒
          intercval-定時器間隔,單位是秒
          isReapeat-是否重復(fù)執(zhí)行任務(wù)
          isAsync-任務(wù)是否異步執(zhí)行,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
    }
    
    /*
     取消某個定時任務(wù)
     */
    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()
    }
}
二、介紹下內(nèi)存的幾大區(qū)域?

iOS的虛擬內(nèi)存地址由低至高,可分為:代碼段、數(shù)據(jù)段、堆、棧、內(nèi)核區(qū),如下所示:

iOS內(nèi)存的幾大區(qū)域

  • 代碼段存放:編譯之后的代碼

  • 數(shù)據(jù)段存放:

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

    • 已初始化的全局變量、已初始化的靜態(tài)變量

    • 未初始化的全部變量、未初始化的靜態(tài)變量

  • :函數(shù)調(diào)用開銷,比如局部變量,分配的內(nèi)存空間地址越來越小

  • :通過alloc、malloc、calloc等動態(tài)分配的空間,分配的內(nèi)存空間地址越來越大

三、講一下你對iOS內(nèi)存管理的理解
    1. iOS中使用引用計數(shù)來管理OC對象的內(nèi)存,新建的OC對象引用計數(shù)默認是1,當引用計數(shù)減為0時,OC對象就會被銷毀,其內(nèi)存就會被釋放掉
    1. 調(diào)用retain會讓OC對象的引用計數(shù)+1,調(diào)用release會讓OC對象的引用計數(shù)-1
    1. MRC時,當調(diào)用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調(diào)用release或者autorelease來釋放它
    1. 可以通過一下私有函數(shù)來查看自動釋放池的情況
先聲明此私有函數(shù)
extern void _objc_autoreleasePoolPrint(void);

然后就可以調(diào)用了,系統(tǒng)會自動幫我們找到此函數(shù)的實現(xiàn)
_objc_autoreleasePoolPrint();
四、ARC都幫我們做了什么?
ARC是LLVM編譯器和RunLoop相互協(xié)作的一個結(jié)果,幫我們自動進行內(nèi)存管理(如下所示),并且處理了弱引用(對象銷毀時,指向這個對象的弱引用,都會被置為nil)
  • 使用assign修飾普通數(shù)據(jù)類型時,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){
        //如果新對象和舊對象不相同,就先讓舊對象的引用計數(shù)-1
        [_status release];
        //然后把新對象的引用計數(shù)+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){
        //如果新對象和舊對象不相同,就先讓舊對象的引用計數(shù)-1
        [_name release];
        //然后把新對象的引用計數(shù)+1,在賦值給_status
      此處會使用copy產(chǎn)生一個不可變的對象,這就是為什么NSMutableArray等可變類型不能使用copy的根本原因?。。?        _name = [name copy];
    }
}

- (NSString *)name{
    return _name;
}
五、思考下面兩段代碼能發(fā)生什么事情?有什么區(qū)別
    1. 第一段代碼會崩潰報,并且報下面這個錯誤;第二段代碼不會崩潰
Thread 3: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    1. 因為第一段代碼字符串過長,name存儲的是真正的指針,開啟多個線程對self.name進行賦值時,本質(zhì)上就是多個線程同時調(diào)用下面的代碼,也就是說同一個對象可能會被release多次,從而導(dǎo)致EXC_BAD_INSTRUCTION;
- (void)setName:(NSString *)name{
    if (_name != name){
        [_name release];
        _name = [name copy];
    }
}
    1. 第二段代碼不會崩潰,是因為第二段字符串比較短,蘋果對這種小對象的存儲,專門采用了TaggedPoint技術(shù)做了優(yōu)化,name的指針中存儲的是具體的數(shù)據(jù),也就是字符串a(chǎn)bcASCII碼值,只有當指針不夠存儲數(shù)據(jù)的時候,才會使用動態(tài)分配內(nèi)存的方式存儲數(shù)據(jù)。
    1. 優(yōu)化方法:改成串行隊列、加鎖
    • 改成串行隊列
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,如果發(fā)現(xiàn)消息接受者是Tagged Pointer類型,就會從指針中提取數(shù)據(jù),節(jié)省了以前調(diào)用開銷,例如: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指針的實現(xiàn)原理

為了處理weak指針,Runtime專門維護了一個hash表用于存儲指向某個對象的所有weak指針,這個hash表的Key是所指對象的地址,Value是指向這個對象的weak指針組成的數(shù)組;

當這個對象銷毀時,就會從hash表中取出指向這個對象的弱引用置為nil,并且從hash表中刪除

七、autorelease對象在什么時機會被調(diào)用release?

在RunLoop休眠之前或者離開的時候調(diào)用release

iOS在RunLoop注冊了兩個Observer觀察者:

  • 第1個Observer監(jiān)聽了kCFRunLoopEntry事件,會調(diào)用objc_autoreleasePoolPush()

  • 第2個Observer

    • 監(jiān)聽了kCFRunLoopBeforeWaiting事件,會調(diào)用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()

    • 監(jiān)聽了kCFRunLoopBeforeExit事件,會調(diào)用objc_autoreleasePoolPop()

八、方法里有局部對象,出了方法以后會立即釋放嗎?

一般情況下ARC是通過release管理內(nèi)存的,所以出了作用域會立即釋放;

但是,如果ARC是通過autorelease管理內(nèi)存的,就是在RunLoop休眠之前或者RunLoop退出的時候進行的釋放

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容