【发布时间】: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