今天在翻看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é)束了,讓我們在下篇文章中再相見 ~~