1. 程式人生 > >Android優化總結

Android優化總結

目錄介紹

  • 1.OOM和崩潰優化
    • 1.1 OOM優化
    • 1.2 ANR優化
    • 1.3 Crash優化
  • 2.記憶體洩漏優化
    • 2.0 動畫資源未釋放
    • 2.1 錯誤使用單利
    • 2.2 錯誤使用靜態變數
    • 2.3 handler記憶體洩漏
    • 2.4 執行緒造成記憶體洩漏
    • 2.5 非靜態內部類
    • 2.6 未移除監聽
    • 2.7 持有activity引用
    • 2.8 資源未關閉
    • 2.9 其他原因
  • 3.佈局優化
    • 3.1 include優化
    • 3.2 ViewStub優化
    • 3.3 merge優化
    • 3.4 其他建議
  • 4.程式碼優化
    • 4.1 lint程式碼檢測
    • 4.2 程式碼規範優化
    • 4.3 View異常優化
    • 4.4 去除淡黃色警告優化
    • 4.5 合理使用集合
    • 4.6 Activity不可見優化
    • 4.7 節制的使用Service
  • 5.網路優化
    • 5.1 圖片分類
    • 5.2 獲取網路資料優化
    • 5.3 網路請求異常攔截優化
  • 6.執行緒優化
    • 6.1 使用執行緒池
  • 7.圖片優化
    • 7.1 bitmap優化
    • 7.2 glide載入優化
  • 8.載入優化
    • 8.1 懶載入優化
    • 8.2 啟動頁優化
  • 9.其他優化
    • 9.1 靜態變數優化
    • 9.2 註解替代列舉
    • 9.3 多渠道打包優化
    • 9.4 TrimMemory和LowMemory優化
    • 9.5 輪詢操作優化
    • 9.6 去除重複依賴庫優化
    • 9.7 四種引用優化
    • 9.8 載入loading優化
    • 9.9 物件池Pools優化
  • 10.RecyclerView優化
    • 10.1 頁面為何卡頓
    • 10.2 具體優化方案

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

1.OOM和崩潰優化

1.2 ANR優化

  • ANR的產生需要滿足三個條件
    • 主執行緒:只有應用程式程序的主執行緒響應超時才會產生ANR;
    • 超時時間:產生ANR的上下文不同,超時時間也會不同,但只要在這個時間上限內沒有響應就會ANR;
    • 輸入事件/特定操作:輸入事件是指按鍵、觸屏等裝置輸入事件,特定操作是指BroadcastReceiver和Service的生命週期中的各個函式,產生ANR的上下文不同,導致ANR的原因也會不同;
  • ANR優化具體措施
    • 將所有耗時操作,比如訪問網路,Socket通訊,查詢大量SQL 語句,複雜邏輯計算等都放在子執行緒中去,然 後通過handler.sendMessage、runonUIThread、AsyncTask 等方式更新UI。無論如何都要確保使用者介面作的流暢 度。如果耗時操作需要讓使用者等待,那麼可以在介面上顯示度條。
    • 使用AsyncTask處理耗時IO操作。在一些同步的操作主執行緒有可能被鎖,需要等待其他執行緒釋放相應鎖才能繼續執行,這樣會有一定的ANR風險,對於這種情況有時也可以用非同步執行緒來執行相應的邏輯。另外,要避免死鎖的發生。
    • 使用Handler處理工作執行緒結果,而不是使用Thread.wait()或者Thread.sleep()來阻塞主執行緒。
    • Activity的onCreate和onResume回撥中儘量避免耗時的程式碼
    • BroadcastReceiver中onReceive程式碼也要儘量減少耗時,建議使用IntentService處理。
    • 各個元件的生命週期函式都不應該有太耗時的操作,即使對於後臺Service或者ContentProvider來講,應用在後臺執行時候其onCreate()時候不會有使用者輸入引起事件無響應ANR,但其執行時間過長也會引起Service的ANR和ContentProvider的ANR

2.記憶體洩漏優化

  • 記憶體檢測第一種:程式碼方式獲取記憶體
    /**
     * 記憶體使用檢測:可以呼叫系統的getMemoryInfo()來獲取當前記憶體的使用情況
     */
    private void initMemoryInfo() {
        ActivityManager activityManager = (ActivityManager) Utils.getApp()
                .getSystemService(Context.ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
        if (activityManager != null) {
            activityManager.getMemoryInfo(memoryInfo);
            LogUtils.d("totalMem=" + memoryInfo.totalMem + ",availMem=" + memoryInfo.availMem);
            if (!memoryInfo.lowMemory) {
                // 執行在低記憶體環境
            }
        }
    }
    
  • 記憶體檢測第二種:leakcanary工具
    • LeakCanary的原理是監控每個activity,在activity ondestory後,在後臺執行緒檢測引用,然後過一段時間進行gc,gc後如果引用還在,那麼dump出記憶體堆疊,並解析進行視覺化顯示。

2.0 動畫資源未釋放

  • 問題程式碼
    public class LeakActivity extends AppCompatActivity {
        private TextView textView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_leak);
            textView = (TextView)findViewById(R.id.text_view);
            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView,"rotation",0,360);
            objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
            objectAnimator.start();
        }
    }
    
  • 解決辦法
    • 在屬性動畫中有一類無限迴圈動畫,如果在Activity中播放這類動畫並且在onDestroy中去停止動畫,那麼這個動畫將會一直播放下去,這時候Activity會被View所持有,從而導致Activity無法被釋放。解決此類問題則是需要早Activity中onDestroy去去呼叫objectAnimator.cancel()來停止動畫。
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mAnimator.cancel();
    }
    

