1. 程式人生 > >記憶體洩露與分析

記憶體洩露與分析

原文連結:http://www.jianshu.com/p/402225fce4b2#

為什麼要做效能優化?

  1. 手機效能越來越好,不用糾結這些細微的效能?

    • Android每一個應用都是執行的獨立的Dalivk虛擬機器,根據不同的手機分配的可用記憶體可能只有(32M、64M等),所謂的4GB、6GB執行記憶體其實對於我們的應用不是可以任意索取

    • 優秀的演算法與效率低下的演算法之間的執行效率要遠遠超過計算機硬體的的發展,雖然手機單核、雙核到4核、8核的發展,但效能優化任然不可忽略

  2. 手機應用一般使用的週期比較短,用完就關了。不像伺服器應用要長年累月執行,似乎影響不大?

    • 現在一般的使用者都不會重啟手機,可能一個月都不會重啟。像微信這樣的APP,每天都在使用。如果一旦發生記憶體洩漏,那麼可能一點一點的累積,程式就會出現OOM。
  3. 等應用出現卡頓、發燙等,再來關注效能優化?

    • 似乎是沒錯的。現在一般我們也都是等出現問題了再來找原因。但是學好效能優化的目的不僅僅如此,我們在編碼階段就應該從源頭來杜絕一些坑,這樣的成本比後期再來尋找原因要少得多

    所以為了我們的應用的健壯性、有良好的使用者體驗。效能優化技術,需要我們用心去研究和應用。

什麼是記憶體洩漏?

JVM記憶體管理


Java採用GC進行記憶體管理。深入的JVM記憶體管理知識,推薦《深入理解Java虛擬機器》。關於記憶體洩漏我們要知道,JVM記憶體分配的幾種策略。

  1. 靜態的

    靜態的儲存區,記憶體在程式編譯的時候就已經分配好了,這塊記憶體在程式整個執行期間都一直存在,它主要存放靜態資料、全域性的static資料和一些常量。

  2. 棧式的

    在執行方法時,方法一些內部變數的儲存都可以放在棧上面建立,方法執行結束的時候這些儲存單元就會自動被註釋掉。棧 記憶體包括分配的運算速度很快,因為內在在處理器裡面。當然容量有限,並且棧式一塊連續的記憶體區域,大小是由作業系統決定的,他先進後 出,進出完成不會產生碎片,執行效率高且穩定

  3. 堆式的

    也叫動態記憶體 。我們通常使用new 來申請分配一個記憶體。這裡也是我們討論記憶體洩漏優化的關鍵儲存區。GC會根據記憶體的使用情況,對堆記憶體裡的垃圾記憶體進行回收。堆記憶體是一塊不連續的記憶體區域,如果頻繁地new/remove會造成大量的記憶體碎片,GC頻繁的回收,導致記憶體抖動,這也會消耗我們應用的效能

我們知道可以呼叫 System.gc();進行記憶體回收,但是GC不一定會執行。面對GC的機制,我們是否無能為力?其實我們可以通過宣告一些引用標記來讓GC更好對記憶體進行回收。

型別 回收時機 生命週期
StrongReference (強引用) 任何時候GC是不能回收他的,哪怕記憶體不足時,系統會直接丟擲異常OutOfMemoryError,也不會去回收 程序終止
SoftReference (軟引用) 當記憶體足夠時不會回收這種引用型別的物件,只有當記憶體不夠用時才會回收 記憶體不足,進行GC的時候
WeakReference (弱引用) GC一執行就會把給回收了 GC後終止
PhantomReference (虛引用) 如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收 任何時候都有可能

開發時,為了防止記憶體溢位,處理一些比較佔用記憶體並且生命週期長的物件時,可以儘量使用軟引用和弱引用。

Tip

成員變數全部儲存在堆中(包括基本資料型別,引用及引用的物件實體),因為他們屬於類,類物件最終還是要被new出來的

區域性變數的基本資料型別和引用存在棧中,應用的物件實體儲存在堆中。因為它們屬於方法當中的變數,生命週期會隨著方法一起結束

記憶體洩漏的定義

當一個物件已經不需要使用了,本該被回收時,而有另外一個正在使用的物件持有它的引用,從而導致了物件不能被GC回收。這種導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏

記憶體洩漏與記憶體溢位的區別

  • 記憶體洩漏(Memory Leak)
    程序中某些物件已經沒有使用的價值了,但是他們卻還可以直接或間接地被引用到GC Root導致無法回收。當記憶體洩漏過多的時候,再加上應用本身佔用的記憶體,日積月累最終就會導致記憶體溢位OOM

  • 記憶體溢位(OOM)
    當 應用的heap資源超過了Dalvik虛擬機器分配的記憶體就會記憶體溢位

