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

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

需要注意兩個地方:小心循環(huán)引用乐导、不準(zhǔ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ù)跟屏幕刷新頻率一致棵磷,當(dāng)?shù)魩瑫r账阻,會響應(yīng)減少;CADisplayLink需要加到RunLoop中才能生效泽本;CADisplayLink會對target產(chǎn)生強引用淘太,所以需要通過代理類規(guī)避循環(huán)引用,代理類的方法有兩種,如下所示:

    • 第一種蒲牧,通過forwardingTarget將消息轉(zhuǎn)發(fā)給target撇贺,VC控制器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. 小心不準(zhǔn)時

CADisplayLinkTimer都需要依賴RunLoop,如果RunLoop的任務(wù)過于繁重续搀,那么就會造成不準(zhǔn)時塞琼,所以想準(zhǔn)時的話,我們應(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ù)默認(rèn)是1司浪,當(dāng)引用計數(shù)減為0時泊业,OC對象就會被銷毀,其內(nèi)存就會被釋放掉
    1. 調(diào)用retain會讓OC對象的引用計數(shù)+1啊易,調(diào)用release會讓OC對象的引用計數(shù)-1
    1. MRC時吁伺,當(dāng)調(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的根本原因!1薄蒸播!
        _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碼值,只有當(dāng)指針不夠存儲數(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ù)組;

當(dāng)這個對象銷毀時犬绒,就會從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)系作者
  • 序言:七十年代末血久,一起剝皮案震驚了整個濱河市突照,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌氧吐,老刑警劉巖讹蘑,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異筑舅,居然都是意外死亡座慰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門翠拣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來版仔,“玉大人,你說我怎么就攤上這事误墓÷福” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵谜慌,是天一觀的道長然想。 經(jīng)常有香客問我,道長欣范,這世上最難降的妖魔是什么变泄? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任令哟,我火速辦了婚禮,結(jié)果婚禮上妨蛹,老公的妹妹穿的比我還像新娘屏富。我一直安慰自己,他們只是感情好蛙卤,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布狠半。 她就那樣靜靜地躺著,像睡著了一般表窘。 火紅的嫁衣襯著肌膚如雪典予。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天乐严,我揣著相機與錄音瘤袖,去河邊找鬼。 笑死昂验,一個胖子當(dāng)著我的面吹牛捂敌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播既琴,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼占婉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了甫恩?” 一聲冷哼從身側(cè)響起逆济,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎磺箕,沒想到半個月后奖慌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡松靡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年简僧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雕欺。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡岛马,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屠列,到底是詐尸還是另有隱情啦逆,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布笛洛,位于F島的核電站夏志,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏撞蜂。R本人自食惡果不足惜盲镶,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蝌诡。 院中可真熱鬧溉贿,春花似錦、人聲如沸浦旱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颁湖。三九已至宣蠕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間甥捺,已是汗流浹背抢蚀。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镰禾,地道東北人皿曲。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像吴侦,于是被迫代替她去往敵國和親屋休。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350