參考資料《Android開(kāi)發(fā)藝術(shù)探索》
Android設(shè)備作為一種移動(dòng)設(shè)備,不管是內(nèi)存還是CPU的性能都受到了一定的限制,無(wú)法做到像PC設(shè)備那樣具有超大的內(nèi)存和高性能的CPU。鑒于這一點(diǎn),這也意味著Android程序不可能無(wú)限制的使用內(nèi)存和CPU資源,過(guò)多地使用內(nèi)存會(huì)導(dǎo)致程序內(nèi)存溢出,即OOM。而過(guò)多地使用CPU資源,一般是指做大量的耗時(shí)任務(wù),會(huì)導(dǎo)致手機(jī)變卡頓甚至出現(xiàn)程序無(wú)法響應(yīng)的情況,即ANR。由此看來(lái),Android程序的性能問(wèn)題就變得異常突出了,這對(duì)開(kāi)發(fā)人員也提出了更高的要求。
性能優(yōu)化中很重要的問(wèn)題就是內(nèi)存泄漏,內(nèi)存泄漏并不會(huì)導(dǎo)致程序功能異常,但是它會(huì)導(dǎo)致Android程序的內(nèi)存占用過(guò)大,這將提高內(nèi)存溢出的發(fā)生幾率。
Android的性能優(yōu)化方法
布局優(yōu)化
布局優(yōu)化的思想很簡(jiǎn)單,就是盡量減少布局文件的層級(jí),這個(gè)道理是很淺顯的,布局中的層級(jí)少了,這就意味著Android繪制時(shí)的工作量少了,那么程序的性能自然就高了。
如何進(jìn)行布局優(yōu)化呢?首先刪除布局中無(wú)用的空間和層級(jí),其次有選擇地使用性能較低的ViewGroup,比如RelativeLayout。如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,這是因?yàn)镽elativeLayout的功能比較復(fù)雜,它的布局過(guò)程需要花費(fèi)更多的CPU時(shí)間。FrameLayout和LinearLayout一樣都是一種簡(jiǎn)單高效的ViewGroup,因此可以考慮使用他們,但是很多時(shí)候單純通過(guò)一個(gè)LinearLayout或者FrameLayout無(wú)法實(shí)現(xiàn)產(chǎn)品效果,需要通過(guò)嵌套的方式來(lái)完成。這種情況下還是建議采用RelativeLayout,因?yàn)閂iewGroup的嵌套就相當(dāng)于增加了布局的層級(jí),同樣會(huì)降低程序的性能。
布局優(yōu)化的另外一種手段是采用<include>標(biāo)簽,<merge>標(biāo)簽和ViewStub。<include>標(biāo)簽主要用于布局重用,<merge>標(biāo)簽一般和<include>配合使用,它可以降低減少布局的層級(jí),而ViewStub則提供了按需加載的功能,當(dāng)需要時(shí)才會(huì)將ViewStub中的布局加載到內(nèi)存,這提高了程序的初始化效率。
<include>標(biāo)簽
<include>標(biāo)簽可以將一個(gè)指定的布局文件加載到當(dāng)前的布局文件中
<LinearLayout style="@style/style_apply_relative">
<include
android:id="@+id/titleView"
layout="@layout/item_title" />
</LinearLayout>
上面的代碼中,@layout/item_title指定了另外一個(gè)布局文件,通過(guò)這種方法就不用把item_title這個(gè)布局文件的內(nèi)容再重復(fù)寫(xiě)一遍了,這就是<include>的好處。<include>標(biāo)簽只支持以android:layout_開(kāi)頭的屬性,比如android:layout_width,android:layout_height,其他屬性是不支持的,比如android:background。當(dāng)然,android:id這個(gè)屬性是特例,如果<include>指定了這個(gè)id屬性,同時(shí)被包含的布局文件的根元素也指定了id屬性,那么以<include>指定的id屬性為準(zhǔn)。需要注意的是,如果<include>標(biāo)簽指定了android:layout_這種屬性,那么要求android:layout_width和android:layout_height必須存在,否則其他android:layout_形式的屬性無(wú)法生效。
<merge>標(biāo)簽
<merge>標(biāo)簽一般和<include>標(biāo)簽一起使用從而減少布局的層級(jí)。在上面的示例中,由于當(dāng)前布局是一個(gè)豎直方向的LinearLayout,這個(gè)時(shí)候如果被包含的布局文件中也采用了豎直方向的LinearLayout,那么顯然被包含的布局文件中的LinearLayout是多余的,通過(guò)<merge>標(biāo)簽就可以去掉多余的那一層LinearLayout,如下所示
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="match_parent"
android:layout_height="match_parent">
</merge>
ViewStub
ViewStub繼承了View,它非常輕量級(jí)且寬高都是0,因此它本身不參與任何的布局和繪制過(guò)程。ViewStub的意義在于按需加載所需的布局文件,在實(shí)際開(kāi)發(fā)中,有許多布局文件在正常情況下不會(huì)顯示,比如網(wǎng)絡(luò)異常時(shí)的界面,這個(gè)時(shí)候就沒(méi)有必要在這個(gè)那個(gè)界面初始化的時(shí)候?qū)⑵浼虞d進(jìn)來(lái),通過(guò)ViewStub就可以做到在是使用的時(shí)候在加載,提高了程序初始化時(shí)的性能。下面是一個(gè)ViewStub的示例:
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/viewStub"
android:inflatedId="@+id/panel_import"
android:layout="@layout/activity_main"
android:layout_gravity="bottom"/>
其中viewStub是ViewStub的id,而panel_import是layout/activity_main這個(gè)布局的根元素的id。如何做到按需加載呢?在需要加載ViewStub中的布局時(shí),可以按照如下兩種方式進(jìn)行:
((ViewStub)findViewById(R.id.viewStub)).setVisibility(View.VISIBLE);
或
View inflate = ((ViewStub) findViewById(R.id.viewStub)).inflate();
當(dāng)ViewStub通過(guò)setVisibility或者inflate方法加載后,ViewStub就會(huì)被它內(nèi)部的布局替換掉,這個(gè)時(shí)候哦ViewStub就不再是整個(gè)布局結(jié)構(gòu)中的一部分了。另外,目前ViewStub還不支持<merge>標(biāo)簽。
繪制優(yōu)化
繪制優(yōu)化是指View的onDraw方法要避免執(zhí)行大量的操作,這主要體現(xiàn)在兩個(gè)方面。
首先,onDraw中不要?jiǎng)?chuàng)建新的布局對(duì)象,這是因?yàn)閛nDraw方法可能會(huì)被頻繁的調(diào)用,這樣就會(huì)在一瞬間產(chǎn)生大量的臨時(shí)對(duì)象,這不僅占用了過(guò)多的內(nèi)存而且還會(huì)導(dǎo)致系統(tǒng)更加頻繁gc,降低了程序的執(zhí)行效率。
另外一方面,onDraw方法中不要做耗時(shí)的任務(wù),也不能執(zhí)行成千上萬(wàn)次的循環(huán)操作,盡管每次循環(huán)都很輕量級(jí),但是大量的循環(huán)仍然十分搶占cpu的時(shí)間片,這會(huì)造成view的繪制過(guò)程不流暢。按照Google官方給出的性能優(yōu)化典范中的標(biāo)準(zhǔn),view的繪制幀率保證60fps是最佳的,這就要求每幀的繪制時(shí)間不超過(guò)16ms,雖然程序很難保證16ms這個(gè)時(shí)間,但是盡量降低onDraw方法的復(fù)雜度總是切實(shí)有效的。
內(nèi)存泄漏優(yōu)化
內(nèi)存泄漏在開(kāi)發(fā)過(guò)程中是一個(gè)需要重視的問(wèn)題,但是由于內(nèi)存泄漏問(wèn)題對(duì)開(kāi)發(fā)人員的經(jīng)驗(yàn)和開(kāi)發(fā)意識(shí)有較高的要求,因此這也是開(kāi)發(fā)人員最容易犯的錯(cuò)誤之一。內(nèi)存泄漏的優(yōu)化分為兩個(gè)方面,一方面是在開(kāi)發(fā)過(guò)程中避免削除有內(nèi)存泄漏的代碼,另一方面是通過(guò)一些分析工具比如MAT來(lái)找出潛在的內(nèi)存泄漏繼而解決。
場(chǎng)景1:靜態(tài)變量導(dǎo)致的內(nèi)存泄漏
下面這種情況是最簡(jiǎn)單的內(nèi)存泄漏,下面的代碼將導(dǎo)致Activity無(wú)法正常銷毀,因?yàn)殪o態(tài)變量mContext引用了它。
public class AActivity extends Activity {
private static Context mContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
mContext = this;
}
}
上面的代碼也可以改造一下,如下所示。view是一個(gè)靜態(tài)變量,它內(nèi)部持有了當(dāng)前Activity,所以Activity仍然無(wú)法釋放。
public class AActivity extends Activity {
private static View view;;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
view = new View(this);
}
}
場(chǎng)景2:?jiǎn)卫J綄?dǎo)致的內(nèi)存泄漏
靜態(tài)變量導(dǎo)致的內(nèi)存泄漏都太過(guò)于明顯,而單例模式所帶來(lái)的內(nèi)存泄漏是我們?nèi)菀缀鲆暤?,如下所示?/p>
public class TestManager {
private List<OnDataArrivedListener> mOnDataArrivedListeners = new ArrayList<OnDataArrivedListener>();
private static class SingletonHolder{
public static final TestManager INSTANCE = new TestManager();
}
private TestManager(){
}
public static TestManager getInstance(){
return SingletonHolder.INSTANCE;
}
public synchronized void registerListener(OnDataArrivedListener listener){
if(mOnDataArrivedListeners.contains(listener)){
mOnDataArrivedListeners.add(listener);
}
}
public synchronized void unregisierListener(OnDataArrivedListener listener){
mOnDataArrivedListeners.remove(listener);
}
public interface OnDataArrivedListener{
public void onDataArrived(Object data);
}
}
接著再讓Activity實(shí)現(xiàn)OnDataArrivedListener接口并向TestManager注冊(cè)監(jiān)聽(tīng),如下所示。下面的代碼由于缺少解注冊(cè)的操作所以會(huì)引起內(nèi)存泄漏,泄漏的原因是Activity的對(duì)象被單例模式的TestManager所持有,而單例模式的特點(diǎn)是其生命周期和Application保持一致,因此Activity對(duì)象無(wú)法被及時(shí)釋放
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
TestManager.getInstance().registerListener(this);
}
場(chǎng)景3:屬性動(dòng)畫(huà)導(dǎo)致的內(nèi)存泄漏
屬性動(dòng)畫(huà)中有一類無(wú)限循環(huán)的動(dòng)畫(huà),如果在Activity中播放此類動(dòng)畫(huà)并沒(méi)有在onDestroy中去停止動(dòng)畫(huà),那么動(dòng)畫(huà)會(huì)一直播放下去,盡管已經(jīng)無(wú)法在界面上看到動(dòng)畫(huà)效果了,并且這個(gè)時(shí)候Activity的View會(huì)被動(dòng)畫(huà)持有,而View又持有了Activity,最終Activity無(wú)法釋放。解決方式是在Activity的onDestroy中調(diào)用animator.cancal()來(lái)停止動(dòng)畫(huà)。
響應(yīng)速度優(yōu)化和ANR日志分析
響應(yīng)速度優(yōu)化的核心思想是避免在主線程中做耗時(shí)操作,但是有時(shí)候的確有很多耗時(shí)操作,那么可以將這些耗時(shí)操作放在線程中去執(zhí)行,即采用異步的方式執(zhí)行耗時(shí)操作。相應(yīng)速度過(guò)慢更多的體現(xiàn)在Activity的啟動(dòng)速度上面,如果在主線程中做太多的事情,會(huì)導(dǎo)致Activity啟動(dòng)是出現(xiàn)黑屏現(xiàn)象,甚至出現(xiàn)ANR。Android規(guī)定,Activity如果5秒內(nèi)無(wú)法響應(yīng)屏幕觸摸事件或者鍵盤(pán)輸入事件就會(huì)出現(xiàn)ANR,而B(niǎo)oardacastReceiver如果10秒內(nèi)還未執(zhí)行完操作也會(huì)出現(xiàn)ANR。
一些性能優(yōu)化建議
- 避免創(chuàng)建過(guò)多的對(duì)象;
- 不要過(guò)多是用枚舉,枚舉占用的內(nèi)存空間要比整型大;
- 常量請(qǐng)使用static final來(lái)修飾;
- 使用一些Android特有的數(shù)據(jù)結(jié)構(gòu),比如SparseArray和Pair等,它們都具有更好的性能;
- 適當(dāng)使用軟引用和弱引用;
- 采用內(nèi)存緩存和磁盤(pán)緩存;
- 盡量采用靜態(tài)內(nèi)部類,這樣可以避免潛在的由于內(nèi)部類而導(dǎo)致的內(nèi)存泄漏。