You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决LeakCanary检测到的SubroutineParentItemAdapter接口泄漏

RecyclerView Adapter静态接口引发Fragment内存泄漏问题分析与修复

问题描述

在SubroutineFragment中使用SubroutineParentItemAdapter时,LeakCanary持续检测到Adapter的mOnClickListener接口存在内存泄漏。尝试重写onDetachedFromRecyclerView方法置空接口引用,但未解决问题,需明确泄漏原因、修复方案及自定义接口避免泄漏的规范实践。

泄漏栈信息

┬───
│ GC Root: Input or output parameters in native code
│
├─ dalvik.system.PathClassLoader instance
│    Leaking: NO (SubroutineParentItemAdapter↓ is not leaking and A ClassLoader
│    is never leaking)
│    ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    Leaking: NO (SubroutineParentItemAdapter↓ is not leaking)
│    ↓ Object[1914]
├─ com.habitdev.sprout.ui.menu.subroutine.adapter.SubroutineParentItemAdapter
│  class
│    Leaking: NO (a class is never leaking)
│    ↓ static SubroutineParentItemAdapter.<strong>mOnClickListener</strong>
╰→ com.habitdev.sprout.ui.menu.subroutine.SubroutineFragment instance
​     Leaking: YES (ObjectWatcher was watching this because com.habitdev.sprout.
​     ui.menu.subroutine.SubroutineFragment received Fragment#onDestroy()
​     callback. Conflicts with Fragment#mFragmentManager is not null)
​     Retaining 1.2 kB in 34 objects
​     key = 7e3f4168-59c3-4b4e-89f8-b7c2112ed785
​     watchDurationMillis = 7398
​     retainedDurationMillis = 2398

相关代码

SubroutineFragment代码

public class SubroutineFragment extends Fragment implements SubroutineParentItemAdapter.OnClickListener, SubroutineModifyFragment.onClickBackPress {

    private FragmentSubroutineBinding binding;
    protected SubroutineModifyFragment subroutineModifyFragment = new SubroutineModifyFragment();

    public SubroutineFragment() {
        subroutineModifyFragment.setmOnClickBackPress(this);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        binding = FragmentSubroutineBinding.inflate(inflater, container, false);
        setRecyclerViewAdapter();
        onBackPress();
        return binding.getRoot();
    }

    /**
     * Initialized and sets adapter for recyclerView
     */
    private void setRecyclerViewAdapter() {
        HabitWithSubroutinesViewModel habitWithSubroutinesViewModel = new ViewModelProvider(requireActivity()).get(HabitWithSubroutinesViewModel.class);
        binding.subroutineRecyclerView.setLayoutManager(new LinearLayoutManager(requireActivity()));

        SubroutineParentItemAdapter parentAdapterItem = new SubroutineParentItemAdapter();
        parentAdapterItem.setHabitsOnReform(habitWithSubroutinesViewModel.getAllHabitOnReform());

        setEmptyRVBackground(parentAdapterItem);

        parentAdapterItem.setmOnClickListener(this);
        parentAdapterItem.setSubroutineLifecycleOwner(getViewLifecycleOwner());
        parentAdapterItem.setHabitWithSubroutinesViewModel(habitWithSubroutinesViewModel);

        binding.subroutineRecyclerView.setAdapter(parentAdapterItem);

        habitWithSubroutinesViewModel.getAllHabitOnReformLiveData().observe(getViewLifecycleOwner(), habits -> {
            parentAdapterItem.setHabitsOnReform(habits);
            setEmptyRVBackground(parentAdapterItem);
        });
    }

    /**
     * Displays No subroutine on reform
     *
     * @param adapter SubroutineParentItemAdapter
     */
    private void setEmptyRVBackground(SubroutineParentItemAdapter adapter) {
        if (adapter.getItemCount() > 0) {
            binding.subroutineEmptyLottieRecyclerView.setVisibility(View.INVISIBLE);
            binding.subroutineEmptyLbl.setVisibility(View.INVISIBLE);
        } else {
            binding.subroutineEmptyLottieRecyclerView.setVisibility(View.VISIBLE);
            binding.subroutineEmptyLbl.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Interface from SubroutineParentItemAdapter
     *
     * @param habit Habit on Modify
     */
    @Override
    public void onModifySubroutine(Habits habit) {
        subroutineModifyFragment.setHabit(habit);
        getChildFragmentManager().beginTransaction()
                .addToBackStack(SubroutineFragment.this.getTag())
                .add(binding.subroutineFrameLayout.getId(), subroutineModifyFragment)
                .commit();
        binding.subroutineContainer.setVisibility(View.GONE);
    }

    /**
     * Interface from SubroutineModify Fragment
     */
    @Override
    public void returnSubroutineFragment() {
        getChildFragmentManager()
                .beginTransaction()
                .remove(subroutineModifyFragment)
                .commit();
        binding.subroutineContainer.setVisibility(View.VISIBLE);
    }

    /**
     * Handle on BackPress
     */
    private void onBackPress() {
        OnBackPressedCallback callback = new OnBackPressedCallback(true) {
            @Override
            public void handleOnBackPressed() {
                //Do something
            }
        };
        requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), callback);
    }

    /**
     * On Fragment Destroy
     */
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}

SubroutineParentItemAdapter代码

public class SubroutineParentItemAdapter extends RecyclerView.Adapter<SubroutineParentItemAdapter.ParentItemViewHolder> {

    private List<Habits> habitsOnReform;
    protected HabitWithSubroutinesViewModel habitWithSubroutinesViewModel;
    protected LifecycleOwner subroutineLifecycleOwner;

    public interface OnClickListener {
        void onModifySubroutine(Habits habit);
    }

    private static OnClickListener mOnClickListener;

    public void setmOnClickListener(OnClickListener mOnClickListener) {
        SubroutineParentItemAdapter.mOnClickListener = mOnClickListener;
    }

    public void setHabitWithSubroutinesViewModel(HabitWithSubroutinesViewModel habitWithSubroutinesViewModel) {
        this.habitWithSubroutinesViewModel = habitWithSubroutinesViewModel;
    }

    public void setSubroutineLifecycleOwner(LifecycleOwner subroutineLifecycleOwner) {
        this.subroutineLifecycleOwner = subroutineLifecycleOwner;
    }

    public SubroutineParentItemAdapter() {}

    @NonNull
    @Override
    public ParentItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ParentItemViewHolder(
                LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_subroutine_parent_item, parent, false)
        );
    }

    @Override
    public void onBindViewHolder(@NonNull ParentItemViewHolder holder, int position) {

        long uid = holder.bindData(habitsOnReform.get(position), mOnClickListener);

        LayoutAnimationController animationController = AnimationUtils.loadLayoutAnimation(holder.childRecycleView.getContext(), R.anim.layout_animation_fall);
        holder.childRecycleView.setLayoutAnimation(animationController);

        List<Subroutines> habitWithSubroutines;

        habitWithSubroutines = habitWithSubroutinesViewModel.getAllSubroutinesOfHabit(uid);
        SubroutineChildItemAdapter childAdapterItem = new SubroutineChildItemAdapter(habitWithSubroutines);
        holder.childRecycleView.setAdapter(childAdapterItem);

        childAdapterItem.setHabitWithSubroutines(habitWithSubroutines);

        habitWithSubroutinesViewModel.getAllSubroutinesOnReformHabitLiveData(uid).observe(subroutineLifecycleOwner, childAdapterItem::setHabitWithSubroutines);

        setItemTouchHelper(holder, childAdapterItem);
    }

    private void setItemTouchHelper(SubroutineParentItemAdapter.ParentItemViewHolder holder, SubroutineChildItemAdapter childAdapterItem) {

        ItemTouchHelper itemTouchHelper;
        ItemTouchHelper.Callback itemTouchHelperCallback = new ItemTouchHelper.Callback() {
            @Override
            public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
                return makeMovementFlags(0, ItemTouchHelper.END | ItemTouchHelper.START);
            }

            @Override
            public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
                return false;
            }

            @Override
            public boolean isLongPressDragEnabled() {
                return false;
            }

            @Override
            public boolean isItemViewSwipeEnabled() {
                return true;
            }

            @Override
            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
                switch (direction) {
                    case ItemTouchHelper.END:
                    case ItemTouchHelper.START:
                        SubroutineChildItemAdapter.ChildItemViewHolder childItemViewHolder;
                        childItemViewHolder = (SubroutineChildItemAdapter.ChildItemViewHolder) viewHolder;
                        childAdapterItem.notifyItemChanged(childItemViewHolder.getAbsoluteAdapterPosition());
                        break;
                }
            }
        };
        itemTouchHelper = new ItemTouchHelper(itemTouchHelperCallback);
        itemTouchHelper.attachToRecyclerView(holder.childRecycleView);
    }


    @Override
    public int getItemCount() {
        return habitsOnReform.size();
    }

    @Override
    public int getItemViewType(int position) {
        return super.getItemViewType(position);
    }

    public void setHabitsOnReform(List<Habits> habitsOnReform) {
        this.habitsOnReform = habitsOnReform;
    }

    public static class ParentItemViewHolder extends RecyclerView.ViewHolder {

        RelativeLayout itemLayout;
        TextView HabitsTitle;
        Button ModifySubroutine;
        RecyclerView childRecycleView;
        Drawable cloud, amethyst, sunflower, nephritis, bright_sky_blue, alzarin;

        public ParentItemViewHolder(@NonNull View itemView) {
            super(itemView);
            itemLayout = itemView.findViewById(R.id.subroutine_parent_item_layout);
            HabitsTitle = itemView.findViewById(R.id.subroutine_parent_item_habit_title);
            ModifySubroutine = itemView.findViewById(R.id.subroutine_parent_item_modify_subroutine);
            childRecycleView = itemView.findViewById(R.id.subroutine_child_recyclerview);

            cloud = ContextCompat.getDrawable(itemView.getContext(), R.drawable.background_btn_cloud_selector);
            amethyst = ContextCompat.getDrawable(itemView.getContext(), R.drawable.background_btn_amethyst_selector);
            sunflower = ContextCompat.getDrawable(itemView.getContext(), R.drawable.background_btn_sunflower_selector);
            nephritis = ContextCompat.getDrawable(itemView.getContext(), R.drawable.background_btn_nephritis_selector);
            bright_sky_blue = ContextCompat.getDrawable(itemView.getContext(), R.drawable.background_btn_brightsky_blue_selector);
            alzarin = ContextCompat.getDrawable(itemView.getContext(), R.drawable.background_btn_alzarin_selector);
        }

        @SuppressLint("ClickableViewAccessibility")
        long bindData(Habits habit, OnClickListener mOnClickListener) {

            if (habit.getColor().equals(AppColor.ALZARIN.getColor())) {
                itemLayout.setBackground(alzarin);
            } else if (habit.getColor().equals(AppColor.AMETHYST.getColor())) {
                itemLayout.setBackground(amethyst);
            } else if (habit.getColor().equals(AppColor.BRIGHT_SKY_BLUE.getColor())) {
                itemLayout.setBackground(bright_sky_blue);
            } else if (habit.getColor().equals(AppColor.NEPHRITIS.getColor())) {
                itemLayout.setBackground(nephritis);
            } else if (habit.getColor().equals(AppColor.SUNFLOWER.getColor())) {
                itemLayout.setBackground(sunflower);
            } else {
                itemLayout.setBackground(cloud);
            }

            HabitsTitle.setText(habit.getHabit());

            HabitsTitle.setPadding(padding_inPx(10), padding_inPx(0), padding_inPx(10), padding_inPx(5));

            itemLayout.setOnClickListener(view -> {
                childRecycleView.getVisibility();
                if (childRecycleView.getVisibility() == View.GONE) {
                    LayoutAnimationController animationController = AnimationUtils.loadLayoutAnimation(childRecycleView.getContext(), R.anim.layout_animation_fall);
                    childRecycleView.setLayoutAnimation(animationController);
                    childRecycleView.setVisibility(View.VISIBLE);
                } else {
                    LayoutAnimationController animationController = AnimationUtils.loadLayoutAnimation(childRecycleView.getContext(), R.anim.layout_animation_up);
                    childRecycleView.setLayoutAnimation(animationController);
                    if (!childRecycleView.isAnimating()) childRecycleView.setVisibility(View.GONE);
                }
            });

            itemLayout.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View view, MotionEvent motionEvent) {
                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
                        HabitsTitle.setPadding(padding_inPx(10), padding_inPx(5), padding_inPx(10), padding_inPx(0));
                    } else if (motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_CANCEL) {
                        HabitsTitle.setPadding(padding_inPx(10), padding_inPx(0), padding_inPx(10), padding_inPx(5));
                    }
                    return false;
                }
            });

            if (habit.isModifiable()) {
                ModifySubroutine.setOnClickListener(view -> {
                    mOnClickListener.onModifySubroutine(habit); //OnclickListener Here
                });
            } else {
                ModifySubroutine.setVisibility(View.GONE);
            }

            return habit.getPk_habit_uid();
        }

        int padding_inPx(int dp) {
            final float scale = itemLayout.getResources().getDisplayMetrics().density;
            return (int) (dp * scale + 0.5f);
        }
    }

    /**
     * Detach mOnClickListener
     * @param recyclerView
     */
    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        super.onDetachedFromRecyclerView(recyclerView);
        if (mOnClickListener != null) {
            mOnClickListener = null;
        }
    }
}

泄漏原因分析

  1. 静态变量生命周期不匹配:mOnClickListener是Adapter类的静态成员,其生命周期与应用进程一致,而SubroutineFragment是短生命周期组件。当Fragment执行onDestroy()后,静态变量仍持有Fragment实例的强引用,导致Fragment无法被GC回收,引发内存泄漏。
  2. onDetachedFromRecyclerView无效的本质:静态变量属于类级别共享,而非实例私有。onDetachedFromRecyclerView是实例方法,仅当前Adapter实例从RecyclerView分离时执行,但如果存在多个Adapter实例,静态引用可能被其他实例覆盖,或者当前实例的置空操作无法影响已持有Fragment引用的静态变量状态。

修复方案

方案1:移除静态修饰符(推荐)

将Adapter中的静态接口引用改为实例成员,使其生命周期与Adapter实例绑定:

// 移除static修饰符
private OnClickListener mOnClickListener;

public void setmOnClickListener(OnClickListener mOnClickListener) {
    this.mOnClickListener = mOnClickListener;
}

同时在Fragment的onDestroyView中主动断开引用:

// 将parentAdapterItem改为Fragment成员变量
private SubroutineParentItemAdapter parentAdapterItem;

private void setRecyclerViewAdapter() {
    // ... 其他代码
    parentAdapterItem = new SubroutineParentItemAdapter();
    // ... 其他代码
}

