性能優化技巧知識梳理(2) 內存優化

1、前言

對於應用中的內存優化,和佈局優化相似,也有不少的技巧,這裏咱們分爲如下幾方面來總結:html

  • Java優化技巧
  • 避免沒必要要對象的建立
  • 保證不使用對象的釋放
  • 使用性能優化工具,定位內存問題

2、Java 優化技巧

首先,咱們介紹一些Java語法中的優化技巧,強烈推薦你們在編程時參考阿里巴巴編寫的<<阿里巴巴Java開發手冊>>,下載地址,這裏簡要介紹一些經常使用的知識點:android

  • 儘可能採用原始數據類型,而不是對象,例如int要比Integer佔用更少的內存。
  • 若是一個方法不須要訪問對象的成員變量,或者調用非靜態方法,那麼應當將它聲明爲static
  • 將常量聲明爲static final
  • 避免內部的getXXX()/setXXX()方法,而是直接訪問變量。
  • 使用加強的for循環,而不是for(int i = 0; i < 100; i++)這樣的循環。
  • 避免使用float類型,當對精度要求不高,採用int類型。

3、避免沒必要要對象的建立

(1) 單例對象在須要的時候初始化

在使用單例時,咱們應當僅在使用到該單例時纔去初始化它,這裏咱們能夠經過「靜態初始化會在類被加載時觸發」這一原理,來實現懶加載。數據庫

public class OptSingleton {
    
    private OptSingleton() {}
    
    public static OptSingleton getInstance() {
        return Holder.INSTANCE;
    }
    
    private static class Holder {
        public static final OptSingleton INSTANCE = new OptSingleton();
    }
}
複製代碼

(2) 避免進行自動裝箱

自動裝箱指的是將原始的數據類型轉換成爲引用類型,例如int轉換成爲Integer,這種自動裝箱操做,雖然方便了咱們的使用,可是在某些場景下的不當使用有可能會致使性能問題,主要有兩點:編程

  • 第一點:使用操做符時的自動裝箱
public static void badAssemble() {
        Integer sum = 0;
        for (int i = 0; i < (1 << 30); i++) {
            sum = sum + i;
        }
    }
複製代碼

就有自動裝箱的過程,其中sum+i能夠分解爲下面這兩句,也就是說,在循環的過程當中,咱們建立了大量的臨時對象Integer,而建立完以後,它們很快又會被GC回收掉,所以,會出現內存抖動的現象。數組

int result = sum + i;
Integer sum = new Integer(result);
複製代碼

咱們使用Android Studio提供的檢測工具能夠驗證上面的結論: 瀏覽器

而若是咱們使用正常的寫法,那麼是不會出現上面的狀況的:

public static void badAssemble() {
        int sum = 0;
        for (int i = 0; i < (1 << 30); i++) {
            sum = sum + i;
        }
    }
複製代碼

此時的監測結果爲: 安全

  • 第二點:使用容器時的自動裝箱

當咱們使用例如HashMap這種容器的時候,除了要存儲保存的數據,還要存儲Key值,這些Key值就是由自動裝箱的過程所產生的。性能優化

此時,咱們就能夠考慮選用Android平臺上提供的優化容器來儘量地避免裝箱操做,這些容器包括SparseArraySparseBooleanArraySparseIntArraySparseLongArray,這些容器有如下特色:bash

  • key值都爲原始數據類型int,避免了隱式裝箱的操做,這同時也是它的侷限性。
  • 其內部是經過兩個數組存儲數據的,一個用於key,另外一個用於value,爲了優化性能,它內部對數據還採起了壓縮的方式來表示稀疏數組的數據,從而節約內存空間。
  • 在查找數據時,採用的是二分查找法,相比於HashMap須要遍歷Entry數組找到相等的hash值,通常來講,咱們的數據量都不會太大,而在數據量較小時,二分查找要比遍歷數組,查找速度更快。

(3) 預先指定容器的大小

當咱們使用例如HashMapArrayList這些容器時,每每不習慣給它們指定一個初始值,然而當這些容器存儲空間不足時,就會去自動擴容,其擴容的大小每每是原始大小的兩倍。網絡

所以,當咱們須要存儲額外的一個元素的時候恰好容器不夠了,那麼就須要擴容,可是這時候就會出現額外的浪費空間。

(4) 對於佔用資源的 Activity,合理的使用 LaunchMode

