1. 程式人生 > >記憶體洩漏與優化分析指南

記憶體洩漏與優化分析指南

前言

在android開發中,我們都或多或少的會遇到一些記憶體洩漏的問題,雖然大都知道哪些情況會導致記憶體洩露,但是還是不可避免的會遇到類似的問題,因此,知道如何去查詢記憶體洩露就顯得非常重要了。本篇和大家分享下如何進行記憶體洩漏的定位分析,以及對記憶體佔用的優化分析。相信大家看了之後會有所收穫。

為了有一個良好的分析體驗,我特意新建了一個用於分析記憶體方面的專案,該專案是一個簡易的新聞客戶端,結構上大致是這樣的,mvp開發模式,網路資料方面採用Retrofit + rxjava,列表使用LRecyclerView,新聞頁面由ViewPager將十幾個不同型別的新聞列表Fragment頁面組合在一起。種情況由於頁面的切換,以及資料列表的重新整理載入等,在開發中還是比較典型的,在記憶體控制上也是有較高要求的,因此是比較適合用來做記憶體分析的。

專案地址


記憶體快照分析方法

這裡我們直接使用Android Studio的記憶體分析工具進行分析。開啟Android Monitor,可看到Logcat,切換到Monitors,可看到記憶體,CPU相關資訊。
1. 找到當前分析的應用,這裡為com.test.memory。
2. 點選幾次Initiate GC,用於通知垃圾收集進行垃圾回收,避免無效的記憶體分析。
3. 點選Dump Java Heap,過一會就會開啟記憶體快照。

接下來分析記憶體快照
1. 點選選擇PackageTreeView,這樣就可以按包名層級進行類的查詢。
2. 左上部分就是應用相關的類的記憶體資訊了。通常我們只需按包名com.test.memory找到自己應用下的類進行分析,這裡找到NewsListFragment進行分析,它代表一種型別的新聞列表頁面。
3. 可以看到TotalCount這一列是12,也就是說當前有12個NewsListFragment物件,也就是12個NewsListFragment新聞列表頁面了,因為之前有將所有型別的頁面都開啟過了。
4. Shallow Size這一欄,可以看到是2880,代表的意思就是NewsListFragment的所有物件佔用了多少記憶體,這裡是12個的總大小,因此一個NewsListFragment的大小是240。注意,這裡僅僅是指NewsListFragment本身佔用的記憶體,而作為它的引用屬性物件所佔的記憶體是不算其中的,比如它持有的檢視View的大小是不算其中的,而只算一個int型別引用的大小,4位元組。所以Shallow Size通常並不大,因為它只是當前物件本身的大小,不算它引用物件的大小在其中。
5. Retained Size這一欄,是1757826,也就是1.75M大小了,也就是說12個NewsListFragment所持有的總大小是1.75M,這裡的持有大小,它不但包括NewsListFragment本身的大小,還包括它持有物件的大小,並且是它持有物件可被回收的大小。因此Retained Size是指,如果NewsListFragment這個物件被回收時,它最終能被回收的記憶體,也就是它本身的記憶體,和一部分只有被它引用的物件的記憶體,而還被其他物件持有的記憶體是不算在其中的,例如context物件,它不僅被NewsListFragment引用,所以它的記憶體大小是不算入在Retained Size中的。
6. 右上部分代表的是所選擇類的所有物件和其屬性所佔記憶體情況。例如這裡是NewsListFragment類的12個物件的具體記憶體情況,和它其中各個屬性引用物件的記憶體情況。這裡可以分析其中的哪些屬性或引用物件佔用的記憶體較高。
7. 下面的部分指的是當前的NewsListFragment物件被哪些物件引用了,可以檢視它的引用樹,可用於查詢最終導致記憶體洩露無法被釋放的最終根源。

記憶體洩露分析

明白瞭如何檢視記憶體快照資訊,知道它們代表的含義之後,接下來舉一個例子來分析下記憶體洩露問題。場景是這樣的,在每個新聞列表頁面個NewsListFragment的onCreateView方法時,我會新增一個LeakAnimView在其上,並開始執行縮放動畫,當onDestoryView時移除LeakAnimView,並停止它的動畫。程式碼如下:

public class NewsListFragment extends BaseListFragment<NewsPresenter>
    implements NewsContract.View {

  private
LeakAnimView animView; @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if(ControInfos.isTestLeak){ //如果測試記憶體洩露問題,則執行 animView = new LeakAnimView(view.getContext()); RelativeLayout parent = (RelativeLayout) view; RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100); params.addRule(RelativeLayout.CENTER_IN_PARENT); parent.addView(animView, params); animView.start(); } } @Override public void onDestroyView() { super.onDestroyView(); if(ControInfos.isTestLeak){ //如果測試記憶體洩露問題,則執行 if(animView != null && animView.getParent() != null){ RelativeLayout parent = (RelativeLayout) animView.getParent(); animView.cancel(); parent.removeView(animView); animView = null; } } } ... }

下面是LeakAnimView的實現:

/**
 * 存在記憶體洩露的動畫View,由於動畫Cancel之後,還是會回撥onAnimationEnd,所以需要額外判斷是否取消狀態,否則動畫會一直執行下去,導致記憶體洩露問題
 */
public class LeakAnimView extends View{
    private static final String TAG = "AnimView";

    private AnimatorSet animatorSet, animatorSet2;
    private ObjectAnimator scaleX, scaleY;
    private ObjectAnimator scaleX2, scaleY2;

    private boolean isAnimating;

    public LeakAnimView(Context context) {
        super(context);

        init();
    }

    public LeakAnimView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    private void init(){
        setBackgroundColor(Color.RED);

        animatorSet = new AnimatorSet();
        scaleX = ObjectAnimator.ofFloat(this, "scaleX", 0.5f, 1f);
        scaleY = ObjectAnimator.ofFloat(this, "scaleY", 0.5f, 1f);
        animatorSet.play(scaleX).with(scaleY);
        animatorSet.setDuration(500);
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.d(TAG, "animatorSet onAnimationStart");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.d(TAG, "animatorSet onAnimationEnd");

                //取消動畫時,該方法依然會被回撥,所以下個動畫會執行,存在記憶體洩露問題,所以要做狀態的判斷

                if(ControInfos.exitstLeak){
                    //這裡存在記憶體洩露問題
                    animatorSet2.start();
                }else{
                    //這裡解決了記憶體洩露問題
                    if(isAnimating){
                        animatorSet2.start();
                    }
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.d(TAG, "animatorSet onAnimationCancel");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        animatorSet2 = new AnimatorSet();
        scaleX2 = ObjectAnimator.ofFloat(this, "scaleX", 1f, 0.5f);
        scaleY2 = ObjectAnimator.ofFloat(this, "scaleY", 1f, 0.5f);
        animatorSet2.play(scaleX2).with(scaleY2);
        animatorSet2.setDuration(500);
        animatorSet2.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.d(TAG, "animatorSet2 onAnimationStart");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.d(TAG, "animatorSet2 onAnimationEnd");

                //取消動畫時,該方法依然會被回撥,所以下個動畫會執行,存在記憶體洩露問題,所以要做狀態的判斷

                if(ControInfos.exitstLeak){
                    //這裡存在記憶體洩露問題
                    animatorSet.start();
                }else{
                    //這裡解決了記憶體洩露問題
                    if(isAnimating){
                        animatorSet.start();
                    }
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.d(TAG, "animatorSet2 onAnimationCancel");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

    }

    public void start(){
        if(isAnimating){
            return;
        }
        isAnimating = true;
        animatorSet.start();
    }

    public void cancel(){
        if(isAnimating){
            isAnimating = false;
            if(animatorSet.isRunning() || animatorSet.isStarted()){
                animatorSet.cancel();
            }
            if(animatorSet2.isRunning() || animatorSet2.isStarted()){
                animatorSet2.cancel();
            }
        }
    }
}

上面只給出測試導致記憶體洩露的部分,其他程式碼實現可以看專案原始碼。其實導致記憶體洩露的原因也比較簡單,但是如果對動畫不是很熟悉的話,容易踩這個坑,做一個迴圈動畫,動畫1執行完後執行動畫2,動畫2執行完後執行動畫1,如此迴圈。重點是取消的時候,除了會回撥onAnimationCancel之外,仍然會回撥onAnimationEnd,而如果不在其中做標記判斷的話,那麼又會去執行下一個動畫,那麼取消方法並不能停止動畫,動畫會一直持有LeakAnimView,然後導致NewsListFragment即便是所屬的Activity頁面關閉了也不能被釋放,這時就存在記憶體洩露問題了。

如圖所示,NewsListFragment物件是12個,而LeakAnimView卻有62個之多,如果左右滑動更多的話,會一直增加,而從底部引用樹中也可以看出是動畫導致的記憶體洩露。那麼關閉頁面之後,看看這些NewsListFragment和LeakAnimView能不能被回收

發現這些物件並沒有隨著所屬Activity頁面的關閉而被回收。那麼在修改了記憶體洩露問題之後,看看效果是怎麼樣的

可以看到,LeakAnimView變成了12個,無論怎麼樣左右滑動頁面,它都只儲存在12以內,這說明記憶體洩露不存在了,同時當將頁面關閉時,可以看到LeakAnimView和NewsListFragment物件數量都為0,都被回收了。

記憶體佔用分析

上面我們通過分析將記憶體洩露的問題解決了,但是我們深知當前的狀態並不是完美的。雖然不存在記憶體洩露,但是記憶體佔用的問題還是可以進行優化的。特別是在每個列表頁面資料量大,頁面的佈局複雜,帶有重量級的控制元件在其中時,如果這些不能隨著PageAdapter的滑動進行一定的釋放的話,記憶體佔用也是會非常高,導致記憶體溢位的問題。這裡我們還是以LeakAnimView來做個例子吧,我們知道,當我們瀏覽過所有的NewsListFragment頁面後,NewsListFragment的物件數量維持在12,相應的LeakAnimView也是在12個。

但是不覺得有點奇怪嗎?我在NewsListFragment的onDestroyView中是做了移除操作的,並且將animView設為null了,照理說應該沒有被其他物件引用了,應該是可以被回收的,這樣的話,除了有兩三個LeakAnimView物件還存在之外,其他應該都是被回收的啦,但是為啥沒有呢?我們看下其中一個LeakAnimView物件的引用樹,發現了問題。

當前的LeakAnimView物件被[email protected] (0x12f534d8)給引用了,當然它還有被其他給引用,不過經分析,有效的引用是屬於RelativeLayout.DependencyGraph.Node的,那這個是幹嘛用的,跟進程式碼發現,原來RelativeLayout中有個Node來管理它的子View,每個子View作為一個節點Node,DependencyGraph則是用來管理節點Node,Node還持有的當前的LeakAnimView物件的話,說明Node沒有被釋放,執行release方法,也就是DependencyGraph沒有執行clear方法。

public class RelativeLayout{
    ...

    private static class DependencyGraph {
        ...

        void clear() {
            final ArrayList<Node> nodes = mNodes;
            final int count = nodes.size();

            for (int i = 0; i < count; i++) {
                nodes.get(i).release();
            }
            nodes.clear();

            mKeyNodes.clear();
            mRoots.clear();
        }

        static class Node {
            ...

            void release() {
                view = null;
                dependents.clear();
                dependencies.clear();

                sPool.release(this);
            }
        }
    }
}

再找哪裡呼叫了DependencyGraph的clear方法,發現是在RelativeLayout的sortChildren方法中,而sortChildren是在onMeasure方法中被呼叫的

public class RelativeLayout{
    ...

    private void sortChildren() {
        final int count = getChildCount();
        if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
            mSortedVerticalChildren = new View[count];
        }

        if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
            mSortedHorizontalChildren = new View[count];
        }

        final DependencyGraph graph = mGraph;
        graph.clear();

        for (int i = 0; i < count; i++) {
            graph.add(getChildAt(i));
        }

        graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
        graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mDirtyHierarchy) {
            mDirtyHierarchy = false;
            sortChildren();
        }
        ...
    }
}

也就是說onMeasure沒有在NewsListFragment執行onDestroyView時執行。那這個怎麼解決,我現在也沒有比較好的解決方案,想了一個做驗證性的方法,通過反射主動呼叫RelativeLayout的sortChildren方法

public class NewsListFragment extends BaseListFragment<NewsPresenter>
    implements NewsContract.View {
    ...
     @Override
    public void onDestroyView() {
        super.onDestroyView();

        if(ControInfos.isTestLeak){
          //如果測試記憶體洩露問題,則執行
          if(animView != null && animView.getParent() != null){
            Log.e("NewsListFragment", "remove pre, animView parent : " + animView.getParent());
            RelativeLayout parent = (RelativeLayout) animView.getParent();
            animView.cancel();
            parent.removeView(animView);

            //這裡通過反射主動呼叫RelativeLayout的sortChildren方法,達到清除animView被RelativeLayout.DependencyGraph.Node持有引用的問題
            ReflectUtil.invokeMethod(parent.getClass().getName(), "sortChildren", parent, null, new Object[]{});

            Log.e("NewsListFragment", "remove post, animView parent : " + animView.getParent());
            Log.e("NewsListFragment", "remove post, parent size : " + parent.getChildCount());

            animView = null;
          }
        }
    }
}

現在測試一下看看效果。

很欣喜的看到,這個只有2個LeakAnimView物件了(當前的NewsListFragment和旁邊的NewsListFragment所持有的LeakAnimView物件)。說明確實是由於被RelativeLayout.DependencyGraph.Node持有的引用導致LeakAnimView物件不能被回收了。當然通過反射去實現不一定是合適的辦法,大家可以想想其他更合適的方法去實現。

顯然,這樣省去了10個LeakAnimView物件所佔用的記憶體,那麼再延伸到NewsListFragment持有的View的話,是不是可以想辦法去實現回收其他10個NewsListFragment中的View的記憶體呢,那麼想想,記憶體佔用是不是會減少很多?具體怎麼去做需要大家自己去做嘗試和驗證。

總結

好啦,到總結的時候了。無論是記憶體洩露的檢測分析,還是記憶體佔用的優化分析,都可以通過檢視Android Studio匯出的記憶體快照進行分析。記憶體洩露問題著重看類物件的數量Total Size,看是否符合預期,而記憶體佔用則更注重去找記憶體佔用較大的物件Shallow Size,分析它的數量,以及哪裡佔用了較大記憶體,分析是否合理,然後進行鍼對性的優化,更深的體會就得自己親自嘗試了,

我的GitHub
微信公眾號 hesong ,微信掃一掃下方二維碼即可關注: