內存優化深刻版

內存優化之路

最近一直想着本身之後的路如何走,Android的坑位愈來愈少,對於能力的要求也愈來愈高。曾想着換一個方向,可是最終都放棄了,畢竟這是本身喜歡的東西。因此,繼續下去,不斷的在Android方向發展吧。機會是給準備的人的,不斷的充實本身,時刻準備着~html

進入正題。優化工做是一個開發工程師進階必備的一種能力。包括內存優化,電量優化,網絡優化等等。這些優化所須要的能力,實際上是對於各類知識的一種綜合運用處理能力。java

img

概念

內存優化,是對於應用程序的內存使用、空間佔用進行必定的優化處理。android

做用

經過內存優化,可以有效的避免內存泄漏、內存溢出、內存佔用過大等問題,從而最大程度上保證應用的流暢性、穩定性、以及存活機率算法

基礎知識

內存管理機制

內存的管理,主要是對於進程、對象以及變量分配以及回收數據庫

內存區域

Android將內存分爲了:堆區、方法區、棧區。其中堆區是內存優化的主要區域。c#

對象的生命週期以及大小都是有區別的。爲了可以更合理的利用堆區,將其又按照生命週期的長短區分爲了年輕代、老年代和持久代緩存

img

內存的分配

Android應用在啓動時,會經過Zygote進行來孵化應用所須要的進程。當進程建立以後,會交由Framework層來進行託管。性能優化

而對於對象和變量的內存分配,採用一種動態內存分配策略,可是並不會說能夠無限的增加,會有一個上限的限制。而這個最大值則跟具體的設備有關。畢竟隨着手機性能的增長,手機的處理能力更強了,從原來的512M到如今的6G內存,單個應用的可處理能力也在增長。網絡

對於應用中建立的不一樣對象的具體分配策略,則以下圖所示ide

img

這裏強調一下靜態分配區域中的靜態常量,這種常量會一直存活於程序的整個運行期間。後面咱們會講到這種常量致使的問題。

內存的回收

對於Android系統,依賴gc來自動的執行對於內存的回收工做。而回收工做主要依賴於各類回收算法。在Android的ART虛擬機,用到的算法主要有4種:

img

ART虛擬機會自動的根據實際的狀況,自動的選擇回收算法來進行內存的回收工做。好比說,當咱們的應用處於前臺的時候,顯示速度對於咱們來講是最重要的,這時候ART虛擬機會選擇簡單的標記清除算法。當應用處於後臺的時候,對於速度要求就低一些,這時候可能會採用標記整理算法來進行垃圾的回收工做。

ART虛擬機還具有對於內存的整理能力,從而減小內存空洞的問題

Low Memory Killer 機制

應用進程建立之後,ActivityManagerService就會根據進程的狀態計算一個其對應的OomAdj值,而後將這個值傳遞給Kernel中,kernel存在一個低內存回收機制(LMK)。當內存達到必定閾值時,觸發清理OomAdj較高的進程。

img

對於一些後臺佔用過大的程序,其回收以後的效益最大,其OomAdj對應的值高一些,回收的機率更大。

經過LMK機制可以保證資源的合理利用,防止過大的後臺應用影響到前臺程序的正常使用。

四種引用

在Java中,對象的引用主要分爲四種。

image-20200614133204982

強引用沒法回收,當對象用完之後,不移除對應的引用關係,就會致使對象沒法回收,而發生內存泄漏等狀況。因此在Android中要注意這種問題。

內存問題

對於對象的使用不當的話,可能會致使3種內存問題:內存抖動、內存泄漏,內存溢出

內存抖動

定義

內存抖動是指內存的頻繁分配和回收會致使內存不穩定。在內存上一般呈現一種鋸齒狀頻繁進行GC

內存抖動一般會致使應用的頁面卡頓,長時間的內存抖動可能會致使應用的OOM。

抖動致使OOM?
  • 當應用在前臺運行時,爲了保證流暢度,會使用標記清除算法,這種算法會致使大量的內存碎片。
  • 內存碎片可能會由於空間特別小而沒法被分配使用,當聚沙成塔的時候,可能會致使OOM

實戰解決

對於內存抖動的問題,通常可使用Memory Profiler來進行排查處理便可處理。

測試案例:

@SuppressLint("HandlerLeak")
    private static Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            for (int i = 0; i < 100; i++) {
                String[] strings = new String[100000];
            }
            handler.sendEmptyMessageDelayed(0, 100);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        handler.sendEmptyMessage(0);
    }
複製代碼

這裏咱們模擬了一建立大對象的操做。當程序運行起來之後,咱們點擊Android Studio中的Profile按鈕,就能夠看到咱們的內存狀況。這裏看到內存呈現了鋸齒狀,也就是所謂的內存抖動。

image-20200614154041323

當遇到這種狀況的時候,就能夠知道發生了內存的抖動,那麼就須要查找哪些佔用內存比較高。

image-20200614160302585