對於Activity來講,其默認的啓動模式是standard,也就是說,每次啓動這個Activity,都會建立一個新的實例,像相似於瀏覽器這種內存大戶,每次外部打開一個網頁,都須要建立一個Activity,而Activity又會去實例化WebView,那麼是至關耗費資源的,這時,咱們就能夠考慮使用singleTask或者singleInstance來實現。

(5) 處理屏幕旋轉致使的重建

當屏幕發生旋轉時,若是咱們沒有在AndroidManifest.xml中,對其configChanges屬性進行聲明,那麼就會致使Activity進行重建,此時,就須要從新加載Activity所須要展現的數據。

此時,咱們就能夠對其進行以下的聲明:

android:configChanges="keyboardHidden|orientation|screenSize"
複製代碼

接着在ActivityonConfigurationChanged進行監聽,對佈局進行相應的改變,而不須要從新加載數據。

(6) 處理字符串拼接

在代碼中,咱們常用到字符串拼接的操做,這裏有兩點注意:

採用高效的拼接方式

例以下面的操做,就會建立大量的臨時對象:

public static void badString() {
        String result = "result";
        String append = "append";
        for (int i = 0; i < (1 << 30); i++) {
            result += append;
        }
    }
複製代碼

內存檢測的結果以下,能夠發現,咱們出現了大量內存抖動的狀況:

而若是咱們採用 StringBuilder的方式進行拼接:

public static void goodString() {
        StringBuilder result = new StringBuilder("result");
        String append = "append";
        for (int i = 0; i < (1 << 20); i++) {
            result.append(append);
        }
    }
複製代碼

那麼最終的結果爲:

所以,在處理字符串拼接的時候,應當儘可能避免直接使用"+"號,而是使用如下兩種方式的一種:

  • 使用靜態方法,String.format方法進行拼接。
  • 非線程安全的StringBuilder,或者是線程安全的StringBuffer

避免沒必要要的字符串拼接

當咱們須要打印Log時,通常會將它們寫在一個公共類中,而後使用一個DEBUG開關,讓他們在外發版本上關閉:

private static final boolean DEBUG = true;
    
    public static void LogD(String tag, String msg) {
        if (DEBUG) {
            Log.d(tag, msg);
        }
    }
複製代碼

可是這種方式有一點弊端,就是,咱們在調用該方法時msg通常都是經過拼接多個字符串進行傳入的,也就是說,即便沒有打印該Log,也會進行字符串拼接的操做,所以,咱們應當儘可能將DEBUG開關放在字符串拼接的外部,避免沒必要要拼接操做。

(7) 減小沒必要要的異常

在某些時候,若是咱們能預見到某些有可能會發生異常的場景,那麼提早進行判斷,將能夠避免因爲異常所帶來的代價,以啓動第三方應用爲例,咱們能夠先判斷該intent所對應的應用是否存在,再去啓動它,而不是等到異常發生時再去捕獲:

public static void startApp(Context context) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("www.qq.com"));
        intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));
        if (intent.resolveActivity(context.getPackageManager()) == null) {
            return;
        }
        try {
            context.startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

(8) 線程複用

當執行異步操做時,不要經過new Thread的方式啓動一個新的線程來執行操做,而是儘量地對已經建立的線程進行復用,通常來講,主要有兩種方式:

(9) 合理的適應對象池

例如,咱們最經常使用的Handler發送消息,當須要建立一個消息時,可使用Handler提供的obtainMessage方法,獲取到Message對象,其內部,就會去Message中所維護的一個靜態鏈表中,查找當前可用的Message對象,並將其標誌位置爲0,代表其正在使用。

使用對象池時,應當注意兩點:

  • 將對象放回對象池時,注意初始化,防止出現髒數據。
  • 合理的控制對象池的增加,防止出現大量無用對象。

(10) 使用 inBitmap 對內存塊複用

inBitmap指的是複用內存塊,不須要從新給這個Bitmap申請一塊新的內存,避免了一次內存的分配和回收,關於inBitmap的詳細解釋,能夠參見這篇文章,Managing Bitmap Memory,其Demo對應的下載地址,對於inBItmap屬性的使用,有如下兩點限制:

  • 該屬性只能在3.0以後使用,在2.3上,bitmap的數據是存儲在native的內存區域中。
  • 4.4以前,inBitmap只能重用相同大小的bitmap內存區域,而在4.4以後,能夠重用任何bitmap內存區域,只要這塊內存比將要分配的內存大就能夠。

(11) 使用註解替代枚舉

public class Constant {

    public static final int FLAG_START = 0;
    public static final int FLAG_STOP = 1;
    public static final int FLAG_PAUSE = 2;

    @IntDef({FLAG_START, FLAG_STOP, FLAG_PAUSE})
    public @interface VideoState {}
}
複製代碼

當咱們定義的形參時,在參數以前,加上以前定義的註解:

public static void accept(@Constant.VideoState int videoState) {
        Log.d("OptUtils", "state=" + videoState);
    }
複製代碼

若是咱們傳入了不屬於上面的三個值,那麼IDE就會警告咱們:

(12) 謹慎初始化 Application

當咱們在項目當中,引入一些第三方庫,或者將一些組件放到其它進程,加入咱們自定義了Application的子類,而且在AndroidManifest.xml中進行了聲明,那麼在啓動這些運行在其它進程中的組件時,就會調用該ApplicationonCreate()方法,此時,咱們就應當根據進程所要求的資源進行初始化。

例以下面,咱們將RemoteActivity聲明在remote進程當中,而且給application指定了自定義的OptApplication

<application
        android:name=".OptApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".OptActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".RemoteActivity" android:process=":remote"/>
    </application>
複製代碼

OptApplication中,判斷一下調用該方法進程名,進行不一樣邏輯的初始化操做:

public class OptApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if (isMainProcess()) {
            //對主進程的資源進行初始化。
            Log.d("OptApplication", "isMainProcess=" + true);
        } else {
            //對其它進程資源進行初始化。
            Log.d("OptApplication", "isMainProcess=" + false);
        }
    }

    private boolean isMainProcess() {
        ActivityManager am = ((ActivityManager) getSystemService(Context.ACTIVITY_SERVICE));
        List<ActivityManager.RunningAppProcessInfo> process = am.getRunningAppProcesses();
        String mainProcessName = getPackageName();
        int myPid = android.os.Process.myPid();
        for (ActivityManager.RunningAppProcessInfo info : process) {
            if (info.pid == myPid && mainProcessName.equals(info.processName)) {
                return true;
            }
        }
        return false;
    }
}
複製代碼

(13) 避免在 onDraw 方法中建立對象

onDraw方法中建立臨時對象,不只會影響繪製的性能,並且這些臨時對象在onDraw方法執行完以後又很快被回收,那麼將會形成內存抖動。

(14) 合理地使用 ArrayMap 替代 HashMap

前面咱們介紹了SparseArray,它的侷限性是其key值只能爲原始數據類型int,而若是咱們要求它的key值爲引用類型時,那麼能夠考慮使用ArrayMap

SparseArray同樣,它會對key使用二分法進行添加、查找、刪除等操做,在添加、刪除、查找數據的時候都是先使用二分查找法獲得相應的index,而後經過index進行添加、查找、刪除操做。

若是在數據量較大的狀況,那麼它的性能將退化至少50%

(15) 謹慎使用抽象編程

抽象可以提高代碼的靈活性與可維護性,然而,抽象會致使一個顯著的額外內存開銷:它們須要同等量的代碼用於可執行,這些代碼會被mapping到內存中。

(16) 使用 Protocol Buffers

在平時的網絡數據傳輸時,通常用的最多的是JSON或者xml,而Protocal BuffersGoogle爲序列化結構數據而設計的,相比於普通的數據傳輸方式,它具備如下優勢:

  • 編碼/解碼方式簡單
  • 序列化 & 反序列化 & 速度快
  • 數據壓縮效果更好

關於Protocol Buffers的詳細介紹,你們能夠閱讀 Carson_Ho 所寫的一系列文章,推薦閱讀:Protocol Buffer 序列化原理大揭祕 - 爲何Protocol Buffer性能這麼好?

(17) 謹慎使用依賴注入框架

諸如Guice或者RoboGuice這些依賴注入框架,它們能夠減小大量findViewById的繁瑣操做,可是這些註解的框架爲了要搜尋代碼中的註解,一般都須要經歷較長的初始化過程,而且還可能將一些你用不到的對象也一併加載到內存當中,這些用不到的對象會一直佔用內存空間,等到好久以後才釋放。

(18) 謹慎使用多進程

在咱們有大量須要運行在後臺的任務,例如音樂、視頻、下載等業務,那麼能夠將它們放在獨立的進程當中。可是,咱們不該當濫用它們,由於每建立一個新的進程,那麼必然要分配一些內存來保存該進程的一些信息,這都將增長內存的佔用。

(19) 使用 ProGurad 優化代碼

經過ProGuard對代碼進行優化、壓縮、混淆,能夠移除不須要的代碼、重命名類、域與方法等,作法就是在buildTypes的指定類型下增長下面的代碼:

buildTypes {
        release {
            //對於release版本採用進行混淆。
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
        }
        debug {
            //對於debug版本不混淆。
            minifyEnabled false
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
        }
複製代碼

這裏的混淆規文件有兩份,若是有多份,那麼可使用逗號分隔,第一個是Android自帶的混淆文件,而第二個則是應用自定義的混淆規則文件,關於混淆文件的語法,能夠參考這篇文章: ProGuard 代碼混淆技術詳解

(20) 謹慎使用第三方 Library

在項目中引入第三方Library時,應當注意如下幾點:

  • 不要導入無用的功能:若是須要使用到定位功能,那麼就只須要導入定位的Library便可,不要引入導航等Library
  • 不要導入功能重複的Library:目前存在不少開源的第三方網絡框架,例如Volley/OkHttp/Retrofit等,那麼在咱們引入一個新的網絡框架時應當先檢查代碼中原有的網絡框架,將以前的代碼都替換成爲新的框架,而不是導入多份。
  • 使用爲移動平臺定製的Library:不少開源項目都會針對移動平臺進行項目的優化與裁剪,咱們應當首先考慮使用擁有這些版本的開源庫。

(21) 使用 AnimatedVectorDrawable 替換幀動畫

圖片壓縮知識梳理(6) - VectorDrawable 及 AnimatedVectorDrawable 使用詳解 中,咱們介紹了AnimatedVectorDrawable的使用,在須要實現一些簡單圖形的動畫時,它比幀動畫效率更高、佔用內存更小。

(22) 讀取和屏幕分辨率匹配的圖片

當咱們讀取圖片時,應當儘可能結合當前手機的分辨率進行處理,這裏有兩點建議:

  • 在圖片加載到內存以前,對其進行縮放,避免加載進入過大的圖片,以從資源文件中讀取圖片爲例,咱們傳入預期的寬高,先將Bitmap.ConfiginJustDecodeBounds置爲true,獲取到目標圖片的寬高而不是將整張圖片都加載到內存中,在根據預期的寬高計算出一個比例,去加載一個適合屏幕分辨率的圖片,具體的操做以下面的代碼塊所示:
public static int calculateInSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) {
        int srcWidth = options.outWidth;
        int srcHeight = options.outHeight;
        int inSampleSize = 1;
        if(srcHeight > dstHeight && srcWidth > dstHeight) {
            int halfWidth = srcWidth / 2;
            int halfHeight = srcHeight / 2;
            while ((halfHeight / inSampleSize) > dstHeight && (halfWidth / inSampleSize) > dstWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

    public static Bitmap decodeResource(Resources res, @DrawableRes int resId, Bitmap.Config config, int dstWidth, int dstHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = config;
        if(dstWidth <= 0 && dstHeight <= 0) {
            return BitmapFactory.decodeResource(res, resId, options);
        }
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, dstWidth, dstHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
複製代碼
  • 將圖片放在與屏幕分辨率匹配的文件夾當中

圖片基礎知識梳理(2) - Bitmap 佔用內存分析 一文當中,咱們分析過,在res目錄下能夠創建多個不一樣的圖片文件夾,即drawable-xhpi/drawable-xxhdpi/drawable-xxxhdpi,只有當圖片放在機型對應分辨率下的文件夾時,纔不會進行縮放操做,若是某張圖片放在比它分辨率低的文件夾當中,那麼將會進行放大操做,不只會使圖片變得模糊,還要佔用額外的內存。

所以,咱們應當將圖片放在對應機型分辨率的文件夾當中。

3、保證不使用對象的釋放

(1) 避免 Activity 泄露

Activity泄露是咱們在開發中最長碰見的內存泄露類型,下面總結幾點你們比較容易犯的錯誤:

在 Activity 中定義非靜態的 Handler 內部類

例以下面這樣,咱們在Activity中定義了一個非靜態的內部類LeakHandler,那麼做爲內部類,leakHandler默認持有外部類的實例,也就是LeakActivity

public class LeakActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LeakHandler leakHandler = new LeakHandler();
        leakHandler.sendEmptyMessageDelayed(0, 50000);
    }
    
    private class LeakHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}
複製代碼

在調用了sendEmptyMessageDelayed以後,那麼會建立一個Message對象放到Looper的隊列MessageQueue當中等待被執行,而該Message中的target會執行發送它的Handler,也就是LeakHandler,那麼在該消息被處理以前,會一直存在一條從LeakActivityMessageQueue的引用鏈,所以,在這段時間內若是Activity被銷燬,它的內存也沒法釋放,就是形成內存泄露。

對於這種問題,有如下幾個處理的技巧:

  • Handler定義爲靜態內部類,這樣它就不會持有外部的類的引用,若是須要在handleMessage中調用Activity中的方法,那麼能夠傳入它做爲參數,並持有它的弱引用以保證它可以回收。
private static class SafeHandler extends Handler {
        
        private WeakReference<Activity> mActHolder;
        
        public SafeHandler(Activity activity) {
            mActHolder = new WeakReference<>(activity);    
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mActHolder != null) {
                Activity activity = mActHolder.get();
                if (activity != null && !activity.isDestroyed()) {
                    //僅在 Activity 沒有被銷燬時,才執行操做。
                }
            }
        }
    }
複製代碼
  • ActivityonDestroy()方法中,經過removeCallbacksAndMessages(null)方法移除全部未執行的消息。

單例中的成員變量或者 static 成員變量持有了 Activity 的引用

根據持有的方式,能夠簡單地分爲直接持有、間接持有兩種類型:

  • 直接持有:在Android的不少Api中,都會使用到上下文信息Context,而Activity繼承於Context類,所以咱們常常會將它傳給其它類,並將它做爲這些類的成員變量以便後續的操做,那麼若是這個成員變量所屬的類是一個單例,或者說它是該類中的一個靜態成員變量,那麼就會致使該Activity所佔用的內存沒法被釋放。
  • 間接持有:某個中間對象持有了Activity,而該中間對象又做爲了單例中的成員變量或者某類中的static成員變量,這些對象最多見的有如下兩類: (a) Activity的非靜態內部類,例如監聽器,那麼它就會默認持有Activity的引用。 (b) Activity中的控件,其mContext變量指向了它所屬的Activity

當出現這種狀況時,咱們應當注意這幾點:

  • 若是可使用ApplicationContext,那麼就用Activity.getApplicationContext()來替代,不要用Activity
  • 若是必須使用Activity,那麼確保在ActivityonDestroy()方法執行時,將它們到Activity的引用鏈千方百計切斷,將引用設爲空,或者註銷監聽器。

固然不只是Activity,對於應用當中的某些大對象,例如Bitmap等,咱們也應當注意,是否出了相似於上面這種直接和間接引用的狀況。

(2) 對於只執行一次的後臺任務,使用 IntentService 替代 Service

當咱們須要將某些任務的生命週期和Activity分離開來,那麼通常會使用Service,可是Service就須要咱們進行手動管理,若是忘記,那麼將會致使額外的內存佔用,而且擁有Service進程的oom_adj值通常會高於沒有Service的進程,系統會更傾向於將它保留。

對於一些短時的後臺任務,咱們能夠考慮採用IntentService,它的onHandleIntent回調是在異步線程中執行的,而且任務執行完畢後,該Service會自動銷燬,不須要手動管理。

(3) 在 onLowMemory() / onTrimMemory() 回調當中,釋放沒必要要的資源

爲了能讓各個應用知曉當前系統內存的使用狀況,提供了兩種類型的回調onLowMemoryonTrimMemory,在ApplicationActivityFragementServiceContentProvider這些組件中,均可以收到這兩個回調,進行相應的處理。

onLowMemory

當最後一個後臺應用(優先級爲background的進程)被殺死以後,前臺應用就會收到onLowMemory回調。

onTrimMemory(int level)

onLowMemory相比,onTrimMemory的回調更加頻繁,每次計算進程優先級時,只要知足對應的條件,就會觸發。level參數則代表了當前內存的佔用狀況,各等級的解釋以下表所示,等級從上到下,進程被殺的可能性逐漸增大:

咱們應當根據當前的等級,釋放掉一些沒必要要的內存,以避免應用進程被殺死。

(4) 及時關閉 Cursor

不管是使用數據庫,仍是ContentProvider來查詢數據,在查詢完畢以後,必定要記得關閉Cursor

4、使用性能優化工具,定位內存問題

關於內存的優化工具,以前一系列的文章已經介紹過了,你們能夠查看下面這三篇文章:

5、特別鳴謝

以上的總結,借鑑了網上幾位大神的總結,特此鳴謝:

參考的文章包括如下幾篇:


更多文章,歡迎訪問個人 Android 知識梳理系列:

相關文章
相關標籤/搜索