上一章講了Android性能優(yōu)化之耗電優(yōu)化
,感興趣的可以看下。這一章來說說Android內(nèi)存方面如何優(yōu)化,雖說是講內(nèi)存優(yōu)化但是并不涉及虛擬機(jī)底層原理,力求通俗易懂。

養(yǎng)成好習(xí)慣先上圖。內(nèi)存從狀態(tài)上來說只有已使用和未使用兩種。本章內(nèi)存優(yōu)化也從這兩方面下手:已使用的內(nèi)存如何保證虛擬機(jī)的順利回收、未使用的內(nèi)存如何在滿足需求的情況下盡量小的申請(qǐng)。
如何保證已使用內(nèi)存順利被回收?
- Java對(duì)象生命周期
創(chuàng)建階段
申請(qǐng)內(nèi)存空間,構(gòu)造對(duì)象并初始化相關(guān)屬性值使用階段
根據(jù)對(duì)象應(yīng)用調(diào)用相關(guān)方法完成業(yè)務(wù)邏輯。對(duì)象至少被一個(gè)強(qiáng)引用持有,除非對(duì)象創(chuàng)建時(shí)顯示聲明使用軟引用、弱引用和虛引用。不可見階段
當(dāng)一個(gè)對(duì)象處于不可見階段時(shí),說明程序本身不再持有該對(duì)象的任何強(qiáng)引用,當(dāng)然對(duì)象還是存在著的。不可達(dá)階段
對(duì)象處于不可達(dá)階段是指該對(duì)象不再被任何強(qiáng)引用所持有。GC會(huì)發(fā)現(xiàn)對(duì)象已不可達(dá)收集階段
當(dāng)垃圾回收器發(fā)現(xiàn)該對(duì)象已經(jīng)處于“不可達(dá)階段”并且垃圾回收器已經(jīng)對(duì)該對(duì)象的內(nèi)存空間重新分配做好準(zhǔn)備時(shí),則對(duì)象進(jìn)入了“收集階段”。終結(jié)階段
當(dāng)對(duì)象執(zhí)行完finalize()方法后仍然處于不可達(dá)狀態(tài)時(shí),則該對(duì)象進(jìn)入終結(jié)階段。在該階段,等待垃圾回收器對(duì)該對(duì)象空間進(jìn)行回收。對(duì)象空間重新分配階段
若垃圾回收器對(duì)該對(duì)象的所占用的內(nèi)存空間進(jìn)行回收或者再分配,則該對(duì)象徹底消失,這個(gè)階段稱之為“對(duì)象空間重新分配階段”。
以上是Java對(duì)象生命周期的簡要介紹,要保證內(nèi)存順利回收,正確使用Java對(duì)象生命周期很重要,如果不能及時(shí)回收,我們就稱之為“發(fā)生了內(nèi)存泄露”。
在不可見階段,程序本身不再持有對(duì)象強(qiáng)引用,但對(duì)象仍可能被JVM等系統(tǒng)下的某些已裝載的靜態(tài)變量或線程或JNI等強(qiáng)引用持有著,這些特殊的強(qiáng)引用被稱為”GC root”。存在著這些GC root會(huì)導(dǎo)致對(duì)象的內(nèi)存泄露情況,無法被回收。

