內存管理

使用CADisplayLink顽耳、NSTimer有什么注意點坠敷?

CADisplayLink妙同、NSTimer會對target產(chǎn)生強引用 如果target又對他們產(chǎn)生強引用 那么就會引發(fā)循環(huán)引用

介紹下內存的幾大區(qū)域

代碼段      - 編譯之后的代碼
數(shù)據(jù)段      - 已初始化的數(shù)據(jù) (已初始化的全局變量、靜態(tài)變量等)
           - 為初始化的數(shù)據(jù) (未初始化的全局變量膝迎、靜態(tài)變量等)
棧         - 函數(shù)調用開銷粥帚,比如局部變量。分配的內存空間地址越來越小
堆         - 通過alloc限次、malloc芒涡、calloc等動態(tài)分配的空間,分配的內存空間地址越來越大

講一下你對 iOS 內存管理的理解

ARC 都幫我們做了什么卖漫?

LLVM + Runtime 相互協(xié)作的結果
- ARC利用LLVM編譯器自動幫我們生成release retain autorelease 
- 弱引用利用Runtime 在程序運行時 進行操作

weak指針的實現(xiàn)原理

將弱引用存在哈希表里面费尽,如果對象要銷毀 就取出當前對象所對應的弱引用表 把弱引用表里面存儲的弱引用都清除

autorelease對象在什么時機會被調用release

autorelease對象什么時候調用release 由RunLoop來控制
它可能是在某次RunLoop循環(huán)中 RunLoop休眠之前調用了release

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

會馬上釋放
使用ARC 如果編譯器加入的是autorelease 那么它可能是在某次RunLoop循環(huán)中 RunLoop休眠之前調用了release
如果是加入的是relese 那么出了方法后會立即釋放

思考以下2段代碼能發(fā)生什么事羊始?有什么區(qū)別旱幼?

圖1.png

圖2.png
圖1 崩潰 因為set方法的本質是釋放舊值 存儲新值 (有可能是多條線程同時釋放舊值  報壞內存訪問)
解決方案 1.設置name為atomic 2.在self.name設置加鎖解鎖
圖2 name是Tagger Pointer 不是指針

CADisplayLink、NSTimer使用注意

CADisplayLink突委、NSTimer會對target產(chǎn)生強引用 如果target又對他們產(chǎn)生強引用 那么就會引發(fā)循環(huán)引用
    //保證調用頻率和平面的刷幀頻率一致 60FPS
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
解決NSTimer的循環(huán)引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
會產(chǎn)生循環(huán)引用

把弱引用的對象傳入到target也無法解決循環(huán)引用的原因是 NSTimer內部對target進行了強引用 無關是傳入的是強引用對象還是弱引用對象

解決方案一
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
解決方案二
創(chuàng)建一個中間對象 弱引用target
@interface LMProxy : NSObject
+(instancetype) proxyWithTarget:(id)target;
@property (weak,nonatomic) id target;
@end

#import "LMProxy.h"

@implementation LMProxy