記憶體洩漏帶來的影響

  • 應用卡頓
    洩漏的記憶體影響了GC的記憶體分配,過多的記憶體洩漏會影響應用的執行效率

  • 應用異常(OOM)
    過多的記憶體洩漏,最終會導致 Dalvik分配的記憶體,出現OOM

Android開發常見的記憶體洩漏

單例造成的記憶體洩漏

  1. 錯誤示例
  2. 當呼叫getInstance時,如果傳入的context是Activity的context。只要這個單例沒有被釋放,那麼這個
    Activity也不會被釋放一直到程序退出才會釋放。

    public class CommUtil {
        private static CommUtil instance;
        private Context context;
        private CommUtil(Context context){
        this.context = context;
        }
    
        public static CommUtil getInstance(Context mcontext){
        if(instance == null){
            instance = new CommUtil(mcontext);
        }
        return instance;
        }
  3. 解決方案

    能使用Application的Context就不要使用Activity的Content,Application的生命週期伴隨著整個程序的週期

    非靜態內部類建立靜態例項造成的記憶體洩漏

  4. 錯誤示例

 private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
            mManager = new TestResource();
        }

    }
    class TestResource {

    }
  1. 解決方案

將非靜態內部類修改為靜態內部類。(靜態內部類不會隱式持有外部類)

Handler造成的記憶體洩漏

  1. 錯誤示例

mHandler是Handler的非靜態匿名內部類的例項,所以它持有外部類Activity的引用,我們知道訊息佇列是在一個Looper執行緒中不斷輪詢處理訊息,那麼當這個Activity退出時訊息佇列中還有未處理的訊息或者正在處理訊息,而訊息佇列中的Message持有mHandler例項的引用,mHandler又持有Activity的引用,所以導致該Activity的記憶體資源無法及時回收,引發記憶體洩漏。

 private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {

        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
  1. 解決方案

建立一個靜態Handler內部類,然後對Handler持有的物件使用弱引用,這樣在回收時也可以回收Handler持有的物件,這樣雖然避免了Activity洩漏,不過Looper執行緒的訊息佇列中還是可能會有待處理的訊息,所以我們在Activity的Destroy時或者Stop時應該移除訊息佇列中的訊息

   private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

執行緒造成的記憶體洩漏

  1. 錯誤示例

非同步任務和Runnable都是一個匿名內部類,因此它們對當前Activity都有一個隱式引用。如果Activity在銷燬之前,任務還未完成, 那麼將導致Activity的記憶體資源無法回收,造成記憶體洩漏

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();


        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();
  1. 解決方案

使用 靜態內部類,避免了Activity的記憶體資源洩漏,當然在Activity銷燬時候也應該取消相應的任務AsyncTask::cancel(),避免任務在後臺執行浪費資源

   static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;

        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }

        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
//——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();

資源未關閉造成的記憶體洩漏

  1. 錯誤示例

對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏

  1. 解決方案

在Activity銷燬時及時關閉或者登出

使用了靜態的Activity和View

  1. 錯誤示例
static view; 

    void setStaticView() { 
      view = findViewById(R.id.sv_button); 
    } 

    View svButton = findViewById(R.id.sv_button); 
    svButton.setOnClickListener(new View.OnClickListener() { 
      @Override public void onClick(View v) { 
        setStaticView(); 
        nextActivity(); 
      } 
    }); 


    static Activity activity; 

    void setStaticActivity() { 
      activity = this; 
    } 

    View saButton = findViewById(R.id.sa_button); 
    saButton.setOnClickListener(new View.OnClickListener() { 
      @Override public void onClick(View v) { 
        setStaticActivity(); 
        nextActivity(); 
      } 
    });
  1. 解決方案

    應該及時將靜態的應用 置為null,而且一般不建議將View及Activity設定為靜態

註冊了系統的服務,但onDestory未登出

  1. 錯誤示例

    SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
    Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
  2. 解決方案

    //不需要用的時候記得移除監聽
        sensorManager.unregisterListener(listener);

不需要用的監聽未移除會發生記憶體洩露

  1. 錯誤示例

    //add監聽,放到集合裡面
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean b) {
                //監聽view的載入,view加載出來的時候,計算他的寬高等。
            }
        });
  2. 解決方案

    //計算完後,一定要移除這個監聽
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);

Tip

tv.setOnClickListener();//監聽執行完回收物件,不用考慮記憶體洩漏
tv.getViewTreeObserver().addOnWindowFocusChangeListene,add監聽,放到集合裡面,需要考慮記憶體洩漏

如何進行記憶體洩漏的分析

使用Android Studio Monitors

