對於應用中的內存優化,和佈局優化相似,也有不少的技巧,這裏咱們分爲如下幾方面來總結:html
Java
優化技巧首先,咱們介紹一些Java
語法中的優化技巧,強烈推薦你們在編程時參考阿里巴巴編寫的<<阿里巴巴Java
開發手冊>>,下載地址,這裏簡要介紹一些經常使用的知識點:android
int
要比Integer
佔用更少的內存。static
。static final
。getXXX()/setXXX()
方法,而是直接訪問變量。for
循環,而不是for(int i = 0; i < 100; i++)
這樣的循環。float
類型,當對精度要求不高,採用int
類型。在使用單例時,咱們應當僅在使用到該單例時纔去初始化它,這裏咱們能夠經過「靜態初始化會在類被加載時觸發」這一原理,來實現懶加載。數據庫
public class OptSingleton {
private OptSingleton() {}
public static OptSingleton getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
public static final OptSingleton INSTANCE = new OptSingleton();
}
}
複製代碼
自動裝箱指的是將原始的數據類型轉換成爲引用類型,例如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
平臺上提供的優化容器來儘量地避免裝箱操做,這些容器包括SparseArray
、SparseBooleanArray
、SparseIntArray
、SparseLongArray
,這些容器有如下特色:bash
key
值都爲原始數據類型int
,避免了隱式裝箱的操做,這同時也是它的侷限性。key
,另外一個用於value
,爲了優化性能,它內部對數據還採起了壓縮的方式來表示稀疏數組的數據,從而節約內存空間。HashMap
須要遍歷Entry
數組找到相等的hash
值,通常來講,咱們的數據量都不會太大,而在數據量較小時,二分查找要比遍歷數組,查找速度更快。當咱們使用例如HashMap
、ArrayList
這些容器時,每每不習慣給它們指定一個初始值,然而當這些容器存儲空間不足時,就會去自動擴容,其擴容的大小每每是原始大小的兩倍。網絡
所以,當咱們須要存儲額外的一個元素的時候恰好容器不夠了,那麼就須要擴容,可是這時候就會出現額外的浪費空間。
對於Activity
來講,其默認的啓動模式是standard
,也就是說,每次啓動這個Activity
,都會建立一個新的實例,像相似於瀏覽器這種內存大戶,每次外部打開一個網頁,都須要建立一個Activity
,而Activity
又會去實例化WebView
,那麼是至關耗費資源的,這時,咱們就能夠考慮使用singleTask
或者singleInstance
來實現。
當屏幕發生旋轉時,若是咱們沒有在AndroidManifest.xml
中,對其configChanges
屬性進行聲明,那麼就會致使Activity
進行重建,此時,就須要從新加載Activity
所須要展現的數據。
此時,咱們就能夠對其進行以下的聲明:
android:configChanges="keyboardHidden|orientation|screenSize"
複製代碼
接着在Activity
的onConfigurationChanged
進行監聽,對佈局進行相應的改變,而不須要從新加載數據。
在代碼中,咱們常用到字符串拼接的操做,這裏有兩點注意:
例以下面的操做,就會建立大量的臨時對象:
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
開關放在字符串拼接的外部,避免沒必要要拼接操做。
在某些時候,若是咱們能預見到某些有可能會發生異常的場景,那麼提早進行判斷,將能夠避免因爲異常所帶來的代價,以啓動第三方應用爲例,咱們能夠先判斷該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();
}
}
複製代碼
當執行異步操做時,不要經過new Thread
的方式啓動一個新的線程來執行操做,而是儘量地對已經建立的線程進行復用,通常來講,主要有兩種方式:
HandlerThread + Handler
的方式,將須要執行的異步操做,用Runnable
封裝起來,放入到子線程的隊列當中執行,其原理能夠參考,Framework 源碼解析知識梳理(4) - 從源碼角度談談 Handler 的應用。ThreadPoolExecutor
,並根據具體狀況定義參數,關於具體的應用能夠參考:多線程知識梳理(6) - 線程池四部曲之 ThreadPoolExecutor。例如,咱們最經常使用的Handler
發送消息,當須要建立一個消息時,可使用Handler
提供的obtainMessage
方法,獲取到Message
對象,其內部,就會去Message
中所維護的一個靜態鏈表中,查找當前可用的Message
對象,並將其標誌位置爲0
,代表其正在使用。
使用對象池時,應當注意兩點:
inBitmap
指的是複用內存塊,不須要從新給這個Bitmap
申請一塊新的內存,避免了一次內存的分配和回收,關於inBitmap
的詳細解釋,能夠參見這篇文章,Managing Bitmap Memory,其Demo
對應的下載地址,對於inBItmap
屬性的使用,有如下兩點限制:
3.0
以後使用,在2.3
上,bitmap
的數據是存儲在native
的內存區域中。4.4
以前,inBitmap
只能重用相同大小的bitmap
內存區域,而在4.4
以後,能夠重用任何bitmap
內存區域,只要這塊內存比將要分配的內存大就能夠。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
就會警告咱們:
當咱們在項目當中,引入一些第三方庫,或者將一些組件放到其它進程,加入咱們自定義了Application
的子類,而且在AndroidManifest.xml
中進行了聲明,那麼在啓動這些運行在其它進程中的組件時,就會調用該Application
的onCreate()
方法,此時,咱們就應當根據進程所要求的資源進行初始化。
例以下面,咱們將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;
}
}
複製代碼
在onDraw
方法中建立臨時對象,不只會影響繪製的性能,並且這些臨時對象在onDraw
方法執行完以後又很快被回收,那麼將會形成內存抖動。
前面咱們介紹了SparseArray
,它的侷限性是其key
值只能爲原始數據類型int
,而若是咱們要求它的key
值爲引用類型時,那麼能夠考慮使用ArrayMap
。
和SparseArray
同樣,它會對key
使用二分法進行添加、查找、刪除等操做,在添加、刪除、查找數據的時候都是先使用二分查找法獲得相應的index
,而後經過index
進行添加、查找、刪除操做。
若是在數據量較大的狀況,那麼它的性能將退化至少50%
。
抽象可以提高代碼的靈活性與可維護性,然而,抽象會致使一個顯著的額外內存開銷:它們須要同等量的代碼用於可執行,這些代碼會被mapping
到內存中。
在平時的網絡數據傳輸時,通常用的最多的是JSON
或者xml
,而Protocal Buffers
是Google
爲序列化結構數據而設計的,相比於普通的數據傳輸方式,它具備如下優勢:
關於Protocol Buffers
的詳細介紹,你們能夠閱讀 Carson_Ho 所寫的一系列文章,推薦閱讀:Protocol Buffer 序列化原理大揭祕 - 爲何Protocol Buffer性能這麼好?。
諸如Guice
或者RoboGuice
這些依賴注入框架,它們能夠減小大量findViewById
的繁瑣操做,可是這些註解的框架爲了要搜尋代碼中的註解,一般都須要經歷較長的初始化過程,而且還可能將一些你用不到的對象也一併加載到內存當中,這些用不到的對象會一直佔用內存空間,等到好久以後才釋放。
在咱們有大量須要運行在後臺的任務,例如音樂、視頻、下載等業務,那麼能夠將它們放在獨立的進程當中。可是,咱們不該當濫用它們,由於每建立一個新的進程,那麼必然要分配一些內存來保存該進程的一些信息,這都將增長內存的佔用。
經過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 代碼混淆技術詳解。
在項目中引入第三方Library
時,應當注意如下幾點:
Library
便可,不要引入導航等Library
。Library
:目前存在不少開源的第三方網絡框架,例如Volley/OkHttp/Retrofit
等,那麼在咱們引入一個新的網絡框架時應當先檢查代碼中原有的網絡框架,將以前的代碼都替換成爲新的框架,而不是導入多份。Library
:不少開源項目都會針對移動平臺進行項目的優化與裁剪,咱們應當首先考慮使用擁有這些版本的開源庫。在 圖片壓縮知識梳理(6) - VectorDrawable 及 AnimatedVectorDrawable 使用詳解 中,咱們介紹了AnimatedVectorDrawable
的使用,在須要實現一些簡單圖形的動畫時,它比幀動畫效率更高、佔用內存更小。
當咱們讀取圖片時,應當儘可能結合當前手機的分辨率進行處理,這裏有兩點建議:
Bitmap.Config
的inJustDecodeBounds
置爲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
,只有當圖片放在機型對應分辨率下的文件夾時,纔不會進行縮放操做,若是某張圖片放在比它分辨率低的文件夾當中,那麼將會進行放大操做,不只會使圖片變得模糊,還要佔用額外的內存。
所以,咱們應當將圖片放在對應機型分辨率的文件夾當中。
Activity
泄露是咱們在開發中最長碰見的內存泄露類型,下面總結幾點你們比較容易犯的錯誤:
例以下面這樣,咱們在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
,那麼在該消息被處理以前,會一直存在一條從LeakActivity
到MessageQueue
的引用鏈,所以,在這段時間內若是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 沒有被銷燬時,才執行操做。
}
}
}
}
複製代碼
Activity
的onDestroy()
方法中,經過removeCallbacksAndMessages(null)
方法移除全部未執行的消息。根據持有的方式,能夠簡單地分爲直接持有、間接持有兩種類型:
Android
的不少Api
中,都會使用到上下文信息Context
,而Activity
繼承於Context
類,所以咱們常常會將它傳給其它類,並將它做爲這些類的成員變量以便後續的操做,那麼若是這個成員變量所屬的類是一個單例,或者說它是該類中的一個靜態成員變量,那麼就會致使該Activity
所佔用的內存沒法被釋放。Activity
,而該中間對象又做爲了單例中的成員變量或者某類中的static
成員變量,這些對象最多見的有如下兩類: (a) Activity
的非靜態內部類,例如監聽器,那麼它就會默認持有Activity
的引用。 (b) Activity
中的控件,其mContext
變量指向了它所屬的Activity
。當出現這種狀況時,咱們應當注意這幾點:
Application
的Context
,那麼就用Activity.getApplicationContext()
來替代,不要用Activity
。Activity
,那麼確保在Activity
的onDestroy()
方法執行時,將它們到Activity
的引用鏈千方百計切斷,將引用設爲空,或者註銷監聽器。固然不只是Activity
,對於應用當中的某些大對象,例如Bitmap
等,咱們也應當注意,是否出了相似於上面這種直接和間接引用的狀況。
當咱們須要將某些任務的生命週期和Activity
分離開來,那麼通常會使用Service
,可是Service
就須要咱們進行手動管理,若是忘記,那麼將會致使額外的內存佔用,而且擁有Service
進程的oom_adj
值通常會高於沒有Service
的進程,系統會更傾向於將它保留。
對於一些短時的後臺任務,咱們能夠考慮採用IntentService
,它的onHandleIntent
回調是在異步線程中執行的,而且任務執行完畢後,該Service
會自動銷燬,不須要手動管理。
爲了能讓各個應用知曉當前系統內存的使用狀況,提供了兩種類型的回調onLowMemory
和onTrimMemory
,在Application
、 Activity
、Fragement
、Service
、ContentProvider
這些組件中,均可以收到這兩個回調,進行相應的處理。
當最後一個後臺應用(優先級爲background
的進程)被殺死以後,前臺應用就會收到onLowMemory
回調。
與onLowMemory
相比,onTrimMemory
的回調更加頻繁,每次計算進程優先級時,只要知足對應的條件,就會觸發。level
參數則代表了當前內存的佔用狀況,各等級的解釋以下表所示,等級從上到下,進程被殺的可能性逐漸增大:
不管是使用數據庫,仍是ContentProvider
來查詢數據,在查詢完畢以後,必定要記得關閉Cursor
。
關於內存的優化工具,以前一系列的文章已經介紹過了,你們能夠查看下面這三篇文章:
以上的總結,借鑑了網上幾位大神的總結,特此鳴謝:
參考的文章包括如下幾篇: