Android效能優化
Android效能優化
簡介
這篇文章主要打算從幾個方面講解一下怎麼去對app進行效能優化。不打算涉及任何概念相關的東西,比如渲染機制等等,因為網上已經夠多這方面的了。僅僅從自己的優化過程出發,列出一個能一步步去執行的清單和具體操作辦法。
啟動速度
檢視每個介面的開啟速度
在logcat中輸入Diaplayed即可檢視每個頁面的開啟時間。當然了,不同配置的手機開啟速度肯定是不同的,這個其實沒有太大的參考意義,只是知道就行。

image
設定閃屏啟動圖
一般是在splash頁的主題設定
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"><item name="android:windowBackground"@mipmap/launch</item>//閃屏頁圖片<item name="android:windowFullscreen"true</item><item name="android:windowContentOverlay"@null</item> </style>
這樣就會先展示一張圖片出來,而不是出現白屏的效果,視覺效果優化而已,稍微提升使用者體驗,但是其實並沒有加快啟動速度。
還有一種啟動圖的思路是不用單獨的Activity去載入廣告和啟動頁,而是做成SplashFragment直接放到主介面裡面,2,3秒之後移除。但是這種方法就沒辦法解決閃屏白屏的問題。這兩種可以酌情選擇吧。
application初始化
我們通常會在application裡面做一些第三方元件的初始化操作,比如圖片載入,資料庫等,如果初始化的東西太多,肯定會影響到app的啟動。建議將一些在開屏不必須的元件初始化放到子執行緒去做初始化操作,或者通過IntentService來做。
我自己就是通過IntentService來做的,因為IntentService的特點就是會開啟一個工作執行緒(HandlerThread)來處理耗時操作,並且在完成後會自動停止。
//初始化的IntentService,和普通的service啟動方式並沒有太大區別 Intent intent = new Intent(mContext, InitializeService.class); intent.setAction(ACTION_INIT_WHEN_APP_CREATE); startService(intent);
public class InitializeService extends IntentService { public InitializeService() { super("InitializeService"); } @Override protected void onHandleIntent(Intent intent) { //初始化操作(你自己定義的操作) init(); if (intent != null) { final String action = intent.getAction(); if (ACTION_INIT_WHEN_APP_CREATE.equals(action)) { //這裡是個網路請求(你自己定義的請求) performInit(); } } } }
我知道其實我就算這麼一說,很多小夥伴在操作的時候還是通常會按照原來的習慣去操作。因為現在手機的配置越來越好了,app本身就已經得到了buff加成。但是如果app啟動速度實在到了不可忍受的地步,可以參考我上面的做法
佈局優化
佈局巢狀層級過多。從前有個人說過一句話:沒有什麼佈局是一層巢狀解決不了的,如果有那就兩層。所以寫著寫著經常四五層巢狀就出來了,再加上文字圖片的繪製,分分鐘。。。
巢狀過多我們可以通過開發者選項裡面的除錯GPU過渡繪製開啟。然後在介面上會顯示出不同的顏色塊。

佈局層級顏色
通常我們可以從這幾個角度去優化:
-
在主題裡面使用WindowBackground屬性,而不是每個xml的根佈局裡面去新增android:background="@color/bg_set_frag"
-
減少巢狀層級,這裡我強烈推薦ConstraintLayout( ofollow,noindex">ConstraintLayout教程 ),但是我認識好多小夥伴都沒用過這個,還是習慣於常規的線性佈局,相對佈局,幀佈局這些。
-
檢查背景顏色設定,很多父佈局裡面設定了背景顏色,在子佈局裡面還設定同樣的顏色,這樣就會導致繪製兩遍。還有列表所在的佈局已經有顏色了,列表的item裡面還設定了跟他一樣的顏色。如果顏色不同另當別論。
-
還有就是善用<include>,<merge>,<ViewStub>
include就不介紹了這個還比較常用。
merge作用就是可以消除不必要的解析結點一般比較適用於兩種情況,第一種佈局頂結點是FrameLayout且不需要設定background或padding等屬性,可以用merge代替 。第二種某佈局作為子佈局被其他佈局include時,使用merge當作該佈局的頂節點,這樣在被引入時頂結點會自動被忽略,而將其子節點全部合併到主佈局中。
ViewStub可以理解為延遲載入的佈局,需要他的時候才去載入,而不是一次性全部載入進來,減輕了CPU和GPU的負擔。一般用來載入一些頁面狀態,比如沒網路,網路請求錯誤,空資料等狀態。你需要的時候才要他加載出來,不需要的時候他就乖乖等著又不吃你家大米。
<!-- 定義一個ViewStub 給其父Layout指定Id為inflatedStart --> <ViewStub android:id="@+id/stub" android:inflatedId="@+id/inflatedStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout="@layout/no_data" />
上面這個layout就是實際要顯示的佈局,看你需求去設定了。
View mNoDataView; ...... //比如載入空資料時候的狀態 public void showEmptyData() { if (mNoDataView == null) { ViewStub noDataViewStub = (ViewStub)findViewById(R.id.no_data_view); //這個mNoDataView就對應android:layout="@layout/no_data" mNoDataView = noDataViewStub.inflate(); } else { mNoDataView.setVisibility(View.VISIBLE); } }
記憶體優化
提到記憶體優化就不得不提記憶體洩漏。為什麼會產生記憶體洩漏呢?簡單來講就是長生命週期持有對短生命週期物件的引用,導致對無用物件的引用一直未被釋放,就會導致記憶體洩漏。這句話我見過很多版本,角度都各不相同,這段是我自己的總結。
常見的記憶體洩漏
1.內部類導致的洩漏。
- Thread執行緒導致的記憶體洩漏:在Activity中建立一個內部類去繼承Thread,然後讓該Thread執行一些後臺任務,未執行完時,關閉Activity,此時會記憶體洩露.
...... new Thread(new Runnable() { @Override public void run() { try { //模擬程式碼執行五秒鐘,在這期間會一直持有對外部Activity的引用 //此時如果Activity被銷燬了就會導致記憶體洩漏 Thread.sleep(5*1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); ......
解決辦法有兩種:1.保證任務在Activity銷燬之前完成。2.改成靜態內部類的形式
...... new Thread(new MyRunnable()); ...... static class MyRunnable implements Runnable{ @Override public void run() { try { Thread.sleep(5*1000); } catch (InterruptedException e) { e.printStackTrace(); } } } ......
- Handler導致的記憶體洩漏:內部類Handler會拿著外部類Activity的引用,而那個Message又拿著Handler的引用。這個Message又要在訊息佇列裡排隊等著被handler中的死迴圈來取訊息。從而形成了一個引用鏈,最後導致關於外部類Activity的引用不會被釋放。
//採用靜態內部類的形式建立Handler,並使用WeakReference引用Activity private static class MyHandler extends Handler { private final WeakReference<Main2Activity> mActivity; public MyHandler(Main2Activity activity) { mActivity = new WeakReference<Main2Activity>(activity); } @Override public void handleMessage(Message msg) { Main2Activity activity = mActivity.get(); if (activity != null) { // ...dosomething } } }
//在銷燬Activity之後清空handler裡面的訊息 @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }
- 非靜態內部類的靜態例項:非靜態的內部類建立了一個靜態例項。非靜態內部類會持有外部類Activity的引用,後來又建立了一個這個內部類的靜態例項。這個靜態例項不會在Activity被關掉時一塊被回收(注:靜態例項的生命週期跟應用的生命週期一樣長)。
解決方案:將內部類改為靜態內部類,使用弱引用或者軟引用的方式,來引用外部類。
2.Context導致的記憶體洩漏。
一般我們寫工具方法的時候會使用單例模式去建立一個物件,在構造器裡面要傳入一個context,這個context若是ApplicationContext就不會存在記憶體洩漏,但是如果是其他的context就會導致記憶體洩漏
public class SingleInstance { private Context mContext; private static SingleInstance instance; private SingleInstance(Context context) { //這樣寫會有記憶體洩漏 //this.mContext = context; //不管傳入什麼context,都應該使用ApplicationContext,單例的生命週期和應用的一樣長,這樣就防止了記憶體洩漏。 this.mContext = context.getApplicationContext(); } public static SingleInstance getInstance(Context context) { if (instance == null) { synchronized (SingleInstance.class) { if (instance == null) { instance = new SingleInstance(context); } } } return instance; } }
補充關於Context作用域的問題

Context的使用
3.其他可能導致記憶體洩漏的補充。
- 監聽器的登出:在Android程式裡面存在很多需要register與unregister的監聽器,我們需要確保在合適的時候及時unregister那些監聽器。自己手動add的listener,需要記得及時remove這個listener。比如說:廣播接收者BroadcastReceiver、EventBus等。
- Cursor物件和IO流是否及時關閉
- WebView的洩漏:Android中的WebView存在很大的相容性問題,不僅僅是Android系統版本的不同對WebView產生很大的差異,另外在不同手機廠商出貨的ROM裡面,WebView也存在著很大的差異。所以通常根治這個問題的辦法是為展示WebView的頁面開啟另外一個程序,通過AIDL與主程序進行通訊,WebView所在的程序可以根據業務的需要選擇合適的時機進行銷燬,從而達到記憶體的完整釋放。
- 圖片Bitmap的使用,採用軟引用,記得recycle