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

CADisplayLink娩井、NSTimer使用注意點:

1.CADisplayLink、NSTimer會對target產(chǎn)生強(qiáng)引用,如果target又對他們產(chǎn)生強(qiáng)引用,就會產(chǎn)生循環(huán)引用
2.這兩個定時器存在不準(zhǔn)時的可能性

  • 解決循環(huán)引用的問題

解決方案1:消息轉(zhuǎn)發(fā)機(jī)制+中間對象進(jìn)行處理

//MXTestProxy:NSObject
+ (instancetype)proxyWithTarget:(id)target {
    MXTestProxy *pro = [[MXTestProxy alloc]init];
    pro.target = target;
    return pro;
    
}

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


//CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:[MXTestProxy proxyWithTarget:self] selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

//注:CADisplayLink無需設(shè)置時間,因為其保證調(diào)用頻率和屏幕的刷幀頻率一致,60FTP(1秒調(diào)用60次).

由此,可以使用系統(tǒng)內(nèi)部的NSProxy類,NSProxy本來就是用于設(shè)計消息轉(zhuǎn)發(fā)的

//MXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target {
    
    //NSProxy對象不需要調(diào)用init,因為它根本沒有init方法,只需要調(diào)用alloc即可
    MXProxy *proxy = [MXProxy alloc];
    proxy.target = target;
    return proxy;
}

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

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

//CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:[MXProxy proxyWithTarget:self] selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
  • 繼承自NSObject,方法調(diào)用流程:即原先消息機(jī)制的3個部分
  • 繼承自NSProxy,直接就進(jìn)入消息轉(zhuǎn)發(fā),但其并沒有- (id)forwardingTargetForSelector:(SEL)aSelector方法
MXProxy *pro = [MXProxy proxyWithTarget:self];
NSLog(@"%d",[pro isKindOfClass:[UIViewController class]]);
//該運行結(jié)果為1

以上結(jié)果為1,是因為若是繼承自NSProxy的對象調(diào)用對應(yīng)的NSObject的方法,由于其內(nèi)部就是消息轉(zhuǎn)發(fā),故會令消息轉(zhuǎn)發(fā)者發(fā)送對應(yīng)消息,即假設(shè)proxy是繼承自NSProxy,則[proxy isKindOfClass]方法的實際調(diào)用者還是消息轉(zhuǎn)發(fā)者,而不是proxy

解決方案2:定義NSTimer使用block方法創(chuàng)建

__weak typeof(self)weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    [weakSelf test];
}];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes];

若向原先的方式使用weakSelf,即往target中傳入weakSelf會失敗,因為這個是對block才有效,因為block特性是若外部變量使用的是弱指針進(jìn)行引用,則block會對該變量有一個弱引用,同理,若是強(qiáng)指針進(jìn)行引用,則block會對該變量有一個強(qiáng)引用.故傳入弱指針解決循環(huán)引用只對block有效,定時器中傳入的target,由于外部只是將參數(shù)地址傳入,后賦值給timer內(nèi)部對應(yīng)的成員變量,故傳入的是強(qiáng)指針還是弱指針是沒有效果的

  • 解決定時器不準(zhǔn)的問題

原因:CADisplayLink和NSTimer底層都是由runloop實現(xiàn)的,是依賴于runloop的,如果runloop的任務(wù)過于繁重,可能導(dǎo)致這兩個定時器不準(zhǔn)時

即假設(shè)定時器設(shè)置每隔1s調(diào)用一次方法,則runloop會每跑一次圈,就計算下時間,若沒達(dá)到1s,則繼續(xù)跑圈,當(dāng)達(dá)到1s,就處理定時器任務(wù)但runloop的跑圈時間是不固定的,故會導(dǎo)致定時器時間不準(zhǔn)時

由于GCD的定時器是直接與系統(tǒng)內(nèi)核掛鉤的,與runloop無關(guān),故無論外部的runloop發(fā)生怎樣的操作,都不會影響GCD定時器的運行

dispatch_queue_t queue = 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_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//第二個參數(shù):從什么時候開始,若需要延遲執(zhí)行,則傳入dispatch_time(DISPATCH_TIME_NOW, 延遲的秒數(shù) * NSEC_PER_SEC)
//第三個參數(shù):每個幾秒執(zhí)行
//第四個參數(shù):傳入0即可

dispatch_source_set_event_handler(timer, ^{
    NSLog(@"123");
});
dispatch_resume(timer);

注:GCD創(chuàng)建的對象在ARC環(huán)境中都不需要我們?nèi)ス芾韮?nèi)存

內(nèi)存布局:

Snip20180713_2.png
  • 堆區(qū)的地址是從小到大分配的,且內(nèi)存地址(十六進(jìn)制)的最低位一定是0.因為內(nèi)存對齊(最小單位為16)
  • 棧區(qū)的地址是從大到小分配的

Tagged Pointer:

  • 從64bit開始,iOS引入了tagged pointer技術(shù),用于優(yōu)化NSNumber、NSDate、NSString等小對象的存儲
  • 在沒有使用tagged pointer之前,NSNumber等對象需要動態(tài)分配內(nèi)存,維護(hù)引用計數(shù)等,NSNumber指針存儲的是堆中NSNumber對象的地址值
  • 使用tagged pointer之后,NSNumber指針里存儲的數(shù)據(jù)變成了Tag+ Data,也就是將數(shù)據(jù)存儲在了指針中
  • 當(dāng)指針不夠存儲數(shù)據(jù)時,才會使用動態(tài)分配內(nèi)存的方式來存儲數(shù)據(jù)
  • objc_msgSend能識別tagged pointer,直接從指針提取數(shù)據(jù),節(jié)省了以前的調(diào)用開銷

一道面試題:

@property (strong, nonatomic) NSString *name;

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

for (int i = 0; i<100; i++) {
    dispatch_async(queue, ^{
        self.name = [NSString stringWithFormat:@"123hjgjhgjhgjg" ];
    });
}
//運行結(jié)果崩潰,會報壞內(nèi)存訪問

原因:for循環(huán)中實際是頻繁調(diào)用setter方法,而ARC環(huán)境中的setter方法實際會轉(zhuǎn)換為MRC中對應(yīng)的代碼內(nèi)容

- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        [name retain];
    }
}

當(dāng)其他線程同時執(zhí)行setter方法時,可能存在當(dāng)name屬性已經(jīng)release,但其他線程繼續(xù)調(diào)用release,導(dǎo)致其壞內(nèi)存訪問

其中,如上的代碼策略對應(yīng)的不同,即strong對應(yīng)retain,copy對應(yīng)copy

但上述面試題中,若self.name的字符串為tagged pointer時.不會報錯,是因為tagged pointer本身就不是OC對象,是指針的賦值,不存在調(diào)用setter和getter進(jìn)行賦值

  • 如何判斷一個指針是否為Tagged Pointer倔幼?
    iOS平臺霹粥,最高有效位是1(第64bit),其中需要保證其有64位,即0x后面的數(shù)字須有16位
    Mac平臺洞渔,最低有效位是1

MRC:

  • 在iOS中漂羊,使用引用計數(shù)來管理OC對象的內(nèi)存
  • 一個新創(chuàng)建的OC對象引用計數(shù)默認(rèn)是1,當(dāng)引用計數(shù)減為0阶女,OC對象就會銷毀颊糜,釋放其占用的內(nèi)存空間
  • 調(diào)用retain會讓OC對象的引用計數(shù)+1,調(diào)用release會讓OC對象的引用計數(shù)-1

在進(jìn)行setter操作時,會先進(jìn)行判斷是否為同一對象,若為同一對象,則不會做任何事情,若為不同對象,則需要先釋放之前的對象,在retain新的對象

  • 若是使用retain修飾,則setter會執(zhí)行上面的代碼
  • 若是使用assign修飾,則setter只會進(jìn)行單純的賦值操作
//MRC中setter方法寫法
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        [name retain];
    }
}

//dealloc方法
- (void)dealloc {
    [_name release];
    _name = nil;
    
    //也可以用下面這一句代替上面兩句,這兩句是等價的
    //self.name = nil;
    
    //在dealloc方法中,父類的dealloc方法放在最后執(zhí)行
    [super dealloc]
}
//在換屬性時/自己掛掉時,要記得release操作

在MRC中,若使用retain關(guān)鍵詞,系統(tǒng)會自動生成上述的setter和getter,但dealloc中還是需要自己完成,即釋放還是需要自己去完成

內(nèi)存管理的經(jīng)驗總結(jié):

  • 當(dāng)調(diào)用alloc秃踩、new衬鱼、copy、mutableCopy方法返回了一個對象憔杨,在不需要這個對象時鸟赫,要調(diào)用release或者autorelease來釋放它
  • 想擁有某個對象,就讓它的引用計數(shù)+1;不想再擁有某個對象抛蚤,就讓它的引用計數(shù)-1
  • 通過類方法創(chuàng)建的對象,在系統(tǒng)內(nèi)部已經(jīng)自動幫忙調(diào)用release,即除了alloc台谢、copy、new方法創(chuàng)建對象,需要調(diào)用release,其余是不需要release的

使用MRC進(jìn)行開發(fā)

self.dataArr =  [[[NSMutableArray alloc]init]autorelease];
//含義是:NSMutableArray *dataArr = [[NSMutableArray alloc]init];
self.dataArr = dataArr;
[dataArr release];
故在dealloc方法中,還需要調(diào)用self.dataArr = nil;

Copy:

  • 拷貝的目的:產(chǎn)生一個副本對象,跟原對象互不影響
    修改了原對象不會影響副本對象
    修改了副本對象,不會影響原對象

  • iOS提供了兩個拷貝方法:
    1.copy:不可變拷貝,產(chǎn)生不可變副本
    2.mutableCopy:可變拷貝,產(chǎn)生可變副本

若兩個對象(str1和str2)都為不可變字符串,即NSString,若str1 = [str2 copy],會發(fā)現(xiàn)str1和str2的內(nèi)存地址是相同的
原因:由于拷貝的目的是,產(chǎn)生一個副本對象,跟原對象互不影響
且str1是一個不可變字符串,本身就沒法修改內(nèi)容,故可以直接令拷貝出來的字符串對象也指向原先相同的字符串對象

  • 深拷貝和淺拷貝:
    1.深拷貝:內(nèi)容拷貝,產(chǎn)生新的對象
    2,淺拷貝:指針拷貝,沒有產(chǎn)生新的對象(拷貝的內(nèi)容沒有拷貝)
Snip20180713_3.png

注意點:
1.當(dāng)策略寫的是copy,屬性不要寫不可變類型
基本上有關(guān)文字的,使用copy修飾,對于字典和數(shù)組,還是使用strong來的多
2.屬性的修飾也一定是copy,不存在mutableCopy,因為mutableCopy只存在于NSString等foundation框架的部分類
3.自定義對象只需管好copy即可
4.自定義類需要實現(xiàn)copy操作,需要手動實現(xiàn)copyWithZone方法

引用計數(shù)的存儲:

在64位系統(tǒng)中,引用計數(shù)可以直接存儲在優(yōu)化過的isa指針中,若引用計數(shù)過大,則isa中has_sidetable_rc的值為1,并且引用計數(shù)會存儲在Side Table類中

//Side Table結(jié)構(gòu)體定義:
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; //是一個存放對象引用計數(shù)的散列表
    weak_table_t weak_table;
}

__strong: 強(qiáng)引用指針
__weak: 弱指針,當(dāng)所指內(nèi)容不存在時,指針會自動變?yōu)閚il
__unsafe_unretained :也不會產(chǎn)生強(qiáng)引用,但當(dāng)指針?biāo)傅膬?nèi)容不存在時,會報野指針錯誤

幾道面試題:

1.weak指針的實現(xiàn)原理:
將弱引用存儲到一張哈希表中,對象要銷毀時,會取出當(dāng)前對象對應(yīng)的弱引用表,把弱引用表中的內(nèi)容給清除掉(runtime)

2.ARC幫助我們做了什么?
ARC即LLVM+runtime的結(jié)果
ARC通過LLVM編譯器,自動生成retain,release,autorelease代碼,弱引用這樣的存在,是通過runtime在對象銷毀時,自動將弱引用清空掉

自動釋放池:

使用release方法,會導(dǎo)致在release代碼后,若繼續(xù)使用被銷毀的對象,則會報壞內(nèi)存訪問錯誤,若使用autorelease,則無需關(guān)心這個問題

從源碼可以看出,@autoreleasepool通過轉(zhuǎn)換為c++代碼,即開頭是一個構(gòu)造函數(shù):objc_autoreleasePoolPush()函數(shù),結(jié)尾是一個析構(gòu)函數(shù):
objc_autoreleasePoolPop()函數(shù)

自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool岁经、AutoreleasePoolPage
__AtAutoreleasePool是一個結(jié)構(gòu)體,內(nèi)部包含了構(gòu)造函數(shù):objc_autoreleasePoolPush()和析構(gòu)函數(shù):
objc_autoreleasePoolPop(),而push和pop兩個函數(shù)都是與AutoreleasePoolPage相關(guān)

