【问题标题】:view pager memory analyzer查看寻呼机内存分析器
【发布时间】:2013-12-23 09:30:11
【问题描述】:

我编写了一个阅读器应用程序,它使用带有 FragmentStatePagerAdapter 的视图寻呼机。它没有明显的运行时错误。但是,当页面被滑动时,内存使用量会不断增加。因此,我测试了一个简单版本的设置并在 Nexus 7 上进行了测试。用户需要在几次滑动和几次旋转之间交替进行。多次执行这些操作后,从日志中注意到堆使用量不断增加。我认为这暗示着内存泄漏。但是,我无法找出它的原因。附上代码、GC日志和MAT的两个泄漏嫌疑人。任何有用的见解将不胜感激。谢谢。

MainActivity.java:

package com.example.leakypager;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.Log;

public class MainActivity extends FragmentActivity {

    public static final String TAG = "MainActivity";
    public static final String EXTRA_argument = "pageNumber";
    private static final int viewPagerId = 1;
    private static final int totalPages = 10;

    private int mPageNumber;
    private ViewPager mViewPager; 
    private int mTotalPages;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Set a default value to page 1
        mPageNumber = getIntent().getIntExtra(EXTRA_argument, 1);

        Log.i(TAG, "Called onCreate() on page number " + mPageNumber);

        mViewPager = new ViewPager(this);
        mViewPager.setId(viewPagerId);
        setContentView(mViewPager);

        mTotalPages = totalPages;

        FragmentManager fragmentManager;
        fragmentManager = getSupportFragmentManager();      

        mViewPager.setAdapter(new FragmentStatePagerAdapter(fragmentManager) {
            @Override
            public Fragment getItem(int pos) {
                // The page number is one greater than the pager index.
                Fragment pageFragment;
                pageFragment = PageFragment.newInstance(pos+1);                 
                return pageFragment;
            }

            @Override
            public int getCount() {
                return mTotalPages;  
            }

            @Override
            public int getItemPosition(Object item) {
                return POSITION_NONE;
            }
        });

        if (mPageNumber >= 1 && mPageNumber <= mTotalPages) {
            // The pager item index is one less than the page number.
            mViewPager.setCurrentItem(mPageNumber-1); 
        }
    }
}

PageFragment.java:

package com.example.leakypager;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;


public class PageFragment extends Fragment {
    private static final String EXTRA_int = "argument";
    private static final String TAG = "PageFragment";

    private int mPageNumber;
    private String mData;
    private String mNote = "";

    private TextView mDataView;
    private EditText mNoteView;


    public static PageFragment newInstance(int pagenumber) {
        Bundle args = new Bundle();
        args.putInt(EXTRA_int, pagenumber);
        PageFragment fragment = new PageFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);     
        mPageNumber = (Integer) getArguments().getInt(EXTRA_int);
        mData = "This is page " + mPageNumber;
    }

    public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
        Log.i(TAG, "onCreateView() on page " + mPageNumber);
        View v;

        v = inflater.inflate(R.layout.fragment_page, parent, false);

        mDataView = (TextView) v.findViewById(R.id.fragmentPage_data_id);
        mNoteView = (EditText) v.findViewById(R.id.fragmentPage_note_id);

        mNoteView.addTextChangedListener(new TextWatcher() {
            @Override
            public void afterTextChanged(Editable arg0) {}
            @Override
            public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {}

            @Override
            public void onTextChanged(CharSequence c, int start, int before, int count) {
                if (c==null) c="";  // do not allow c to be null
                mNote = c.toString().trim();
            }
        });
     getActivity().getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
        mDataView.setText(mData);
        mNoteView.setText(mNote);       

        return v;
    } // end to implementing onCreateView() for PageFragment

}

布局文件仅包含一个文本视图和一个线性布局内的编辑文本。 (除非要求,否则不会在此处复制)。

带有 GC 过滤器的日志显示如下结果:

12-06 00:07:03.205: D/dalvikvm(23276): GC_FOR_ALLOC freed 232K, 4% free 7758K/8052K, paused 16ms, total 18ms
12-06 00:07:10.095: D/dalvikvm(23276): GC_FOR_ALLOC freed 272K, 5% free 7989K/8324K, paused 19ms, total 19ms
12-06 00:07:20.945: D/dalvikvm(23276): GC_FOR_ALLOC freed 460K, 7% free 8043K/8564K, paused 21ms, total 22ms
12-06 00:07:23.805: D/dalvikvm(23276): GC_FOR_ALLOC freed 511K, 7% free 8043K/8616K, paused 17ms, total 17ms
12-06 00:07:42.535: D/dalvikvm(23276): GC_FOR_ALLOC freed 421K, 6% free 8134K/8616K, paused 18ms, total 21ms
12-06 00:08:03.925: D/dalvikvm(23276): GC_FOR_ALLOC freed 537K, 7% free 8109K/8708K, paused 20ms, total 20ms
12-06 00:08:11.665: D/dalvikvm(23276): GC_FOR_ALLOC freed 537K, 8% free 8083K/8708K, paused 27ms, total 30ms
12-06 00:08:15.265: D/dalvikvm(23276): GC_FOR_ALLOC freed 456K, 7% free 8138K/8708K, paused 18ms, total 18ms
12-06 00:08:20.015: D/dalvikvm(23276): GC_FOR_ALLOC freed 313K, 5% free 8315K/8708K, paused 25ms, total 26ms
12-06 00:08:24.685: D/dalvikvm(23276): GC_FOR_ALLOC freed 448K, 6% free 8378K/8888K, paused 18ms, total 19ms
12-06 00:08:27.955: D/dalvikvm(23276): GC_FOR_ALLOC freed 330K, 5% free 8560K/8952K, paused 28ms, total 28ms
12-06 00:08:36.055: D/dalvikvm(23276): GC_FOR_ALLOC freed 449K, 6% free 8625K/9136K, paused 37ms, total 37ms
12-06 00:08:42.015: D/dalvikvm(23276): GC_FOR_ALLOC freed 438K, 6% free 8699K/9200K, paused 60ms, total 63ms
12-06 00:08:45.775: D/dalvikvm(23276): GC_FOR_ALLOC freed 454K, 6% free 8760K/9276K, paused 27ms, total 30ms
12-06 00:08:51.185: D/dalvikvm(23276): GC_FOR_ALLOC freed 491K, 6% free 8771K/9328K, paused 36ms, total 36ms
12-06 00:08:57.845: D/dalvikvm(23276): GC_FOR_ALLOC freed 382K, 5% free 8866K/9328K, paused 28ms, total 30ms
12-06 00:09:03.885: D/dalvikvm(23276): GC_FOR_ALLOC freed 491K, 6% free 8887K/9440K, paused 26ms, total 27ms
12-06 00:09:06.905: D/dalvikvm(23276): GC_FOR_ALLOC freed 442K, 6% free 8955K/9460K, paused 26ms, total 27ms
12-06 00:09:11.215: D/dalvikvm(23276): GC_FOR_ALLOC freed 408K, 5% free 9058K/9528K, paused 23ms, total 25ms
12-06 00:09:19.355: D/dalvikvm(23276): GC_FOR_ALLOC freed 445K, 6% free 9130K/9640K, paused 47ms, total 47ms
12-06 00:09:24.965: D/dalvikvm(23276): GC_FOR_ALLOC freed 479K, 6% free 9208K/9748K, paused 29ms, total 30ms
12-06 00:09:30.735: D/dalvikvm(23276): GC_FOR_ALLOC freed 456K, 6% free 9330K/9848K, paused 35ms, total 36ms
12-06 00:09:39.765: D/dalvikvm(23276): GC_FOR_ALLOC freed 473K, 6% free 9477K/10012K, paused 37ms, total 37ms
12-06 00:09:54.795: D/dalvikvm(23276): GC_FOR_ALLOC freed 344K, 4% free 9803K/10208K, paused 36ms, total 36ms
12-06 00:10:05.605: D/dalvikvm(23276): GC_FOR_ALLOC freed 478K, 6% free 10104K/10644K, paused 28ms, total 28ms
12-06 00:10:17.165: D/dalvikvm(23276): GC_FOR_ALLOC freed 499K, 6% free 10483K/11044K, paused 40ms, total 41ms
12-06 00:10:28.855: D/dalvikvm(23276): GC_FOR_ALLOC freed 679K, 7% free 10811K/11552K, paused 44ms, total 46ms
12-06 00:10:43.815: D/dalvikvm(23276): GC_FOR_ALLOC freed 873K, 8% free 11054K/11988K, paused 47ms, total 47ms
12-06 00:11:02.155: D/dalvikvm(23276): GC_FOR_ALLOC freed 891K, 8% free 11360K/12312K, paused 41ms, total 41ms
12-06 00:11:22.725: D/dalvikvm(23276): GC_FOR_ALLOC freed 807K, 7% free 11852K/12720K, paused 58ms, total 58ms
12-06 00:11:46.165: D/dalvikvm(23276): GC_FOR_ALLOC freed 932K, 8% free 12382K/13376K, paused 45ms, total 45ms

