今天在翻看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
刚看到这里的时候我就有点怀疑自己了,因为我根本没有用到AppCompatTextView,那这玩意儿时从哪里冒出来的呢?见鬼了。本着相信科学的心态,我打印了我的TextView对象,结果可想而之,真的神奇般的变成了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,这个对象是什么时候被初始化的呢?我们继续追踪溯源,来看看代码:
// 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);
}
}
好了现在已经看到mFactory2原来就是AppCompatDelegateImplV9,我们进入它的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等,其它一些控件都会被替换为对应的AppCompatView类。
那么Google大神为什么要做这样的操作呢?
相信接触过AppCompatView的你肯定会知道AppCompatXXXView继承XXXView,在此基础上,添加了XXXTint属性、文本自适应大小属性等许多新的特性,framework在创建View实例的时候,自动帮我们转换为新特性版本的View,这样新特性就能兼容老版本来使用了,是不是很方便呢
~~ 好了文章到这里就结束了,让我们在下篇文章中再相见 ~~