學徒淺析Android——PreferenceActivity在Android 8.0和 7.0上細微變化

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

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章