有一天,切换Android Studio的Logcat窗口,发现了一堆莫名奇妙标签为“FragmentManager”的打印:
05-21 15:46:05.713 32174-32174/silladus.. E/FragmentManager: Fragment no longer exists for key key: index 7
Activity state:
05-21 15:46:05.714 32174-32174/silladus.. E/FragmentManager: Local Activity e497260 State:
mResumed=true mStopped=false mFinished=false
mChangingConfigurations=false
mCurrentConfig={1.0 460mcc11mnc [zh_CN] ldltr sw360dp w360dp h616dp 480dpi nrml long port finger -keyb/v/h -nav/h s.302 themeId=0}
mLoadersStarted=true
Active Fragments in c636db7:
#0: HomeFragment{5a635e3 #0 id=0x7f090054 HomeFragment}
mFragmentId=#7f090054 mContainerId=#7f090054 mTag=HomeFragment
mState=5 mIndex=0 mWho=android:fragment:0 mBackStackNesting=0
mAdded=true mRemoving=false mFromLayout=false mInLayout=false
...
这是从哪冒出来的,怎么冒出来的,之前怎么没看到?
带着这些疑问找到FragmentManager
类,搜索”Fragment no longer exists for key”,找到了下面一段代码
@Override
public Fragment getFragment(Bundle bundle, String key) {
int index = bundle.getInt(key, -1);
if (index == -1) {
return null;
}
Fragment f = mActive.get(index);
if (f == null) {
throwException(new IllegalStateException("Fragment no longer exists for key "
+ key + ": index " + index));
}
return f;
}
发现mActive
是一个Fragment
集合SparseArray<Fragment> mActive;
当从mActive
取不到指定下标的Fragment
时会执行throwException
方法,该方法代码如下:
private void throwException(RuntimeException ex) {
Log.e(TAG, ex.getMessage());
LogWriter logw = new LogWriter(Log.ERROR, TAG);
PrintWriter pw = new FastPrintWriter(logw, false, 1024);
if (mHost != null) {
Log.e(TAG, "Activity state:");
try {
mHost.onDump(" ", null, pw, new String[] { });
} catch (Exception e) {
pw.flush();
Log.e(TAG, "Failed dumping state", e);
}
} else {
Log.e(TAG, "Fragment manager state:");
try {
dump(" ", null, pw, new String[] { });
} catch (Exception e) {
pw.flush();
Log.e(TAG, "Failed dumping state", e);
}
}
pw.flush();
throw ex;
}
在该方法里打印了”Activity state:”,接着执行mHost.onDump()
方法。mHost
是FragmentHostCallback
,我们知道Fragment
与Activity
有关联,在Activity
中搜索打印里出现的内容”Local Activity”,找到了如下代码:
/**
* Print the Activity's state into the given stream. This gets invoked if
* you run "adb shell dumpsys activity <activity_component_name>".
*
* @param prefix Desired prefix to prepend at each line of output.
* @param fd The raw file descriptor that the dump is being sent to.
* @param writer The PrintWriter to which you should dump your state. This will be
* closed for you after you return.
* @param args additional arguments to the dump request.
*/
public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
dumpInner(prefix, fd, writer, args);
}
void dumpInner(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
writer.print(prefix); writer.print("Local Activity ");
writer.print(Integer.toHexString(System.identityHashCode(this)));
writer.println(" State:");
String innerPrefix = prefix + " ";
writer.print(innerPrefix); writer.print("mResumed=");
writer.print(mResumed); writer.print(" mStopped=");
writer.print(mStopped); writer.print(" mFinished=");
writer.println(mFinished);
writer.print(innerPrefix); writer.print("mChangingConfigurations=");
writer.println(mChangingConfigurations);
writer.print(innerPrefix); writer.print("mCurrentConfig=");
writer.println(mCurrentConfig);
mFragments.dumpLoaders(innerPrefix, fd, writer, args);
mFragments.getFragmentManager().dump(innerPrefix, fd, writer, args);
if (mVoiceInteractor != null) {
mVoiceInteractor.dump(innerPrefix, fd, writer, args);
}
if (getWindow() != null &&
getWindow().peekDecorView() != null &&
getWindow().peekDecorView().getViewRootImpl() != null) {
getWindow().peekDecorView().getViewRootImpl().dump(prefix, fd, writer, args);
}
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
final AutofillManager afm = getAutofillManager();
if (afm != null) {
afm.dump(prefix, writer);
}
}
现在要找到这段代码调用的源头,有两个地方调用,其中在FragmentHostCallback
的实现类里面调用了。好家伙,这不正是我们想要的吗!该实现类代码片段如下:
class HostCallbacks extends FragmentHostCallback<Activity> {
public HostCallbacks() {
super(Activity.this /*activity*/);
}
@Override
public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
Activity.this.dump(prefix, fd, writer, args);
}
...
}
HostCallback
实例引用传给FragmentController
,FragmentController
里面有HostCallback
,有FragmentManager
,FragmentManager
通过HostCallback
(Activity
里FragmentHostCallback
的实现类)调用Activity
里面的打印,这时候思路都已经变得清晰起来。
回到该方法,dumpInner()
里面完成部分打印后又调用了mFragments.getFragmentManager().dump(innerPrefix, fd, writer, args)
,跳到该方法找到FragmentManager
实现类FragmentManagerImpl
方法的实现看到如下代码片段:
@Override
public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
String innerPrefix = prefix + " ";
int N;
if (mActive != null) {
N = mActive.size();
if (N > 0) {
writer.print(prefix); writer.print("Active Fragments in ");
writer.print(Integer.toHexString(System.identityHashCode(this)));
writer.println(":");
for (int i=0; i<N; i++) {
Fragment f = mActive.valueAt(i);
writer.print(prefix); writer.print(" #"); writer.print(i);
writer.print(": "); writer.println(f);
if (f != null) {
f.dump(innerPrefix, fd, writer, args);
}
}
}
}
...
}
方法代码很多足有100行,但是看到前面部分的代码就知道这就是我们要找的。到现在,这段莫名其妙的打印轨迹已经接近水落石出,起因就是FragmentManager
(FragmentManagerImpl
实现)里的getFragment(Bundle bundle, String key)
方法被调用了。现阶段是要搞清楚哪里调用了这个方法。
幸运的是,我们很快就找到了调用的源头,这一大段打印是进入主界面HomeFragment
时出现的。回顾问题出现前后代码的比较发现了。Glide的with总体提供了接受4种类型的方法,Context、Activity、Fragment、View。其中参数类型Context、Activity直接返回当前Context类型的RequestManager
,Fragment参数的会找一下所属的Activity,而View参数的还会寻找所在的Fragment。这就是问题所在:
st=>start: Glide.with(View view)
op1=>operation: RequestManagerRetriever.class
get(View view)|current
op2=>operation: findFragment(View view, Activity activity)
op3=>operation: findAllFragmentsWithViews(FragmentManager fm, ArrayMap<Fragment> result)
op4=>operation: findAllFragmentsWithViewsPreO(FragmentManager fm, ArrayMap<Fragment> result)
e=>end: FragmentManagerImpl.class
getFragment(Bundle bundle)
st->op1->op2->op3->op4->e
- 1
Glide.with(View view)
- 2 RequestManagerRetriever.class
get(View view)
- 3
findFragment(View view, Activity activity)
- 4
findAllFragmentsWithViews(FragmentManager fm, ArrayMap<Fragment> result)
- 5
findAllFragmentsWithViewsPreO(FragmentManager fm, ArrayMap<Fragment> result)
- 6 FragmentManagerImpl.class
getFragment(Bundle bundle)
private void findAllFragmentsWithViewsPreO(
@NonNull android.app.FragmentManager fragmentManager,
@NonNull ArrayMap<View, android.app.Fragment> result) {
int index = 0;
while (true) {
tempBundle.putInt(FRAGMENT_INDEX_KEY, index++);
android.app.Fragment fragment = null;
try {
fragment = fragmentManager.getFragment(tempBundle, FRAGMENT_INDEX_KEY);
} catch (Exception e) {
// This generates log spam from FragmentManager anyway.
}
if (fragment == null) {
break;
}
if (fragment.getView() != null) {
result.put(fragment.getView(), fragment);
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
findAllFragmentsWithViews(fragment.getChildFragmentManager(), result);
}
}
}
}
到这里fragmentManager.getFragment()
的调用已然找到。所以避免这些打印就是不传View,传View的参数要慎用。