ViewPager Fragment OOM 内存优化总结篇

853 阅读4分钟

ViewPager 内存优化总结篇

原理分析+代码实践

  • 很多文章都介绍到了viewpager + fragment + 多图片的内存优化问题,但是没有讲到具体的原理,以及实践后的效果,这篇文档将通过原理分析+代码实践的方式全面介绍我们项目开发过程中的遇到的常见内存OOM以及如何解决?

1. ViewPager 如其名所述,是负责翻页的一个 View。准确说是一个  ViewGroup,包含多个 View 页,在手指横向滑动屏幕时,其负责对 View 进行切换。为了生成这些 View 页,需要提供一个  PagerAdapter 来进行和数据绑定以及生成最终的 View 页。

2. FragmentStatePagerAdapte、FragmentPagerAdapter、PagerAdapter的区别?

先上Adapter总结

  • 相同点:都会保存item(fragment)的状态,即当前item显示的同时,Adapter会预加载下一个item,保存前一个item在内存中
  • 重点:viewPager.setOffscreenPageLimit(n),代表需要保存(前)/预加载(后)的item的数量,n<=1
  • 不同点:item(fragment)存储、恢复、销毁的方式不同
  • 重点:问题核心onDestroy调不调用?Fragment示例销不销毁?
例如:依次从左向右有fragment1,fragment2,fragment3三个页面
 
FragmentPagerAdapter在滑动到fragment3时,fragment1会依次调用onPause()、onStop()、onDestroyView(),再向左滑动到fragment2时,fragment1会调用onCreateView()、onActivityCreated()、onStart()、onResume()。
结论:FragmentPagerAdapter会保留页面的状态,并不会完全销毁掉。
 
FragmentStatePagerAdapter在滑动到fragment3时,fragment1会依次调用onPause()、onStop()、onDestroyView()、onDestroy()、onDetach()方法,再向左滑动到fragment2时,fragment1会调用onAttach()、onCreate()、onCreateView()、onActivityCreated()、onStart()、onResume()。
结论:FragmentStatePagerAdapter会完全销毁滑动过去的item,当需要初始化的时候,会重新初始化页面。

原理分析

  • PagerAdapter,ViewPager 的支持者,ViewPager 将调用它来取得所需显示的页,而 PageAdapter 也会在数据变化时,通知 ViewPager。这个类也是FragmentPagerAdapter 以及 FragmentStatePagerAdapter 的基类。如果继承自该类,至少需要实现 instantiateItem(), destroyItem(), getCount() 以及 isViewFromObject()。
  • FragmentStatePagerAdapter,在我们切换不同的Fragment的时候,我们会把前面的Fragment销毁,而我们系统在销毁前,会把我们的我们Fragment的Bundle在我们的onSaveInstanceState(Bundle)保存下来。等用户切换回来的时候,我们的Fragment就会根据我们的instance state恢复出来。
  • FragmentPagerAdapter,使用这种Adapter,我们的Fragment在切换的时候,不会销毁,如官方文档所述,该类内的每一个生成的Fragmen都将保存在内存之中,而只是调用事务中的detach方法,这种方法,我们只会把我们的Fragment的view销毁,而保留了以前的Fragment对象。所以通过这种方式创建的Fragment一直不会被销毁。
  • 场景使用不同:卡片特别的多的情况下,采用PagerAdapter会导致内存一直飙升;如果需要处理有很多页,并且数据动态性较大、占用内存较多的情况,应该使用FragmentStatePagerAdapter
  • 速滑卡顿:懒加载方案,通过isPageShow、isVisibleToUser控制切换到当前Fragment时再进行网络请求,可以定义LazyAbstractFragment
//view onViewCreated/onDestroyView
private boolean isPageShow = false;
//Fragment可见
private boolean isVisibleToUser = false;

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    this.isVisibleToUser = isVisibleToUser;
    //缓存的Fragment调用
    if (isVisibleToUser && isPageShow) {
        requestNet();
    }
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    isPageShow = true;
    //首次加载的Fragment调用setUserVisibleHint()=>onViewCreated();
    if (isVisibleToUser && isPageShow) {
        requestNet();
    }
}