+(instancetype) proxyWithTarget:(id)target{
    
    LMProxy *proxy = [[LMProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}

ViewController.m
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LMProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
解決方案三

NSProxy 沒有init方法 直接alloc就行
NSProxy 跟 NSObject 都是基類 專門用于消息轉發(fā) (如果想直接消息轉發(fā) 直接使用NSProxy 這樣效率更高)
如果繼承于NSProxy 大部分的方法就會自動進行消息轉發(fā)

@interface LMProxy : NSProxy
+(instancetype) proxyWithTarget:(id)target;
@property (weak,nonatomic) id target;
@end

@implementation LMProxy

+(instancetype) proxyWithTarget:(id)target{
    
    LMProxy *proxy = [LMProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
     return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
解決CADisplayLink的循環(huán)引用

創(chuàng)建中間對象的方式解決

GCD定時器

RunLoop每跑一圈 他的時間是不固定的
NSTimer依賴于RunLoop柏卤,如果RunLoop的任務過于繁重,可能會導致NSTimer不準時

self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//幾秒之后開始執(zhí)行
NSTimeInterval start = 2.0;
//時間間隔
NSTimeInterval interval = 1.0;
    
dispatch_source_set_timer(self.timer , dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0);
dispatch_source_set_event_handler(self.timer, ^{
        
});
dispatch_resume(self.timer);
封裝GCD定時器
@interface MJTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async;

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async;

//取消任務
+ (void)cancelTask:(NSString *)name;
@end

@implementation MJTimer

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore;
+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       timers_ = [NSMutableDictionary dictionary];
       semaphore = dispatch_semaphore_create(1);
    });
}

+ (NSString *)execTask:(void(^)(void))task
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async{
    
    if (!task || start < 0 || (interval <= 0 && repeates)) return nil;
    
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0);
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    //定時器唯一表示
    NSString *name = [NSString stringWithFormat:@"%lu",(unsigned long)timers_.count];
    //存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore);
    //設置回調
    dispatch_source_set_event_handler(timer, ^{
        if (!repeates) {
            //如果非重復 取消定時器
            [self cancelTask:name];
        }
    });
    //啟動計時器
    dispatch_resume(timer);
    return name;
}

+ (void)cancelTask:(NSString *)name{
    if (name.length == 0) return;
    dispatch_source_t timer = timers_[name];
    if (!timer) return;
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_source_cancel(timer);
    [timers_ removeObjectForKey:name];
    dispatch_semaphore_signal(semaphore);
}

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async{
    
    if (!target || start < 0 || (interval <= 0 && repeates)) return nil;
    
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
         [target performSelector:selector];
#pragma clang diagnostic pop
        }
    } start:start interval:interval repeates:repeates async:async];
}

@end

iOS程序的內存布局

內存布局.png
代碼段      - 編譯之后的代碼
數(shù)據(jù)段      - 已初始化的數(shù)據(jù) (已初始化的全局變量鸯两、靜態(tài)變量等)
           - 為初始化的數(shù)據(jù) (未初始化的全局變量闷旧、靜態(tài)變量等)
棧         - 函數(shù)調用開銷,比如局部變量钧唐。分配的內存空間地址越來越小
堆         - 通過alloc忙灼、malloc、calloc等動態(tài)分配的空間钝侠,分配的內存空間地址越來越大

Tagged Pointer

節(jié)省內存空間
iOS平臺该园,最高有效位是1(第64bit) 或者 Mac平臺,最低有效位是1 就是Tagged Pointer
當指針不夠存儲數(shù)據(jù)時帅韧,才會使用動態(tài)分配內存的方式來存儲數(shù)據(jù)
objc_msgSend能識別Tagged Pointer里初,比如NSNumber的intValue方法,直接從指針提取數(shù)據(jù)忽舟,節(jié)省了以前的調用開銷

OC對象的內存管理

在iOS中 使用引用計數(shù)來管理OC對象的內存

- 一個新創(chuàng)建的OC對象的引用計數(shù)默認是1 當引用計數(shù)減為0 OC對象就會銷毀双妨,釋放其占用的內存空間
- 調用retain會讓OC對象的引用計數(shù)+1,調用release會讓OC對象的引用計數(shù)-1
copy 和 mutableCopy
copy 和 mutableCopy.png
拷貝的目的
產(chǎn)生一個副本對象叮阅,跟源對象互不影響
修改了源對象刁品,不會影響副本對象
修改了副本對象,不會影響源對象
copy         不可變拷貝浩姥,產(chǎn)生不可變副本
mutableCopy  可變拷貝挑随,產(chǎn)生可變副本 
淺拷貝   指針拷貝  沒有產(chǎn)生新對象
深拷貝   內容拷貝  有產(chǎn)生新對象
內存管理的經(jīng)驗總結
- 當調用alloc new copy mutbaleCopy 方法返回了一個對象 在不需要這個對象時 要調用release或者autorelease來釋放它
- 想擁有某個對象 就讓它的引用計數(shù)+1 不想再擁有某個對象 就讓它的引用計數(shù)-1

