標籤:des android style blog http io ar color os
Android C++架構層的引用計數技術
C++中對指標的使用時很頭疼的事情,一個是經常會忘記free 指標,造成記憶體泄露,另外一個就是野指標問題:訪問已經free掉的指標。程式debug工作的相當大部分,都是花費在這。Android中通過引用計數來自動管理指標的生命週期,動態申請的記憶體將會在不再需要時被自動釋放(有點類似Java的記憶體回收),不用程式員明確使用delete來釋放對象,也不需要考慮一個對象是否已經在其它地方被釋放了,從而使程式編寫工作減輕不少,而程式的穩定性也大大提高。
Android提供的引用計數技術,主要是通過RefBase類及其子類sp (strong pointer)和wp(weak pointer)實現的,具體的原理和細節就不說了,可以參看《深入理解Android:卷1》,說的還是比較清楚。
引用計數的問題
任何東西都不會是萬能的,Android C++中的引用計數問題,和Java一樣,並不能完全避免記憶體泄露,另外還有一個問題就是效能(overhead)問題也很突出,本文就不說了。
使用了android的sp指標,也不能說就不需要程式員去關心指標的細節了。通常由於設計或使用的不良,更有可能導致記憶體無法回收,也就是記憶體泄露問題,甚至於還可能導致不明就裡的野指標問題。而此時導致的記憶體使用量問題,由於其具有更多的欺騙性和隱蔽性,往往更難發覺和調試。
循環參考及其解法
使用引用計數的智能指標管理方法中,常見的java記憶體泄露問題在C++中一樣存在。在Android的強指標引用中,一個最常見的就是強指標的循環參考問題。而這又是程式員比較容易犯的問題:在程式員對強弱指標的理解不是很深入的情況下,想當然的認為使用了強指標,系統會根據引用計數自動收回。
循環參考,就是對象A有個強指標,引用對象B;對象B中,也有個強指標,引用對象A;這樣A和B就互鎖。A對象釋放B對象的引用是在本身被析構回收時,而析構回收的前提是A對象沒有被引用,則需要B對象先釋放,B對象釋放的前提是A對象釋放...如此則A和B都無法釋放,這樣即產生了記憶體泄露。
我們可以寫個程式看一下這種泄露情況。
先定義一個類,這裡假定叫Bigclass:
namespace android{class Bigclass : public RefBase { public: Bigclass(char *name){ strcpy(mName, name); ALOGD("Construct: %s", mName); } ~Bigclass(){ ALOGD("destruct: %s", mName); } void setStrongRefs(sp<Bigclass> b){ spB = b; } private: sp<Bigclass> spB; char mName[64]; };}
該類非常簡單,只有一個sp指標和一個name成員。循環參考樣本:
void testStrongCrossRef(){ sp<Bigclass> A = new Bigclass("testStrongClassA"); sp<Bigclass> B = new Bigclass("testStrongClassB"); A->setStrongRefs(B); B->setStrongRefs(A);}int main(){ ALOGD("Start testStrongClasses.."); testStrongCrossRef(); ALOGD("testStrongClasses Should be destructed!!"); return 0;}
輸出的結果,如預期,對象沒有被釋放,泄露了:
D/TEST ( 1552): Start testStrongClasses..D/TEST ( 1552): Construct: testStrongClassAD/TEST ( 1552): Construct: testStrongClassBD/TEST ( 1552): testStrongClasses Should be destructed!!
為瞭解決這一問題,Android在又引入了弱指標,弱指標並不能通過引用計數來控制所引用對象的生命週期,這樣就可以消除上例中的引用環路問題,使得問題解決。我們將上述的類稍作修改,增加了弱引用的介面:
namespace android{class Bigclass : public RefBase { public: Bigclass(char *name){ strcpy(mName, name); ALOGD("Construct: %s", mName); } ~Bigclass(){ ALOGD("destruct: %s", mName); } void setStrongRefs(sp<Bigclass> b){ spB = b; } void setWeakRefs(sp<Bigclass> b){ wpB = b; } private sp<Bigclass> spB; wp<Bigclass> wpB; char mName[64]; };}
先來測試一下,將上例中的強指標換成弱指標,會是什麼情況:
void testWeakCrossRef(){ sp<Bigclass> A = new Bigclass("testWeakClassA"); sp<Bigclass> B = new Bigclass("testWeakClassB"); A->setWeakRefs(B); B->setWeakRefs(A);}
輸出結果:
D/TEST ( 2889): Start testWeakClass ..D/TEST ( 2889): Construct: testWeakClassAD/TEST ( 2889): Construct: testWeakClassBD/TEST ( 2889): destruct: testWeakClassBD/TEST ( 2889): destruct: testWeakClassAD/TEST ( 2889): testWeakClass Should be destructed!!
在出了testWeakClassA和testWeakClassB在對象A和B出了範圍後,沒有強引用了,兩個對象都釋放了,這個符合預期。
這裡testWeakClassA和testWeakClassB之間的參考關聯性,全部都是弱引用,因此二者間的生命週期互不相干,這裡二者用sp<Bigclass>對象A和B與建立一般的棧對象 Bigclass A, Bigclass B 的生命週期一樣。
Android中,最常用的肯定不是上面兩種:
- 強強引用——互不相讓,互相綁死,這是絕對禁止的。
- 弱弱引用——互不相干,各管生死。這個對於想要使用引用計數自動管理對象生命週期來說,沒什麼用處。
最常用的一般是強弱參考關聯性。強弱引用需要是有從屬關係的,具體那個類是用sp引用,哪個是用wp引用,要看設計的邏輯了。
測試樣本:
void testCrossRef(){ sp<Bigclass> A = new Bigclass("testNormalClassA"); sp<Bigclass> B = new Bigclass("testNormalClassB"); A->setStrongRefs(B); B->setWeakRefs(A);}
輸出結果:
D/TEST ( 2889): Start test Normal pointer reference ..D/TEST ( 2889): Construct: testNormalClassAD/TEST ( 2889): Construct: testNormalClassBD/TEST ( 2889): destruct: testNormalClassAD/TEST ( 2889): destruct: testNormalClassBD/TEST ( 2889): Test Normal pointer reference Should be destructed!!
這種情況下,消除了循環參考,沒有了記憶體泄露問題。 和上一個弱弱引用的例子比較,這裡testNormalClassB的析構在testWeakClassA之後,testWeakClassB的生命週期是受testWeakClassA控制的,只有testWeakClassA析構,testWeakClassB才會析構。(上面的弱弱引用的測例,說明在無幹預的情況下,應該是testWeakClassB先析構的)
對於強弱指標的使用,使用弱指標是需要特別注意,弱指標指向的對象,可能已經被銷毀了,使用前需要通過promote()方法探測一下,詳細資料可參考《深入理解Android》
野指標問題
強弱引用的問題,相信大多數Android程式員都明白,這裡主要要強調一點就是:使用的時候要小心,一不小心可能就出錯,舉個例子:
我們在剛才定義的BigClass中增加另外一個建構函式:
Bigclass(char *name, char * other){ strcpy(mName, name); ALOGD("Construct another: %s", mName); setWeakRefs(this); }
這個建構函式,是將本對象中wp類型成員變數的用自己構造,也就是wp指標指向自己,這是允許的。
在寫個測試案例:
void testCrash(){ sp<Bigclass> A = new Bigclass("testCrashA", "testCrash"); sp<Bigclass> B = new Bigclass("testCrashB"); A->setWeakRefs(B); B->setWeakRefs(A);}
輸出結果:
D/TEST ( 3709): Construct another: testCrashAD/TEST ( 3709): destruct: testCrashAD/TEST ( 3709): Construct: testCrashBD/TEST ( 3709): destruct: testCrashB
好像沒有什麼問題,程式也沒有崩潰呀?
沒有崩潰,那是幸運,因為這個測試代碼和上下文太簡單了。 我們看下輸出就知道了:testCrashB物件建構的時候, testClassA已經析構了!!!!
也就是說,A對象,在其建立後,馬上就消亡了,testCrash()方法中操作的A對象所指向的,都是野指標!!!
為何會出現野指標?問題出在剛才定義的建構函式Bigclass(char *name, char * other)中。
setWeakRefs(this);
這裡的this是一個Bigclass *類型的,這樣在參數壓棧的時候需要構建一個臨時的sp<Bigclass>強指標對象,調用完成後,該對象析構。該sp對象通過sp的sp<T*>建構函式構造的。
這裡我們看一下這個臨時sp對象的構造和析構過程:
構造完成後:new出來的這個Bigclass對象testCrashA,這個RefBase的子類對象,其強引用計數為1(從INITIAL_STRONG_VALUE增加到1)
析構完畢後:sp的析構會減小該Bigclass對象指標(即對象 testCrashA,也是這裡的this指標指向的對象)的強引用的計數,這裡是從1減小到INITIAL_STRONG_VALUE,當一個對象的引用計數減小到INITIAL_STRONG_VALUE時,會觸發該Bigclass對象的delete操作,也就析構了該this指標指向的對象了。
這裡,建構函式中就刪除構造的對象,比較難想象吧!有興趣可以驗證一下看看,將剛才的建構函式修改一下:
Bigclass(char *name, char * other){ ALOGD("start Construct another: %s,", mName); strcpy(mName, name); setWeakRefs(this); ALOGD("end Construct another: %s,", mName); }
看一下析構是否在start和end之間。跑一下,你會有意想不到的列印 :)
這裡可以給一條規則:絕對不能在建立的RefBase對象還沒有被一個確定的長範圍sp對象引用前,通過局部短範圍sp指標引用。
建議
沒什麼好建議,搞明白設計意圖和工作原理對調試和寫代碼都非常有好處。
轉載:http://blog.csdn.net/freshui/article/details/9049193#
Android的引用計數(強弱指標)技術及一些問題