@Override
public void onDestroyView() {
    super.onDestroyView();
    isPageShow = false;
    releaseView();
}

public abstract void requestNet();

public abstract void releaseView();

PagerAdapter源码

@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    return this.instantiateItem((View)container, position);
}

public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    this.destroyItem((View)container, position, object);
}

FragmentPagerAdapter源码:

@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    if (this.mCurTransaction == null) {
        this.mCurTransaction = this.mFragmentManager.beginTransaction();
    }

    long itemId = this.getItemId(position);
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        this.mCurTransaction.attach(fragment);
    } else {
        fragment = this.getItem(position);
        this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
    }

    if (fragment != this.mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    if (this.mCurTransaction == null) {
        this.mCurTransaction = this.mFragmentManager.beginTransaction();
    }

    this.mCurTransaction.detach((Fragment)object);
}

FragmentStatePagerAdapter源码:

@NonNull
public Object instantiateItem(@NonNull ViewGroup container, int position) {
    Fragment fragment;
    if (this.mFragments.size() > position) {
        fragment = (Fragment)this.mFragments.get(position);
        if (fragment != null) {
            return fragment;
        }
    }

    if (this.mCurTransaction == null) {
        this.mCurTransaction = this.mFragmentManager.beginTransaction();
    }

    fragment = this.getItem(position);
    if (this.mSavedState.size() > position) {
        SavedState fss = (SavedState)this.mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }

    while(this.mFragments.size() <= position) {
        this.mFragments.add((Object)null);
    }

    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    this.mFragments.set(position, fragment);
    this.mCurTransaction.add(container.getId(), fragment);
    return fragment;
}

public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (this.mCurTransaction == null) {
        this.mCurTransaction = this.mFragmentManager.beginTransaction();
    }

    while(this.mSavedState.size() <= position) {
        this.mSavedState.add((Object)null);
    }

    this.mSavedState.set(position, fragment.isAdded() ? this.mFragmentManager.saveFragmentInstanceState(fragment) : null);
    this.mFragments.set(position, (Object)null);
    this.mCurTransaction.remove(fragment);
}

3. OOM

  • 不管是使用FragmentStatePagerAdapter还是FragmentPagerAdaper,除了要考虑使用场景的问题,还要注意释放资源,viewpager+Fragment+多图片内存溢出崩溃,页面的体验会让产品暴揍你! 常见问题:
  • 图片加载开源库有很多,常见的Glide,Glide会回收图片释放内存,然而如果该图片一直被imageview保持引用就会出现无法回收的状态,最终这些图片越来越多,导致内存溢出。所以应该在适当的时间释放掉这些引用
  • 必须在onDestroyView() 释放资源Drawables、Bitmap、Adapter等,onDestroy() 释放变量List、Listener等
private void unbindDrawables(View view) {
    if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
    }
    if (view instanceof ImageView) {
        ((ImageView) view).setImageDrawable(null);
    }
    if (view instanceof ViewGroup && !(view instanceof AdapterView)) {
        for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
        }
        ((ViewGroup) view).removeAllViews();
    }
}
  • RecycleView中Adapter的优化,在 onViewRecycled() 中释放ImageView的引用,当然有时候也会遇到NestedScrollView嵌套RecyclerView导致RecyclerView复用失效导致的OOM,也是需要注意的点
@Override
    public void onViewRecycled(VideoViewHolder holder) {
        if (holder != null) {
            holder.img.setImageDrawable(null);
            Glide.with(imageView.getContext()).clear(imageView);
        }
        super.onViewRecycled(holder);
    }

接下来的文章我们将介绍ViewPager2,Google官方已经不再维护ViewPager,建议切换到ViewPager2