iOS性能調(diào)優(yōu)之--內(nèi)存管理

前言

????????iOS內(nèi)存管理無論是早期的MRC還是現(xiàn)在的ARC本質(zhì)都是通過引用計數(shù)(Reference Counting)機制管理內(nèi)存,當(dāng)一個對象被創(chuàng)建出來時,它的引用計數(shù)從0到1,當(dāng)有外部對象對它進(jìn)行強引用時,它的應(yīng)用計數(shù)會+1,當(dāng)該對象收到一條release消息時,它的引用計數(shù)會-1;當(dāng)對象的引用計數(shù)為0時,對象將被釋放,對象指向的內(nèi)存被回收.

1. ARC內(nèi)存管理的本質(zhì)

????????MRC時代需要程序員手動管理對象的生命周期,也就是對象的引用計數(shù)有程序員來控制,什么時候retain,什么時候release,完全自己掌握.ARC(Automatic Reference Counting)自動引用計數(shù)是編譯器的一個特性,能夠自動管理OC對象內(nèi)存生命周期.在ARC中你需要專注于寫你的代碼, retain ,release, autorelease操作交給編譯器去處理就行了.

MRC_ARC_示意圖_來源_Apple_Document.jpg

????????ARC 下編譯器如何自動管理內(nèi)存,其中,能想到的是在類的 dealloc 方法中,對該類的所持有的成員變量(strong)執(zhí)行 release 操作,讓所有成員變量的引用計數(shù)為0。對于局部變量,更可能是的對象在出作用域之前,編譯器自動給對象加上一條 release消息.這些工作都是編譯器為我們處理了.

// 作用域
    {
        NSString *str = [[NSString alloc]initWithFormat:@"%@",@"str"];
        
        NSLog(@"%@",str);
        
        // 在對象出作用域時,編譯器自動給對象發(fā)一條release消息
        [str release];
    }

????????ARC,則無需我們自己顯式持有(retain)和釋放(release)對象,ARC通過對對像加上所有權(quán)修飾符(__strong等),編譯器通過對象的所有權(quán)修飾符將會自動管理對象的引用計數(shù).

2. 所有權(quán)修飾符

基礎(chǔ)知識:指針是其實也是一個對象,它指向一個內(nèi)存地址單元,內(nèi)存單元里存著各種變量.這樣指針就可以指向這樣變量,當(dāng)我們用的時候我們就可以從內(nèi)存單元取出變量內(nèi)容.
????????Objective-C對象的ARC是通過所有權(quán)修飾符來管理對象的持有和釋放。所有權(quán)修飾符一共有4種:

2.1 __strong 修飾符

????????默認(rèn)的修飾符,只要有一個強指針指向這個對象,這個對象就一直不會銷毀,這個對象指向的指針也不會置為NULL.

//這里person_one 可以理解為一個指針 指向 Person創(chuàng)建的出來的對象(指針)的內(nèi)存,可以讀取內(nèi)存上的內(nèi)容
    Person * __strong person_one = [[Person alloc]init];
    
    Person * __strong person_two = person_one; 
    
    person_one = nil;
    
    NSLog(@"person_one:%@,person_one地址:%p",person_one,person_one);
    NSLog(@"person_two:%@,person_two地址:%p",person_two,person_two);

Log:
2018-03-19 16:19:09.822168 TestARC[16592:5864784] person_one:(null),person_one地址:0x0
2018-03-19 16:19:22.443524 TestARC[16592:5864784] person_two:<Person: 0x17001e450>,person_two地址:0x17001e450
strong所有權(quán)修飾.png

????????我們可以看到,person_two是person_one的淺拷貝對象,也就是指針拷貝對象,而person_two是通過__strong修飾,相當(dāng)于強指針,指向的是與person_one一塊內(nèi)存區(qū)域.而這塊內(nèi)存區(qū)域被retain了兩次,引用計數(shù)為2,即使person_one = nil將引用計數(shù)-1了,person_two依然可以打印出內(nèi)存地址.person_one的指針已經(jīng)被置為NULL,所以打印出的地址是0x0.