@Override
public void onDestroyView() {
    super.onDestroyView();
    if (parentAdapterItem != null) {
        parentAdapterItem.setmOnClickListener(null);
    }
    binding = null;
}

方案2:弱引用包装(不推荐,仅适配特殊场景)

若必须保留静态变量,用WeakReference包装接口,允许GC回收Fragment实例:

private static WeakReference<OnClickListener> mOnClickListenerRef;

public void setmOnClickListener(OnClickListener mOnClickListener) {
    mOnClickListenerRef = new WeakReference<>(mOnClickListener);
}

// 在调用接口前检查弱引用有效性
@Override
public void onBindViewHolder(@NonNull ParentItemViewHolder holder, int position) {
    OnClickListener listener = mOnClickListenerRef != null ? mOnClickListenerRef.get() : null;
    long uid = holder.bindData(habitsOnReform.get(position), listener);
    // ... 其他代码
}

// ViewHolder中调用接口前也需判空
if (habit.isModifiable() && listener != null) {
    ModifySubroutine.setOnClickListener(view -> {
        listener.onModifySubroutine(habit);
    });
}

自定义接口避免内存泄漏的规范实践

  • 禁用静态变量持有短生命周期组件引用:静态变量生命周期过长,极易引发内存泄漏,优先使用实例成员变量。
  • 主动清理引用:在Fragment/Activity的销毁回调(如onDestroyView、onDestroy)中,主动置空Adapter或其他组件中的接口引用。
  • 弱引用适配特殊场景:若必须用长生命周期持有短生命周期对象,用WeakReference包装,避免阻止GC回收。
  • Lambda表达式注意捕获引用:使用Lambda作为接口实现时,避免直接捕获外部类强引用,必要时用弱引用包装外部类实例。
  • 绑定组件生命周期:接口引用的生命周期应与使用它的组件
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 04:02:00