ViewPager 内存优化总结篇
原理分析+代码实践
- 很多文章都介绍到了viewpager + fragment + 多图片的内存优化问题,但是没有讲到具体的原理,以及实践后的效果,这篇文档将通过原理分析+代码实践的方式全面介绍我们项目开发过程中的遇到的常见内存OOM以及如何解决?
1. ViewPager 如其名所述,是负责翻页的一个 View。准确说是一个 ViewGroup,包含多个 View 页,在手指横向滑动屏幕时,其负责对 View 进行切换。为了生成这些 View 页,需要提供一个 PagerAdapter 来进行和数据绑定以及生成最终的 View 页。
-
- ViewPager 通过 setAdapter() 来建立与 PagerAdapter 的联系。这个联系是双向的,一方面,ViewPager 会拥有 PagerAdapter 对象,从而可以在需要时调用 PagerAdapter 的方法;另一方面,ViewPager 会在 setAdapter() 中调用 PagerAdapter 的 registerDataSetObserver() 方法,注册一个自己生成的 PagerObserver 对象,从而在 PagerAdapter 有所需要时(如 notifyDataSetChanged() 或 notifyDataSetInvalidated() 时),可以调用 Observer 的 onChanged() 或 onInvalidated() 方法,从而实现 PagerAdapter 向 ViewPager 方向发送信息。
-
- 在 PagerObserver.onChanged(),以及 PagerObserver.onInvalide() 中被调用。因此当 PagerAdapter.notifyDataSetChanged() 被触发时,ViewPager.dataSetChanged() 也可以被触发。该函数将使用 getItemPosition() 的返回值来进行判断,如果为 POSITION_UNCHANGED,则什么都不做;如果为 POSITION_NONE,则调用 PagerAdapter.destroyItem() 来去掉该对象,并设置为需要刷新 (needPopulate = true) 以便触发 PagerAdapter.instantiateItem() 来生成新的对象。
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);
}