2.1 錯誤使用單利

  • 在開發中單例經常需要持有Context物件,如果持有的Context物件生命週期與單例生命週期更短時,或導致Context無法被釋放回收,則有可能造成記憶體洩漏。比如:在一個Activity中呼叫的,然後關閉該Activity則會出現記憶體洩漏。
  • 解決辦法:
    • 要保證Context和AppLication的生命週期一樣,修改後程式碼如下:
    • this.mContext = context.getApplicationContext();
    • 1、如果此時傳入的是 Application 的 Context,因為 Application 的生命週期就是整個應用的生命週期,所以這將沒有任何問題。
    • 2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應的 Activity 退出時,由於該 Context 的引用被單例物件所持有,其生命週期等於整個應用程式的生命週期,所以當前 Activity 退出時它的記憶體並不會被回收,這就造成洩漏了。

2.2 錯誤使用靜態變數

  • 使用靜態方法是十分方便的。但是建立的物件,建議不要全域性化,全域性化的變數必須加上static。全域性化後的變數或者物件會導致記憶體洩漏!
  • 原因分析
    • 這裡內部類AClass隱式的持有外部類Activity的引用,而在Activity的onCreate方法中呼叫了。這樣AClass就會在Activity建立的時候是有了他的引用,而AClass是靜態型別的不會被垃圾回收,Activity在執行onDestory方法的時候由於被AClass持有了引用而無法被回收,所以這樣Activity就總是被AClass持有而無法回收造成記憶體洩露。

2.3 handler記憶體洩漏

  • 造成記憶體洩漏原因分析
    • 通過內部類的方式建立mHandler物件,此時mHandler會隱式地持有一個外部類物件引用這裡就是MainActivity,當執行postDelayed方法時,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,MessageQueue是在一個Looper執行緒中不斷輪詢處理訊息,那麼當這個Activity退出時訊息佇列中還有未處理的訊息或者正在處理訊息,而訊息佇列中的Message持有mHandler例項的引用,mHandler又持有Activity的引用,所以導致該Activity的記憶體資源無法及時回收,引發記憶體洩漏。
  • 解決Handler記憶體洩露主要2點
    • 有延時訊息,要在Activity銷燬的時候移除Messages監聽
    • 匿名內部類導致的洩露改為匿名靜態內部類,並且對上下文或者Activity使用弱引用。

2.4 執行緒造成記憶體洩漏

  • 早時期的時候處理耗時操作多數都是採用Thread+Handler的方式,後來逐步被AsyncTask取代,直到現在採用RxJava的方式來處理非同步。
  • 造成記憶體洩漏原因分析
    • 在處理一個比較耗時的操作時,可能還沒處理結束MainActivity就執行了退出操作,但是此時AsyncTask依然持有對MainActivity的引用就會導致MainActivity無法釋放回收引發記憶體洩漏。
  • 解決辦法
    • 在使用AsyncTask時,在Activity銷燬時候也應該取消相應的任務AsyncTask.cancel()方法,避免任務在後臺執行浪費資源,進而避免記憶體洩漏的發生。

2.5 非靜態內部類

  • 非靜態內部類建立靜態例項造成的記憶體洩漏。有的時候我們可能會在啟動頻繁的Activity中,為了避免重複建立相同的資料資源,可能會出現這種寫法。
  • 問題程式碼
    private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
    }
    
    class TestResource {
         //裡面程式碼引用上下文,Activity.this會導致記憶體洩漏
    }
    
  • 解決辦法
    • 將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。
  • 分析問題
    • 這樣就在Activity內部建立了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體洩漏,因為非靜態內部類預設會持有外部類的引用,而該非靜態內部類又建立了一個靜態的例項,該例項的生命週期和應用的一樣長,這就導致了該靜態例項一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。

2.6 未移除監聽

  • 問題程式碼
    //add監聽,放到集合裡面
    tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
        @Override
        public void onWindowFocusChanged(boolean b) {
            //監聽view的載入,view加載出來的時候,計算他的寬高等。
        }
    });
    
  • 解決辦法
    //計算完後,一定要移除這個監聽
    tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
    
  • 注意事項:
    tv.setOnClickListener();//監聽執行完回收物件,不用考慮記憶體洩漏
    tv.getViewTreeObserver().addOnWindowFocusChangeListene,add監聽,放到集合裡面,需要考慮記憶體洩漏
    

2.7 持有activity引用

2.8 資源未關閉

  • 在使用IO、File流或者Sqlite、Cursor等資源時要及時關閉。這些資源在進行讀寫操作時通常都使用了緩衝,如果及時不關閉,這些緩衝物件就會一直被佔用而得不到釋放,以致發生記憶體洩露。因此我們在不需要使用它們的時候就及時關閉,以便緩衝能及時得到釋放,從而避免記憶體洩露。
  • BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命週期結束之後一定要 unregister 或者 close 掉,否則這個 Activity 類會被 system 強引用,不會被記憶體回收。值得注意的是,關閉的語句必須在finally中進行關閉,否則有可能因為異常未關閉資源,致使activity洩漏。