以下是 MAT 报告的两个有趣的泄漏嫌疑人:

Suspect 1. 
49 instances of "android.support.v4.app.Fragment$SavedState", 
loaded by "dalvik.system.PathClassLoader @ 0x420ebce8" occupy 
1,892,544 (16.76%) bytes. These instances are referenced from 
one instance of "java.lang.Object[]", loaded by "<system class loader>"

Keywords
java.lang.Object[]
android.support.v4.app.Fragment$SavedState
dalvik.system.PathClassLoader @ 0x420ebce8


Suspect 2.
19 instances of "com.example.leakypager.MainActivity", loaded by
"dalvik.system.PathClassLoader @ 0x420ebce8" occupy 1,419,320 (12.57%) bytes.
Biggest instances:
com.example.leakypager.MainActivity @ 0x422adfa0 - 246,896 (2.19%) bytes.
com.example.leakypager.MainActivity @ 0x4238a430 - 237,880 (2.11%) bytes.
com.example.leakypager.MainActivity @ 0x420edc60 - 160,600 (1.42%) bytes.

Keywords
dalvik.system.PathClassLoader @ 0x420ebce8
com.example.leakypager.MainActivity

(以下更新于 2013 年 12 月 17 日添加) 在马丁(谢谢!)的大力帮助下进行了一些调查后,我发现有两个罪魁祸首。首先是尚未发布的TextWatcher(TextChangedListener)。这很容易解决。第二个罪魁祸首非常讨厌。

第二个罪魁祸首来自 EditText,它仍然保留对已销毁活动的引用 (mContext)。只有在运行 Android 4.3 的 Nexus 7 上测试应用程序时才会出现此问题。它不会出现在运行 4.1.3 的三星 TF 中。 MAT 显示以下从现有活动到死活动的传入链接:

com.example.leakySimplePager.MainActivity @ 0x4213b220 (dead)
'- mContext android.widget.EditText @ 0x42114ec8                
 '- mTextView android.widget.Editor @ 0x4214b4b0
  '- this$0 android.widget.Editor$EasyEditSpanController @ 0x420fafa0
   '- [2] java.lang.Object[13] @ 0x420e4a08  
    '- mSpans android.text.SpannableString @ 0x42129b90    
     '- text android.widget.TextView$SavedState @ 0x42129b68      
      '- [2] java.lang.Object[13] @ 0x42129b20        
       '- mValues android.util.SparseArray @ 0x42129ab8          
        '- value java.util.HashMap$HashMapEntry @ 0x4210e420            
         '- [2] java.util.HashMap$HashMapEntry[4] @ 0x4210e3f8              
          '- table java.util.HashMap @ 0x4210e3c0                
           '- mMap android.os.Bundle @ 0x4210e398                  
            '- mState android.support.v4.app.Fragment$SavedState @ 0x42129bb0                    
             '- [0] java.lang.Object[12] @ 0x42123300                      
              '- array java.util.ArrayList @ 0x42106830                        
               '- mSavedState com.example.leakySimplePager.MainActivity$ViewPagerAdapter @ 0x42114e90
                '- mViewPagerAdapter com.example.leakySimplePager.MainActivity @ 0x42140910 (alive)   
                 '- mContext com.android.internal.policy.impl.PhoneWindow$DecorView @ 0x420efc78                              
                  '- mCurRootView android.view.inputmethod.InputMethodManager @ 0x42102ee8                                

live Activity 之间的连接是通过每个 TextView 的 SavedState(EditText 是一个 TextView),当视图发生变化时由系统自动保存。 (参考:http://developer.android.com/guide/components/activities.html#SavingActivityState)这个不幸的 SavedState 被 FragmentStatePagerAdapter 作为其工作的一部分引用。与侦听器不同,当活动被销毁时,我无法从 ViewPager 中释放 FragmentStatePagerAdapter。

我观察到这个问题不仅发生在 EditText 上,而且还发生在支持更改其文本格式/颜色等的 TextView 上。不知何故,由于 TextView 无法释放其 mContext,我目前的解决方法是显式通过调用 mNoteView.setSaveEnabled(false) 阻止它保存状态;创建视图时。它不会伤害我的实际应用程序,因为每当片段恢复活动时,我已经保存、加载/重新加载 EditText 的数据。但是,Android 开发人员的文档似乎并不鼓励这种解决方法。也许有更好的方法来做到这一点。欢迎任何新的见解或专家建议!

【问题讨论】:

  • 你为什么这样做... @Override public int getItemPosition(Object item) { return POSITION_NONE; } 那是非常低效的。
  • 在原始应用程序中,我每次都需要刷新位置,因为当用户从一份菜单。这些功能没有包含在这个简单的代码中,因为我想专注于测试 viewpager 的内存使用情况。
  • 明白。我在这里还没有真正看到任何奇怪的东西……为了测试,您是否尝试过暂时切换到 FragmentPagerAdapter 而不是 State 版本?
  • 我没想到!会试试的。但我确实关闭了 @Override getItemPosition(Object item) { return POSITION_NONE; } 并再次测试。同样的现象。我怀疑某些活动没有被 GCed,因为某些 SavedStates 没有被处理掉。但是,当我在 FragmentStatePagerAdapter 中跟踪 mSavedState 数组的值时,它似乎永远不会大于 10(这是预期的,因为只有 10 页)。

标签: android memory-leaks garbage-collection


【解决方案1】:

我不确定发生了什么,但我将开始在答案中抛出想法,希望您能找到问题(比 cmets 容易得多)。

1) 尝试切换到 FragmentPagerAdapter(工作方式不同)。我认为这不会有太大变化。

2)通过制作更多最终的东西来改进你的片段......例如:

final View v = inflater.inflate(R.layout.fragment_page, parent, false);

mDataView 和 mNoteView 都是字段,可以是本地的吗?

尝试制作:

final TextView dataView = (TextView) v.findViewById(R.id.fragmentPage_data_id);
final EditText noteView = (EditText) v.findViewById(R.id.fragmentPage_note_id);

final 将使它们在您的匿名课程中可用。

我对此有点怀疑(在每个片段内部和 onCreateView 内部:

getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);

我宁愿在我的 Activity 中有一个 Fragments 可以调用的方法,让 Activity 处理它(也可以是在一个逻辑位置的一行代码,即 Activity,它可以控制窗口。

另外,如果您只在此处更改 DataView 和 EditView,并且能够将它们设为最终版本,那么也请修复这些行:

    dataView.setText(mData);
    noteView.setText(mNote);       

最后,我个人要改变的是你的活动……

我将完全为该片段适配器创建一个内部类,而不是使用匿名。

创建一个字段来保存它:

ViewPagerAdapter mViewPagerAdapter;

(这将是你的内部类)

private class ViewPagerAdapter extends FragmentStatePagerAdapter {

然后在你的 onCreate 中实例化它:

mPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mPagerAdapter);

另外,删除 FragmentManager 的实例,因为您似乎不再使用它,节省内存;)

这还可以让您执行以下操作:mPagerAdapter.notifyDataSetChanged();,以备不时之需。

所以这些都是我会这样做的事情,如果你看到任何变化,请尝试告诉我。

【讨论】:

  • 谢谢。我能够采纳一些建议,但不是全部,因为原始应用程序需要以这种方式完成某些事情。在检查到 MAT 中 GC 根的路径后,发现 SavedState 有一个指向 bundle 的指针(经过许多链接之后)指向一个指向 Activity 的 EditText。删除 EditText 后,内存问题就消失了。老实说,这对我来说还没有意义。但会继续探索,直到有更令人满意的答案。
  • 当我认为泄漏是由 EditText 引起时,我可能被误导了。还没有找到解释。但会继续努力。
  • 非常感谢,马丁!经过更多调查,我认为问题确实与EditText有关。一旦我更换它,就没有更多的泄漏。以下讨论似乎与我的问题密切相关。甚至 MAT 中显示的 GC 根路径也非常相似:stackoverflow.com/questions/14069501/…
  • 很高兴知道你找到了罪魁祸首! :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-03
  • 1970-01-01
  • 2015-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多