iOS底層原理-內存管理

CADisplayLink、NSTimer使用注意點:

1.CADisplayLink、NSTimer會對target產生強引用,如果target又對他們產生強引用,就會產生循環引用
2.這兩個定時器存在不準時的可能性

  • 解決循環引用的問題

解決方案1:消息轉發機制+中間對象進行處理

//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無需設置時間,因為其保證調用頻率和屏幕的刷幀頻率一致,60FTP(1秒調用60次).

由此,可以使用系統內部的NSProxy類,NSProxy本來就是用于設計消息轉發的

//MXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target {
    
    //NSProxy對象不需要調用init,因為它根本沒有init方法,只需要調用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,方法調用流程:即原先消息機制的3個部分
  • 繼承自NSProxy,直接就進入消息轉發,但其并沒有- (id)forwardingTargetForSelector:(SEL)aSelector方法
MXProxy *pro = [MXProxy proxyWithTarget:self];
NSLog(@"%d",[pro isKindOfClass:[UIViewController class]]);
//該運行結果為1

以上結果為1,是因為若是繼承自NSProxy的對象調用對應的NSObject的方法,由于其內部就是消息轉發,故會令消息轉發者發送對應消息,即假設proxy是繼承自NSProxy,則[proxy isKindOfClass]方法的實際調用者還是消息轉發者,而不是proxy

解決方案2:定義NSTimer使用block方法創建

__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特性是若外部變量使用的是弱指針進行引用,則block會對該變量有一個弱引用,同理,若是強指針進行引用,則block會對該變量有一個強引用.故傳入弱指針解決循環引用只對block有效,定時器中傳入的target,由于外部只是將參數地址傳入,后賦值給timer內部對應的成員變量,故傳入的是強指針還是弱指針是沒有效果的

  • 解決定時器不準的問題

原因:CADisplayLink和NSTimer底層都是由runloop實現的,是依賴于runloop的,如果runloop的任務過于繁重,可能導致這兩個定時器不準時

即假設定時器設置每隔1s調用一次方法,則runloop會每跑一次圈,就計算下時間,若沒達到1s,則繼續跑圈,當達到1s,就處理定時器任務但runloop的跑圈時間是不固定的,故會導致定時器時間不準時

由于GCD的定時器是直接與系統內核掛鉤的,與runloop無關,故無論外部的runloop發生怎樣的操作,都不會影響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);
//第二個參數:從什么時候開始,若需要延遲執行,則傳入dispatch_time(DISPATCH_TIME_NOW, 延遲的秒數 * NSEC_PER_SEC)
//第三個參數:每個幾秒執行
//第四個參數:傳入0即可

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

注:GCD創建的對象在ARC環境中都不需要我們去管理內存

內存布局:

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

Tagged Pointer:

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

一道面試題:

@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" ];
    });
}
//運行結果崩潰,會報壞內存訪問

原因:for循環中實際是頻繁調用setter方法,而ARC環境中的setter方法實際會轉換為MRC中對應的代碼內容

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

當其他線程同時執行setter方法時,可能存在當name屬性已經release,但其他線程繼續調用release,導致其壞內存訪問

其中,如上的代碼策略對應的不同,即strong對應retain,copy對應copy

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

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

MRC:

  • 在iOS中,使用引用計數來管理OC對象的內存
  • 一個新創建的OC對象引用計數默認是1,當引用計數減為0,OC對象就會銷毀,釋放其占用的內存空間
  • 調用retain會讓OC對象的引用計數+1,調用release會讓OC對象的引用計數-1

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

  • 若是使用retain修飾,則setter會執行上面的代碼
  • 若是使用assign修飾,則setter只會進行單純的賦值操作
//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方法放在最后執行
    [super dealloc]
}
//在換屬性時/自己掛掉時,要記得release操作

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

內存管理的經驗總結:

  • 當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調用release或者autorelease來釋放它
  • 想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1
  • 通過類方法創建的對象,在系統內部已經自動幫忙調用release,即除了alloc、copy、new方法創建對象,需要調用release,其余是不需要release的

使用MRC進行開發

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

Copy:

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

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

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

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

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

引用計數的存儲:

在64位系統中,引用計數可以直接存儲在優化過的isa指針中,若引用計數過大,則isa中has_sidetable_rc的值為1,并且引用計數會存儲在Side Table類中

//Side Table結構體定義:
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; //是一個存放對象引用計數的散列表
    weak_table_t weak_table;
}

__strong: 強引用指針
__weak: 弱指針,當所指內容不存在時,指針會自動變為nil
__unsafe_unretained :也不會產生強引用,但當指針所指的內容不存在時,會報野指針錯誤

幾道面試題:

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

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

自動釋放池:

使用release方法,會導致在release代碼后,若繼續使用被銷毀的對象,則會報壞內存訪問錯誤,若使用autorelease,則無需關心這個問題

從源碼可以看出,@autoreleasepool通過轉換為c++代碼,即開頭是一個構造函數:objc_autoreleasePoolPush()函數,結尾是一個析構函數:
objc_autoreleasePoolPop()函數

自動釋放池的主要底層數據結構是:__AtAutoreleasePoolAutoreleasePoolPage
__AtAutoreleasePool是一個結構體,內部包含了構造函數:objc_autoreleasePoolPush()和析構函數:
objc_autoreleasePoolPop(),而push和pop兩個函數都是與AutoreleasePoolPage相關

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

Snip20180713_4.png
  • 其中begin()中即為起始指針的地址(0X1000)加上指針自身大小(56字節)
    end()即為起始指針的地址(0X1000)加上AutoreleasePoolPage(4096字節)大小
  • begin和end之間才是存放autorelease對象的,其余是存放AutoreleasePoolPage原先的一些成員變量的
  • 調用push方法會將一個POOL_BOUNDARY入棧,并且返回其存放的內存地址
  • 調用pop方法時傳入一個POOL_BOUNDARY的內存地址,會從最后一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY
  • id *next指向了下一個能存放autorelease對象地址的區域
  • 可以通過以下私有函數來查看自動釋放池的情況:extern void _objc_autoreleasePoolPrint(void);
  • autorelease對象在什么時機調用release?
    iOS在主線程的Runloop中注冊了2個Observer
    第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
    第2個Observer監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
    監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...
    歐辰_OSR閱讀 29,646評論 8 265
  • 面向對象的三大特性:封裝、繼承、多態 OC內存管理 _strong 引用計數器來控制對象的生命周期。 _weak...
    運氣不夠技術湊閱讀 1,135評論 0 10
  • 從上圖可以看到,棧里面存放的是值類型,堆里面存放的是對象類型。對象的引用計數是在堆內存中操作的。下面我們講講堆和棧...
    jackyshan閱讀 1,666評論 2 11
  • 37.cocoa內存管理規則 1)當你使用new,alloc或copy方法創建一個對象時,該對象的保留計數器值為1...
    如風家的秘密閱讀 893評論 0 4
  • 內存管理ARC處理原理ARC是Objective-C編譯器的特性,而不是運行時特性或者垃圾回收機制,ARC所做的只...
    陽明AGI閱讀 361評論 0 3