調(diào)用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
1)每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存朋沮,除了用來存放它內(nèi)部的成員變量,剩下的空間用來存放autorelease對象(即調(diào)用autorelease方法的對象)的地址
2)所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起

Snip20180713_4.png
  • 其中begin()中即為起始指針的地址(0X1000)加上指針自身大小(56字節(jié))
    end()即為起始指針的地址(0X1000)加上AutoreleasePoolPage(4096字節(jié))大小
  • begin和end之間才是存放autorelease對象的,其余是存放AutoreleasePoolPage原先的一些成員變量的
  • 調(diào)用push方法會將一個POOL_BOUNDARY入棧缀壤,并且返回其存放的內(nèi)存地址
  • 調(diào)用pop方法時傳入一個POOL_BOUNDARY的內(nèi)存地址樊拓,會從最后一個入棧的對象開始發(fā)送release消息,直到遇到這個POOL_BOUNDARY
  • id *next指向了下一個能存放autorelease對象地址的區(qū)域
  • 可以通過以下私有函數(shù)來查看自動釋放池的情況:extern void _objc_autoreleasePoolPrint(void);
  • autorelease對象在什么時機(jī)調(diào)用release?
    iOS在主線程的Runloop中注冊了2個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()

  • 方法里有局部對象,出了方法后會立即釋放嗎?
    一般情況下,局部變量在方法結(jié)束后就會被立即釋放
    但當(dāng)局部變量有調(diào)用autorelease時,由于系統(tǒng)會在runloop的observer監(jiān)聽到runloop準(zhǔn)備休眠時自動調(diào)用自動釋放池的pop和push方法,若多個方法在同一個runloop運行循環(huán)中時,會待runloop進(jìn)入下一個循環(huán)(休眠)時進(jìn)行對autorelease的操作,有可能導(dǎo)致需等待多個方法結(jié)束后才會被釋放

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末条篷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蛤织,更是在濱河造成了極大的恐慌赴叹,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件指蚜,死亡現(xiàn)場離奇詭異稚瘾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)姚炕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門摊欠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柱宦,你說我怎么就攤上這事些椒。” “怎么了掸刊?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵免糕,是天一觀的道長。 經(jīng)常有香客問我忧侧,道長石窑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任蚓炬,我火速辦了婚禮松逊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肯夏。我一直安慰自己经宏,他們只是感情好犀暑,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著烁兰,像睡著了一般耐亏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上沪斟,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天广辰,我揣著相機(jī)與錄音,去河邊找鬼主之。 笑死轨域,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杀餐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼朱巨,長吁一口氣:“原來是場噩夢啊……” “哼史翘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冀续,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤琼讽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后洪唐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钻蹬,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年凭需,在試婚紗的時候發(fā)現(xiàn)自己被綠了问欠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡粒蜈,死狀恐怖顺献,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情枯怖,我是刑警寧澤注整,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站度硝,受9級特大地震影響肿轨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蕊程,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一椒袍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧藻茂,春花似錦槐沼、人聲如沸曙蒸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纽窟。三九已至,卻和暖如春兼吓,著一層夾襖步出監(jiān)牢的瞬間臂港,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工视搏, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留审孽,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓浑娜,卻偏偏與公主長得像佑力,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子筋遭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,394評論 8 265
  • 面向?qū)ο蟮娜筇匦裕悍庋b打颤、繼承、多態(tài) OC內(nèi)存管理 _strong 引用計數(shù)器來控制對象的生命周期漓滔。 _weak...
    運氣不夠技術(shù)湊閱讀 1,106評論 0 10
  • 從上圖可以看到编饺,棧里面存放的是值類型,堆里面存放的是對象類型响驴。對象的引用計數(shù)是在堆內(nèi)存中操作的透且。下面我們講講堆和棧...
    jackyshan閱讀 1,654評論 2 11
  • 37.cocoa內(nèi)存管理規(guī)則 1)當(dāng)你使用new,alloc或copy方法創(chuàng)建一個對象時豁鲤,該對象的保留計數(shù)器值為1...
    如風(fēng)家的秘密閱讀 848評論 0 4
  • 內(nèi)存管理ARC處理原理ARC是Objective-C編譯器的特性秽誊,而不是運行時特性或者垃圾回收機(jī)制,ARC所做的只...
    陽明先生_X自主閱讀 339評論 0 3