引用計數(shù)的存儲

在64bit中 引用計數(shù)可以直接存儲在優(yōu)化過的isa指針中 如果不夠存儲 就會存儲在SideTable中 SideTale中有個
RefcountMap的散列表
SideTable結構.png

refcnts是一個存放著對象引用計數(shù)的散列表

自動釋放池

  struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 構造函數(shù),在創(chuàng)建結構體的時候調用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析構函數(shù)勒叠,在結構體銷毀的時候調用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };
自動釋放池的主要底層數(shù)據(jù)結構是:__AtAutoreleasePool兜挨、AutoreleasePoolPage
調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
AutoreleasePoolPage的結構
AutoreleasePoolPage的結構.png
AutoreleasePoolPage對象占用4096字節(jié)內存 除了用來存放他的成員變量  剩下的空間用來存放autorelease對象的地址

所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起
程序運行過程中 存在著多個AutoreleasePoolPage對象

image.png
child 存放著下一個AutoreleasePoolPage對象的地址(如果是最后一個 那么地址為空)
parent  存放著上一個AutoreleasePoolPage對象的地址 (如果是第一個 那么地址為空)
AutoreleasePoolPage的調用
1.調用push會將一個POOL_BOUNDARY入棧 并返回其存放的內存地址
2.調用pop方法時傳入一個POOL_BOUNDARY的內存地址 會從最后一個入棧的對象開始發(fā)送release消息 直到遇到
POOL_BOUNDARY

id *next指向了下一個能存放autorelease對象地址的區(qū)域
可以通過以下私有函數(shù)來查看自動釋放池的情況 extern void _objc_autoreleasePoolPrint(void);

RunLoop和Autorelease

iOS在主線程的RunLoop注冊了2個Observer
- 第一個Observer監(jiān)聽了kCFRunLoopEntry事件 會調用objc_autoreleasePoolPush()
- 第二個Observer監(jiān)聽了
  a.監(jiān)聽了kCFRunLoopBeforeWaiting事件 會調用objc_autoreleasePoolPop() objc_autoreleasePoolPush()
  b.監(jiān)聽了kCFRunLoopBeforeExit事件  會調用objc_autoreleasePoolPop() 
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末膏孟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拌汇,更是在濱河造成了極大的恐慌柒桑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件噪舀,死亡現(xiàn)場離奇詭異幕垦,居然都是意外死亡,警方通過查閱死者的電腦和手機傅联,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門先改,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蒸走,你說我怎么就攤上這事仇奶。” “怎么了比驻?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵该溯,是天一觀的道長。 經(jīng)常有香客問我别惦,道長狈茉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任掸掸,我火速辦了婚禮氯庆,結果婚禮上,老公的妹妹穿的比我還像新娘扰付。我一直安慰自己堤撵,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布羽莺。 她就那樣靜靜地躺著实昨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盐固。 梳的紋絲不亂的頭發(fā)上荒给,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音刁卜,去河邊找鬼志电。 笑死,一個胖子當著我的面吹牛长酗,可吹牛的內容都是我干的溪北。 我是一名探鬼主播桐绒,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼夺脾,長吁一口氣:“原來是場噩夢啊……” “哼之拨!你這毒婦竟也來了?” 一聲冷哼從身側響起咧叭,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蚀乔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后菲茬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吉挣,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年婉弹,在試婚紗的時候發(fā)現(xiàn)自己被綠了睬魂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡镀赌,死狀恐怖氯哮,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情商佛,我是刑警寧澤喉钢,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站良姆,受9級特大地震影響肠虽,放射性物質發(fā)生泄漏。R本人自食惡果不足惜玛追,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一税课、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痊剖,春花似錦伯复、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氮惯,卻和暖如春叮雳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妇汗。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工帘不, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杨箭。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓寞焙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子捣郊,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內容