Fragment的用法很多,体现在:
但只有一个目的:随便折腾,别搞出内存泄漏(即替换Fragment要能及时回收)。这里记录下给出建议方案,只要保证了内存不泄漏,你可以随便折腾Fragment的用法。
这里我们借助LeakCanary。
Leakcanary官网上提到的常见的三种内存泄漏:
Common causes for memory leaks¶
Most memory leaks are caused by bugs related to the lifecycle of objects. Here are a few common Android mistakes:
1.Adding a Fragment instance to the backstack without clearing that Fragment’s view fields in Fragment.onDestroyView() (more details in this StackOverflow answer).
2.Storing an Activity instance as a Context field in an object that survives activity recreation due to configuration changes.
3.Registering a listener, broadcast receiver or RxJava subscription which references an object with lifecycle, and forgetting to unregister when the lifecycle reaches its end.
LeakCanary最基本的使用:
集成
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.7’
即可。
打印:D LeakCanary: LeakCanary is running and ready to detect leaks
表示成功启动。
日志过滤:Leakanary,在Fragment 或者Activity关闭destory的时候,是否有objects引用。
并在5s后,是否未释放,有泄漏的objects。
比如
08-12 11:16:18.942 7343 7343 D LeakCanary: Watching instance of androidx.constraintlayout.widget.ConstraintLayout (com.allan.fragmentstester.CombineFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 7304ce05-e4e2-453c-9178-79cd1bdfc3e7
08-12 11:16:24.104 7343 7384 D LeakCanary: Found 1 object retained, not dumping heap yet (app is visible & < 5 threshold)
它监控了Fragment的onDestoryView,并推迟5s后,检查object的引用,给出提示。在多次反复测试以后,Found x object
,就证明有泄漏。最新版还会有详细报告。
但是LeakCanary对于Fragment的检测机制存在误区
。看我的分析。
//默认进入的第一个Fragment
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = new AFragment();//fragmentManager.findFragmentByTag(TAG_A);
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.root_layout, fragment)
.addToBackStack(null)
.commit();
//跳转第二个Fragment
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment toFragment = new BFragment();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction
.replace(R.id.root_layout, toFragment)
.addToBackStack(null)
.commit();
A BFragment里面的代码,用全局变量保存View、Button等。很常规的操作对吧。replace + addToBackstack。
接着断点调试,关注FragmentManager下面的
mFragmentStore(FragmentStore fragment1.2.6库,FragmentManagerImp 1.1.x库)下面的:
mAdded;
**mNonConfig **这个ViewModel的mChildConfigs的size;
mActive的Size。
通过断点和置空前后对比,不论是否在onDestoryView给所有的成员View变量button = null,这些size都是在不断的膨胀!
经过分析和理解LeakCanary的设计,我认为是LeakCanary的“误报”,因为使用addBackStack
的,Fragment onDestoryView
以后,Fragment就是被设计成,存留在FragmentManager的缓存中。android的Fragment设计就是有缓存,会被设置为CREATE状态,后续用的时候,直接拿出来变变状态即可使用。
我们平时开发,在Fragment的onDestoryView() 中置空View的引用(也即Leakcanary官网上第一条常见内存问题)能够达到内存泄漏解除。但是,这是为什么呢?
这是因为,置空了View的引用,导致LeakCanary的引用因子变低,它采用的是引用个数达到5个才认为泄漏;更甚,来回切换Fragment,不断增加fragment数量size,仍然会出现内存泄漏,LeakCanary反而不报objects占用了,岂不是掩耳盗铃
?
所以,把内存泄漏的锅,甩给到Fragment内部View的引用,我一直觉得奇怪,Fragment的生命周期存活才是问题的关键,Fragment的成员变量有什么错呢?
这个锅,成员变量不背!
内存泄漏检查的是gcRoot,它只检测了fragment的子对象对于fragment本身的引用,但是没有统计基础库FragmentManager对于fragment的引用
, 其实这是错误的。
在于FragmentManager,FragmentStore的引用问题。
//FragmentManager.class
private final ArrayList<Fragment> mAdded = new ArrayList<>();
private final HashMap<String, FragmentStateManager> mActive = new HashMap<>();
//FragmentStateManager中包含mFragment
//FragmentManagerViewModel.class的成员
private final HashMap<String, Fragment> mRetainedFragments = new HashMap<>();
mAdded
是不会包含Destoryed的Fragment的;从调试结果来看,可以看出mActive中包含了Destory的那个Fragment(即,用变量保存过View、Button的Fragment)。
当然不仅仅在mActive这个map中持有引用,其实还有FragmentManagerViewModel mRetainedFragments等等都会持有。
boolean beingRemoved = f.mRemoving && !f.isInBackStack();
if (beingRemoved) {
makeInactive(fragmentStateManager);
}
mRemoveing是true了,但是mBackStackNesting = 2,因为被添加到了回退步骤中。所以无法被移除mActive。
所以添加了addBackStack,就不会被makeInactive,进而不会被mActive移除,进而持有着fragment的引用,导致size不断增加(如图中,来回切换了几次后,size都到了10,本来就是2个Fragment的事情)。由于我们在onDestoryView View =null,更加导致LeakCanary不报错了,掩耳盗铃!
通过阅读fragment库的源码,了解到 mActive和mRetainedFragments在标记一个FragmentStore.java(老版本FragmentManagerImpl.java) makeInactive()
的时候,从map中移除或者设置null,才能让Fragment失去引用,也就能把内存回收了。
直接Replace : onDestroyView() – onDestroy() – onDetach()
Fragment会被回收。
Replace 并且 addToBackStack : onDestroyView()
Fragment不能回收。
根本原因是replace fragment和addBackStack的使用有误,和检测内存泄漏机制的问题。
回到前面写的代码。每次都是new新的Fragment,并且直接replace并将他添加addToBackStack!
这才是内存泄漏的根本原因。还会导致backStack的步数增加!
做个试验,上面代码去除addToBackStack,size有1个。其他的老Fragment被移除了引用。
所以正确的使用是如下几条:
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE); //tag
if (fragment == null) {
fragment = new CombineFragment(); //没有才新建
}
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction
.replace(R.id.root_layout, fragment, TAG_COMBINE) //tag
.addToBackStack(null)
.commit();
这样能够保证,不论你是否addToBackStack,最多只会有1对1的Fragment个数在内存中。即例子中,2个Fragment来回切换,缓存最多只有2个,不会随着切换操作到10个,100个;
所以强烈建议使用TAG缓存,即使你有必要2个同名Fragment一起显示的时候,也可以使用不同Tag做区分。解决内存泄漏的关键点。降低到1个Fragment的冗余。
通过4.1tag的方法,已经解决了内存泄漏问题,但是没有解决我们backStack的步数问题;有addToBackStack
,就一定要有popBackStack()
的反操作;
而且在popBackStack()以后,对应的Fragment也会被移除mActive,回收内存,有断点调试为证。
在onDestoryView(),解除对于Fragment的子View引用 = null。
根据前面的分析,这仅仅是为了应对leakCanary和公司的类似检测机制而已。根本上来讲(addToBack的)Fragment是被FragmentManager所引用的,内部子对象,从链条上看,打破没有意义。仅仅降低了LeakCanary的内存泄漏因子。
因为,replace回来,就必定会onCreateView和其他生命周期,即使是使用Tag的缓存策略。
这样必然导致如果在onCreateView等进行了数据请求操作,会重复加载。
这里给出2种解决方案:
//activity onCreate或者onNewIntent调用:
//主要目的是为空,就新建;不为空,需要判断,当前是不是第一个Fragment,要么回到他;要么就不用管了。
private void processIntent() {
if (getIntent() != null) {
Uri uri = getIntent().getData();
String type = uri != null ? uri.getQueryParameter("type") : "";
if (type满足条件) {
jumpToBFragment();
return;
}
}
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE);
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (fragment == null) {
fragment = new CombineFragment();
transaction.replace(R.id.root_layout, fragment, TAG_COMBINE)
.commit();
} else {
List<Fragment> frs = getSupportFragmentManager().getFragments();
boolean needPopAndShow = false;
if (frs != null && frs.size() > 1) {
// for (Fragment f : frs) { //double check
// if (fragment == f && !f.isVisible()) {
// needPopAndShow = true;
// }
// }
needPopAndShow = true;
}
if (needPopAndShow) {
Log.d(TAG, "pop and show");
fragmentManager.popBackStackImmediate();
transaction.show(fragment).commit();
}
CombineFragment cf = (CombineFragment) fragment;
cf.onNewIntent();
}
}
//跳转到下一个Fragment.
private void jumpToBFragment() {
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment toFragment = fragmentManager.findFragmentByTag(TAG_ONE_TOP);
if (toFragment != null && toFragment.isAdded() && fromNewIntent) {
OneTopFragment oneTopFragment = (OneTopFragment) toFragment;
oneTopFragment.onNewIntent(); //刷新数据
} else {
toFragment = new OneTopFragment(); //add显示
Fragment current = fragmentManager.findFragmentByTag(TAG_COMBINE);
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (current != null) {
transaction.hide(current);
}
transaction.add(R.id.root_layout, toFragment, TAG_ONE_TOP)
.addToBackStack(null)
.commit();
}
}
//backTo第一个Fragment
private void backToFirst() {
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.popBackStackImmediate();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE);
Log.d(TAG, "backToCombine: combineFragment exist " + (fragment != null));
FragmentTransaction transaction = fragmentManager.beginTransaction();
if (fragment == null) {
// fragment = new CombineFragment();
// transaction.replace(R.id.root_layout, fragment, TAG_COMBINE)
// .commit();
//如果返回找不到CombineFragment,直接就关闭啦。因为说明oneTop是被其他单独拉起的。但是通过event key回去被系统popBack却没有实现。
finish();
} else {
transaction.show(fragment).commit();
}
return true;
}
//处理下OnBack按键的逻辑
@Override
public void onBackPressed() {
super.onBackPressed();
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE);
Log.d(TAG, "onBackPressed: combineFragment exist " + (fragment != null));
if (fragment == null) {
finish();
}
}
这样的目的是,其实是隐藏了一下,图层而已。内存消耗没有减少。因为Fragment是可以多个,往上贴的。通过控制第一个Fragment的show,hide来达到隐藏的目的。
这个就看你的使用场景了。比如我的CombineFragment就是希望常驻,oneTopFragment只是偶尔弹出,最后回退到CombineFragment希望它不会变。
其他部分代码,是决策Fragment的backStack。自行查看。
引用一下:(https://blog.csdn.net/ganduwei/article/details/82844848)
((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar)这句代码导致Activity中引用了Fragment的mToolbar,如果Fragment关闭后,没有去掉这个引用就会导致无法释放Fragment。
LoginFragment中有创建菜单,而它的上一级Fragment没有创建菜单,这样导致从LoginFragment返回到上一级后,AppCompatActivity中的FragmentManangerImpl没有执行dispatchCreateOptionsMenu方法,所有mCreatedMenus中还是保存了LoginFragment的实例。如果上一级Fragment有创建菜单不会有此问题;
5.1 Fragment中的菜单由自己来创建,不交给Activity,代码如下:
mToolbar.setNavigationIcon(R.drawable.ic_back);
mToolbar.setNavigationOnClickListener(v -> {
});
mToolbar.inflateMenu(R.menu.toolbar_menu);
mToolbar.setOnMenuItemClickListener(menuItem -> {
return true;
});
5.2 菜单还是交给Activity管理,如果上一级Fragment有创建菜单那不用处理,如果没有需要在上一级Fragment清除掉引用,代码如下:
((AppCompatActivity) getActivity()).setSupportActionBar(null);
( getActivity()).onCreatePanelMenu(0,null);
onCreatePanelMenu方法会使dispatchCreateOptionsMenu被调用,从而给mCreatedMenus重新赋值。
当然最好是使用第一个方法,每个Fragment中的菜单由自己来管理。
研究了以后,我发现,我敢使用Fragment了!