圖中灰色的孤立無援的對(duì)象對(duì)于GC Roots來說不可達(dá),會(huì)被回收。知道了內(nèi)存泄露會(huì)影響回收,下面說下哪些方面會(huì)導(dǎo)致內(nèi)存泄露
- 引起內(nèi)存泄露的情況
- 資源沒有適時(shí)關(guān)閉
sqlite的cursor、讀寫文件使用的File文件流等在使用完后沒有及時(shí)關(guān)閉。雖然cursor會(huì)在系統(tǒng)回收時(shí)自動(dòng)關(guān)閉,但是這樣效率較低。對(duì)于資源對(duì)象使用還是應(yīng)該養(yǎng)成良好習(xí)慣,使用完畢close并置空。 - 注冊(cè)對(duì)象未注銷
在Android中主要是指注冊(cè)的廣播在Activity銷毀時(shí)反注銷。
在Activity中如果有使用的觀察者模式在生命周期發(fā)生變化時(shí)根據(jù)需求注銷。
在Activity中使用的各類傳感器(光線、重力等)在頁面銷毀時(shí)及時(shí)注銷,否則不光導(dǎo)致內(nèi)存泄露還會(huì)因?yàn)閭鞲衅黝l繁的采樣導(dǎo)致耗電及cpu的占用。 - 使用static修飾變量
這里只說一點(diǎn),被static修飾的變量可以認(rèn)為是直接被GC Roots引用了,那你就知道其生命周期有多長了。這時(shí)候你如果用static 修飾Bitmap、View、Context和Activity等后果有多嚴(yán)重了吧。 - 非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例
先看幾行代碼:
- 資源沒有適時(shí)關(guān)閉
public class MainActivity extends AppCompatActivity {
public static People people;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
people = new People();
}
class People{
int age ;
String name ;
}
}
非靜態(tài)內(nèi)部類People持有外部類即當(dāng)前Activity的引用,而該非靜態(tài)內(nèi)部類實(shí)例又是static修飾的,導(dǎo)致Activity一直被持有而不得釋放,最終導(dǎo)致Activity所包含的view不能釋放,如果view tree中包含多圖片,那泄露的內(nèi)存是很大的。
- Handler
眾所周知handler用來發(fā)送和處理消息回調(diào)的。
handler導(dǎo)致泄露主要是handler實(shí)例是作為非靜態(tài)匿名內(nèi)部類方式創(chuàng)建,并且MessageQueue隊(duì)列中有未處理消息,這時(shí)如果退出Activity,MessageQueue中還有Message,而Message持有handler實(shí)例,handler實(shí)例作為非靜態(tài)內(nèi)部類持有Activity引用,最終的連鎖反應(yīng)導(dǎo)致Activity泄露。
handler引起的內(nèi)存泄露一般是臨時(shí)性的,因?yàn)橄㈥?duì)列里的Message在延時(shí)到時(shí)間或者某一情況激活后還是會(huì)執(zhí)行的,除非你是故意搞事情。創(chuàng)建handler時(shí)最好使用靜態(tài)內(nèi)部類,同時(shí)在Activity退出時(shí)執(zhí)行
handler.removeCallbacksAndMessages(null);清空隊(duì)列消息
- Webview
webview的使用總是會(huì)莫名的出現(xiàn)各種問題或泄露。最好的辦法就是把web頁面放在一個(gè)獨(dú)立的進(jìn)程,如果需要交互使用aidl。 - 容器中的對(duì)象未清理對(duì)象
?Android中使用的容器最多的就是List和Map。用來存儲(chǔ)對(duì)象集合,如果對(duì)象集合和頁面相關(guān),那么在退出頁面時(shí)注意清空集合。同時(shí)不要使用static修飾集合。
如何盡量小的申請(qǐng)內(nèi)存?
上面說完了如何保證GC順利回收,現(xiàn)在來講講要最小使用內(nèi)存應(yīng)該怎么做:
- 慎用自動(dòng)封裝
來幾行代碼嘗嘗:
Integer num=0;
for (int i=0;i<100;i++) {
num+=i;
}```
Java基本數(shù)據(jù)類型是有自動(dòng)裝箱機(jī)制的。每次執(zhí)行循環(huán)都會(huì)發(fā)生一次裝箱操作創(chuàng)建一個(gè)Integer對(duì)象,造成內(nèi)存消耗。包括其他基本數(shù)據(jù)類型都有可能造成這種情況。
- 內(nèi)存復(fù)用
- 視圖復(fù)用
在ListView中使用ViewHolder復(fù)用item組件,一方面節(jié)省內(nèi)存,一方面提高滑動(dòng)流暢性。都用過不多介紹。
- 使用對(duì)象池
看過Handler、Looper、Message、MessageQueue這一套消息循環(huán)源碼的同志應(yīng)該知道里面的Message使用了對(duì)象池模式。
>對(duì)象池類似線程池, 首先初始化一個(gè)固定大小池子,每次創(chuàng)建對(duì)象時(shí)候先去池子中找有沒有,如果有直接取出,如果沒有new出來使用后還到池子里。這樣便可達(dá)到對(duì)象復(fù)用的目的。
對(duì)象池模式適用于那些頻繁使用創(chuàng)建的對(duì)象,比如一個(gè)聊天app,里面對(duì)象最多的恐怕就是聊天信息(每條聊天信息對(duì)應(yīng)一個(gè)信息對(duì)象)。都知道對(duì)象的創(chuàng)建是很耗費(fèi)時(shí)間和內(nèi)存的,沒事不要new著玩。如果每條消息都創(chuàng)建一個(gè)對(duì)象,那可想而知該APP的性能。
對(duì)象池的使用也很簡單,少量代碼即可完成:
public class People {
private static final Pools.SynchronizedPool<People> sPool = new Pools.SynchronizedPool<People>(
20);//需要維持對(duì)象的數(shù)量
int age;
String name;
public static People obtain() {
People instance = sPool.acquire();
return (instance != null) ? instance : new People();
}
public void recycle() {
sPool.release(this);
}
}
>注意:對(duì)象申請(qǐng)(obtain)和釋放(recycle)成對(duì)出現(xiàn),使用一個(gè)對(duì)象后一定要釋放還給對(duì)象池。
- Bitmap復(fù)用
如果設(shè)置了options.inBitmap屬性,以后再使用帶有該options參數(shù)的decode方法加載圖片資源時(shí),decode會(huì)嘗試重用已存在的位圖內(nèi)存,這樣節(jié)省了加載和分配的時(shí)間,同時(shí)也節(jié)省了內(nèi)存空間。
>該屬性從3.0開始引進(jìn),低版本不支持inBitmap,4.4系統(tǒng)之前只能重用大小相同的內(nèi)存區(qū)域,4.4以后可以重用任何比所需內(nèi)存小的區(qū)域。具體使用可參考[官網(wǎng)](https://developer.android.com/topic/performance/graphics/manage-memory.html)。
- 純色規(guī)則形狀背景用Color Res代替圖片
經(jīng)常遇到一些按鈕背景是純色顯示,比如選中狀態(tài)背景變?yōu)榧兓?,但是設(shè)計(jì)已經(jīng)發(fā)來了切圖用還是不用?大聲say NO!如果背景使用圖片來顯示,那背景每個(gè)像素都要繪制。
假設(shè)一個(gè)分辨率為100x100的圖片,占用4通道。那該圖片內(nèi)存占用就是100x100x4 =4萬Byte≈40KB;但是如果使用``` android:background="@color/colorAccent"```引用color值的方式,由于是純色,只需渲染一個(gè)像素而其他像素復(fù)用這個(gè)像素值即可。這樣只需要4Byte即完成了背景設(shè)置。
- 選擇合適數(shù)據(jù)類型
- 使用ArrayMap替換HashMap
先看一下HashMap模型和ArrayMap模型:


>HashMap是一個(gè)散列鏈表,稀疏陣列導(dǎo)致內(nèi)存稍大,而ArrayMap提供了和HashMap一樣的功能,但是避免了內(nèi)存過度開銷。在執(zhí)行插入或刪除操作時(shí),從性能上看ArrayMap比HashMap稍差,但是如果對(duì)象數(shù)很小,比如1000以內(nèi)不用擔(dān)心性能問題。如果想深入了解這2個(gè)的原理請(qǐng)自行搜索,這里不過多闡述。
- 枚舉替身來了
JDK1.5就支持了枚舉類型,使用Enum關(guān)鍵字定義。使用枚舉類型很多時(shí)候出于參數(shù)類型安全迫不得已作出的選擇。
```
public String getValue(int type){
switch (type) {
case 1:
break;
case 2:
break;
case 3:
break;
default:
throw new IllegalArgumentException("不合法參數(shù)");
}
return "";
}
試想一下如果一個(gè)函數(shù)的參數(shù)為int type,函數(shù)處理時(shí)只用到了1,2,3三種值,如果是其他值就拋出異常,這無疑增加了程序的不穩(wěn)定性,按以前此時(shí)最好的解決辦法就是參數(shù)改為枚舉類型,增加了限定也就提高了穩(wěn)定性。但是枚舉類型就是一把雙刃劍,增加安全同時(shí)也大大增加了內(nèi)存占用,尤其是在移動(dòng)設(shè)備上,資源有限更應(yīng)該注意內(nèi)存節(jié)省。
谷歌或許考慮到了這些問題,在提供的注解包里添加了注解方式檢查類型安全,目前支持int和String兩種,看下使用方式:
```
//1、先聲明需要的類型常量值
public static final int TYPE_1 = 1;
public static final int TYPE_2 = 2;
//2、創(chuàng)建注解接口同時(shí)把上一步聲明的常量囊括到這里
@IntDef({TYPE_1,TYPE_2})
@Retention(RetentionPolicy.SOURCE)
public @interface _TYPE{
}
//3、在函數(shù)參數(shù)中增加 注解接口名稱
public String getValue(@_TYPE int type){
switch (type) {
case 1:
break;
case 2:
break;
}
return "";
}
經(jīng)過上面的步驟,再調(diào)用getValue()函數(shù)時(shí)如果傳入其他int則報(bào)錯(cuò)編譯不通過,這樣通過注解就增加了安全性:

結(jié)語:基本上APP大部分內(nèi)存還是被圖片占用,處理好圖片尤為重要,但是關(guān)于圖片三級(jí)緩存及縮放,目前都使用第三方框架如ImageLoader,所以這里一筆帶過。以上就是日常內(nèi)存優(yōu)化需要注意的地方,自己做個(gè)總結(jié),也希望能對(duì)各位看官有所幫助。