TextView變身AppCompatTextView

今天在翻看bugly的時候,看到了一個奇怪的bug日志,如下:

java.lang.NullPointerException
Attempt to invoke virtual method 'android.text.TextPaint android.widget.TextView.getPaint()' on a null object reference
android.support.v7.widget.AppCompatTextViewAutoSizeHelper.setRawTextSize(AppCompatTextViewAutoSizeHelper.java:603)
android.support.v7.widget.AppCompatTextViewAutoSizeHelper.setTextSizeInternal(AppCompatTextViewAutoSizeHelper.java:599)
android.support.v7.widget.AppCompatTextHelper.setTextSizeInternal(AppCompatTextHelper.java:373)
android.support.v7.widget.AppCompatTextHelper.setTextSize(AppCompatTextHelper.java:355)
android.support.v7.widget.AppCompatTextView.setTextSize(AppCompatTextView.java:191)
android.widget.TextView.setTextSize(TextView.java:2914)
...

哪里奇怪呢?
第8行->第7行:TextView.setTextSize -> AppCompatTextView.setTextSize
剛看到這里的時候我就有點(diǎn)懷疑自己了,因?yàn)槲腋緵]有用到AppCompatTextView,那這玩意兒時從哪里冒出來的呢?見鬼了。本著相信科學(xué)的心態(tài),我打印了我的TextView對象,結(jié)果可想而之,真的神奇般的變成了AppCompatTextView,由于View都是由LayoutInflater加載的(setContentView -> LayoutInflater.inflater),所以我去查看了LayoutInflater源碼:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            try {
                ...
                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);

                        // 此處省略N多行代碼
                    ......
                }
            } 
            ...
            return result;
        }
    }

下面我們來看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;
            }

           ...
            return view;
        } 
        ...
    }

我們看到了mFactory2,這個對象是什么時候被初始化的呢?我們繼續(xù)追蹤溯源,來看看代碼:

    // AppCompatActivity.java -> onCreate
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && mThemeId != 0) {
            // If DayNight has been applied, we need to re-apply the theme for
            // the changes to take effect. On API 23+, we should bypass
            // setTheme(), which will no-op if the theme ID is identical to the
            // current theme ID.
            if (Build.VERSION.SDK_INT >= 23) {
                onApplyThemeResource(getTheme(), mThemeId, false);
            } else {
                setTheme(mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
    }
    
    // AppCompatDelegateImplV9.java -> installViewFactory
    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }
    
    // LayoutInflater.java -> setFactory2
    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);
        }
    }

好了現(xiàn)在已經(jīng)看到mFactory2原來就是AppCompatDelegateImplV9,我們進(jìn)入它的createView方法中:

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        // 此處省略N多行
        ...

        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 */
        );
    }

再來看AppCompatViewInflater.java的createView方法:

final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        // 此處省略N多行
        ...

        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);
        }
        // 此處省略N多行
        ...

        return view;
    }
    
    @NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }

代碼看到這里,一切都變得清晰了,在使用AppCompatActivity的時候,api會自動將我們寫的TextView替換為AppCompatTextView,不僅僅是TextView,通過上面代碼,我們還可以看到像ImageView、Button等,其它一些控件都會被替換為對應(yīng)的AppCompatView類。

那么Google大神為什么要做這樣的操作呢?

相信接觸過AppCompatView的你肯定會知道AppCompatXXXView繼承XXXView,在此基礎(chǔ)上,添加了XXXTint屬性、文本自適應(yīng)大小屬性等許多新的特性,framework在創(chuàng)建View實(shí)例的時候,自動幫我們轉(zhuǎn)換為新特性版本的View,這樣新特性就能兼容老版本來使用了,是不是很方便呢

~~ 好了文章到這里就結(jié)束了,讓我們在下篇文章中再相見 ~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容