你requestWindowFeature抛异常和我setContentView有什么关系

·  阅读 122
你requestWindowFeature抛异常和我setContentView有什么关系
requestWindowFeature(Window.FEATURE_NO_TITLE)
复制代码

requestWindowFeature大家应该都用过

Caused by: android.util.AndroidRuntimeException: requestFeature() must be called before adding content
复制代码

上面的报错想必也遇到过,看看报错的原因:requestFeature() must be called before adding content,简单翻译一下,必须在添加content之前使用,那不就是得在setContentView之前调用吗,是不是很疑惑,setContentView到底做错了什么

看看是什么导致了异常的发生

开始溯源

//Activity.requestWindowFeature
public final boolean requestWindowFeature(int featureId) {
    return getWindow().requestFeature(featureId);
}
复制代码
  • getWindow,这里是获取到了PhoneWindow的实例,所以得去找PhoneWindow的requestFeature

穿插一个小技巧:

image.png

  • 在使用as看源码的时候,勾上右上角的小勾,才能搜索到源码的文件

回到正题

PhoneWindow.requestFeature

@Override
public boolean requestFeature(int featureId) {
    if (mContentParentExplicitlySet) {
        throw new AndroidRuntimeException("requestFeature() must be called before adding content");
    }
复制代码
  • 在这里,找到了问题的触发点,因为 mContentParentExplicitlySet 设为了true,所以抛出了异常,接下来找到设true的地方,事情就解决了。

setContentView干了啥

重头戏来了,这篇文章并不是为了单单查明 requestWindowFeature 崩溃的原因,而是想着接着契机,看一看setContentView的流程到底是个啥

--------------------------------- 这里是重点和非重点的分割线 ---------------------------

储备知识

看了上面的流程,会不会有一丝丝的疑惑,这个PhoneWindow是哪来的?此处需要 performLaunchActivity 解释一下了,如果小伙伴不知道 performLaunchActivity 干啥的,建议自行百度。

ActivityThread.performLaunchActivity

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    activity = mInstrumentation.newActivity(
        cl, component.getClassName(), r.intent);
        ...
    activity.attach(appContext, this, getInstrumentation(), r.token,
        r.ident, app, r.intent, r.activityInfo, title, r.parent,
        r.embeddedID, r.lastNonConfigurationInstances, config,
        r.referrer, r.voiceInteractor, window, r.configCallback,
        r.assistToken, r.shareableActivityToken);
        ...
    if (r.isPersistable()) {
        mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
    } else {
        mInstrumentation.callActivityOnCreate(activity, r.state);
    }
}
复制代码
  • 这个方法里,创建了activity,反射,一个很管用的技能。
  • 然后创建的activity调用了attach方法,在这里,PhoneWindow就诞生了,看下面代码的最后一行

Activity.attach

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
        IBinder shareableActivityToken) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
复制代码

回到ActivityThread.performLaunchActivity,发现,callActivityOnCreate也就是activity onCreate的回调在PhoneWindow创建之后才会被调用。

正题

Activity一般会继承Activity这个父类,也有可能会继承AppCompatActivity这个父类,二者的setContentView并不是同样的流程

继承Activity的setContentView

// Activity.setContentView
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
复制代码
  • 从Activity的setContentView点击进去,看到如上的代码,又是getWindow(),这下明白了,又到PhoneWindow中去了。

PhoneWindow.setContentView

// PhoneWindow.setContentView
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}
复制代码
  • 代码不多,做了两件事,第一件事创建DecorView:installDecor(), 第二件事渲染:mLayoutInflater.inflate(layoutResID, mContentParent。
  • 仔细的看返回值,是不是似曾相识,mContentParentExplicitlySet就在这里被设置成了true,于是,requestWindowFeature报错的原因也就发现了,还真就是setContentView导致的。但是,已经不重要了,重要的是继续探索setContentView

installDecor

// PhoneWindow.installDecor
private void installDecor() {
...
    if (mDecor == null) {
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

    }
...
复制代码
  • 这里主要也是两件事创建DecorView和contentParent:generateDecor和generateLayout

generateDecor

// PhoneWindow.generateDecor
protected DecorView generateDecor(int featureId) {
    // System process doesn't have application context and in that case we need to directly use
    // the context we have. Otherwise we want the application context, so we don't cling to the
    // activity.
    Context context;
    if (mUseDecorContext) {
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
            context = getContext();
        } else {
            context = new DecorContext(applicationContext, this);
            if (mTheme != -1) {
                context.setTheme(mTheme);
            }
        }
    } else {
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}
复制代码
  • 这里没啥好说的,创建了DecorView

generateLayout

// PhoneWindow.generateLayout
protected ViewGroup generateLayout(DecorView decor) {
    TypedArray a = getWindowStyle();
    ...
    int layoutResource;
    ...
    else {
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }
    ...
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ...
    return contentParent;
}
复制代码
  • 首先会拿到自定义的属性,利用这些属性进行设置
  • 在一通设置之后,定义一个layoutResource用来接收对应的xml布局, 其中,最简单的无非就是screen_simple这个xml了
  • 最后返回的是contentParent,到此,installDecor任务完成

screen_simple.xml

<!--screen_simple.xml-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
复制代码

mLayoutInflater.inflate(layoutResID, mContentParent);

创建DecorView和contentParent完成之后,就是渲染的过程了

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
              + Integer.toHexString(resource) + ")");
    }

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
复制代码
  • 将xml文件解析之后,继续调用inflate方法

inflate

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    if (TAG_MERGE.equals(name)) {
        if (root == null || !attachToRoot) {
            throw new InflateException("<merge /> can be used only with a valid "
                    + "ViewGroup root and attachToRoot=true");
        }
        rInflate(parser, root, inflaterContext, attrs, false);
    } else {
        // Temp is the root view that was found in the xml
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        ViewGroup.LayoutParams params = null;

        if (root != null) {
            if (DEBUG) {
                System.out.println("Creating params from root: " +
                        root);
            }
            // Create layout params that match root, if supplied
            params = root.generateLayoutParams(attrs);
            if (!attachToRoot) {
                // Set the layout params for temp if we are not
                // attaching. (If we are, we use addView, below)
                temp.setLayoutParams(params);
            }
        }
    }
复制代码
  • 假设还以screen_simple为例,根布局不是merge标签,所以会走到createViewFromTag方法中

createViewFromTag

// LayoutInflater.createViewFromTag
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
        ...
    if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) {
                view = onCreateView(context, parent, name, attrs);
            } else {
                view = createView(context, name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }
    
    return view;
}
复制代码
  • 判断控件的名称中是否存在.,这个很好理解,如果是TextView,就没有点,走if,

androidx.constraintlayout.widget.ConstraintLayout中就是有.的,走else。或者,换个角度想,是为了识别是不是Android sdk中的控件。

onCreateView

既然没.的话,无法知道全部的路径,自然就得拼上前缀,才好进行识别,onCreateView就是做了这个事情,onCreateView的具体实现位于PhoneLayoutInflater

// PhoneLayoutInflater.onCreateView

private static final String[] sClassPrefixList = {
    "android.widget.",
    "android.webkit.",
    "android.app."
};

@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    for (String prefix : sClassPrefixList) {
        try {
            View view = createView(name, prefix, attrs);
            if (view != null) {
                return view;
            }
        } catch (ClassNotFoundException e) {
            // In this case we want to let the base class take a crack
            // at it.
        }
    }

    return super.onCreateView(name, attrs);
}
复制代码
  • 前缀有三种,在onCreateView中使用for循环调用onCreateView方法,进行补全,具体的操作,在createView方法中。

createView

@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
        @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {

        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
        mContext.getClassLoader()).asSubclass(View.class);
        
    constructor = clazz.getConstructor(mConstructorSignature);
    constructor.setAccessible(true);
    sConstructorMap.put(name, constructor);
    ...
    final View view = constructor.newInstance(args);
}
复制代码
  • 通过反射创建对象,随后取到构造函数的对象,最后创建view。
  • view创建完成之后,回到上面的inflate方法,对子View进行相同的操作

rInflateChildren(parser, temp, attrs, true);

// LayoutInflater.rInflateChildren
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
复制代码
  • 调用的是rInflate方法
void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}
复制代码
  • 可以发现,一系类的判断之后,调用的还是createViewFromTag方法,和上面无异。

到底,setContentView的简易分析就结束了,这个时候,如果回头去看网上很流行的那张图,会不会有不同的理解?

image.png

(图是我借的,侵删)

其实,还剩下一部分,继承自AppcompatActivity的那部分还没有讲,大同小异,是为了适配而进行的设计。

分类:
Android
标签: