【问题标题】:Memory Leak due to PopupWindow由于 PopupWindow 导致的内存泄漏
【发布时间】:2025-12-19 07:55:07
【问题描述】:

我有一个 FragmentA。当我单击 FragmentA 中的按钮时,我会转到 FragmentB。在 FragmentB 我有一个 PopupWindow。 PopupWindow 有一个包含两个页面的 ViewPager。

我从这段代码中得到了帮助 - Emojicon

我有 2 个单独的类,View1 和 View2,分别用于 ViewPager 的第 1 页和第 2 页的视图。这两个类 View1 和 View2 都扩展了父类 ViewBase。

这是我的问题:

场景 1: 当我在 FragmentA 时,内存图显示 13MB 利用率。当我在不显示 PopupWindow 的情况下转到 FragmentB 时,内存图显示为 16MB,当我回到 FragmentA 时,它下降到 13MB。这很好。

场景 2: 当我在 FragmentA 时,内存图显示 13MB 利用率。当我通过显示 PopupWindow 转到 FragmentB 时,内存图显示为 20MB,而当我回到 FragmentA 时,它并没有下降到 13MB。

我尝试了 Eclipse MAT 和 Heap dump 来找出问题,但仍然没有帮助。我可以在 MAT 中看到,当我回到 FragmentA 并持有 PopupWindow、View1 和 View2 的实例时,FragmentB 仍在内存中。他们都没有被释放。 FragmentB 不应该在内存中。

请帮帮我。

这是我的 DemoPopupWindow.java

public class DemoPopupWindow extends PopupWindow {

// Views
private TabLayout mTabLayout;
private CustomViewPager mViewPager;
private PagerAdapter mViewPagerAdapter;
private RelativeLayout mLayout;
private View mRootView;

// Variables
private int mGreyColor, mPrimaryColor;
private OnSoftKeyboardOpenCloseListener onSoftKeyboardOpenCloseListener;
private int keyBoardHeight = 0;
private Boolean pendingOpen = false;
private Boolean isOpened = false;
private Context mContext;

ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        Rect r = new Rect();
        mRootView.getWindowVisibleDisplayFrame(r);

        int screenHeight = mRootView.getRootView().getHeight();
        int heightDifference = screenHeight - (r.bottom);
        if (heightDifference > 100) {
            keyBoardHeight = heightDifference;
            setSize(WindowManager.LayoutParams.MATCH_PARENT, keyBoardHeight);
            if (isOpened == false) {
                if (onSoftKeyboardOpenCloseListener != null)
                    onSoftKeyboardOpenCloseListener.onKeyboardOpen(keyBoardHeight);
            }
            isOpened = true;
            if (pendingOpen) {
                showAtBottom();
                pendingOpen = false;
            }
        } else {
            isOpened = false;
            if (onSoftKeyboardOpenCloseListener != null)
                onSoftKeyboardOpenCloseListener.onKeyboardClose();
        }
    }
};

/**
 * Constructor
 * @param rootView
 * @param mContext
 */
public DemoPopupWindow(View rootView, Context mContext){
    super(mContext);
    this.mContext = mContext;
    this.mRootView = rootView;

    Resources resources = mContext.getResources();
    View customView = createCustomView(resources);

    setContentView(customView);
    setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
    setSize((int) mContext.getResources().getDimension(R.dimen.keyboard_height), WindowManager.LayoutParams.MATCH_PARENT);

}

/**
 * Set keyboard close listener
 * @param listener
 */
public void setOnSoftKeyboardOpenCloseListener(OnSoftKeyboardOpenCloseListener listener){
    this.onSoftKeyboardOpenCloseListener = listener;
}

/**
 * Show PopupWindow
 */
public void showAtBottom(){
    showAtLocation(mRootView, Gravity.BOTTOM, 0, 0);
}

/**
 * Show PopupWindow at bottom
 */
public void showAtBottomPending(){
    if(isKeyBoardOpen())
        showAtBottom();
    else
        pendingOpen = true;
}