如上圖,經過順序操做之後,在4這個位置咱們發現了建立大批量對象位置是在SplashActivity的handlerMessage的方法中。經過雙擊就能夠跳轉到咱們上面寫的那一部分代碼了。

解決技巧

對於內存抖動,應該重點關注:循環或者頻繁調用的地方。由於這兩個地方很容易形成對象的頻繁分配會回收。

常見的內存抖動案例

有一些常見的內存抖動的案例,在咱們進行代碼編寫的時候,應該儘可能避免的。

  • 儘可能避免在循環體內部建立對象,把對象的建立移到循環體之外。
  • 在自定義View時,onDraw方法會頻繁調用,儘可能不要在方法內建立對象
  • 對於大對象的使用(Bitmap,線程)等,要使用緩存池技術,經過複用防止頻繁的申請和釋放。在結束使用的時候,須要手動的釋放池中的對象。
  • 當涉及到字符串的變動時,使用StringBuilder來替代,並且要合理化進行初始化。

內存泄漏

定義

虛擬機進行GC時,會選擇一些還存活的對象做爲GC Roots,而後經過可達性分析來判斷對象是否回收。而內存泄漏是指一些再也不使用的對象被長期持有,致使內存沒法被釋放,其本質是由於再也不使用的對象被其餘生命週期更長的對象所持有。而在內存上的表現則是應用的內存逐漸上升,可用內存逐漸變小

發生內存泄漏會致使咱們的內存不足,頻繁GC,若是泄漏狀況嚴重,會致使OOM。

實戰解決

對於內存泄漏一般會使用Memory Profile+MAT來進行內存泄漏的分析。

這裏咱們寫一個簡單的內存泄漏的案例,而後經過案例排查。

public class CallBackManager {
        private static List<Activity> list=new ArrayList<Activity>();
        public static void addCallBack(Activity activity){
            list.add(activity);
        }
    }
    public class SecondActivity extends AppCompatActivity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_sedsd);
            CallBackManager.addCallBack(this);
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        findViewById(R.id.iv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(SecondActivity.class);
            }
        });
    }

複製代碼

這裏咱們準備了兩個頁面,第二個頁面註冊回調管理。咱們看一下內存的狀況。

image-20200614174532108

當咱們進入第二個頁面,而後退出。通過循環屢次的點擊以後,內存慢慢的上升了~~~這種狀況基本就是內存泄漏致使的。

image-20200614175439168

按照上圖的步驟點開之後,會看到內存中存在着18個對應的SecondActivity對象。也就是SecondActivity發生了內存泄漏。可是對於如何發生了泄漏,如今什麼實例發生泄漏,在這裏是沒法看出來的,須要經過MAT來幫助咱們定位問題。

在AndroidStudio中,將dump的文件導出。而後經過Android SDK的自帶轉換工具hprof-conv.exe,將文件轉化爲可以被MAT識別的hprof文件。轉化語句爲:

./hprof-conv file.hprof converted.hprof
複製代碼

而後經過MAT打開轉化後的.hprof文件。

GIF 2020-6-14 18-49-28

gif可能效果不太好,這裏只是演示一下如何去使用MAT,咱們看一下最後找到的結果。

image-20200614185316940

這裏左邊有小圓點的是咱們的gcRoot,也就是持有SecondActivity的實例,致使其沒法回收的根源。能夠看到持有實例的是CallBackManager的list對象

這時候咱們就能夠回到項目中去找這個對象,而後進行一個處理了。

常見的內存泄漏案例

集合類

集合類添加元素後,會持有集合對象的引用信息,致使集合對象沒法回收而內存泄漏。在程序退出,或者集合再也不使用的時候,先clear,再置爲空。

List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
        // 釋放objectList
        objectList.clear();
        objectList=null;
複製代碼

Static修飾的成員變量

Static修飾的成員變量的週期=應用的生命週期。若是static修飾的成員變量引用耗費資源的實例(例如Context),那麼當實例結束生命週期的時候,由於靜態變量的持有而沒法被回收,從而出現內存泄漏。

解決方案:

  • 儘可能避免Static修飾的成員變量引用耗費資源過多的實例。若是是Context,儘可能使用ApplicationContext
  • 使用*弱引用(WakReference)*代替強引用持有實例。

典型:單例中的靜態變量

對於單例中的,可能第二種弱引用並非很合適,這時候只能使用第一種方案。

非靜態內部類/匿名內部類

非靜態內部類或者匿名類都會持有外部類的引用。而靜態的內部類則不會。

因此若是非靜態內部類的實例若是沒法回收的話,好比說用Static修飾,也會致使外部類沒法釋放而內存泄漏。

解決方案:

  • 非靜態內部類設置爲靜態內部類。
  • 將內部類抽取出來封裝爲一個單例,若是須要使用Context的時候,儘可能使用ApplicationContext
  • 儘可能避免static修飾的內部非靜態類實例對象

若是是Runnable,Thread等匿名類,若是內部耗時工做,那麼工做線程實例持有外部類引用,也會形成實例沒法回收的問題。

