App優化以及內存泄漏溢出優化

目錄介紹

  • 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優化
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章