AndroidMonitors是Android Studio自帶的功能,我們可以通過裡面的Memory模組來進行記憶體洩漏的分析,平時開發我們也可以通過該模組來觀察記憶體的抖動情況。


這裡我們首先知道,標註1是進行GC的操作,標註2是進行Dump操作,也就是可以生成我們瞬時的堆記憶體快照,我們主要也是通過分析堆記憶體的快照來進行記憶體洩漏分析。
一般我們先進行幾次gc操作,待記憶體平穩後,執行dump操作。會生成一個phrof的記憶體快照


此時我們可以看到幾個面板:

  1. ClassName:堆記憶體中存在的類
  2. Instaance:類存在的例項
  3. ReferenceTree:持有該類的引用

幾個屬性的含義:

  1. Depth:引用的層級
  2. Shallow Size:物件的大小(Byte)
  3. Dominating Size:釋放該物件能節省的堆記憶體(Byte)

將快照轉換為Mat能夠匯入的格式
在as的captures中可以右鍵選擇export to standard .hprof 將快照轉換為mat能夠帶入的檔案格式


使用MAT

MAT是一款功能更強大的記憶體洩漏分析工具,在實際的記憶體分析中,我們可以結合Monitors進行記憶體洩漏分析。


匯入快照後,我們可以通過Histogram檢視記憶體快照


在Histogram中,我們可以通過篩選過濾出我們專案中的包和類,這個操作實際中很有用。


選中具體的物件後,右鍵list objects--with incoming references可以檢視對改物件持有的應用


我們可以看到,這個時候引用還是非常的,我們需要過濾一些無用的軟引用之類的。通過右鍵-megre shortest path to GC roots-exclude all phantom/sofe/weak etc.refrences進行過濾,這個時候基本就能查出我們自己寫的程式碼的引用


另外Mat還支援2個快照進行比對,這個功能也是非常有用的。
我們可以在Navigation History中選擇 Histogram,然後右鍵選擇Add to compare basket加入比較選項,將2個快照的Histogram加入後在compare basket欄中點選紅色感嘆號就可以執行快照的比對。


使用leakcanary

Square開源了一個記憶體洩露自動探測神器——LeakCanary,它是一個Android和Java的記憶體洩露檢測庫,可以大幅度減少了開發中遇到的OOM問題。

通過官方的文件介紹,我們可以輕鬆在專案整合

加入依賴:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }

Application 配置:

public class ExampleApplication extends Application {

  ......
  //在自己的Application中新增如下程式碼
public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (ExampleApplication) context
            .getApplicationContext();
    return application.refWatcher;
}

  //在自己的Application中新增如下程式碼
private RefWatcher refWatcher;

@Override
public void onCreate() {
    super.onCreate();
    ......
        //在自己的Application中新增如下程式碼
    refWatcher = LeakCanary.install(this);
    ......
}

.....
}

使用 RefWatcher 監控那些本該被回收的物件:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}

最後如果有記憶體洩漏,會接收到相應的推送。


這樣我們就能在編碼的階段,儘量的避免出現記憶體洩漏的情況。

如何對自己的專案進行記憶體洩漏分析

上面說了這麼多,怎麼來對我們自己的專案進行記憶體洩漏的分析呢?

一般我們都是在不知道專案中那裡有存在記憶體洩漏的情況下,怎麼來查找出那個地方出現了記憶體洩漏?

這裡我們主要檢查Activity及Fragment的記憶體洩漏情況。

使用Memory Usage檢視Activity及Fragment的記憶體洩漏情況,首先先執行自己專案到MainActivity,觀察 Menory Usage。


待gc記憶體穩定後,我們可以執行一些操作,如進入其他的Activity執行其他操作,然後 檢測記憶體的抖動情況及gc穩定後,記憶體與初始記憶體的對比。

這裡我使用開啟不保留活動來模擬MainActivity的異常退出及恢復。繼續看Menory Usage。



這個時候,我只有在MainActivity出現過, 理論上應當只有一個MainActivity的例項,這個地方就是一個值得懷疑的記憶體洩漏的點。這個時候我們就可以通過Mioniter和Mat進行記憶體分析


這個時候我們可以看到引用的的可懷疑物件,接著我們就進入原始碼分析。


果然這裡有一個單例持有了MainActivity的使用。

分析記憶體洩漏是一個體力活,我們大概在專案中主要要記住。

  1. 使用leakcanary 在編碼階段進行檢測

  2. 結合記憶體抖動及Memory Usage 檢查Activity及Fragment的的洩漏情況

  3. 使用Monitor及Mat進行引用持有分析找出懷疑的物件

  4. 分析原始碼,找到元凶