/**
 * Check whether keyboard is open or not
 * @return
 */
public Boolean isKeyBoardOpen(){
    return isOpened;
}

/**
 * Set soft keyboard size
 */
public void setSizeForSoftKeyboard(){
    mRootView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}

/**
 * Remove global layout listener
 */
public void removeGlobalListener() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        mRootView.getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener);
    } else {
        mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
    }
}

/**
 * Set PopupWindow size
 * @param width
 * @param height
 */
public void setSize(int width, int height){
    keyBoardHeight = height;
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, keyBoardHeight);
    mLayout.setLayoutParams(params);
    setWidth(width);
    setHeight(height);
}

/**
 * Create PopupWindow View
 * @return
 */
private View createCustomView(Resources resources) {
    LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
    View view = inflater.inflate(R.layout.popup, null, false);

    mViewPager = (CustomViewPager) view.findViewById(R.id.pager);
    mLayout = (RelativeLayout) view.findViewById(R.id.layout);

    mViewPagerAdapter = new ViewPagerAdapter(
            Arrays.asList(
                    new View1(mContext, this),
                    new View2(mContext, this)
            )
    );
    mViewPager.setAdapter(mViewPagerAdapter);

    mPrimaryColor = resources.getColor(R.color.color_primary);
    mGreyColor = resources.getColor(R.color.grey_color);

    mTabLayout = (TabLayout) view.findViewById(R.id.tabs);
    mTabLayout.addTab(mTabLayout.newTab());
    mTabLayout.addTab(mTabLayout.newTab());
    mTabLayout.setupWithViewPager(mViewPager);

    return view;
}

/**
 * ViewPager Adapter
 */
private static class ViewPagerAdapter extends PagerAdapter {
    private List<ViewBase> views;

    public ViewPagerAdapter(List<ViewBase> views) {
        super();
        this.views = views;
    }

    @Override
    public int getCount() {
        return views.size();
    }


    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        View v = views.get(position).mRootView;
        ((ViewPager)container).addView(v, 0);
        return v;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object view) {
        ((ViewPager)container).removeView((View)view);
    }

    @Override
    public boolean isViewFromObject(View view, Object key) {
        return key == view;
    }
}

/**
 * Soft keyboard open close listener
 */
public interface OnSoftKeyboardOpenCloseListener{
    void onKeyboardOpen(int keyBoardHeight);
    void onKeyboardClose();
}
}

请注意,我没有在此处粘贴完整的 PopupWindow 类,而只粘贴了必要的部分。

这是我在 FragmentB 中使用此 DemoPopupWindow 的方式

mPopupWindow = new DemoPopupWindow(mLayout, getActivity());
    mPopupWindow.setSizeForSoftKeyboard();


    // If the text keyboard closes, also dismiss the PopupWindow
    mPopupWindow.setOnSoftKeyboardOpenCloseListener(new DemoPopupWindow.OnSoftKeyboardOpenCloseListener() {

        @Override
        public void onKeyboardOpen(int keyBoardHeight) {

        }

        @Override
        public void onKeyboardClose() {
            if (mPopupWindow.isShowing())
                mPopupWindow.dismiss();
        }
    });

在 FragmentB 的 onDestroy 中我调用这个方法来移除 GlobalLayoutListener

mPopupWindow.removeGlobalListener();

我在 FragmentB 中有一个按钮来显示和关闭 PopupWindow。

这是我的 ViewBase.java

public class ViewBase {

public View mRootView;
DemoPopupWindow mPopup;
private Context mContext;

public ViewBase (Context context, DemoPopupWindow popup) {
    mContext = context;
    mPopup = popup;
}

public ViewBase () {
}
}

这是我的观点1

