如何解决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; } } }
泄漏原因分析
- 静态变量生命周期不匹配:
mOnClickListener是Adapter类的静态成员,其生命周期与应用进程一致,而SubroutineFragment是短生命周期组件。当Fragment执行onDestroy()后,静态变量仍持有Fragment实例的强引用,导致Fragment无法被GC回收,引发内存泄漏。 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作为接口实现时,避免直接捕获外部类强引用,必要时用弱引用包装外部类实例。
- 绑定组件生命周期:接口引用的生命周期应与使用它的组件
相关产品推荐
相关产品推荐

