iOS中堆和棧的使用方法
堆和棧都是一種數(shù)據(jù)項(xiàng)按序排列的數(shù)據(jù)結(jié)構(gòu),只能在一端(稱為棧頂(top))對(duì)數(shù)據(jù)項(xiàng)進(jìn)行插入和刪除。堆,隊(duì)列優(yōu)先,先進(jìn)先出(FIFO—first in first out);棧,先進(jìn)后出(FILO—First-In/Last-Out)。一般情況下,如果有人把堆棧合起來說,那它的意思是棧,而不是堆。
堆棧空間分配
- 棧區(qū)(stack):由編譯器自動(dòng)分配釋放,存放函數(shù)的參數(shù)值,局部變量等值。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
- 堆區(qū)(heap):一般由程序員分配釋放,若程序員不釋放,則可能會(huì)引起內(nèi)存泄漏。其類似于鏈表。
堆棧緩存方式
iOS 中應(yīng)用程序使用的計(jì)算機(jī)內(nèi)存不是統(tǒng)一分配空間,運(yùn)行代碼使用的空間在三個(gè)不同的內(nèi)存區(qū)域,分成三個(gè)段:“text segment “,“stack segment ”,“heap segment ”。
代碼區(qū)(text segment ):是應(yīng)用程序運(yùn)行時(shí)應(yīng)用程序代碼存在的內(nèi)存段,運(yùn)行前就已經(jīng)確定(編譯時(shí)確定),通常為只讀的。代碼區(qū)的指令中包括操作碼和要操作的對(duì)象(或?qū)ο蟮刂芬?,代碼區(qū)指令根據(jù)程序設(shè)計(jì)流程依次執(zhí)行,每一個(gè)指令,每一個(gè)單個(gè)函數(shù)、過程、方法和執(zhí)行代碼都存在這個(gè)內(nèi)存段中直到應(yīng)用程序退出。一般使用中很少涉及。
棧(Stack):當(dāng)我們創(chuàng)建一個(gè)值類型,如結(jié)構(gòu)體,系統(tǒng)將其存儲(chǔ)在一個(gè)被稱為棧的內(nèi)存區(qū)域中,是由CPU直接管理和優(yōu)化的。當(dāng)一個(gè)函數(shù)聲明一個(gè)變量,變量將存儲(chǔ)在棧中,當(dāng)函數(shù)調(diào)用完畢后棧會(huì)自動(dòng)釋放該變量。因此棧是非常易于管理的、有效的,由于是CPU直接控制,速度非???。
堆(Heap):當(dāng)我們創(chuàng)建了一個(gè)引用類型,如類,系統(tǒng)將把類實(shí)例存儲(chǔ)在一個(gè)被稱為堆的內(nèi)存區(qū)域中。系統(tǒng)使用堆來存儲(chǔ)其他對(duì)象引用的數(shù)據(jù)。堆是一個(gè)大的內(nèi)存池,系統(tǒng)可以從該池中請(qǐng)求并動(dòng)態(tài)分配內(nèi)存塊。堆不會(huì)像棧一樣自動(dòng)釋放對(duì)象,需要額外的工作來完成。這使得在堆中創(chuàng)建和刪除數(shù)據(jù)比棧慢。
棧使用的是一級(jí)緩存, 他們通常都是被調(diào)用時(shí)處于存儲(chǔ)空間中,調(diào)用完畢立即釋放。堆則是存放在二級(jí)緩存中,生命周期由虛擬機(jī)的垃圾回收算法來決定(并不是一旦成為孤兒對(duì)象就能被回收)。所以調(diào)用這些對(duì)象的速度要相對(duì)來得低一些。
stack 中的一個(gè)指針僅僅是一個(gè)整型變量,保存了heap(堆)中特定內(nèi)存地址的數(shù)據(jù)。簡(jiǎn)而言之,操作系統(tǒng)使用stack 段中的指針值訪問heap 段中的對(duì)象。如果stack 對(duì)象的指針沒有了,則heap 中的對(duì)象就不能訪問。這也是內(nèi)存泄露的原因。
在iOS 操作系統(tǒng)的stack 段和heap 段中,你都可以創(chuàng)建數(shù)據(jù)對(duì)象。stack 對(duì)象的優(yōu)點(diǎn)主要有兩點(diǎn),一是創(chuàng)建速度快,二是管理簡(jiǎn)單,它有嚴(yán)格的生命周期。stack 對(duì)象的缺點(diǎn)是它不靈活。創(chuàng)建時(shí)長(zhǎng)度是多大就一直是多 大,創(chuàng)建時(shí)是哪個(gè)函數(shù)創(chuàng)建的,它的owner 就一直是它。不像heap 對(duì)象那樣有多個(gè)owner ,其實(shí)多個(gè)owner 等同于引用計(jì)數(shù)。只有 heap 對(duì)象才是采用“引用計(jì)數(shù)”方法管理它。
堆棧數(shù)據(jù)結(jié)構(gòu)區(qū)別
- 堆(數(shù)據(jù)結(jié)構(gòu)):堆可以被看成是一棵樹,如:堆排序。
- 棧(數(shù)據(jù)結(jié)構(gòu)):一種先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)。
堆和棧究竟有什么區(qū)別? 主要的區(qū)別由以下幾點(diǎn):
1、管理方式不同;
管理方式:對(duì)于棧來講,是由編譯器自動(dòng)管理,無需我們手工控制;對(duì)于堆來說,釋放工作由程序員控制,容易產(chǎn)生memory leak。
2、空間大小不同;
空間大?。簵J且粔K空間較小,但是運(yùn)行速度很快的內(nèi)存區(qū)域。棧上的內(nèi)存分配遵循后進(jìn)先出的原則,通過移動(dòng)棧的尾指針實(shí)現(xiàn) push(入棧)和 pop(出棧)操作。我們的程序是由一個(gè)個(gè)方法組成的,CPU 會(huì)負(fù)責(zé)調(diào)度并執(zhí)行這些方法。當(dāng)我們的程序執(zhí)行到某個(gè)方法的時(shí)候,需要在棧上為方法需要的內(nèi)存開辟空間,此時(shí)把棧的尾指針向棧底移動(dòng)。當(dāng)方法執(zhí)行完畢后需要釋放掉這些空間,此時(shí)會(huì)把棧的尾指針移向棧頂,這就完成了一次棧上的內(nèi)存分配。只要棧的剩余空間大于stack 對(duì)象申請(qǐng)創(chuàng)建的空間,操作系統(tǒng)就會(huì)為程序提供這段內(nèi)存空間,否則將報(bào)異常提示棧溢出。
堆是內(nèi)存中的另一塊區(qū)域,空間比棧大的多,但是運(yùn)行速度要比棧上的運(yùn)行速度慢。堆可以在運(yùn)行時(shí)動(dòng)態(tài)的分配內(nèi)存,補(bǔ)充棧上內(nèi)存分配的不足。一般來講在32位系統(tǒng)下,堆內(nèi)存可以達(dá)到4G的空間,從這個(gè)角度來看堆內(nèi)存幾乎是沒有什么限制的。
操作系統(tǒng)對(duì)于內(nèi)存heap 段是采用鏈表進(jìn)行管理的。操作系統(tǒng)有一個(gè)記錄空閑內(nèi)存地址的鏈表,當(dāng)收到程序的申請(qǐng)時(shí),會(huì)遍歷鏈表,尋找第一個(gè)空間大于所申請(qǐng)的heap 節(jié)點(diǎn),然后將該節(jié)點(diǎn)從空閑節(jié)點(diǎn)鏈表中刪除,并將該節(jié)點(diǎn)的空間分配給程序。iOS使用了名為 ARC(自動(dòng)引用計(jì)數(shù))的技術(shù)。在多線程環(huán)境中,多個(gè)線程會(huì)共享堆上的內(nèi)存,為了確保線程安全,不得不在堆上進(jìn)行加鎖操作,但是加鎖操作是很耗費(fèi)性能的,你在堆上所獲的的數(shù)據(jù)安全性實(shí)際上是在犧牲性能的代價(jià)下得來的。
NSString 的對(duì)象就是stack 中的對(duì)象,NSMutableString 的對(duì)象就是heap 中的對(duì)象。前者創(chuàng)建時(shí)分配的內(nèi)存長(zhǎng)度固定且不可修改;后者是分配內(nèi)存長(zhǎng)度是可變的,可有多個(gè)owner, 適用于計(jì)數(shù)管理內(nèi)存管理模式。
3、能否產(chǎn)生碎片不同;
碎 片問題:對(duì)于堆來講,頻繁的new/delete勢(shì)必會(huì)造成內(nèi)存空間的不連續(xù),從而造成大量的碎片,使程序效率降低。對(duì)于棧來講,則不會(huì)存在這個(gè)問題,因 為棧是先進(jìn)后出的隊(duì)列,他們是如此的一一對(duì)應(yīng),以至于永遠(yuǎn)都不可能有一個(gè)內(nèi)存塊從棧中間彈出,在他彈出之前,在他上面的后進(jìn)的棧內(nèi)容已經(jīng)被彈出。
4、生長(zhǎng)方向不同;
生長(zhǎng)方向:對(duì)于堆來講,生長(zhǎng)方向是向上的,也就是向著內(nèi)存地址增加的方向;對(duì)于棧來講,它的生長(zhǎng)方向是向下的,是向著內(nèi)存地址減小的方向增長(zhǎng)。
5、分配方式不同;
分配方式:堆都是動(dòng)態(tài)分配的,沒有靜態(tài)分配的堆。棧有2種分配方式:靜態(tài)分配和動(dòng)態(tài)分配。靜態(tài)分配是編譯器完成的,比如局部變量的分配。動(dòng)態(tài)分配由alloca函數(shù)進(jìn)行分配,但是棧的動(dòng)態(tài)分配和堆是不同的,他的動(dòng)態(tài)分配是由編譯器進(jìn)行釋放,無需我們手工實(shí)現(xiàn)。
6、分配效率不同;
分 配效率:棧是機(jī)器系統(tǒng)提供的數(shù)據(jù)結(jié)構(gòu),計(jì)算機(jī)會(huì)在底層對(duì)棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較 高。堆則是C/C++函數(shù)庫提供的,它的機(jī)制是很復(fù)雜的,例如為了分配一塊內(nèi)存,庫函數(shù)會(huì)按照一定的算法(具體的算法可以參考數(shù)據(jù)結(jié)構(gòu)/操作系統(tǒng))在堆內(nèi) 存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于內(nèi)存碎片太多),就有可能調(diào)用系統(tǒng)功能去增加程序數(shù)據(jù)段的內(nèi)存空間,這樣就有機(jī)會(huì)分到 足夠大小的內(nèi)存,然后進(jìn)行返回。顯然,堆的效率比棧要低得多。
從這里我們可以看到,堆和棧相比,由于大量new/delete的使用,容 易造成大量的內(nèi)存碎片;由于沒有專門的系統(tǒng)支持,效率很低;由于可能引發(fā)用戶態(tài)和核心態(tài)的切換,內(nèi)存的申請(qǐng),代價(jià)變得更加昂貴。所以棧在程序中是應(yīng)用最廣 泛的,就算是函數(shù)的調(diào)用也利用棧去完成,函數(shù)調(diào)用過程中的參數(shù),返回地址,局部變量都采用棧的方式存放。所以,我們推薦大家盡量用棧,而不是用 堆。
但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。另外,棧數(shù)據(jù)在多個(gè)線程或者多個(gè)棧之間是不可以共享的,但是在棧內(nèi)部多個(gè)值相等的變量是可以指向一個(gè)地址的。和堆相比不是那么靈活,有時(shí)候分配大量的內(nèi)存空間,還是用堆好一些。無論是堆還是 棧,都要防止越界現(xiàn)象的發(fā)生(除非你是故意使其越界),因?yàn)樵浇绲慕Y(jié)果要么是程序崩潰,要么是摧毀程序的堆、棧結(jié)構(gòu),產(chǎn)生以想不到的結(jié)果,就算是在你的程 序運(yùn)行過程中,沒有發(fā)生上面的問題,你還是要小心,說不定什么時(shí)候就崩掉了。
Swift中的使用
Swift 中的數(shù)據(jù)類型分為引用類型(類)和值類型(枚舉、結(jié)構(gòu)體)。引用類型存儲(chǔ)在 “堆” 上,值類型存儲(chǔ)在 “棧” 上。Swift 管理引用類型采用自動(dòng)引用計(jì)數(shù)(ARC)的管理方法。值類型是由處理器來管理的,不需要程序員來管理。
在 Swift 中,典型的有 struct,enum,以及 tuple 都是值類型。而平時(shí)使用的Int,Double,F(xiàn)loat,String,Array,Dictionary,Set 其實(shí)都是用結(jié)構(gòu)體實(shí)現(xiàn)的,也是值類型。Swift 中,值類型的賦值為深拷貝(Deep Copy),值語義(Value Semantics)即新對(duì)象和源對(duì)象是獨(dú)立的,當(dāng)改變新對(duì)象的屬性,源對(duì)象不會(huì)受到影響,反之同理。
在 Swift 中,class 和閉包是引用類型。引用類型的賦值是淺拷貝(Shallow Copy),引用語義(Reference Semantics)即新對(duì)象和源對(duì)象的變量名不同,但其引用(指向的內(nèi)存空間)是一樣的,因此當(dāng)使用新對(duì)象操作其內(nèi)部數(shù)據(jù)時(shí),源對(duì)象的內(nèi)部數(shù)據(jù)也會(huì)受到影響。
值類型作為參數(shù)傳入時(shí),函數(shù)體內(nèi)部不能修改其值。引用類型作為參數(shù)傳入時(shí),函數(shù)體內(nèi)部不能修改其指向的內(nèi)存地址,但是可以修改其內(nèi)部的變量值。
值類型的優(yōu)點(diǎn)是:不變性,值類型的變量是嚴(yán)格的被一個(gè)所有者控制的;獨(dú)立性,引用類型是相互依賴的,是一種隱式的依賴;還有可交換性。
對(duì)于面向?qū)ο缶幊蹋捎趯?shí)例對(duì)象是可變的,導(dǎo)致對(duì)象的另一個(gè)享有者在合適的時(shí)候會(huì)去改變這個(gè)對(duì)象的屬性。swift支持類的單繼承,導(dǎo)致從多個(gè)class繼承到更多地功能,增加了復(fù)雜度,并且會(huì)導(dǎo)致class緊耦合的問題。在多線程情況下,可以同時(shí)改變同一個(gè)引用。
選擇值類型而不是引用類型的一個(gè)主要原因是能讓你的代碼變得更加簡(jiǎn)單。Swift的核心是面向協(xié)議,引用類型有許多的享有者。值類型被賦給一個(gè)變量或者常量,傳給函數(shù)做參數(shù)時(shí)是它的值被拷貝的。這就讓值類型在任何時(shí)候只有一個(gè)享有者,從而降低復(fù)雜度。你在任何情況下用一個(gè)值類型,都能夠假設(shè)你的其他代碼不會(huì)使它改變,這通常在多線程環(huán)境中很有用,如果一個(gè)線程中使用的數(shù)據(jù)被另一個(gè)線程給意外的修改了,這通常會(huì)產(chǎn)生非常嚴(yán)重的Bug,且相當(dāng)難以調(diào)試。Class = 高復(fù)雜度,值 = 低復(fù)雜度。而且,swift對(duì)值類型的操作上進(jìn)行了一些優(yōu)化,因此才有了swift大量使用值類型代替引用類型的說法。
由于只有當(dāng)你需要修改數(shù)據(jù)時(shí)兩者的區(qū)別才會(huì)得到體現(xiàn),所以當(dāng)你的實(shí)例不會(huì)對(duì)數(shù)據(jù)進(jìn)行修改的時(shí)候,值類型和引用類型看起來是完全相同的。你也許會(huì)想,寫一個(gè)完全不可變的類,通過使用不可變的存儲(chǔ)屬性,以及避免暴露修改數(shù)據(jù)的接口,從而在Swift里實(shí)現(xiàn)一個(gè)不可變的類。事實(shí)上,大多數(shù)的Cocoa類,比如NSURL等,都被設(shè)計(jì)為不可變的類,然而,Swift當(dāng)前并沒有提供任何語言機(jī)制去強(qiáng)制申明一個(gè)類不可改變(比如子類化就能修改一個(gè)類的實(shí)現(xiàn)),只有結(jié)構(gòu)體和枚舉才是強(qiáng)制不可變的。
在Swift里,Array、String和Dictionary都是值類型,他們的行為和C語言中的int類似,每個(gè)實(shí)例都有自己的數(shù)據(jù),你不需要額外做任何事情,比如做一個(gè)顯式的copy,防止其他代碼在你不知情的情況下修改等,更重要的是,你能安全地在線程間傳遞它,而不需要使用同步技術(shù)。在提高安全性的精神下,這個(gè)模型將幫助你在Swift中寫出更多可預(yù)知的代碼。
除此之外,Swift和OC還有其他的類型對(duì)應(yīng),對(duì)應(yīng)關(guān)系如下:
但是,需要關(guān)注的是,對(duì)于原來OC中的數(shù)據(jù)的引用類型,swift中并沒有真正完全的實(shí)現(xiàn)一套數(shù)據(jù)存儲(chǔ)邏輯。只是內(nèi)部保存了對(duì)oc對(duì)象的引用,使得swift api訪問時(shí)行為邏輯和值類型一致。