2.9 其他原因

  • 靜態集合使用不當導致的記憶體洩漏
    • 有時候我們需要把一些物件加入到集合容器(例如ArrayList)中,當不再需要當中某些物件時,如果不把該物件的引用從集合中清理掉,也會使得GC無法回收該物件。如果集合是static型別的話,那記憶體洩漏情況就會更為嚴重。因此,當不再需要某物件時,需要主動將之從集合中移除。
  • 不需要用的監聽未移除會發生記憶體洩露
    • 問題程式碼
      //add監聽,放到集合裡面
      tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
          @Override
          public void onWindowFocusChanged(boolean b) {
              //監聽view的載入,view加載出來的時候,計算他的寬高等。
          }
      });
      
    • 解決辦法
      //計算完後,一定要移除這個監聽
      tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
      
    • 注意事項:
      tv.setOnClickListener();//監聽執行完回收物件,不用考慮記憶體洩漏
      tv.getViewTreeObserver().addOnWindowFocusChangeListene,add監聽,放到集合裡面,需要考慮記憶體洩漏
      

3.佈局優化

3.1 include優化

  • 重用佈局檔案
    • 標籤可以允許在一個佈局當中引入另一個佈局,那麼比如說我們程式的所有介面都有一個公共的部分,這個時候最好的做法就是將這個公共的部分提取到一個獨立的佈局中,然後每個介面的佈局檔案當中來引用這個公共的佈局。
    • 如果我們要在標籤中覆寫layout屬性,必須要將layout_width和layout_height這兩個屬性也進行覆寫,否則覆寫效果將不會生效。
    • 標籤是作為標籤的一種輔助擴充套件來使用的,它的主要作用是為了防止在引用佈局檔案時引用檔案時產生多餘的佈局巢狀。佈局巢狀越多,解析起來就越耗時,效能就越差。因此編寫佈局檔案時應該讓巢狀的層數越少越好。
    • 舉例:比如在LinearLayout裡邊使用一個佈局。裡邊又有一個LinearLayout,那麼其實就存在了多餘的佈局巢狀,使用merge可以解決這個問題。

3.2 ViewStub優化

  • 僅在需要時才載入佈局[ViewStub]
    • 某個佈局當中的元素不是一起顯示出來的,普通情況下只顯示部分常用的元素,而那些不常用的元素只有在使用者進行特定操作時才會顯示出來。
    • 舉例:填資訊時不是需要全部填的,有一個新增更多欄位的選項,當用戶需要新增其他資訊的時候,才將另外的元素顯示到介面上。用VISIBLE效能表現一般,可以用ViewStub。
    • ViewStub也是View的一種,但是沒有大小,沒有繪製功能,也不參與佈局,資源消耗非常低,可以認為完全不影響效能。
    • ViewStub所載入的佈局是不可以使用標籤的,因此這有可能導致加載出來出來的佈局存在著多餘的巢狀結構。
  • 自定義全域性的狀態管理器【充分使用ViewStub】
    • 針對多狀態,有資料,空資料,載入失敗,載入異常,網路異常等。針對空資料,載入失敗,異常使用viewStub佈局,一鍵設定自定義佈局,也是優化的一種。
    • 專案地址:

3.3 merge優化

  • 檢視層級<merge/>
    • 這個標籤在UI的結構優化中起著非常重要的作用,它可以刪減多餘的層級,優化UI。但是就有一點不好,無法預覽佈局效果!

3.4 其他建議

  • 減少太多重疊的背景(overdraw)
    • 這個問題其實最容易解決,建議就是檢查你在佈局和程式碼中設定的背景,有些背景是隱藏在底下的,它永遠不可能顯示出來,這種沒必要的背景一定要移除,因為它很可能會嚴重影響到app的效能。如果採用的是selector的背景,將normal狀態的color設定為”@android:color/transparent”,也同樣可以解決問題。
  • 避免複雜的Layout層級
    • 這裡的建議比較多一些,首先推薦使用Android提供的佈局工具Hierarchy Viewer來檢查和優化佈局。第一個建議是:如果巢狀的線性佈局加深了佈局層次,可以使用相對佈局來取代。第二個建議是:用標籤來合併佈局。第三個建議是:用標籤來重用佈局,抽取通用的佈局可以讓佈局的邏輯更清晰明瞭。記住,這些建議的最終目的都是使得你的Layout在Hierarchy Viewer裡變得寬而淺,而不是窄而深。
    • 總結:可以考慮多使用merge和include,ViewStub。儘量使佈局淺平,根佈局儘量少使用RelactivityLayout,因為RelactivityLayout每次需要測量2次。

4.程式碼優化

  • 都是一些微優化,在效能方面看不出有什麼顯著的提升的。使用合適的演算法和資料結構是優化程式效能的最主要手段。

4.1 建議使用lint檢查去除無效程式碼

  • lint去除無效資源和程式碼
    • 如何檢測哪些圖片未被使用
      • 點選選單欄 Analyze -> Run Inspection by Name -> unused resources -> Moudule ‘app’ -> OK,這樣會搜出來哪些未被使用到未使用到xml和圖片,如下:
    • 如何檢測哪些無效程式碼
      • 使用Android Studio的Lint,步驟:點選選單欄 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘app’ -> OK

4.2 程式碼規範優化

  • 避免建立不必要的物件 不必要的物件應該避免建立:
    • 如果有需要拼接的字串,那麼可以優先考慮使用StringBuffer或者StringBuilder來進行拼接,而不是加號連線符,因為使用加號連線符會建立多餘的物件,拼接的字串越長,加號連線符的效能越低。
    • 當一個方法的返回值是String的時候,通常需要去判斷一下這個String的作用是什麼,如果明確知道呼叫方會將返回的String再進行拼接操作的話,可以考慮返回一個StringBuffer物件來代替,因為這樣可以將一個物件的引用進行返回,而返回String的話就是建立了一個短生命週期的臨時物件。
    • 儘可能地少建立臨時物件,越少的物件意味著越少的GC操作。
    • nDraw方法裡面不要執行物件的建立
  • 靜態優於抽象
    • 如果你並不需要訪問一個對系那個中的某些欄位,只是想呼叫它的某些方法來去完成一項通用的功能,那麼可以將這個方法設定成靜態方法,呼叫速度提升15%-20%,同時也不用為了呼叫這個方法去專門建立物件了,也不用擔心呼叫這個方法後是否會改變物件的狀態(靜態方法無法訪問非靜態欄位)。
  • 對常量使用static final修飾符
    • static int intVal = 42; static String strVal = "Hello, world!";
    • 編譯器會為上面的程式碼生成一個初始方法,稱為方法,該方法會在定義類第一次被使用的時候呼叫。這個方法會將42的值賦值到intVal當中,從字串常量表中提取一個引用賦值到strVal上。當賦值完成後,我們就可以通過欄位搜尋的方式去訪問具體的值了。
    • final進行優化:
    • static final int intVal = 42; static final String strVal = "Hello, world!";
    • 這樣,定義類就不需要方法了,因為所有的常量都會在dex檔案的初始化器當中進行初始化。當我們呼叫intVal時可以直接指向42的值,而呼叫strVal會用一種相對輕量級的字串常量方式,而不是欄位搜尋的方式。
    • 這種優化方式只對基本資料型別以及String型別的常量有效,對於其他資料型別的常量是無效的。
  • 在沒有特殊原因的情況下,儘量使用基本資料型別來代替封裝資料型別,int比Integer要更加有效,其它資料型別也是一樣。
    • 基本資料型別的陣列也要優於物件資料型別的陣列。另外兩個平行的陣列要比一個封裝好的物件陣列更加高效,舉個例子,Foo[]和Bar[]這樣的陣列,使用起來要比Custom(Foo,Bar)[]這樣的一個數組高效的多。

4.3 View異常優化

  • view自定義控制元件異常銷燬儲存狀態
    • 經常容易被人忽略,但是為了追求高質量程式碼,這個也有必要加上。舉個例子!
      @Override
      protected Parcelable onSaveInstanceState() {
          //異常情況儲存重要資訊。
          //return super.onSaveInstanceState();
          final Bundle bundle = new Bundle();
          bundle.putInt("selectedPosition",selectedPosition);
          bundle.putInt("flingSpeed",mFlingSpeed);
          bundle.putInt("orientation",orientation);
          return bundle;
      }
      
      @Override
      protected void onRestoreInstanceState(Parcelable state) {
          if (state instanceof Bundle) {
              final Bundle bundle = (Bundle) state;
              selectedPosition = bundle.getInt("selectedPosition",selectedPosition);
              mFlingSpeed = bundle.getInt("flingSpeed",mFlingSpeed);
              orientation = bundle.getInt("orientation",orientation);
              return;
          }
          super.onRestoreInstanceState(state);
      }
      

4.4 去除淡黃色警告優化

  • 淡黃色警告雖然不會造成崩潰,但是作為程式設計師還是要儘量去除淡黃色警告,規範程式碼

4.5 合理使用集合

  • 使用優化過的資料集合
    • Android提供了一系列優化過後的資料集合工具類,如SparseArray、SparseBooleanArray、LongSparseArray,使用這些API可以讓我們的程式更加高效。HashMap工具類會相對比較低效,因為它需要為每一個鍵值對都提供一個物件入口,而SparseArray就避免掉了基本資料型別轉換成物件資料型別的時間。

4.6 Activity不可見優化

  • 當Activity介面不可見時釋放記憶體
    • 當用戶打開了另外一個程式,我們的程式介面已經不可見的時候,我們應當將所有和介面相關的資源進行釋放。重寫Activity的onTrimMemory()方法,然後在這個方法中監聽TRIM_MEMORY_UI_HIDDEN這個級別,一旦觸發說明使用者離開了程式,此時就可以進行資源釋放操作了。
  • 當時看到這個覺得很新奇的,但是具體還是沒有用到,要是那個大神有具體操作方案,可以分享一下。

4.7 節制的使用Service

  • 節制的使用Service
    • 如果應用程式需要使用Service來執行後臺任務的話,只有當任務正在執行的時候才應該讓Service執行起來。當啟動一個Service時,系統會傾向於將這個Service所依賴的程序進行保留,系統可以在LRUcache當中快取的程序數量也會減少,導致切換程式的時候耗費更多效能。我們可以使用IntentService,當後臺任務執行結束後會自動停止,避免了Service的記憶體洩漏。

