Android效能優化——程式碼優化(一)

1008711發表於2017-04-22


注:根據Android官方的建議,編寫高效程式碼的三個基本準則如下:

  • 不要做冗餘的工作
  • 儘量避免次數過多的記憶體分配操作
  • 深入理解所有語言特性和系統平臺的API,具體到Android開發,熟練掌握java語言,並對SDK的API熟悉,瞭如指掌。

一、資料結構的選擇

正確的選擇合適的資料結構很重要,對java中常見的資料結構例如ArrayList和LinkedList、HashMap和HashSet等,需要做到對它們的聯絡與區別有教深入的理解,這樣在編寫程式碼中面臨選擇時才能作出正確的選擇,下面我們以android開發中使用SparseArray代替HashMap為例進行說明。SparseArray是Android平臺特有的稀疏陣列的實現,它是Integer到Object的一個對映,在特定場合可用於代替HashMap<Integer,<E>>,提高效能。核心實現是二分法查詢演算法。

————————————————————————————————————————

SparseArray家族目前有以下四類:

————————————————————————————————————————

HashMap<Integer, Boolean> booleanHashMap = new HashMap<>();
SparseBooleanArray booleanArray = new SparseBooleanArray();

HashMap<Integer,Integer>  integerHashMap = new HashMap<>();
SparseIntArray intArray = new SparseIntArray();

HashMap<Integer,Long> longHashMap = new HashMap<>();
SparseLongArray sparseLongArray = new SparseLongArray();

HashMap<Integer,String>  stringHashMap = new HashMap<>();
SparseArray<String> sparseArray = new SparseArray<>();複製程式碼

————————————————————————————————————————

需要注意的幾點如下:

  • SparseArray不是執行緒安全。
  • 由於要進行二分法查詢,因此,SparseArray會對插入的資料按照Key值大小順序插入。
  • SparseArray對刪除操作做了優化,它並不會立即刪除這個元素,而是通過設定標識位(DELETED)的方式,後面嘗試重用。

在Android工程中執行Lint進行靜態程式碼塊分析,會有一個名為AndroidLintUseSparseArrays的檢查項,如果違規,它會提示:

————————————————————————————————————————

HashMap can be replaced with SparseArray

————————————————————————————————————————

這樣可以很輕鬆地找到工程中優化的地方。

二、Handler和內部類的正確用法

————————————————————————————————————————

Android程式碼中涉及執行緒間通訊的地方經常會使用Handler,典型的程式碼結構如下:

————————————————————————————————————————

public class HandlerActivity extends Activity {
    
    private final Handler mLeakyHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    
}複製程式碼

————————————————————————————————————————

使用AndroidLint分析這段程式碼,會違反檢查項AndroidLintHanderLeak,得到如下提示:

————————————————————————————————————————

This Handler class should be static or leaks might occur

————————————————————————————————————————

那麼產生記憶體洩漏的原因可能是什麼?Handler和Looper以及MessageQueue一起工作的,在Android中,一個應用啟動後,系統預設會建立一個為主執行緒服務的Looper物件,該Looper物件用於處理主執行緒的所有Message物件,它的生命週期貫穿於整個應用的生命週期。在主執行緒中使用的Handler都會預設繫結到這個Looper物件。在主執行緒中建立Handler物件時,它會立即關聯主執行緒Looper物件的MessageQueue,這時傳送到MessageQueue中的Message物件都會持有這個Handler物件的引用,這樣Looper處理訊息時才能回撥到Handler的handlerMessage方法。因此,如果Message還沒有被處理完成,那麼Handler物件也就不會被垃圾回收。

在上面的程式碼中,將Handler的例項宣告為HandlerActivity類的內部類。而在Java語言中,非靜態內部匿名類會持有外部類的一個隱式的引用,這樣就可能會導致外部類無法被垃圾回收。因此,最終由於MessageQueue中的Message還沒有處理完成,就會持有Handler物件的引用,而非靜態的Handler物件會持有外部類HandlerActivity的引用,這樣Activity無法被垃圾回收,從而導致記憶體洩漏。

一個明顯的會引入記憶體洩漏的例子如下:

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--2:07
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class HandlerActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //延遲10分鐘傳送訊息

        mLeakyHandler.postAtTime(new Runnable() {
            @Override
            public void run() {
                /***/
            }
        }, 1000 * 60 * 10);
        
    }
}複製程式碼

————————————————————————————————————————

由於訊息延遲10分鐘傳送,因此,當使用者進入這個Activity並退出後,在訊息傳送並處理完成之前,這個Activity是不會被系統回收(系統記憶體確實不夠使用的情況例外)

