美文网首页
setContentView原理分析

setContentView原理分析

作者: 风月寒 | 来源:发表于2021-04-21 14:03 被阅读0次
    setContentView(getLayoutId());
    public void setContentView(@LayoutRes int layoutResID) {
            getDelegate().setContentView(layoutResID);
        }
    

    getDelegate()里面调用的是create(),返回一个AppCompatDelegateImpl对象。然后调用AppCompatDelegateImpl的setContentView()。

    public void setContentView(int resId) {
            ensureSubDecor();//1
            ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
            contentParent.removeAllViews();
            LayoutInflater.from(mContext).inflate(resId, contentParent);//2
            mAppCompatWindowCallback.getWrapped().onContentChanged();
        }
    

    首先分析1.

    private void ensureSubDecor() {
            if (!mSubDecorInstalled) {
                mSubDecor = createSubDecor();//3
    
                // If a title was set before we installed the decor, propagate it now
                CharSequence title = getTitle();
                if (!TextUtils.isEmpty(title)) {
                    if (mDecorContentParent != null) {
                        mDecorContentParent.setWindowTitle(title);
                    } else if (peekSupportActionBar() != null) {
                        peekSupportActionBar().setWindowTitle(title);
                    } else if (mTitleView != null) {
                        mTitleView.setText(title);
                    }
                }
    
                applyFixedSizeWindow();
    
                onSubDecorInstalled(mSubDecor);
    
                mSubDecorInstalled = true;
            
                }
            }
        }
    

    调用createSubDecor(),创建一个mSubDecor,我们都知道每个activity都对应一个窗口window,这个窗口是PhoneWindow的实例,PhoneWindow的布局是DecorView,是一个FrameLayout,DecorView又分为两部分,一部分为ActionBar,另一部是ContentView,即activity在setContentView对应的布局。

    private ViewGroup createSubDecor() {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
    
            if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
                a.recycle();
                throw new IllegalStateException(
                        "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
            }
    
            if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
                requestWindowFeature(Window.FEATURE_NO_TITLE);
            } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
                // Don't allow an action bar if there is no title.
                requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
            }
            if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
            }
            if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
            }
            mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
    
            // Now let's make sure that the Window has installed its decor by retrieving it
            ensureWindow();//4
            mWindow.getDecorView();
    
            final LayoutInflater inflater = LayoutInflater.from(mContext);
            ViewGroup subDecor = null;
    
    
            if (!mWindowNoTitle) {
                if (mIsFloating) {
                    // If we're floating, inflate the dialog title decor
                    subDecor = (ViewGroup) inflater.inflate(
                            R.layout.abc_dialog_title_material, null);
    
                    // Floating windows can never have an action bar, reset the flags
                    mHasActionBar = mOverlayActionBar = false;
                } else if (mHasActionBar) {
                    /**
                     * This needs some explanation. As we can not use the android:theme attribute
                     * pre-L, we emulate it by manually creating a LayoutInflater using a
                     * ContextThemeWrapper pointing to actionBarTheme.
                     */
                    TypedValue outValue = new TypedValue();
                    mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
    
                    Context themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
                    } else {
                        themedContext = mContext;
                    }
    
                    // Now inflate the view using the themed context and set it as the content view
                    subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                            .inflate(R.layout.abc_screen_toolbar, null);
    
                    mDecorContentParent = (DecorContentParent) subDecor
                            .findViewById(R.id.decor_content_parent);
                    mDecorContentParent.setWindowCallback(getWindowCallback());
    
                    /**
                     * Propagate features to DecorContentParent
                     */
                    if (mOverlayActionBar) {
                        mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
                    }
                    if (mFeatureProgress) {
                        mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
                    }
                    if (mFeatureIndeterminateProgress) {
                        mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
                    }
                }
            } else {
                if (mOverlayActionMode) {
                    subDecor = (ViewGroup) inflater.inflate(
                            R.layout.abc_screen_simple_overlay_action_mode, null);
                } else {
                    subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
                }
            }
    
            if (subDecor == null) {
                throw new IllegalArgumentException(
                        "AppCompat does not support the current theme features: { "
                                + "windowActionBar: " + mHasActionBar
                                + ", windowActionBarOverlay: "+ mOverlayActionBar
                                + ", android:windowIsFloating: " + mIsFloating
                                + ", windowActionModeOverlay: " + mOverlayActionMode
                                + ", windowNoTitle: " + mWindowNoTitle
                                + " }");
            }
    
            if (mDecorContentParent == null) {
                mTitleView = (TextView) subDecor.findViewById(R.id.title);
            }
    
            // Make the decor optionally fit system windows, like the window's decor
            ViewUtils.makeOptionalFitsSystemWindows(subDecor);
    
            final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                    R.id.action_bar_activity_content);
    
            final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
            if (windowContentView != null) {
        
                while (windowContentView.getChildCount() > 0) {
                    final View child = windowContentView.getChildAt(0);
                    windowContentView.removeViewAt(0);
                    contentView.addView(child);
                }
    
                windowContentView.setId(View.NO_ID);
                contentView.setId(android.R.id.content);
    
                if (windowContentView instanceof FrameLayout) {
                    ((FrameLayout) windowContentView).setForeground(null);
                }
            }
    
            mWindow.setContentView(subDecor);//5
    
            contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
                @Override
                public void onAttachedFromWindow() {}
    
                @Override
                public void onDetachedFromWindow() {
                    dismissPopups();
                }
            });
    
            return subDecor;
        }
    

    先看注释4,

    private void ensureWindow() {
            if (mWindow == null && mHost instanceof Activity) {
                attachToWindow(((Activity) mHost).getWindow());
            }
            if (mWindow == null) {
                throw new IllegalStateException("We have not been given a Window");
            }
        }
    
    public Window getWindow() {
            return mWindow;
        }
    

    此时的mWindow一定是有值的。为什么这么说,我们先来回顾下window的创建时机。我门从activity的启动流程可知,最终会在performLaunchActivity()创建一个activity.然后会调用activity的attach().

    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) {
            attachBaseContext(context);
    
            mFragments.attachHost(null /*parent*/);
    
            mWindow = new PhoneWindow(this, window, activityConfigCallback);
            mWindow.setWindowControllerCallback(this);
            mWindow.setCallback(this);
            mWindow.setOnWindowDismissedCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);
            if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
                mWindow.setSoftInputMode(info.softInputMode);
            }
            if (info.uiOptions != 0) {
                mWindow.setUiOptions(info.uiOptions);
            }
            mUiThread = Thread.currentThread();
    
            mMainThread = aThread;
            mInstrumentation = instr;
            mToken = token;
            mAssistToken = assistToken;
            mIdent = ident;
            mApplication = application;
            mIntent = intent;
            mReferrer = referrer;
            mComponent = intent.getComponent();
            mActivityInfo = info;
            mTitle = title;
            mParent = parent;
            mEmbeddedID = id;
            mLastNonConfigurationInstances = lastNonConfigurationInstances;
            if (voiceInteractor != null) {
                if (lastNonConfigurationInstances != null) {
                    mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
                } else {
                    mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                            Looper.myLooper());
                }
            }
    
            mWindow.setWindowManager(
                    (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                    mToken, mComponent.flattenToString(),
                    (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            if (mParent != null) {
                mWindow.setContainer(mParent.getWindow());
            }
            mWindowManager = mWindow.getWindowManager();
            mCurrentConfig = config;
    
            mWindow.setColorMode(info.colorMode);
    
            setAutofillOptions(application.getAutofillOptions());
            setContentCaptureOptions(application.getContentCaptureOptions());
        }
    

    在该方法中,会创建一个Phonewindow的实例,并赋值给mWindow.

    然后在performLaunchActivity中调用onCreate().而我们的setContentView()就是在onCreate()执行。

    继续回到createSubDecor(),会生成一个LayoutInflater实例,然后通过不同的条件加载不同的布局来创建subDecor,然后在注释5处给mWindow设置布局,并将subDecor返回。

    继续回到setContentView()中。

    public void setContentView(int resId) {
            ensureSubDecor();
            ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
            contentParent.removeAllViews();
            LayoutInflater.from(mContext).inflate(resId, contentParent);
            mAppCompatWindowCallback.getWrapped().onContentChanged();
        }
    

    通过LayoutInflater将我们自己的布局加载到跟布局为contentParent中。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
            return inflate(resource, root, root != null);
        }
        
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
            final Resources res = getContext().getResources();
            
            View view = tryInflatePrecompiled(resource, res, root, attachToRoot);//6
            if (view != null) {
                return view;
            }
            XmlResourceParser parser = res.getLayout(resource);
            try {
                return inflate(parser, root, attachToRoot);//7
            } finally {
                parser.close();
            }
        }
    

    会先调用tryInflatePrecompiled()创建一个view.

    View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
            boolean attachToRoot) {
            if (!mUseCompiledView) {
                return null;
            }
    
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
    
            // Try to inflate using a precompiled layout.
            String pkg = res.getResourcePackageName(resource);
            String layout = res.getResourceEntryName(resource);
    
            try {
                Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
                Method inflater = clazz.getMethod(layout, Context.class, int.class);
                View view = (View) inflater.invoke(null, mContext, resource);
    
                if (view != null && root != null) {
                    // We were able to use the precompiled inflater, but now we need to do some work to
                    // attach the view to the root correctly.
                    XmlResourceParser parser = res.getLayout(resource);
                    try {
                        AttributeSet attrs = Xml.asAttributeSet(parser);
                        advanceToRootNode(parser);
                        ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
    
                        if (attachToRoot) {
                            root.addView(view, params);
                        } else {
                            view.setLayoutParams(params);
                        }
                    } finally {
                        parser.close();
                    }
                }
    
                return view;
            } catch (Throwable e) {
                if (DEBUG) {
                    Log.e(TAG, "Failed to use precompiled view", e);
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            return null;
        }
    

    先看这个mUseCompiledView这个变量的的值,

    private void initPrecompiledViews() {
            // Precompiled layouts are not supported in this release.
            boolean enabled = false;
            initPrecompiledViews(enabled);
        }
    
        private void initPrecompiledViews(boolean enablePrecompiledViews) {
            mUseCompiledView = enablePrecompiledViews;
    
            if (!mUseCompiledView) {
                mPrecompiledClassLoader = null;
                return;
            }
    
            // Make sure the application allows code generation
            ApplicationInfo appInfo = mContext.getApplicationInfo();
            if (appInfo.isEmbeddedDexUsed() || appInfo.isPrivilegedApp()) {
                mUseCompiledView = false;
                return;
            }
    
            // Try to load the precompiled layout file.
            try {
                mPrecompiledClassLoader = mContext.getClassLoader();
                String dexFile = mContext.getCodeCacheDir() + COMPILED_VIEW_DEX_FILE_NAME;
                if (new File(dexFile).exists()) {
                    mPrecompiledClassLoader = new PathClassLoader(dexFile, mPrecompiledClassLoader);
                } else {
                    // If the precompiled layout file doesn't exist, then disable precompiled
                    // layouts.
                    mUseCompiledView = false;
                }
            } catch (Throwable e) {
                if (DEBUG) {
                    Log.e(TAG, "Failed to initialized precompiled views layouts", e);
                }
                mUseCompiledView = false;
            }
            if (!mUseCompiledView) {
                mPrecompiledClassLoader = null;
            }
        }
    

    mUseCompiledView为initPrecompiledViews()中的enable,而enable = false。所以在tryInflatePrecompiled中会直接return,然后最终会调用inflate().

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                .......
                try {
                        //将parser前进到第一个START_TAG
                    advanceToRootNode(parser);
                    final String name = parser.getName();
    
                    //如果是merger标签
                    if (TAG_MERGE.equals(name)) {
                        ......
                        rInflate(parser, root, inflaterContext, attrs, false);
                    } else {
                        // Temp is the root view that was found in the xml
                        //1.根据tag生成view Tag就是我们写在xml的带包名的标签 比如TextView
                        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
                        ViewGroup.LayoutParams params = null;
    
                        if (root != null) {
                            //设置LayoutParams
                            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);
                            }
                        }
                        
                        // Inflate all children under temp against its context.
                        // 递归实例化子View 这里也会根据include等标签 调用不同方法 大家可以自己看一下
                        rInflateChildren(parser, temp, attrs, true);
    
                        //setContentView的话 会将View 添加到android.R.id.Content中
                        if (root != null && attachToRoot) {
                            root.addView(temp, params);
                        }
    
                       if (root == null || !attachToRoot) {
                            result = temp;
                        }
                    }
                } 
                    ......
                return result;
            }
        }
    

    重点主要还是createViewFromTag 看下面的代码 发现createViewFromTag是交由Factory2实现。

     View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
            ......
    
            try {
                //这里会交由Factory2实现 如果Factory没有处理这个Tag 那么会交由系统实现 就是下面的onCreateView和createView
                View view = tryCreateView(parent, name, context, attrs);
    
                if (view == null) {
                    final Object lastContext = mConstructorArgs[0];
                    mConstructorArgs[0] = context;
                    try {
                        //比如TextView等不需要包名
                        if (-1 == name.indexOf('.')) {
                            view = onCreateView(context, parent, name, attrs);
                        } else {
                            view = createView(context, name, null, attrs);
                        }
                    } finally {
                        mConstructorArgs[0] = lastContext;
                    }
                }
    
                return view;
            } 
            .......
        }
    

    tryCreateView会通过Factory2接口实现 还记得我们之前说 AppDelegateImpl继承了Factory2这就是AppCompatActivity对一些Tag进行了拦截创建 我们也可以自己实现Factory2来进行拦截 实现一些像换肤的功能

    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;
        }
    

    会优先判断mFactory2,然后mFactory,最后mPrivateFactory。调用其onCreateView()。AppCompatDelegateImpl实现了Factory2接口。
    最后调用mAppCompatViewInflater.createView()。

    final View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs, boolean inheritContext,
                boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
            final Context originalContext = context;
    
            // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
            // by using the parent's context
            if (inheritContext && parent != null) {
                context = parent.getContext();
            }
            if (readAndroidTheme || readAppTheme) {
                // We then apply the theme on the context, if specified
                context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
            }
            if (wrapContext) {
                context = TintContextWrapper.wrap(context);
            }
    
            View view = null;
    
            // We need to 'inject' our tint aware Views in place of the standard framework versions
            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:
                    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);
            }
    
            if (view != null) {
                // If we have created a view, check its android:onClick
                checkOnClickListener(view, attrs);
            }
    
            return view;
        }
    

    这个方法的主要作用是进行兼容处理。

    现在回到createViewFromTag()。

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
           
            try {
                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;
            } 
        }
    

    -1 == name.indexOf('.')表示用的是系统的控件,因为用我们自己自定义的控件是时候会使用全路径。是系统的控件则调用onCreateView(),自己定义的控件则调用createView()。而onCreateView()最终调用的也是createView()。

    public final View createView(@NonNull Context viewContext, @NonNull String name,
                @Nullable String prefix, @Nullable AttributeSet attrs)
                throws ClassNotFoundException, InflateException {
            Constructor<? extends View> constructor = sConstructorMap.get(name);
            if (constructor != null && !verifyClassLoader(constructor)) {
                constructor = null;
                sConstructorMap.remove(name);
            }
            Class<? extends View> clazz = null;
    
            try {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
    
                if (constructor == null) {
                    // Class not found in the cache, see if it's real, and try to add it
                    clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                            mContext.getClassLoader()).asSubclass(View.class);
    
                    if (mFilter != null && clazz != null) {
                        boolean allowed = mFilter.onLoadClass(clazz);
                        if (!allowed) {
                            failNotAllowed(name, prefix, viewContext, attrs);
                        }
                    }
                    constructor = clazz.getConstructor(mConstructorSignature);
                    constructor.setAccessible(true);
                    sConstructorMap.put(name, constructor);
                } else {
                    // If we have a filter, apply it to cached constructor
                    if (mFilter != null) {
                        // Have we seen this name before?
                        Boolean allowedState = mFilterMap.get(name);
                        if (allowedState == null) {
                            // New class -- remember whether it is allowed
                            clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                                    mContext.getClassLoader()).asSubclass(View.class);
    
                            boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                            mFilterMap.put(name, allowed);
                            if (!allowed) {
                                failNotAllowed(name, prefix, viewContext, attrs);
                            }
                        } else if (allowedState.equals(Boolean.FALSE)) {
                            failNotAllowed(name, prefix, viewContext, attrs);
                        }
                    }
                }
    
                Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = viewContext;
                Object[] args = mConstructorArgs;
                args[1] = attrs;
    
                try {
                    final View view = constructor.newInstance(args);
                    if (view instanceof ViewStub) {
                        // Use the same context when inflating ViewStub later.
                        final ViewStub viewStub = (ViewStub) view;
                        viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                    }
                    return view;
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            } 
        }
    

    最终通过反射进行创建view.

    从整个创建View的流程我们知道,从中我们得出了布局加载过程中的两个耗时点:

    1、布局文件读取慢:IO过程。

    2、创建View慢:使用反射,比直接new的方式要慢3倍。布局嵌套层级越多,控件个数越多,反射的次数就会越频繁。

    针对这两个问题,可以看 https://juejin.cn/post/6844904048068395015#heading-3

    最后引用别人的一张图做总结。

    setContentView.png

    文章引用

    https://juejin.cn/post/6844904048068395015#heading-3

    https://www.jianshu.com/p/41345d37c266

    相关文章

      网友评论

          本文标题:setContentView原理分析

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