2.2 __weak 修飾符

????????當(dāng)沒有強指針指向弱引用的對象時,弱引用的對象將被置為nil,對象的指針置為NULL.

//這里person_one 可以理解為一個指針 指向 Person創(chuàng)建的出來的對象(指針)的內(nèi)存,可以讀取內(nèi)存上的內(nèi)容
    Person * __strong person_one = [[Person alloc]init];
    
    Person * __weak person_two = person_one; 
    
    person_one = nil;
    
    NSLog(@"person_one:%@,person_one地址:%p",person_one,person_one);
    NSLog(@"person_two:%@,person_two地址:%p",person_two,person_two);
Log:
2018-03-19 16:28:21.453255 TestARC[16599:5866487] person_one:(null),person_one地址:0x0
2018-03-19 16:28:25.521762 TestARC[16599:5866487] person_two:(null),person_two地址:0x0
weak所有權(quán)修飾.png

????????我們知道__weak修飾的對象不會對對象進(jìn)行retain,所以person_two指向的內(nèi)存區(qū)域?qū)ο笠糜嫈?shù)還是1.這里只有person_one強引用那塊內(nèi)存區(qū)域,當(dāng)person_one = nil時,引用計數(shù)為0,內(nèi)存區(qū)域被釋放,person_two指向的內(nèi)存地址為:0x0.

2.3 __unsafe_unretained 修飾符

????????就像其表面意思一樣:當(dāng)沒有強指針指向__unsafe_unretained修飾的對象時,這個對象會被置為nil,但是指向?qū)ο蟮闹羔槻粫磺蹇?蘋果官方: the pointer is left dangling.

//這里person_one 可以理解為一個指針 指向 Person創(chuàng)建的出來的對象(指針)的內(nèi)存,可以讀取內(nèi)存上的內(nèi)容
    Person * __strong person_one = [[Person alloc]init];
    
    Person * __unsafe_unretained person_two = person_one; 
    
    person_one = nil;
    
    NSLog(@"person_one:%@,person_one地址:%p",person_one,person_one);
    NSLog(@"person_two:%@,person_two地址:%p",person_two,person_two);
Log:
2018-03-19 16:42:52.400375 TestARC[16608:5869804] person_one:(null),person_one地址:0x0
這里已經(jīng)報錯:Thread 1: EXC_BAD_ACCESS (code=1, address=0xb84d2beb8)
unsafe__unretained所有權(quán)修飾.png

????????這里我們在主線程中收到一條崩潰信息(EXC_BAD_ACCESS),通過__unsafe_unretained官方文檔解釋,我們可以猜出address=0xb84d2beb8應(yīng)該是person_one沒被置為nil之前的內(nèi)存地址,而當(dāng)person_one = nil時,這塊內(nèi)存已經(jīng)被回收,而person_two因為被__unsafe_unretained修飾,其指針還沒有被銷毀,還想指向這塊內(nèi)存地址,所以造成了野指針錯誤.

2.4 __autoreleasing 修飾符

????????autorelease 本質(zhì)上就是延遲調(diào)用 release,這里不做細(xì)致的分析了,大家感興趣的可以自己找相關(guān)資料查看.
????????到這里我們對ARC的引用計數(shù)管理應(yīng)該有了大概的了解.

3. 源碼分析

????????引用計數(shù)的實現(xiàn),我們可以通過查看蘋果的源碼(https://opensource.apple.com/source/objc4/).我們下面主要來看看retain的實現(xiàn)源碼,我們可以在OC的鼻祖類--NSObject中可以看到協(xié)議NSObject中定義的幾個方法:

- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE;

????????以上方法,就是編譯器在合適的時機給對象所要發(fā)送的消息.我們點進(jìn)去retain方法,我們可以在NSObject.mm文件的2138行可以看到其實現(xiàn):

// Replaced by ObjectAlloc
- (id)retain {
    return ((id)self)->rootRetain();
}

????????沿著調(diào)用鏈,我們可以在objc-object.h文件中看到id rootRetain(bool tryRetain, bool handleOverflow)方法的實現(xiàn):

LWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    assert(!UseGC);
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (!newisa.indexed) goto unindexed;
        // don't check newisa.fast_rr; we already called any RR overrides
        if (tryRetain && newisa.deallocating) goto tryfail;
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (carry) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) return rootRetain_overflow(tryRetain);
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits));

    if (transcribeToSideTable) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (!tryRetain && sideTableLocked) sidetable_unlock();
    return (id)this;

 tryfail:
    if (!tryRetain && sideTableLocked) sidetable_unlock();
    return nil;

 unindexed:
    if (!tryRetain && sideTableLocked) sidetable_unlock();
    if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
    else return sidetable_retain();
}

????????最后一行sidetable_retain(),這個也是retain方法的最終調(diào)用的方法.而sidetable_retain()的實現(xiàn):

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable& table = SideTables()[this];

    if (table.trylock()) {
        size_t& refcntStorage = table.refcnts[this];
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            refcntStorage += SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        return (id)this;
    }
    return sidetable_retain_slow(table);
}

????????我們可以看到這個方法中SideTable這個結(jié)構(gòu)體,

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    bool trylock() { return slock.trylock(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<bool HaveOld, bool HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<bool HaveOld, bool HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

????????其中的 RefcountMap 應(yīng)該就是引用計數(shù)哈希表,而weak_table_t則是弱引用表(weak table).
????????RefcountMap 則是一個簡單的 map,其 key 為 object 內(nèi)存地址,value 為引用計數(shù)值.通過SideTable源碼,還可以得出如下結(jié)論:
????????存在全局的若干個SideTable實例,它們保存在 static 成員變量table_buf中;
????????程序運行過程中生成的所有對象都會通過其內(nèi)存地址映射到table_buf中相應(yīng)的????????SideTable實例上.這里之所以會存在多個SideTable實例,object 映射到不同SideTable實例上,猜測是出于性能優(yōu)化的目的,避免SideTable中的 reference table、weak table 過大.
????????回到上面的sidetable_retain方法,其首先通過 object 的地址找到對應(yīng)的 sidetale,然后通過 RefcountMap將該 object 的引用計數(shù)加1.簡單地說,Apple 通過全局的 map 來記錄Reference Counting,其key 為 object 地址,value 為引用計數(shù)值。
????????release、retainCount等相關(guān)方法的代碼在該開源代碼中也能找到,這里不細(xì)說了.

4. ARC開發(fā)環(huán)境需要注意的管理內(nèi)存:

4.1CoreFoundation,Runtime以及其他C語言庫的使用

????????通過malloc,create,copy等創(chuàng)建對象,還需要手動釋放.

4.2 循環(huán)引用

????????循環(huán)引用是兩個或多個對象之間相互持有,形成環(huán)狀,即使在沒有外部對象指針指向這些對象內(nèi)存區(qū)域(堆區(qū))的時候,系統(tǒng)無法將每個對象的引用計數(shù)置為0,從而導(dǎo)致這些開辟出來的內(nèi)存一直發(fā)揮著”占著茅坑不拉屎”的作用.這部分不容易檢測,也容易背鍋.不管新老司機遇到問題不假思索:循環(huán)引用的問題(所以遇到問題的時候,我們更多的是多思考,而不是在沒有分析問題的情況下脫口而出,不僅誤導(dǎo)別人,而且顯得自己很水,多說了兩句,見笑).


循環(huán)引用示意圖.png

5. 內(nèi)存管理檢測

5.1 Analyze靜態(tài)分析

????????靜態(tài)內(nèi)存分析, 指的是在程序沒運行的時候, 通過預(yù)編譯對代碼進(jìn)行預(yù)判斷分析,分析代碼的基本數(shù)據(jù)結(jié)構(gòu),語法等,編譯器檢查是否存在潛在的內(nèi)存泄露及不規(guī)范的地方.常遇到問題:
????????1)The 'viewWillDisappear:' instance method in UIViewController subclass 'xxxxx' is missing a [super viewWillDisappear:] call;這個錯誤提示是:重寫父類中的實例方法viewWillDisappear,沒有在子類中調(diào)用,從下圖我們可以看到確實是這樣,-(void)viewWillDisappear:(BOOL)animated方法內(nèi)部調(diào)用的是[super viewDidAppear:animated];這種是很低級的錯誤.


Analyze_1.png

????????2)Value stored to 'xxxxx' is never read,聲明的變量沒有被用到