如何解決呢?兩個方案:

  • 在子執行緒中使用Handler,這是需要開發者自己建立一個Looper物件,這個Looper物件的生命週期同一般的Java物件,因此這種用法沒有問題。
  • 將Handler宣告為靜態的內部類,前面說過,靜態內部類不會持有外部類的引用,因此,也不會引用記憶體洩漏,經典用法的程式碼如下。

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--2:07
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class HandlerActivity extends Activity {

    /***
     * 宣告一個靜態的Handler內部類,並持有外部類的弱引用
     */
    private static class InnerHandler extends Handler {

        private final WeakReference<HandlerActivity> mActivity;

        public InnerHandler(HandlerActivity activity) {
            this.mActivity = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerActivity activity = mActivity.get();
            if (activity != null) {
                //..
            }
        }
    }

    private final InnerHandler mHandler = new InnerHandler(this);

    /**
     * 靜態的匿名內部類不會持有外部類的引用
     */
    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            /****/
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //延遲10分鐘傳送訊息

        mHandler.postAtTime(sRunnable, 1000 * 60 * 10);
    }
     

}複製程式碼

————————————————————————————————————————

三、正確地使用Context

Context應該是每個入門Android開發的程式設計師第一個接觸到的概念,它代表當前的上下文環境,可以用來實現很多功能的呼叫,語句如下:

————————————————————————————————————————

//獲取資源管理器物件,進而可以訪問到例如string,color等資源
Resources resources = context.getResources();


//啟動指定的Activity
context.startActivity(new Intent(this, MainActivity.class));

//獲取各種系統服務
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

//獲取系統檔案目錄
File internalDir = context.getCacheDir();
File externalDir = context.getExternalCacheDir();

//更多。。。複製程式碼

可見,正確理解Context的概念是很重要的,雖然應用開發中隨處可見Context的使用,但並不是所有的Context例項都具備相同的功能,在使用上需要區別對待,否則很可能會引入問題。我們首先來總結下Context的種類。

(1)、Context的種類

根據Context依託的元件以及用途不同,我們可以將Context分為如下幾種。

  • Application:Android應用中的預設單例類,在Activity或者Service中通過getApplication()可以獲取到這個例項,通過context.getApplicationContext() 可以獲取到應用全域性唯一的Context例項。
  • Activity/Service:這兩個類都是ContextWrapper的子類,在這兩個類中可以通過getBaseContext()獲取到它們的Context例項,不同的Activity或者Service例項,它們的Context都是獨立的,不會複用。
  • BroadcastReceiver:和Activity以及Service不用,BroadcastReceiver本身並不是Context的子類,而是在回撥函式onReceive()中由Android框架傳入一個Context的例項。系統傳入的這個Context例項是經過功能裁剪的,它並不能呼叫registerReceiver()以及bindService()這個兩個函式。
  • ContextProvider:同樣的,ContentProvider也不是Context的子類,但在建立時系統會傳入一個Context例項,這樣在ContentProvider中可以通過呼叫getContext()函式獲取。如果ContentProvider和呼叫者處於相同的應用程式中,那麼getContext()將返回應用全域性唯一的Context的例項。如果是其他程式呼叫的ContentProvider,那麼ContentProvider將持有自身所在程式的Context例項。

(2)、錯誤使用Context導致的記憶體洩漏

錯誤地使用Context可能會導致記憶體洩漏,典型的例子是在實現單例模式時使用Context,如下程式碼是可能會導致記憶體洩漏的單例實現。

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--23:57
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class SingleInstance {

    private Context mContext;
    private static SingleInstance sInstance;

    private SingleInstance(Context context) {
        mContext = context;
    }

    public static SingleInstance getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new SingleInstance(context);
        }
        return sInstance;
    }
    
}複製程式碼

————————————————————————————————————————

如果使用者呼叫getInstance時傳入的Context是一個Activity或者Service的例項,那麼在應用退出之前,由於單例一直存在,會導致對應的Activity或者Service被單例引用,從而不會被垃圾回收,Activity或者Service中關聯的其他View或者資料結構物件也不會被釋放,從而導致記憶體洩漏。正確的做法是使用Application Context,因為它是應用唯一的,而且宣告週期是跟著應用一致的,正確的單例實現如下:

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--23:57
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class SingleInstance {

    private Context mContext;
    private static SingleInstance sInstance;

    private SingleInstance(Context context) {
        mContext = context;
    }

    public static SingleInstance getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new SingleInstance(context.getApplicationContext());//這一句關鍵
        }
        return sInstance;
    }

}複製程式碼

(3)、不同Context的對比

不同元件中的Context能提供的功能不盡相同,總結起來,如下表:

功能ApplicationActivityServiceBroadcastReceiverContentProvider
顯示DialogNOYESNONONO
啟動ActivityNO[1]YESNO[1]NO[1]NO[1]
實現LayoutInflationNO[2]YESNO[2]NO[2]NO[2]
啟動ServiceYESYESYESYESYES
繫結ServiceYESYESYESYESNO
傳送BroadcastYESYESYESYESYES
註冊BroadcastYESYESYESYESNO[3]
載入資源ResourceYESYESYESYESYES

其中NO[1]標記表示對應的元件並不是真的不可以啟動Activity,而是建議不要這麼做,因為這些元件會在新的Task中建立Activity,而不是在原來的Task中。

NO[2]標記也是表示不建議這麼做,因為在非Activity中進行Layout Inflation,會使用系統預設的主題,而不是應用中設定的主題。

NO[3]標記表示在Android4.2及以上的系統上,如果註冊的BroadcastReceiver是null時是可以的,用來獲取sticky廣播的當前值。

四、掌握java的四種引用方式

掌握java的四種引用型別對於寫出記憶體使用良好的應用是很關鍵的。

  • 強引用:Java裡面最廣泛使用的一種,也是物件預設的引用型別。如果一個物件具有強引用,那麼垃圾回收期是不會對它進行回收操作的,當記憶體空間不足時,Java虛擬機器將會丟擲OutOfMemoryError錯誤,這時應用將會終止執行。一句話總結,只要引用存在,垃圾回收器永遠不會回收。Object obj = new Object(); 可以直接通過obj取得對應的物件,只有obj這個引用被釋放之後,物件才會被釋放掉。

  • 軟引用:一個物件如果只有軟引用,那麼當記憶體空間不足時,垃圾回收器不會對它進行回收操作,只有當記憶體空間不足時,這個物件才會被回收。軟引用可以用來實現記憶體敏感的快取記憶體,如果配合引用佇列(ReferenceQueue)使用,當軟引用指向的物件被垃圾回收器回收後,Java虛擬機器將會把這個軟引用加入到與之關聯的引用佇列中。一句話總結,非必須引用,記憶體溢位之前進行回收,
    Object object = new Object();
    SoftReference<Object> sf = new SoftReference<Object>(object);複製程式碼
  • 軟引用:弱引用是比軟引用更弱的一種引用型別,只有弱引用指向的物件的生命週期更短,當垃圾回收器掃描到只具有弱引用的物件時,不論當前記憶體空間是否不足,都會對弱引用物件進行回收。弱引用也可以和一個引用佇列配合使用,當弱引用指向的物件被回收後,Java虛擬機器會將這個弱引用加入到與之關聯的引用佇列中。一句話總結,
  • Object object = new Object();
    WeakReference<Object> reference = new WeakReference<Object>(object);複製程式碼
  • 虛引用:和軟引用和弱引用不同,虛引用並不會對所指向的物件生命週期產生任何影響,也就是物件還是會按照它原來的方式被垃圾回收器回收,虛引用本質上只是一個標記作用,主要用來跟蹤物件被垃圾回收的活動,虛引用必須和引用佇列配合使用,當物件被垃圾回收時,如果存在虛引用,那麼Java虛擬機器會將這個虛引用加入與之關聯的引用佇列中。

五、其他程式碼微優化

(1)、避免建立非必要的物件

————————————————————————————————————————

物件的建立需要記憶體分配,物件的銷燬需要垃圾回收,這些都會一定程度上影響到應用的效能。因此一般來水,最好是重用物件而不是在每次需要的時候去建立一個功能相同的新物件,特別是注意不要在迴圈中重複建立相同的物件。

(2)、對常量使用static final 修飾

————————————————————————————————————————

對於基本資料型別和String型別的常量,建議使用static final 修飾,因為final型別的常量會在會進入靜態dex檔案的域初始化部分,這是對基本資料型別和String型別常量的呼叫不會涉及類的初始化,而是直接呼叫字面量。

(3)、避免內部的Getters/Setters

————————————————————————————————————————

在物件導向程式設計中,Getters/Setters的作用主要是對外遮蔽具體的變數定義,從而達到更好的封裝性。但如果是在類內部還使用Getters/Setters函式訪問變數的話,會降低訪問的速度。根據Android官方文件,在沒有JIT(Just In Time)編譯器時,直接訪問變數的速度是呼叫Getter方法的3倍;在JIT編譯時,直接訪問變數的速度是呼叫Getters方法的7倍。當然,如果你的應用中使用了ProGuard(混淆程式碼)的話,那麼ProGuard會對Getters/Setters進行內聯操作,從而達到直接訪問的效果。

(4)、程式碼的重構

程式碼的重構是一項長期的持之以恆的工作,需要依靠團隊中每一個成員來維護程式碼庫的高質量,要會去享受高質量程式碼帶來的快感,如何有效的進行程式碼重構,除了需要對你所在專案有較深入的理解之外,你還需要一定的方法指導。重構程式碼可以使用不同的設計模式來達到高質量的程式碼,這兒可以關注我的設計模式系列部落格:Android設計模式之——單例模式(一) Android設計模式之——Builder模式(二)


相關文章