Android性能優化——代碼優化(一)


注:根據Android官方的建議,編寫高效代碼的三個基本準則以下:java

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

1、數據結構的選擇

正確的選擇合適的數據結構很重要,對java中常見的數據結構例如ArrayList和LinkedList、HashMap和HashSet等,須要作到對它們的聯繫與區別有教深刻的理解,這樣在編寫代碼中面臨選擇時才能做出正確的選擇,下面咱們以android開發中使用SparseArray代替HashMap爲例進行說明。SparseArray是Android平臺特有的稀疏數組的實現,它是Integer到Object的一個映射,在特定場合可用於代替HashMap<Integer,<E>>,提升性能。核心實現是二分法查找算法。android

————————————————————————————————————————程序員

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 SparseArraybash

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

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

2、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);
    }
     

}複製代碼

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

3、正確地使用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能提供的功能不盡相同,總結起來,以下表:

功能 Application Activity Service BroadcastReceiver ContentProvider
顯示Dialog NO YES NO NO NO
啓動Activity NO[1] YES NO[1] NO[1] NO[1]
實現LayoutInflation NO[2] YES NO[2] NO[2] NO[2]
啓動Service YES YES YES YES YES
綁定Service YES YES YES YES NO
發送Broadcast YES YES YES YES YES
註冊Broadcast YES YES YES YES NO[3]
加載資源Resource YES YES YES YES YES

其中NO[1]標記表示對應的組件並非真的不能夠啓動Activity,而是建議不要這麼作,由於這些組件會在新的Task中建立Activity,而不是在原來的Task中。

NO[2]標記也是表示不建議這麼作,由於在非Activity中進行Layout Inflation,會使用系統默認的主題,而不是應用中設置的主題。

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

4、掌握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虛擬機會將這個虛引用加入與之關聯的引用隊列中。

5、其餘代碼微優化

(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模式(二)

相關文章
相關標籤/搜索