Handler臨時性的內存泄漏

Handler在發送出去Message以後,會保存在MessageQueue中。因爲Message中會保存着Handler的引用,因此若是是延遲處理的消息,那麼極可能致使Handler沒法回收,若是再使用了匿名內部類,那麼所對應的的Activity實例也就沒法回收,從而致使內存泄漏。

解決方案:

  • 使用靜態的Handler內部類,而後使用弱引用,在使用的時候,使用get()獲取引用的Activity信息進行判空處理。
  • 在Activity進行銷燬的時候,經過removeCallbacksAndMessages移除對應的消息隊列。
  • Android Weak Handler:能夠避免內存泄漏的Handler庫,參考:www.jcodecraeer.com/a/anzhuokai…

資源型文件未關閉

對於資源的使用(廣播,文件流,數據庫遊標,圖片資源BitMap等),若是不關閉或者註銷,那麼資源就不會回收,從而致使內存泄漏。應該在資源對象再也不使用的時候,調用其close()方法將其關閉,而後再置爲null。

WebView

WebView因爲其特性問題,只要在應用中使用一次,那麼內存就不會進行釋放。能夠爲WebView建立單獨的進程,經過AIDL方式和主進程進行通訊,當WebView須要銷燬時,直接銷燬其進程來達到內存釋放的目的。

Adapter

在快速滑動ListView或者RecyclerView的時候,會頻繁的建立大量對象,不只浪費資源和時間,內存也會愈來愈大。可使用緩存的convertView和ViewHolder來進行緩存。

內存溢出

當內存使用過大的時候,會致使內存的溢出,也就是OOM。這種狀況不必定發生在相對應的代碼處。內存抖動和內存泄漏都有可能會致使最後的內存溢出。對於內存溢出的狀況,除了要考慮內存抖動和內存泄漏的問題,還應該儘可能使用合適的類型來優化代碼

常見內存優化案:
  • 使用Android優化事後的SparseArray,SparseInt等集合類。代替HashMap。

  • 使用IntDef、StringDef等,來替代枚舉。

  • 經過LruCache方法來實現對大資源的緩存功能

  • BitMap優化。位圖使用RGB_565或者ARGB_4444。

  • 重寫 onTrimMemory/onLowMemory 方法去釋放掉圖片緩存、靜態緩存來自保

  • RecyclView和ListView不可見時,釋放掉對圖片的

    • ListView的3級緩存:在ImageView的DetachFromWindows時釋放掉

    • RecyclerView的5級緩存:在放入到mRecyclerPool時回收(重寫Adapter的onViewRecycled方法)

  • 其餘

    • 使用基本數據類型

    • 使用For加強

    • 使用軟引用和弱引用

    • 採用內存緩存或者磁盤緩存

    • 對於建立複雜的使用池技術

    • 儘可能使用靜態內部類

優化工具

哪怕對於常見的內存泄漏案例瞭如指掌,可是確定仍是會不免出現內存泄漏現象。這時候咱們就須要借用工具來檢測內存的泄漏狀況了。最經常使用的工具分別

  • MAT
  • Memory Profile
  • LeakCanary

對於前兩種工具,咱們在以前的案例中進行了一部分的功能展現,這裏咱們作一些總結的信息。

Memory Profiler:
  • 實時的內存使用狀況,方便直觀

  • 識別內存抖動、泄漏等,

  • 提供堆轉儲、強制GC以及跟蹤內存分配的能力

  • 線下平時使用

Memory Analyzer(MAT)
  • 強大的Java Heap 分析工具,查找內存泄漏以及內存佔用

  • 生成總體的報告,分析問題等等

  • 線下的深刻使用工具

  • 能夠和Memory Profiler結合使用,一個定位問題,一個深刻分析

LeakCanary

LeakCanary是一種自動內存泄漏檢測的神器,僅推薦使用於線下集成。可是其缺點也是比較明顯的。

  • 使用了多進程以及idleHandler,可是進行堆轉儲和引用分析的時候,仍然會致使應用的卡頓。具體的能夠看一下以前作的一個關於LeakCanary的源碼解析

總結

這篇文章對於內存的常見問題以及處理方案進行了總結。

可是內存優化,若是深挖,會涉及到不少不少的知識點。

一個最須要優化的Bitmap,其內存佔是如何計算的等等,爲何就佔用了那麼多的內存;對於LMK機制,咱們如何保證程序在後臺運行的時候可以保證最大程度不被殺掉;線上如何造成一套APM的機制,內存出現問題之後上報等等都是值得研究的地方。

參考

Android性能優化以內存優化

深刻探索 Android 內存優化(煉獄級別)

MAT使用教程

Android性能優化:手把手帶你全面瞭解 內存泄露 & 解決方案

Top團隊大牛帶你玩轉Android性能分析與優化

lowmemorykiller總結

這是一份全面&詳細的內存優化指南

本文由 開了肯 發佈!

同步公衆號[開了肯]

image-20200404120045271
相關文章
相關標籤/搜索