日前在利用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;
}