5.網路優化

5.1 圖片分類

  • 圖片網路優化
    • 比如我之前看到豆瓣介面,提供一種載入圖片方式特別好。介面返回圖片的資料有三種,一種是高清大圖,一種是正常圖片,一種是縮略小圖。當用戶處於wifi下給控制元件設定高清大圖,當4g或者3g模式下載入正常圖片,當弱網條件下載入縮圖【也稱與載入圖】。
    • 簡單來說根據使用者的當前的網路質量來判斷下載什麼質量的圖片(電商用的比較多)。豆瓣開源介面可以參考一下!

5.2 獲取網路資料優化

  • 移動端獲取網路資料優化的幾個點
  • 連線複用:節省連線建立時間,如開啟 keep-alive。
    • 對於Android來說預設情況下HttpURLConnection和HttpClient都開啟了keep-alive。只是2.2之前HttpURLConnection存在影響連線池的Bug,具體可見:Android HttpURLConnection及HttpClient選擇
  • 請求合併:即將多個請求合併為一個進行請求,比較常見的就是網頁中的CSS Image Sprites。如果某個頁面內請求過多,也可以考慮做一定的請求合併。
  • 減少請求資料的大小:對於post請求,body可以做gzip壓縮的,header也可以做資料壓縮(不過只支援http
    • 返回資料的body也可以做gzip壓縮,body資料體積可以縮小到原來的30%左右。(也可以考慮壓縮返回的json資料的key資料的體積,尤其是針對返回資料格式變化不大的情況,支付寶聊天返回的資料用到了)

5.3 網路請求異常攔截優化

  • 在獲取資料的流程中,訪問介面和解析資料時都有可能會出錯,我們可以通過攔截器在這兩層攔截錯誤。
    • 1.在訪問介面時,我們不用設定攔截器,因為一旦出現錯誤,Retrofit會自動丟擲異常。比如,常見請求異常404,500,503等等。
    • 2.在解析資料時,我們設定一個攔截器,判斷Result裡面的code是否為成功,如果不成功,則要根據與伺服器約定好的錯誤碼來丟擲對應的異常。比如,token失效,禁用同賬號登陸多臺裝置,缺少引數,引數傳遞異常等等。
    • 3.除此以外,為了我們要儘量避免在View層對錯誤進行判斷,處理,我們必須還要設定一個攔截器,攔截onError事件,然後使用ExceptionUtils,讓其根據錯誤型別來分別處理。
    • 具體可以直接看lib中的ExceptionUtils類,那麼如何呼叫呢?入侵性極低,不用改變之前的程式碼!
    @Override
    public void onError(Throwable e) {
        //直接呼叫即可
        ExceptionUtils.handleException(e);
    }
    

6.執行緒優化

6.1 使用執行緒池

  • 將全域性執行緒用執行緒池管理
    • 直接建立Thread實現runnable方法的弊端
      • 大量的執行緒的建立和銷燬很容易導致GC頻繁的執行,從而發生記憶體抖動現象,而發生了記憶體抖動,對於移動端來說,最大的影響就是造成介面卡頓
      • 執行緒的建立和銷燬都需要時間,當有大量的執行緒建立和銷燬時,那麼這些時間的消耗則比較明顯,將導致效能上的缺失
    • 為什麼要用執行緒池
      • 重用執行緒池中的執行緒,避免頻繁地建立和銷燬執行緒帶來的效能消耗;有效控制執行緒的最大併發數量,防止執行緒過大導致搶佔資源造成系統阻塞;可以對執行緒進行一定地管理。
    • 使用執行緒池管理的經典例子
      • RxJava,RxAndroid,底層對執行緒池的封裝管理特別值得參考
    • 關於執行緒池,執行緒,多執行緒的具體內容
      • 參考:輕量級執行緒池封裝庫,支援非同步回撥,可以檢測執行緒執行的狀態
      • 該專案中哪裡用到頻繁new Thread
        • 儲存圖片[注意,尤其是大圖和多圖場景下注意耗時太久];某些頁面從資料庫查詢資料;設定中心清除圖片,視訊,下載檔案,日誌,系統快取等快取內容
        • 使用執行緒池管理庫好處,比如儲存圖片,耗時操作放到子執行緒中,處理過程中,可以檢測到執行開始,異常,成功,失敗等多種狀態。

7.圖片優化

7.1 bitmap優化

  • 載入圖片所佔的記憶體大小計算方式
    • 載入網路圖片:bitmap記憶體大小 = 圖片長度 x 圖片寬度 x 單位畫素佔用的位元組數【看到網上很多都是這樣寫的,但是不全面】
    • 載入本地圖片:bitmap記憶體大小 = width * height * nTargetDensity/inDensity 一個畫素所佔的記憶體。注意不要忽略了一個影響項:Density
  • 第一種載入圖片優化處理:壓縮圖片
    • 質量壓縮方法:在保持畫素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,這樣適合去傳遞二進位制的圖片資料,比如分享圖片,要傳入二進位制資料過去,限制500kb之內。
    • 取樣率壓縮方法:設定inSampleSize的值(int型別)後,假如設為n,則寬和高都為原來的1/n,寬高都減少,記憶體降低。
    • 縮放法壓縮:Android中使用Matrix對影象進行縮放、旋轉、平移、斜切等變換的。功能十分強大!
  • 第二種載入圖片優化:不壓縮載入高清圖片如何做?
    • 使用BitmapRegionDecoder,主要用於顯示圖片的某一塊矩形區域,如果你需要顯示某個圖片的指定區域,那麼這個類非常合適。

7.2 glide載入優化

  • 在畫廊中載入大圖
    • 假如你滑動特別快,glide載入優化就顯得非常重要呢,具體優化方法如下所示
      recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
          @Override
          public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
              super.onScrollStateChanged(recyclerView, newState);
              if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                  LoggerUtils.e("initRecyclerView"+ "恢復Glide載入圖片");
                  Glide.with(ImageBrowseActivity.this).resumeRequests();
              }else {
                  LoggerUtils.e("initRecyclerView"+"禁止Glide載入圖片");
                  Glide.with(ImageBrowseActivity.this).pauseRequests();
              }
          }
      });
      

8.載入優化

8.1 懶載入優化

  • 該優化在新聞類app中十分常見
    • ViewPager+Fragment的搭配在日常開發中也比較常見,可用於切換展示不同類別的頁面。
    • 懶載入,其實也就是延遲載入,就是等到該頁面的UI展示給使用者時,再載入該頁面的資料(從網路、資料庫等),而不是依靠ViewPager預載入機制提前載入兩三個,甚至更多頁面的資料。這樣可以提高所屬Activity的初始化速度,也可以為使用者節省流量.而這種懶載入的方式也已經/正在被諸多APP所採用。
  • 具體看這篇文章

8.2 啟動頁優化

  • 啟動時間分析
    • 系統建立程序的時間和應用程序啟動的時間,前者是由系統自行完成的,一般都會很快,我們也干預不了,我覺得能做的就是去優化應用程序啟動,具體說來就是從發Application的onCreate()執行開始到MainActivity的onCreate()執行結束這一段時間。
  • 啟動時間優化
    • Application的onCreate()方法
    • MainActivity的onCreate()方法
    • 優化的手段也無非三種,如下所示:
      • 延遲初始化
      • 後臺任務
      • 啟動介面預載入
  • 啟動頁白屏優化
    • 為什麼存在這個問題?
      • 當系統啟動一個APP時,zygote程序會首先建立一個新的程序去執行這個APP,但是程序的建立是需要時間的,在建立完成之前,介面是呈現假死狀態,於是系統根據你的manifest檔案設定的主題顏色的不同來展示一個白屏或者黑屏。而這個黑(白)屏正式的稱呼應該是Preview Window,即預覽視窗。
      • 實際上就是是activity預設的主題中的android:windowBackground為白色或者黑色導致的。
      • 總結來說啟動順序就是:app啟動——Preview Window(也稱為預覽視窗)——啟動頁
    • 解決辦法
      • 常見有三種,這裡解決辦法是給當前啟動頁新增一個有背景的style樣式,然後SplashActivity引用當前theme主題,注意在該頁面將window的背景圖設定為空!
      • 更多關於啟動頁為什麼白屏閃屏,以及不同解決辦法,可以看我這篇部落格:App啟動頁面優化
  • 啟動時間優化
    • IntentService子執行緒分擔部分初始化工作
      • 現在application初始化內容有:阿里雲推送初始化,騰訊bugly初始化,im初始化,神策初始化,記憶體洩漏工具初始化,頭條適配方案初始化,阿里雲熱修復……等等。將部分邏輯放到IntentService中處理,可以縮短很多時間。
      • 開啟IntentSerVice執行緒,將部分邏輯和耗時的初始化操作放到這裡處理,可以減少application初始化時間
      • 關於IntentService使用和原始碼分析,效能分析等可以參考部落格:IntentService原始碼分析

9.其他優化

9.1 靜態變數優化

  • 儘量不使用靜態變數儲存核心資料。這是為什麼呢? - 這是因為android的程序並不是安全的,包括application物件以及靜態變數在內的程序級別變數並不會一直呆著記憶體裡面,因為它很有會被kill掉。 - 當被kill掉之後,實際上app不會重新開始啟動。Android系統會建立一個新的Application物件,然後啟動上次使用者離開時的activity以造成這個app從來沒有被kill掉的假象。而這時候靜態變數等資料由於程序已經被殺死而被初始化,所以就有了不推薦在靜態變數(包括Application中儲存全域性資料靜態資料)的觀點。

