日前在利用PrepertyActivity配置设置页面时,出现了一个IllegalArgumentException崩溃,这个崩溃只在8.0系统的手机上出现了,在7.0及以下的系统中不会出现,后来经过追根溯源,发现原因是Android8.0的API变更导致的,先把分析过程分享一下,希望能帮助到有同样问题的同学,当时触发的崩溃如下:
java.lang.RuntimeException: Unable to start activity ComponentInfo{XXX/XXX.activity.CustomePreferenceActivity}: java.lang.IllegalArgumentException: No view found for id 0x1020372 (android:id/prefs) for fragment PropertyTopFragment{2004b3d #0 id=0x1020372}
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2828)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2909)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1639)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6631)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:467)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
Caused by: java.lang.IllegalArgumentException: No view found for id 0x1020372 (android:id/prefs) for fragment CustomePreferenceFragment{2004b3d #0 id=0x1020372}
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1271)
at android.app.FragmentManagerImpl.addAddedFragments(FragmentManager.java:2407)
at android.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2186)
at android.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2142)
at android.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2043)
at android.app.FragmentManagerImpl.dispatchMoveToState(FragmentManager.java:3032)
at android.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManager.java:2979)
at android.app.FragmentController.dispatchActivityCreated(FragmentController.java:178)
at android.app.Activity.performCreate(Activity.java:7166)
at android.app.Activity.performCreate(Activity.java:7151)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1218)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2781)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2909)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1639)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6631)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:467)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
根据日志定位到崩溃位置是在CustomePreferenceActivity.onCreateImpl()中执行startPreferencePanel(fragmentName, args, 0, null, null, 0)方法。PreferenceActivity.startPreferencePanel(fragmentName, args, 0, null, null, 0)方法用来加载一个设置页面,该方法在Android7.0上的实现逻辑如下:
public void startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode) {
if (mSinglePane) {
startWithFragment(fragmentClass, args, resultTo, resultRequestCode, titleRes, 0);
} else {
Fragment f = Fragment.instantiate(this, fragmentClass, args);
if (resultTo != null) {
f.setTargetFragment(resultTo, resultRequestCode);
}
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(com.android.internal.R.id.prefs, f);//实际出错位置,找不到R.id.prefs所在的布局
if (titleRes != 0) {
transaction.setBreadCrumbTitle(titleRes);
} else if (titleText != null) {
transaction.setBreadCrumbTitle(titleText);
}
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
transaction.addToBackStack(BACK_STACK_PREFS);
transaction.commitAllowingStateLoss();
}
}
可以看到方法内部会先判断当前是否处于多窗口模式,毕竟Androiod7.0的的特色就是多窗口。如果是传统的单窗口就执行startWithFragment()去加载,如果是多窗口,就直接替换当前的Fragment。结合崩溃日志提示的id,可以在这段处理逻辑中很明显的找到问题根源。就是红色标记处。这说明当前的PreferenceActivity并没有加载布局。重新比对了下Android8.0中的处理逻辑,该方法在Android8.0上的实现逻辑如下:
public void startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode) {
Fragment f = Fragment.instantiate(this, fragmentClass, args);
if (resultTo != null) {
f.setTargetFragment(resultTo, resultRequestCode);
}
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(com.android.internal.R.id.prefs, f);
if (titleRes != 0) {
transaction.setBreadCrumbTitle(titleRes);
} else if (titleText != null) {
transaction.setBreadCrumbTitle(titleText);
}
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
transaction.addToBackStack(BACK_STACK_PREFS);
transaction.commitAllowingStateLoss();
}
可以看到直接取消了对于单窗口的判断。也就是说通过PreferenceActivity加载PreferenceFragment在8.0上是需要确保当前布局必须加载完毕才行,这个布局不是我们自定义的布局,而是PreferenceActivity其本身的布局。PreferenceActivty是如何加载含有R.id.prefs控件的布局呢,可以看下它的onCreate()方法,在onCreate()方法中会执行以下重点操作:
final int layoutResId = sa.getResourceId(
com.android.internal.R.styleable.PreferenceActivity_layout,
com.android.internal.R.layout.preference_list_content);
setContentView(layoutResId);
mSinglePane = hidingHeaders || !onIsMultiPane();
String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
布局R.layout.preference_list_content是含有R.id.prefs的,但是两个参数initialFragment和initialArguments确实我们不曾传递的。7.0和8.0在后续针对这两个参数的处理出现了变化,先看7.0的处理:
if (savedInstanceState != null) {
}else{
if (initialFragment != null && mSinglePane) {
switchToHeader(initialFragment, initialArguments);
}
}
if (initialFragment != null && mSinglePane) {
// Single pane, showing just a prefs fragment.
findViewById(com.android.internal.R.id.headers).setVisibility(View.GONE);
mPrefsContainer.setVisibility(View.VISIBLE);
if (initialTitle != 0) {
CharSequence initialTitleStr = getText(initialTitle);
CharSequence initialShortTitleStr = initialShortTitle != 0
? getText(initialShortTitle) : null;
showBreadCrumbs(initialTitleStr, initialShortTitleStr);
}
} else if (mHeaders.size() > 0) {
setListAdapter(new HeaderAdapter(this, mHeaders, mPreferenceHeaderItemResId,
mPreferenceHeaderRemoveEmptyIcon));
if (!mSinglePane) {
// Multi-pane.
getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
if (mCurHeader != null) {
setSelectedHeader(mCurHeader);
}
mPrefsContainer.setVisibility(View.VISIBLE);
}
} else {//初次加载PreferenceActivty注定会走进此处逻辑处理中
// If there are no headers, we are in the old "just show a screen
// of preferences" mode.
setContentView(com.android.internal.R.layout.preference_list_content_single);切换布局导致的
mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer);
mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs);
mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);
mPreferenceManager.setOnPreferenceTreeClickListener(this);
}
在来看8.0在onCreate()中的处理:
if (savedInstanceState != null) {
}else{
if (initialFragment != null) {
switchToHeader(initialFragment, initialArguments);
}
}
if (mSinglePane && initialFragment != null && initialTitle != 0) {}
if (mHeaders.size() == 0 && initialFragment == null) {
// If there are no headers, we are in the old "just show a screen
// of preferences" mode.
setContentView(com.android.internal.R.layout.preference_list_content_single);
}
我们在调用一个PreferenceActivty时,注定saveInstanceState是null的,参数initialFragment和initialArguments是空值,mHeaders初始化后也是空的,并且只有在saveInstanceState !=null时才会添加数据,所以此时一定会走进setContentView(com.android.internal.R.layout.preference_list_content_single)中,更换后的布局R.layout.preferencelist_content_single是没有R.id.prefs的。那么不论是Android8.0还是Android.7.0的处理流程直接调用replace(com.android.internal.R.id.prefs, fragment),都会出现IllegalArgumentException。Android7.0之所以能够避开崩溃,是因为判断为传统单窗口时调用了startWithFragment(),这个方法的作用就是重新启动PreferenceActivity,并传递一部分参数,确保在onCreate()中saveInstanceState、initialFragment和initialArguments不是空值。可以看下startWithFragment()的处理逻辑:
public void startWithFragment(String fragmentName, Bundle args,
Fragment resultTo, int resultRequestCode, @StringRes int titleRes,
@StringRes int shortTitleRes) {
Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes);
if (resultTo == null) {
startActivity(intent);
} else {
resultTo.startActivityForResult(intent, resultRequestCode);
}
}
public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
@StringRes int titleRes, int shortTitleRes) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClass(this, getClass());实际上就是重启自己,重新调用了onCreate()方法
intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName);表明此时要加载的fragment,initialFragment
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);就是saveInstanceState
intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE, titleRes);
intent.putExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, shortTitleRes);
intent.putExtra(EXTRA_NO_HEADERS, true);
return intent;
}
可以看出startWithFragment就是在利用传入的数据通过Intent再次调起PreferenceActivty自身。这样再次执行onCreate是就不会出现布局替换的现象。通过比较我们可以看到,在代码中可以直接调用startWithFragment()可以更好的实现原有功能。Android8.0针对PreferenceActivty的修改目的不得而知,Google自身建议是直接使用PreferenceFragment,不再维护PreferenceActivty,可能这就导致此处变更并没有出现在变更说明上。如果你的项目中还在继续使用PreferenceActivty,那么就要注意把startPreferencePanel()方法替换成startWithFragment(),规避这个崩溃异常。
延伸:
在设置PreferenceScreen时。同样存在触发startPreferencePanel()方法的场景,当PreferenceFragment加载的是PreferenceScreen构成的xml文件时,页面点击事件会触发onPreferenceTreeClick(),该方法是OnPreferenceTreeClickListener的回调方法。该方法会去执行PreferenceActivity的onPreferenceStartFragment()方法。onPreferenceTreeClick()的实现方法在Android7.0和8.0上并无变动,具体代码构成如下:
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
if (preference.getFragment() != null &&
getActivity() instanceof OnPreferenceStartFragmentCallback) {
return ((OnPreferenceStartFragmentCallback)getActivity()).onPreferenceStartFragment(
this, preference);
}
return false;
}
可以看到实际的执行内容是onPreferenceStartFragment(this, preference)。onPreferenceStartFragment又是执行的startPreferencePanel(),具体代码构成如下:
@Override
public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
startPreferencePanel(pref.getFragment(), pref.getExtras(), pref.getTitleRes(),
pref.getTitle(), null, 0);
return true;
}
综上分析,为了彻底避免startPreferencePanel()内部逻辑变化带来的影响,需要对继承于PreferneceFragment的子类重写onPreferenceTreeClick(),确保不再调用startPreferencePanel(),而是替换为startWithFragment(),替换后的写法如下:
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
if (preference.getFragment() != null &&
getActivity() instanceof OnPreferenceStartFragmentCallback) {
((OnPreferenceStartFragmentCallback)getActivity()).startWithFragment(pref.getFragment(), pref.getExtras(), null, 0, pref.getTitleRes(), 0);
return true;
}
return false;
}