TintContextWrapper強轉Activity失敗原因深度探索
公司Android app遇到這個bug,遂開始一番探尋。
問題
先來看下錯誤日誌:
2018-02-25 17:38:38 java.lang.ClassCastException: android.support.v7.widget.TintContextWrapper cannot be cast to com.xx.mobile.basecore.activity.BaseActivity
at com.xx.agent.yy.store.event.UploadQRCodeEvent.upload(UploadQRCodeEvent.java:61)
at com.xx.agent .yy.databinding.ActivityUploadQrCodeBinding._internalCallbackOnClick(ActivityUploadQrCodeBinding.java:313)
at android.databinding.generated.callback.OnClickListener.onClick(OnClickListener.java:11)
at android.view.View.performClick(View.java:4457)
at android.view.View$PerformClick.run(View.java:18496 )
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5291)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke (Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:849)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:665)
at dalvik.system.NativeStart.main(Native Method)
版本:10.3.0
機型:CHM-TL00H
系統:android 4.4.2
發現在系統版本為Android4.4.2的裝置上出現了android.support.v7.widget.TintContextWrapper cannot be cast to com.xx.mobile.basecore.activity.BaseActivity型別轉換失敗的問題。
這裡的BaseActivity是我們公司核心類庫中的基類,溯源而上繼承的就是v7包中提供的AppCompatActivity。
然而經過測試發現問題只會出現在系統版本為Android5.0以下的裝置上。
Why?
AppCompatActivity
檢視v7包中AppCompatActivity的原始碼
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider
這是AppCompatActivity的繼承關係,最終它就是繼承自我們熟知的Activity,並且實現了各種介面,這裡稍作了解。
以下是AppCompatActivity中的部分方法,重寫自Activity的重要方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onStart() {
super.onStart();
getDelegate().onStart();
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
發現都通過getDelegate()去對Activity的方法進行了代理。
看到getDelegate():
private AppCompatDelegate mDelegate;
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
其實就是去獲取了一個型別為AppCompatDelegate的代理。然後Activity的方法被呼叫的時候,就會走這個代理類AppCompatDelegate對應的方法。
AppCompatDelegate
首先看下它的繼承體系,是一個自上而下層層繼承的關係:
AppCompatDelegate和AppCompatDelegateImplBase都是抽象類,第一個實現類是AppCompatDelegateImplV9,其中有setContentView():
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
它代理了Activity的setContentView(),其中呼叫LayoutInflater的inflate()去填充佈局。
LayoutInflater
檢視LayoutInflater的原始碼我們發現,它的inflate()中會去解析XML檔案,最終會呼叫它的createViewFromTag()去建立XML中對應的View:
/**
* Convenience method for calling through to the five-arg createViewFromTag
* method. This method passes {@code false} for the {@code ignoreThemeAttr}
* argument and should be used for everything except {@code >include>}
* tag parsing.
*/
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
接著去呼叫createViewFromTag()的另一個過載方法,其中有這樣一段程式碼:
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);
}
大概的意思就是去呼叫mFactory2,mFactory,mPrivateFactory的onCreateView()去生成View,
如果生成不了View就會走接下來預設的方法,此處省略了。
總結成一句話,就是去攔截建立View的操作。
看一下這三個成員變數的定義:
private Factory mFactory;
private Factory2 mFactory2;
private Factory2 mPrivateFactory;
大概看一下Factory和Factory2的定義:
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
介面Factory繼承自介面Factory2。
這時候,我們再回頭看AppCompatDelegateImplV9的定義:
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflater.Factory2
發現AppCompatDelegateImplV9就是一個Factory2!
可以猜想是不是mFactory2,mFactory,mPrivateFactory的型別就是AppCompatDelegateImplV9。
順著這個猜想我在AppCompatDelegateImplV9中發現了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");
}
}
}
其中我們關注LayoutInflaterCompat.setFactory2(layoutInflater, this);
跟進一連串函式呼叫鏈,最終來到LayoutInflaterCompat的靜態內部類LayoutInflaterCompatBaseImpl中的setFactory2(),其中就呼叫了LayoutInflater的setFactory2():
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
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);
}
}
在這裡給mFactory和mFactory2賦值了。
哪裡呼叫了AppCompatDelegateImplV9的installViewFactory()呢?發現在AppCompatActivity的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);
}
至此我們知道了建立View的過程是呼叫了AppCompatDelegateImplV9的onCreateView():
/**
* From {@link LayoutInflater.Factory2}.
*/
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
通過註釋知道,如果Activity預設的工廠不去處理(具體的原因我沒去分析了),就去呼叫onCreateView()另一個過載方法:
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
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的成員變數mAppCompatViewInflater的createView()。
AppCompatViewInflater
看到AppCompatViewInflater的createView():
public 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 = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
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;
}
發現在AppCompatViewInflater的createView()中對於一些View進行了包裝,例如:把TextView包裝成了AppCompatTextView。
看一下AppCompatTextView的構造方法:
public AppCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = AppCompatTextHelper.create(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();
}
發現呼叫了TintContextWrapper的wrap()對原來的context進行了包裝。
TintContextWrapper
TintContextWrapper的wrap():
public static Context wrap(@NonNull final Context context) {
if (shouldWrap(context)) {
synchronized (CACHE_LOCK) {
if (sCache == null) {
sCache = new ArrayList<>();
} else {
// This is a convenient place to prune any dead reference entries
for (int i = sCache.size() - 1; i >= 0; i--) {
final WeakReference<TintContextWrapper> ref = sCache.get(i);
if (ref == null || ref.get() == null) {
sCache.remove(i);
}
}
// Now check our instance cache
for (int i = sCache.size() - 1; i >= 0; i--) {
final WeakReference<TintContextWrapper> ref = sCache.get(i);
final TintContextWrapper wrapper = ref != null ? ref.get() : null;
if (wrapper != null && wrapper.getBaseContext() == context) {
return wrapper;
}
}
}
// If we reach here then the cache didn't have a hit, so create a new instance
// and add it to the cache
final TintContextWrapper wrapper = new TintContextWrapper(context);
sCache.add(new WeakReference<>(wrapper));
return wrapper;
}
}
return context;
}
這裡先呼叫shouldWrap()去判斷是否可以包裝:
private static boolean shouldWrap(@NonNull final Context context) {
if (context instanceof TintContextWrapper
|| context.getResources() instanceof TintResources
|| context.getResources() instanceof VectorEnabledTintResources) {
// If the Context already has a TintResources[Experimental] impl, no need to wrap again
// If the Context is already a TintContextWrapper, no need to wrap again
return false;
}
return Build.VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed();
}
很顯然系統版本小於5.0就允許去包裝。
TintContextWrapper繼承自ContextWrapper,把原來的Context包裝了一層,包裝操作就是把原來的Context儲存到了TintContextWrapper的成員變數mBase中。
到這裡終於明白報型別轉換異常的原因了,在系統版本為Android5.0以下時我們在AppCompatActivity中建立View的時候傳遞的Context並不是我們的Acitvity而是包裹了Acitvity的TintContextWrapper,所以我們通過view.getContext()
獲取的自然不是Acitvity了。
解決問題的方法
提供一段官方原始碼作為參考,MediaRouteButton的getActivity():
private Activity getActivity() {
// Gross way of unwrapping the Activity so we can get the FragmentManager
Context context = getContext();
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity)context;
}
context = ((ContextWrapper)context).getBaseContext();
}
throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
}
所做的很簡單,就是拆開包裝。
但是這裡為什麼要用while迴圈呢?
回到AppCompatViewInflater的createView(),看其中一段程式碼:
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
它出現在將各種View包裝成AppcompatView的操作之前,這樣的話就有可能發生對原來的context包裝多次的情況,也就解釋了為什麼要用while迴圈來拆包。
參考: