前言
????????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操作交給編譯器去處理就行了.

????????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

????????我們可以看到,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修飾的對象不會對對象進(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)

????????這里我們在主線程中收到一條崩潰信息(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)別人,而且顯得自己很水,多說了兩句,見笑).

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];這種是很低級的錯誤.

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

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

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

????????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對象(空指針)

????????還存在其他潛在問題錯誤或者不規(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)存泄露的地方


????????不出意外,就可以看到具體內(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).



總結(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