美文网首页
Android setContentView 源码解析

Android setContentView 源码解析

作者: 是刘航啊 | 来源:发表于2020-12-17 09:21 被阅读0次
    文章可能篇幅过长,但会一个步骤一个步骤带大家来分析。
    实现页面的几种方式
    • 继承 AppCompatActivity
    • 继承 Activity
    Activity
    public class Activity {
        ...
        public void setContentView(@LayoutRes int layoutResID) {
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
    
        public Window getWindow() {
            return mWindow;
        }
        ...
    }
    

    这里的 mWindow 其实是 PhoneWindow ,在 Activity 的 attach 方法中初始化

    PhoneWindow
    public class PhoneWindow {
        ...
        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;
        }
        ...
    }
    

    第一次 mContentParent 肯定为 null ,所以会执行 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);
        }
        ...
    }
    
    protected DecorView generateDecor(int featureId) {
        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());
    }
    

    generateDecor 方法的意思就是 new DecorView 并返回

    接着 installDecor 方法往下看
    private void installDecor() {
        ...
        if (mContentParent == null) {
             mContentParent = generateLayout(mDecor);
        }
        ...
    }
    
    protected ViewGroup generateLayout(DecorView decor) {
        ...
        int layoutResource;
        int features = getLocalFeatures();
         if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            ...
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                    && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            ...
        }
        ...
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        ...
        return contentParent;
    }
    

    generateLayout 方法有 3 点
    1 加载系统的布局
    2 将布局加载到创建的 DecorView 中
    3 通过 DecorView 找到 id 为 com.android.internal.R.id.content 的 ViewGroup 返回,也就是 mContentParent

    界面的组成
    界面组成
    AppCompatActivity
    public class AppCompatActivity {
        ...
        public void setContentView(@LayoutRes int layoutResID) {
            getDelegate().setContentView(layoutResID);
        }
        ...
    }
    
    class AppCompatDelegateImpl {
        ...
        public void setContentView(View v) {
            ensureSubDecor();
            ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
            contentParent.removeAllViews();
            contentParent.addView(v);
            mAppCompatWindowCallback.getWrapped().onContentChanged();
        }
        
        private void ensureSubDecor() {
            ...
            mSubDecor = createSubDecor();
            ...
        }
        
        private ViewGroup createSubDecor() {
            ...
             mWindow.setContentView(subDecor);
            ...
            return subDecor;
        }
        ...
    }
    

    ensureSubDecor 方法和上面的 generateLayout 类似,只不过 contentParent 是在 setContentView 中处理的

    LayoutInflater 分析

    经过上面的分析,不论是 Activity 还是 AppCompatActivity 最终都会执行 LayoutInflater.inflate 方法

    public abstract class LayoutInflater {
        ... 
        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();
            }
        }
    
         public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                ...
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ...
            }
        }
    
        View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
            ...
            View view = tryCreateView(parent, name, context, attrs);
            ...
        }
    
        public final View tryCreateView(@Nullable View parent, @NonNull String name,
            @NonNull Context context,
            @NonNull AttributeSet attrs) {
            if (name.equals(TAG_1995)) {
                // Let's party like it's 1995!
                return new BlinkLayout(context, attrs);
            }
            
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            
            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
            
            return view;
        }
        ...
    }
    

    LayoutInflater. inflate -> createViewFromTag -> tryCreateView

    public class AppCompatActivity {
        ...
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            final AppCompatDelegate delegate = getDelegate();
            delegate.installViewFactory();
            delegate.onCreate(savedInstanceState);
            super.onCreate(savedInstanceState);
        }
        ...
    }
    
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                            + " so we can not install AppCompat's");
            }
        }
    }
    

    AppCompatActivity 在 onCreate 中设置了Factory,所以会执行 mFactory2.onCreateView 的方法

    class AppCompatDelegateImpl {
        ...
        public View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs) {
            ...
            return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                    IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                    true, /* Read read app:theme as a fallback at all times for legacy reasons */
                    VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
            );
        }
        ...
    }
    
    public class AppCompatViewInflater {
        ...
        final View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs, boolean inheritContext,
                boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
            ...
            switch (name) {
                case "TextView":
                    view = createTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ImageView":
                    view = createImageView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "Button":
                    view = createButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "EditText":
                    view = createEditText(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "Spinner":
                    view = createSpinner(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ImageButton":
                    view = createImageButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "CheckBox":
                    view = createCheckBox(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "RadioButton":
                    view = createRadioButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "CheckedTextView":
                    view = createCheckedTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "AutoCompleteTextView":
                    view = createAutoCompleteTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "MultiAutoCompleteTextView":
                    view = createMultiAutoCompleteTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "RatingBar":
                    view = createRatingBar(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "SeekBar":
                    view = createSeekBar(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ToggleButton":
                    view = createToggleButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                default:
                    // The fallback that allows extending class to take over view inflation
                    // for other tags. Note that we don't check that the result is not-null.
                    // That allows the custom inflater path to fall back on the default one
                    // later in this method.
                    view = createView(context, name, attrs);
            }
            ...
            if (view == null && originalContext != context) {
                // If the original context does not equal our themed context, then we need to manually
                // inflate it using the name so that android:theme takes effect.
                view = createViewFromTag(context, name, attrs);
            }
        }
        ...
    }
    

    可以看到当 API < 21时 View 被转换成为其他的 View,所以我们可以理解为 AppCompatActivity 会兼容低版本。

    有兴趣的可以去做个小实验,分别继承 Activity 和 AppCompatActivity,将 TextView 打印出来。继承 Activity TextView 还是 TextView,而继承 AppCompatActivity 时 TextView 打印为 AppCompatTextView。
    public abstract class LayoutInflater {
        ...
        View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
            View view = tryCreateView(parent, name, context, attrs);
    
                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;
        }
        ...
    }
    

    经过刚才的分析,继承 AppCompatActivity View 会被创建成对应的 View,而Activity 是没有设置 Factory 的,所以 View 为空,将执行下面的判断。

    if (-1 == name.indexOf('.')) {
        view = onCreateView(context, parent, name, attrs);
    } else {
        view = createView(context, name, null, attrs);
    }
    

    如果没有'.',也就是说如果是系统控件会执行 onCreateView 方法
    如果有'.',就是我们的自定义控件,如'com.xxx',将会执行 createView 方法

    系统控件
    protected View onCreateView(String name, AttributeSet attrs)
                throws ClassNotFoundException {
            return createView(name, "android.view.", attrs);
    }
    

    就是将系统的控件拼接上 android.view ,然后创建出来。

    setContentView 源码大概就介绍到这里了,最后给大家做一个总结。
    1. Activity 和 AppCompatActivity 加载布局前都会创建一个 DecorView,并将系统布局加载到 DecorView 中,通过 DecorView 找到 id 为 android.id.content 的FrameLayout,最后通过 LayoutInflater 加载我们的 xml 布局。
    2. Activity 没有设置Factory ,AppCompatActivity 设置了 Factory。
    3. Activity 不会拦截 View,而 AppCompatActivity 会拦截 View,并将部分 View 转换成对应的 AppCompatView。
    拦截小例子
    public class V8BaseActivity extends Activity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            inflateTest();
            super.onCreate(savedInstanceState);
        }
    
        private void inflateTest() {
            LayoutInflater layoutInflater = LayoutInflater.from(this);
            LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
                @Nullable
                @Override
                public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                    Log.d("View:", name);
                    if (name.equals("Button")) {
                        TextView textView = new TextView(context);
                        textView.setText("test");
                        textView.setTextColor(Color.BLACK);
                        return textView;
                    }
                    return null;
                }
    
                @Nullable
                @Override
                public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                    return onCreateView(null, name, context, attrs);
                }
            });
        }
    }
    
    通过拦截方法将 Button 换成了 TextView
    文章就介绍到这里了,如果有什么写得不对的,可以在下方评论留言,我会第一时间改正。

    Github 源码链接

    相关文章

      网友评论

          本文标题:Android setContentView 源码解析

          本文链接:https://www.haomeiwen.com/subject/uugngktx.html