美文网首页
View.getContext()一定返回Activity吗?

View.getContext()一定返回Activity吗?

作者: code希必地 | 来源:发表于2020-09-03 14:30 被阅读0次

    1、View.getContext()

    @ViewDebug.CapturedViewProperty
        public final Context getContext() {
            return mContext;
        }
    

    代码很简单直接返回成员变量mContext,那么mContext是在哪里赋值的呢?搜索发现mContext只有一个赋值的地方:即View的构造函数中

    public View(Context context) {
            mContext = context;
            //.......
    }
    

    所以getContext()返回的对象,取决于创建View时传什么参数。一般我们创建View的方式有两种:

    • 1、直接通过构造new出对象。
    • 2、通过xml解析创建View对象
      第一种方式没啥好讲的,创建View时传入什么对象,getContext()就返回什么。下面主要看下第二种方式。

    2、通过xml解析创建View对象

    开发中我们经常在xml中写布局,然后在Activity的onCreate()方法中通过setContentView(int layoutId)进行布局的加载。xml中的控件是如何创建的呢?setContentView()在Activity和AppCompatActivity中是不同的。

    2.1、Activity.setContentView()

    ###Activity.java
     public void setContentView(@LayoutRes int layoutResID) {
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
    

    getWindow()返回PhoneWindow的对象。所以Activity的setContentView()不过是在调用PhoneWindow()的setContentView()方法。

    ###PhoneWindow.java
    @Override
        public void setContentView(int layoutResID) {
           if (mContentParent == null) {
                //installDecor()中创建DecorView,并将id为android.R.id.content的mContentParent添加到DecorView中。
                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);
            }
          //省略部分代码.....
        }
    

    代码很简单,如果没有FEATURE_CONTENT_TRANSITIONS标记的话,则直接调用mLayoutInflater.inflate(layoutResID, mContentParent);加载出来。mLayoutInflater是在PhoneWindow的构造函数中创建的,PhoneWindow是在Activity中的attach()方法中创建的。

    ###Activity.java
    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) {
            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);
            //省略部分代码....
        }
    

    所以PhoneWindow中的context就是Activity本身。

    • 问: LayoutInflater中的mContext和PhoneWindow中的context有什么关系?
    • 答: LayoutInflater.from(context)最终会通过调用LayoutInflater的构造函数
    protected LayoutInflater(Context context) {
            mContext = context;
        }
    

    对mContext进行赋值。所以LayoutInflater中的mContext也是Activity本身。

    • 问:LayoutInflater中的mContext和View中的mContext是什么关系?
    • 答:这就需要看下inflate()方法
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            //省略部分代码...
            // Temp is the root view that was found in the xml
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                  //省略部分代码
        }
    

    主要看下createViewFromTag()方法

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
          //省略部分代码
            try {
                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);
                }
    
                if (view == null) {
                    final Object lastContext = mConstructorArgs[0];
                    mConstructorArgs[0] = context;
                    try {
                        if (-1 == name.indexOf('.')) {
                            view = onCreateView(parent, name, attrs);
                        } else {
                            view = createView(name, null, attrs);
                        }
                    } finally {
                        mConstructorArgs[0] = lastContext;
                    }
                }
    
                return view;
            } catch (InflateException e) {
                throw e;
    
            } catch (ClassNotFoundException e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + name, e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
    
            } catch (Exception e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + name, e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            }
        }
    

    如果mFactory2、mFactory、mPrivateFactory不会空,则调用其onCreateView()进行View的创建,继承Activity的Activity mFactory2和mFactory均为空,而mPrivateFactory的实现类就是Activity本身,但是Activity中的onCreateView()方法直接返回null,所以会调用LayoutInflate的createView()去创建View,其中原理很简单就是通过反射进行View的创建。此时LayoutInflater中的mContext就传递到View中了。

    2.1.1、总结

    说了这么多有点乱,总结一下Activity传递到View中的过程:

    • 1、在Activity的attach方法中创建了PhoneWindow将Activity传递到PhoneWindow中。
    • 2、在PhoneWindow的构造函数中创建了LayoutInflater,将Activity传递到LayoutInflater中。
    • 3、通过反射创建View,传递到View中。
      所以,在继承Activity的页面中通过xml加载的View的getContext()返回的对象一定是Activity。

    2.2、AppCompatActivity.setContentView()

    直接上源码

    ###AppCompatActivity.java
    public void setContentView(@LayoutRes int layoutResID) {
        this.getDelegate().setContentView(layoutResID);
    }
    
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (this.mDelegate == null) {
            this.mDelegate = AppCompatDelegate.create(this, this);
        }
        return this.mDelegate;
    }
    
    ###AppCompatDelegate .java
    public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
            return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
        }
    

    可以看到最终调用的是AppCompatDelegateImpl中的setContentView()

    public void setContentView(int resId) {
            this.ensureSubDecor();
            ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
            contentParent.removeAllViews();
            LayoutInflater.from(this.mContext).inflate(resId, contentParent);
            this.mOriginalWindowCallback.onContentChanged();
        }
    

    主要做了3件事:

    • 1、确保 mSubDecor 的初始化
    • 2、从mSubDecor 中找id为android.R.id.content的contentParent
    • 3、通过inflate将id为resId的View添加到contentParent中。

    2.2.1、mSubDecor的初始化

    ###AppCompatDelegateImpl.java
     private void ensureSubDecor() {
            if(!this.mSubDecorInstalled) {
                this.mSubDecor = this.createSubDecor();
                //省略部分代码。。。
                }
              //省略部分代码。。。
        }
    
     private ViewGroup createSubDecor() {
         //...省略... 这部分主要针对 AppCompat 样式检查和适配
    
        // Now let's make sure that the Window has installed its decor by retrieving it
        //这句代码很重要哦
        mWindow.getDecorView();
    
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
     
        //...省略... 这部分主要针对不同的样式设置来初始化不同的 subDecor(inflater 不同的布局 xml )
     subDecor =inflater.inflate(XXX);
    
        //...省略...
        
        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) {
            // There might be Views already added to the Window's content view so we need to
            // migrate them to our content view
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
    
            // Change our content FrameLayout to use the android.R.id.content id.
            // Useful for fragments.
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
    
            // The decorContent may have a foreground drawable set (windowContentOverlay).
            // Remove this as we handle it ourselves
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }
    
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);
    
        //...省略...
    
        return subDecor;
    }
    
    
    • 1、通过mWindow.getDecorView();创建DecorView,并将id为android.R.id.content的ContentParent添加到DecorView中。
    • 2、创建mSubDecor
      -3 、将DecorView中的ContentParent的所有子View都添加到mSubDecor的子View contentView中,并清空ContentParent中所有子View,然后将ContentParent的id置成-1,将contentView的id置成android.R.id.content,依次来达到偷梁换柱的目的。
    • 4、最后通过mWindow.setContentView(subDecor);将subDecor添加到DecorView中。
      还记得我们要干什么吗?当然是看继承AppCompatActivity的Activity中的View中的Context和AppCompatActivity的关系了,我们回到AppCompatDelegateImpl的setContentView()
    public void setContentView(int resId) {
            this.ensureSubDecor();
            ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
            contentParent.removeAllViews();
            LayoutInflater.from(this.mContext).inflate(resId, contentParent);
            this.mOriginalWindowCallback.onContentChanged();
        }
    

    上面分析了ensureSubDecor()做了些什么事,真正添加resId的仍然是通过
    LayoutInflater.from(this.mContext).inflate(resId, contentParent);来完成的。

    2.2.2、LayoutInflater.inflate()

    在分析Activity的setContentView()时我们已经对inflate做了分析,最终创建View的方法是LayoutInflater的createViewFromTag()

    ###LayoutInflater.java
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
          //省略部分代码
            try {
                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);
                }
    
                if (view == null) {
                    final Object lastContext = mConstructorArgs[0];
                    mConstructorArgs[0] = context;
                    try {
                        if (-1 == name.indexOf('.')) {
                            view = onCreateView(parent, name, attrs);
                        } else {
                            view = createView(name, null, attrs);
                        }
                    } finally {
                        mConstructorArgs[0] = lastContext;
                    }
                }
    
                return view;
            } catch (InflateException e) {
                throw e;
    
            } catch (ClassNotFoundException e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + name, e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
    
            } catch (Exception e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + name, e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            }
        }
    

    和Activity不同的是mFactory2不为空,mFactory2是在AppCompatActivity中的onCreate()中赋值的。

    ###AppCompatActivity.java
     @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            final AppCompatDelegate delegate = getDelegate();
            delegate.installViewFactory();
            delegate.onCreate(savedInstanceState);
           //省略部分代码...
        }
    
    ### AppcompatDelegateImpl.java
     public void installViewFactory() {
            LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
            if (layoutInflater.getFactory() == null) {
    //最终调用LayoutInflater的setFactory2()
                LayoutInflaterCompat.setFactory2(layoutInflater, this);
            } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
            }
    
        }
    
    ###LayoutInflater.java
    public void setFactory2(Factory2 factory) {
            if (mFactorySet) {
                throw new IllegalStateException("A factory has already been set on this LayoutInflater");
            }
            if (factory == null) {
                throw new NullPointerException("Given factory can not be null");
            }
            mFactorySet = true;
            if (mFactory == null) {
                mFactory = mFactory2 = factory;
            } else {
                mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
            }
        }
    

    此时LayoutInflater中的mFactory和mFactory2已被赋值了,mFactory和mFactory2的实例就是AppCompatDelegateImpl,那么mPrivateFactory是何时赋值的呢?

    /**
         * @hide for use by framework
         */
        public void setPrivateFactory(Factory2 factory) {
            if (mPrivateFactory == null) {
                mPrivateFactory = factory;
            } else {
                mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
            }
        }
    

    没找到哪里调用的,从注释可以看出是framework调用的。通过断点可知mPrivateFactory 的实例就是AppCompatActivity本身。下面我们看下Factory2的实现类AppCompatDelegateImpl和AppCompatActivity中的onCreateView(View parent, String name, Context context, AttributeSet attrs)方法到底做了什么?

    2.2.3、AppCompatDelegateImpl的onCreateView()

    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            return this.createView(parent, name, context, attrs);
        }
    
    public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
           //省略...这部分创建mAppCompatViewInflater
            boolean inheritContext = false;
            if (IS_PRE_LOLLIPOP) {
                inheritContext = attrs instanceof XmlPullParser ? ((XmlPullParser)attrs).getDepth() > 1 : this.shouldInheritContext((ViewParent)parent);
            }
    
            return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
        }
    

    可以看到最终会调用AppCompatViewInflater的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;
                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);
            }
    
            if (view != null) {
                // If we have created a view, check its android:onClick
                checkOnClickListener(view, attrs);
            }
    
            return view;
        }
    

    这段代码主要为了兼容低版本,将name为TextView、ImageView等控件替换成AppCompatXXX,以TextView转换成AppCompatTextView为例看下。

    ###AppCompatViewInflate.java
     @NonNull
        protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
            return new AppCompatTextView(context, attrs);
        }
    ###AppCompatTextView.java
    public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
            this.mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
            this.mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
            this.mTextHelper = new AppCompatTextHelper(this);
            this.mTextHelper.loadFromAttributes(attrs, defStyleAttr);
            this.mTextHelper.applyCompoundDrawablesTints();
        }
    

    可以看到AppCompatTextView的构造函数对context进行了包装:TintContextWrapper.wrap(context)

     public static Context wrap(@NonNull Context context) {
            if (shouldWrap(context)) {
               //省略代码...
                    TintContextWrapper wrapper = new TintContextWrapper(context);
                    sCache.add(new WeakReference(wrapper));
                    return wrapper;
                }
            } else {
                return context;
            }
        }
    

    如果shouldWrap(context)返回true,则将context包装成TintContextWrapper ,否则直接返回context。

    private static boolean shouldWrap(@NonNull Context context) {
            if (!(context instanceof TintContextWrapper) && !(context.getResources() instanceof TintResources) && !(context.getResources() instanceof VectorEnabledTintResources)) {
                return VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed();
            } else {
                return false;
            }
        }
    

    可以看到在VERSION.SDK_INT >= 21时肯定返回false,在VERSION.SDK_INT<21时会将context包装成TintContextWrapper。所以我们可以得出结论:

    • 1、在Android5.0之前,会使用TintContextWrapper创建name为TextView、ImageView等控件。
    • 2、在Android5.0及以上直接使用context即AppCompatActivity创建View。
      分析完了AppCompatDelegateImpl的onCreateView(),下面看下AppComatActivity的onCreateView()

    2.2.4、AppComatActivity的onCreateView()

    AppComatActivity中并没有重写onCreateView,而在其父类FragmentActivity做了重写

    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            View v = this.dispatchFragmentsOnCreateView(parent, name, context, attrs);
            return v == null ? super.onCreateView(parent, name, context, attrs) : v;
        }
    

    可以看到对于非Fragment的控件会直接调用super.onCreateView(parent, name, context, attrs)即Activity的onCreateView()。

    public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }
    

    Activity的处理很粗暴,直接return null;所以name不为TextView、ImageView等的控件,如LinearLayout,会通过反射进行创建。

    2.2.5、AppCompatActivity传递到View的过程总结

    • 1、创建AppCompatDelegateImpl,将AppCompatActivity赋值给mContext。
    • 2、然后通过LayoutInflater.from(this.mContext).inflate(resId, contentParent);将AppCompatActivity传递给LayoutInflater。
    • 3、在LayoutInflater中的createViewFromTag()方法中调用Factory2的实现类 AppCompatDelegateImpl中的createView()将name为TextView、ImageView等的控件替换成AppCompat开头的控件,在Android5.0以下的创建AppCompatXX时传递的是TintContextWrapper。
    • 4、对于name不为TextView、ImageView的控件依然调用LayoutInflater中的onCreateView,通过反射创建。

    3、总结

    • 1、继承Activity的页面中的View,getContext()直接返回Activity。
    • 2、继承AppCompatActivity的页面中的View,
      • 在Android5.0以下,如果name为TextView、ImageView等的控件的getContext()返回TintContextWrapper,否则则返回AppCompatActivity。
      • 在Android5.0以上,直接返回AppCompatActivity。

    相关文章

      网友评论

          本文标题:View.getContext()一定返回Activity吗?

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