Analyze_2.png

????????3) API Misuse 接口應(yīng)用錯誤,這里主要針對的是系統(tǒng)提供的接口
從下圖中我們可以看到,_cachedStatements是一個字典,字典是不允許出現(xiàn)nil對象的,所以存數(shù)據(jù)之前我們要做容錯判斷.
Analyze_3_1.png

????????改完后就不再提示了


Analyze_3_2.png

????????4)Memory error,內(nèi)存錯誤:nil returned from a method that is expected to return a non-null value,方法返回中需要一個對象(指針),你返回了一個空指針.例如,下圖在UITableView的數(shù)據(jù)源回調(diào)方法返回cell的方法中,本應(yīng)返回一個UITableViewCell對象,可是這里返回了一個nil對象(空指針)
Analyze_4.png

????????還存在其他潛在問題錯誤或者不規(guī)范的地方,大家可以照著這個自己去查找一下自己的項目.

5.2 Instruments內(nèi)存泄露檢測

????????Instruments內(nèi)存分析你應(yīng)用內(nèi)存的使用情況,幫助你查找定位出現(xiàn)問題的代碼區(qū)域.詳細(xì)介紹可以參考apple developer documentation(https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/CommonMemoryProblems.html#//apple_ref/doc/uid/TP40004652-CH91-SW1)
從文檔中我們大概可以看到,一個應(yīng)用所使用的內(nèi)存可能占三種:

Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
泄露的內(nèi)存:應(yīng)用無法再次應(yīng)用或者釋放的內(nèi)存.
Abandoned memory: Memory still referenced by your application that has no useful purpose.
廢棄的內(nèi)存:你的應(yīng)用還占據(jù)著這塊內(nèi)存,但是這塊內(nèi)存無法釋放了,ARC中最有可能的是循環(huán)引用.
Cached memory: Memory still referenced by your application that might be used again for better performance.
緩存的內(nèi)存:能夠被你的應(yīng)用正常釋放回收利用的內(nèi)存.
內(nèi)存泄露:如果程序運行時一直分配內(nèi)存而不及時釋放無用的內(nèi)存,程序占用的內(nèi)存越來越大,直到把系統(tǒng)分配給該APP的內(nèi)存消耗殫盡,程序因無內(nèi)存可用導(dǎo)致崩潰,這樣的情況我們稱之為內(nèi)存泄漏??赡芤鸬膯栴}:
1)內(nèi)存消耗殆盡的時候,程序會因沒有內(nèi)存被殺死,即crash。
2)當(dāng)內(nèi)存快要用完的時候,會非常的卡頓
3)如果是ViewController沒有釋放掉,引起的內(nèi)存泄露,還會引起其他很多問題,尤其是和通知相關(guān)的。沒有被釋放掉的ViewController還能接收通知,還會執(zhí)行相關(guān)的動作,所以會引起各種各樣的異常情況的發(fā)生。

????????以我們現(xiàn)在開發(fā)的項目為例:這里打個廣告,我們現(xiàn)在開發(fā)的應(yīng)用叫做愛學(xué).橫版主要有我的班級,自學(xué),消息,設(shè)置等模塊,下面我們用Instruments來檢查一下:
????????1)打開調(diào)試工具步驟:首先先將待檢測的源碼安裝到你的真機設(shè)備上(Command + r 或者 直接Run運行);然后按著快捷鍵:Command + Control + i,打開Instruments,選擇Leaks.
????????2)定位內(nèi)存泄露區(qū)域
????????我們選擇call_tree,也就是函數(shù)調(diào)用棧,順藤摸瓜,找到內(nèi)存泄露的地方


call_tree.png

memory_leak.png

????????不出意外,就可以看到具體內(nèi)存泄露的代碼了,我們這里是由于使用Runtime了,調(diào)用了class_copyPropertyList方法.我們知道Runtime是OC的底層,是OC的幕后工作者,所寫的OC代碼最終都轉(zhuǎn)換成Runtime的C代碼執(zhí)行.這里通過class_copyPropertyList方法來獲取類的所有成員變量的時候,沒有釋放.所以在使用C語言相關(guān)庫的時候,一定要做好釋放工作(不然裝B就裝大了??,玩笑).最終在使用遍歷完類中的成員變量后,free(properties);就沒問題了.

-(NSArray *)modelInfo:(Class)cls
{
    unsigned int  count = 0;
    objc_property_t  * properties= class_copyPropertyList(cls, &count);
    NSMutableArray  * infoarr = [NSMutableArray new];
    for (int i = 0; i<count; i++)
    {
        objc_property_t property = properties[i];
        NSString * name = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding ];
        
        [infoarr addObject:name];
    }
    free(properties);
    return infoarr;
}

????????我們的學(xué)習(xí)任務(wù)中一個視頻類型的任務(wù),視頻播放器估計是從網(wǎng)上找的別人封裝好的,沒有細(xì)致分析就用了.從下圖中我們可以看到至少有三個環(huán),我們需要打破這種環(huán)狀,消除引用循環(huán),這里不細(xì)說,大家可以根據(jù)需要去詳細(xì)看看怎么處理引用循環(huán).


retain_recycle_1.png

retain_recycle_2.png

retain_recycle_3.png

總結(jié)

????????文中簡單介紹了iOS內(nèi)存管理的相關(guān)內(nèi)容,主要的還是ARC相關(guān)內(nèi)容,這些大都是基于實際開發(fā)中的總結(jié)和平時學(xué)習(xí)的積累,里面不乏一些錯誤和不規(guī)范之處,希望沒有沒有大家沒有被誤導(dǎo),更希望大家多給意見和建議.其實,基礎(chǔ)知識扎牢了,對一些問題的理解,解決可能也會更加游刃有余,而不是天天糾結(jié)于一些"界面"上的問題.

參考文獻(xiàn)

https://blog.devtang.com/2016/07/30/ios-memory-management/
https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/FindingLeakedMemory.html
http://clang.llvm.org/docs/AutomaticReferenceCounting.html

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

  • iOS內(nèi)存管理 概述 什么是內(nèi)存管理 應(yīng)用程序內(nèi)存管理是在程序運行時分配內(nèi)存(比如創(chuàng)建一個對象,會增加內(nèi)存占用)與...
    蚊香醬閱讀 5,823評論 8 119
  • 概述 在iOS中開發(fā)中,我們或多或少都聽說過內(nèi)存管理。iOS的內(nèi)存管理一般指的是OC對象的內(nèi)存管理,因為OC對象分...
    DamonMok閱讀 4,121評論 2 20
  • 內(nèi)存管理 ARC處理原理 ARC是Objective-C編譯器的特性,而不是運行時特性或者垃圾回收機制,ARC所做...
    b485c88ab697閱讀 11,348評論 3 47
  • 29.理解引用計數(shù) Objective-C語言使用引用計數(shù)來管理內(nèi)存,也就是說,每個對象都有個可以遞增或遞減的計數(shù)...
    Code_Ninja閱讀 1,745評論 1 3
  • 從沒有出過遠(yuǎn)門的自己,卻要在一個遙遠(yuǎn)的城市過四年,因為大學(xué),因為青春,因為懵懂!剛剛來到陌生的城市,誰都不認(rèn)識,此...
    effected閱讀 270評論 0 0

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