9.2 註解替代列舉

  • 使用註解限定傳入型別
    • 比如,尤其是寫第三方開源庫,對於有些暴露給開發者的方法,需要限定傳入型別是有必要的。舉個例子:
    • 剛開始的程式碼
      /**
       * 設定播放器型別,必須設定
       * 注意:感謝某人建議,這裡限定了傳入值型別
       * 輸入值:111   或者  222
       * @param playerType IjkPlayer or MediaPlayer.
       */
      public void setPlayerType(int playerType) {
          mPlayerType = playerType;
      }
      
    • 優化後的程式碼,有效避免第一種方式開發者傳入值錯誤
      /**
       * 設定播放器型別,必須設定
       * 注意:感謝某人建議,這裡限定了傳入值型別
       * 輸入值:ConstantKeys.IjkPlayerType.TYPE_IJK   或者  ConstantKeys.IjkPlayerType.TYPE_NATIVE
       * @param playerType IjkPlayer or MediaPlayer.
       */
      public void setPlayerType(@ConstantKeys.PlayerType int playerType) {
          mPlayerType = playerType;
      }
      
      /**
       * 通過註解限定型別
       * TYPE_IJK                 IjkPlayer,基於IjkPlayer封裝播放器
       * TYPE_NATIVE              MediaPlayer,基於原生自帶的播放器控制元件
       */
      @Retention(RetentionPolicy.SOURCE)
      public @interface IjkPlayerType {
          int TYPE_IJK = 111;
          int TYPE_NATIVE = 222;
      }
      @IntDef({IjkPlayerType.TYPE_IJK,IjkPlayerType.TYPE_NATIVE})
      public @interface PlayerType{}
      
  • 使用註解替代列舉,程式碼如下所示
    @Retention(RetentionPolicy.SOURCE)
    public @interface ViewStateType {
        int HAVE_DATA = 1;
        int EMPTY_DATA = 2;
        int ERROR_DATA = 3;
        int ERROR_NETWORK = 4;
    }
    

9.3 多渠道打包優化

  • 還在手動打包嗎?嘗試一下python自動化打包吧……
    • 瓦力多渠道打包的Python指令碼測試工具,通過該自動化指令碼,自需要run一下或者命令列執行指令碼即可實現美團瓦力多渠道打包,打包速度很快。配置資訊十分簡單,程式碼中已經註釋十分詳細。可以自定義輸出檔案路徑,可以修改多渠道配置資訊,簡單實用。 專案地址:https://github.com/yangchong211/YCWalleHelper

9.4 TrimMemory和LowMemory優化

  • 可以優化什麼?
    • 在 onTrimMemory() 回撥中,應該在一些狀態下清理掉不重要的記憶體資源。對於這些快取,只要是讀進記憶體內的都算,例如最常見的圖片快取、檔案快取等。拿圖片快取來說,市場上,常規的圖片載入庫,一般而言都是三級快取,所以在記憶體吃緊的時候,我們就應該優先清理掉這部分圖片快取,畢竟圖片是吃記憶體大戶,而且再次回來的時候,雖然記憶體中的資源被回收掉了,依然可以從磁碟或者網路上恢復它。
  • 大概的思路如下所示
    • 在lowMemory的時候,呼叫Glide.cleanMemory()清理掉所有的記憶體快取。
    • 在App被置換到後臺的時候,呼叫Glide.cleanMemory()清理掉所有的記憶體快取。
    • 在其它情況的onTrimMemory()回撥中,直接呼叫Glide.trimMemory()方法來交給Glide處理記憶體情況。

9.5 輪詢操作優化

  • 什麼叫輪訓請求?
    • 簡單理解就是App端每隔一定的時間重複請求的操作就叫做輪訓請求,比如:App端每隔一段時間上報一次定位資訊,App端每隔一段時間拉去一次使用者狀態等,這些應該都是輪訓請求。比如,電商類專案,某個抽獎活動頁面,隔1分鐘呼叫一次介面,彈出一些獲獎人資訊,你應該某個階段看過這類輪詢操作!
  • 具體優化操作
    • 長連線並不是穩定的可靠的,而執行輪訓操作的時候一般都是要穩定的網路請求,而且輪訓操作一般都是有生命週期的,即在一定的生命週期內執行輪訓操作,而長連線一般都是整個程序生命週期的,所以從這方面講也不太適合。
    • 建議在service中做輪詢操作,輪詢請求介面,具體做法和注意要點,可以直接看該專案程式碼。看app包下的LoopRequestService類即可。
    • 大概思路:當用戶開啟這個頁面的時候初始化TimerTask物件,每個一分鐘請求一次伺服器拉取訂單資訊並更新UI,當用戶離開頁面的時候清除TimerTask物件,即取消輪訓請求操作。

9.6 去除重複依賴庫優化

  • 我相信你看到了這裡會有疑問,網上有許多部落格作了這方面說明。但是我在這裡想說,如何查詢自己專案的所有依賴關係樹
    • 注意要點:其中app就是專案mudule名字。 正常情況下就是app!
    gradlew app:dependencies
    
  • 關於依賴關係樹的結構圖如下所示,此處省略很多程式碼
    |    |    |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
    |    |    |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
    |    +--- com.journeyapps:zxing-android-embedded:3.6.0
    |    |    +--- com.google.zxing:core:3.3.2
    |    |    \--- com.android.support:support-v4:25.3.1
    |    |         +--- com.android.support:support-compat:25.3.1 -> 28.0.0 (*)
    |    |         +--- com.android.support:support-media-compat:25.3.1
    |    |         |    +--- com.android.support:support-annotations:25.3.1 -> 28.0.0
    |    |         |    \--- com.android.support:support-compat:25.3.1 -> 28.0.0 (*)
    |    |         +--- com.android.support:support-core-utils:25.3.1 -> 28.0.0 (*)
    |    |         +--- com.android.support:support-core-ui:25.3.1 -> 28.0.0 (*)
    |    |         \--- com.android.support:support-fragment:25.3.1 -> 28.0.0 (*)
    \--- com.android.support:multidex:1.0.2 -> 1.0.3
    
  • 然後檢視哪些重複jar
    • image
  • 然後修改gradle配置程式碼
    api (rootProject.ext.dependencies["zxing"]){
        exclude module: 'support-v4'
        exclude module: 'appcompat-v7'
    }
    

9.7 四種引用優化

  • 軟引用使用場景
    • 正常是用來處理大圖片這種佔用記憶體大的情況
      • 程式碼如下所示
      Bitmap bitmap = bitmaps.get(position);
      //正常是用來處理圖片這種佔用記憶體大的情況
      bitmapSoftReference = new SoftReference<>(bitmap);
      if(bitmapSoftReference.get() != null) {
          viewHolder.imageView.setImageBitmap(bitmapSoftReference.get());
      }
      //其實看glide底層原始碼可知,也做了相關軟引用的操作
      
    • 這樣使用軟引用好處
      • 通過軟引用的get()方法,取得bitmap物件例項的強引用,發現物件被未回收。在GC在記憶體充足的情況下,不會回收軟引用物件。此時view的背景顯示
      • 實際情況中,我們會獲取很多圖片.然後可能給很多個view展示, 這種情況下很容易記憶體吃緊導致oom,記憶體吃緊,系統開始會GC。這次GC後,bitmapSoftReference.get()不再返回bitmap物件,而是返回null,這時螢幕上背景圖不顯示,說明在系統記憶體緊張的情況下,軟引用被回收。
      • 使用軟引用以後,在OutOfMemory異常發生之前,這些快取的圖片資源的記憶體空間可以被釋放掉的,從而避免記憶體達到上限,避免Crash發生。
  • 弱引用使用場景
    • 弱引用–>隨時可能會被垃圾回收器回收,不一定要等到虛擬機器記憶體不足時才強制回收。
    • 對於使用頻次少的物件,希望儘快回收,使用弱引用可以保證記憶體被虛擬機器回收。比如handler,如果希望使用完後儘快回收,看下面程式碼
    private MyHandler handler = new MyHandler(this);
    private static class MyHandler extends Handler{
        WeakReference<FirstActivity> weakReference;
        MyHandler(FirstActivity activity) {
            weakReference = new WeakReference<>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
            }
        }
    }
    
  • 到底什麼時候使用軟引用,什麼時候使用弱引用呢?
    • 個人認為,如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的效能更在意,想盡快回收一些佔用記憶體比較大的物件,則可以使用弱引用。
    • 還有就是可以根據物件是否經常使用來判斷。如果該物件可能會經常使用的,就儘量用軟引用。如果該物件不被使用的可能性更大些,就可以用弱引用。

9.8 載入loading優化

  • 一般實際開發中會至少有兩種loading
    • 第一種是從A頁面進入B頁面時的載入loading,這個時候特點是顯示loading的時候,頁面是純白色的,載入完資料後才顯示內容頁面。
    • 第二種是在某個頁面操作某種邏輯,比如某些耗時操作,這個時候是區域性loading[一般用個幀動畫或者補間動畫],由於使用頻繁,因為建議在銷燬彈窗時,新增銷燬動畫的操作。
  • 自定義loading載入

9.9 物件池Pools優化

  • 物件池Pools優化頻繁建立和銷燬物件
  • 使用物件池,可以防止頻繁建立和銷燬物件而出現記憶體抖動
    • 在某些時候,我們需要頻繁使用一些臨時物件,如果每次使用的時候都申請新的資源,很有可能會引發頻繁的 gc 而影響應用的流暢性。這個時候如果物件有明確的生命週期,那麼就可以通過定義一個物件池來高效的完成複用物件。
    • 具體參考案例,可以看該專案:https://github.com/yangchong211/YCZoomImage

10.RecyclerView優化

10.1 頁面為何卡頓

  • RecyclerView滑動卡頓的原因有哪些?
    • 第一種:巢狀佈局滑動衝突
      • 導致巢狀滑動難處理的關鍵原因在於當子控制元件消費了事件, 那麼父控制元件就不會再有機會處理這個事件了, 所以一旦內部的滑動控制元件消費了滑動操作, 外部的滑動控制元件就再也沒機會響應這個滑動操作了
    • 第二種:巢狀佈局層次太深,比如六七層等
      • 測量,繪製佈局可能會導致滑動卡頓
    • 第三種:比如用RecyclerView實現畫廊,載入比較大的圖片,如果快速滑動,則可能會出現卡頓,主要是載入圖片需要時間
    • 第四種:在onCreateViewHolder或者在onBindViewHolder中做了耗時的操作導致卡頓。按stackoverflow上面比較通俗的解釋:RecyclerView.Adapter裡面的onCreateViewHolder()方法和onBindViewHolder()方法對時間都非常敏感。類似I/O讀寫,Bitmap解碼一類的耗時操作,最好不要在它們裡面進行。
  • 關於RecyclerView封裝庫

10.2 具體優化方案

  • 03.SparseArray替代HashMap
  • 04.瀑布流圖片錯亂問題解決
  • 05.item點選事件放在哪裡優化
  • 06.ViewHolder優化
  • 07.連續上拉載入更多優化
  • 08.拖拽排序與滑動刪除優化
  • 09.暫停或停止載入資料優化
  • 11.異常情況下儲存狀態
  • 12.多執行緒下插入資料優化
  • 14.recyclerView優化處理
  • 15.adapter優化
  • 具體看這篇部落格:recyclerView優化

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格

專案開源地址:https://github.com/yang