public class View1 extends ViewBase{

// Views
public View mRootView;
DemoPopupWindow mPopup;
private LinearLayout mLayoutText;

// Variables
private Context mContext;
private List<String> mText;

/**
 * Constructor
 */
public View1(Context context, DemoPopupWindow popup) {
    super(context, popup);

    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
    mPopup = popup;
    mRootView = inflater.inflate(R.layout.fragment_view1, null);
    mContext = context;

    // Set parent class rootview
    super.mRootView = mRootView;

    registerViews(mRootView);
    registerListeners();

    populateText();
}

/**
 * Register all the views
 * @param view
 */
private void registerViews(View view) {
    mLayoutText = (LinearLayout) view.findViewById(R.id.view1_layout);
    mText = TextManager.getInstance().getText();
}

/**
 * Populate text
 */
private void populateText() {
    int length = mText.size();
    for(int i=0; i<length; i++) {
        addNewText(mText.get(i).getText());
    }
}

/**
 * Add new text
 * @param text
 */
private void addNewText(final String text) {
    TextView textView = createTextView(text);
    mLayoutText.addView(textView);
    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // Do something
        }
    });
}

/**
 * Create textview
 * @param text
 * @return
 */
private TextView createTextView(final String text) {
    TextView textView = new TextView(mContext);
    FlowLayout.LayoutParams params = new FlowLayout.LayoutParams(FlowLayout.LayoutParams.WRAP_CONTENT, 40);
    params.setMargins(4, 4, 0, 0);
    textView.setLayoutParams(params);
    textView.setClickable(true);
    textView.setGravity(Gravity.CENTER);
    textView.setPadding(10, 0, 10, 0);
    textView.setText(text);
    textView.setTextSize(20);
    return textView;
}
}

再次编辑:

我找到了问题,但我不知道如何解决它。问题出在 mGlobalLayoutListener 上。这是持有一些观点的参考。如果我根本不使用 GlobalLayoutListener,那么 FragmentB 实例将从内存中删除。

即使在调用 removeGlobalLayout() 之后,此侦听器也不会被释放。请帮帮我。

【问题讨论】:

  • 嗨,你能提供PopupWindow代码吗?谢谢
  • 请用部分代码编辑您的问题。您是否正在关闭弹出窗口,即对其调用关闭?
  • 是的,您有内存泄漏。要知道我们在哪里需要代码。你也可以使用 LeakCanary。
  • @jeorfevre 我已经用我的代码编辑了我的问题。看看吧。
  • @Rohan 我已经用我的代码编辑了我的问题。看看吧。

标签: android android-fragments memory memory-leaks android-popupwindow


【解决方案1】:

如何安全移除 GlobalLayoutListener ? 注意您的 Android 版本,因为 api 已弃用! :)

你可以试试这个

 if (Build.VERSION.SDK_INT < 16) {
        v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
    } else {
        v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
    }

【讨论】:

  • 我已经在 onStop() 中关闭了 popupWindow。内存中还有一个 DemoPopupWindow 的实例。
  • 如果您这样做,我们需要查看您的代码执行情况。我怀疑解决方案是在片段解散中,但我认为其他东西是被创造出来的,而不是被解雇的。你能给我更多关于 View1 和 View2 的信息吗? mViewPagerAdapter = new ViewPagerAdapter(Arrays.asList(new View1(mContext, this), new View2(mContext, this)));
  • 请在github上分享你的项目,因为看不到错误!整个项目将尝试重现此错误。
  • 我不能在 github 上分享这个项目,因为它是一个巨大的私人项目。无论如何感谢您的支持。
  • 我找到了问题但不知道如何解决。请再看看我的问题。如果我不添加 GlobalLayoutListener 那么如果 FragmentB 在内存中就没有实例。
【解决方案2】:

您确定 CustomPopupWindow 会导致内存泄漏吗?您在运行堆转储之前是否进行了垃圾收集,也许根本没有泄漏..? 当你回到fragmentA时,它在FragmentB中调用onDestroy并弹出?

【讨论】:

  • 只需在 onGlobalLayout 方法中删除GlobalLayoutListener..在方法开始或停止..但我仍然认为这不是泄漏。