过完年,又重新拾起之前编写的android的项目,希望美化一下界面,于是自定义了一些AlertDialog,然后就遇到了requestFeature() must be called before adding content这个AndroidRuntimeException。上网百度一下,发现大多数文章只是记录一下错误和改正方法,并没有给出内部的原理,于是自己总结一个相关的内部原理。
首先,列举一下会导致这个异常的情况
- 自定义AlertDialog时,AlertDialog.show()和AlertDialog.setContentView方法调用顺序有误会导致异常,例子如下
final AlertDialog dialog = new AlertDialog.Builder(SettingActivity.this).create(); dialog.setContentView(R.layout.dialog2); dialog.show();//错误代码
final AlertDialog dialog = new AlertDialog.Builder(SettingActivity.this).create(); dialog.show(); dialog..setContentView(R.layout.dialog2);//正确
- 在Activity的onCreate方法中调用requestWindowFeature方法的位置不当可能会导致异常,例子如下(引自点击打开链接)
protected void onCreate(Bundle savedInstanceState) { this.setContentView(R.layout.setup_info_page); this.requestWindowFeature(Window.FEATURE_NO_TITLE);//错误代码 super.onCreate(savedInstanceState); }
protected void onCreate(Bundle savedInstanceState) { this.requestWindowFeature(Window.FEATURE_NO_TITLE);//正确 this.setContentView(R.layout.setup_info_page); super.onCreate(savedInstanceState); }
其实上述的错误都是方法调用顺序的问题,但是为什么会出现这种情况呢?我们下面主要来看第一种情况内部原理。
先按照错误的代码的方法调用顺序来看,setContentView(),我们来看看这个方法内部的原理。
- AlertDialog是Dialog的子类,继承了Dialog的setContentView方法,而Dialog类中的方法如下
public void setContentView(int layoutResID) { mWindow.setContentView(layoutResID); }
- mWindow是类型是Window这个抽象类,其真正的类型为PhoneWindow,而PhoneWindow的setContentView方法如下
关于WInodw,PhoneWindow的具体介绍请参考点击打开链接public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); //mContentParent为根View,当其为null既第一次调用setContentView时,会执行这个方法 } else { mContentParent.removeAllViews(); //not the fist times,so remove last views } mLayoutInflater.inflate(layoutResID, mContentParent); //create the view from layout.xml final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
- installDecor也是PhoneWindow的成员函数
private void installDecor() { if (mDecor == null) { //省略 } //根view的生成 if (mContentParent == null) { mContentParent = generateLayout(mDecor); //当mContentParent为null的时候才会调用了generateLayout方法,这一点很关键 //下方代码省略 } }
- 而在protected ViewGroup generateLayout(DecorView decor)中便会调用requestFeature这个方法,但是此时并不会出现异常,而是生成了mContentView。然后顺利执行完整个setContentView的方法
但是当执行Dialog.show()方法的时候便会出现异常,让我们一步一步的来分析源码
- AlertDialog.show()方法继承于Dialog这个父类,show方法代码如下
if (mShowing) { //上方代码省略 mCanceled = false; if (!mCreated) { //mCreate在初始化设置为false dispatchOnCreate(null); //执行此方法 } //后方代码省略 }
- 而在dispatchOnCreate()方法中调用了AlertDialog的onCreate方法,代码如下
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAlert.installContent(); }
- 其中mAlert的类型为AlertController,其installContent方法会直接调用requestFeature方法,mWindow依旧是PhoneWIndow的实例
public void installContent() { /* We use a custom title so never request a window title */ //TODO: 错误之源 mWindow.requestFeature(Window.FEATURE_NO_TITLE); if (mView == null || !canTextInput(mView)) { mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); } mWindow.setContentView(mAlertDialogLayout); setupView(); }
- requestFeature()方法的内部有一个关于mContentParent是否为null的布尔判断,如果不为null,则引发异常,如果为null,则顺利执行。
看完两个成员方法的执行过程,我们再来分析一下二者调用先后顺序的不同,为什么会导致不同的结果
- 当先调用show方法,在调用setContentView方法时,虽然show的方法执行过程中mContentParent和mDecor都会被初始化,但是请注意setContentView的第三步时会对mContentView是否为null进行判断,如果不为null,便不会再执行到第四步,就不会调用requestFeature。所以按照这个顺序调用方法不会引发异常
- 当先调用setContentView方法,在调用show方法时,show方法执行之前mContentParent已经初始化了,在其第三步中直接调用了requestFeature,从而引发了异常。
还有一个问题大家可能会问,show方法和setContentView方法执行过程中涉及的是不同类型的对象啊,为什么mContentParent会共享,其实关键在于mWindow这个对象,这个对象是共享的,mContentView和mDecor都是mWindow的成员变量。
至于出现异常的第二种场景,其原理应该相似,这里就不多说啦。