ReportFragment cannot be cast to ReportFragment

環境

  • Replugin插件框架

問題原因

問題原因的分析這段文字沒有經過潤色,純粹是初稿,不一定易於閱讀,這段看不下去的同學可以直接看文章最後的解決方案。
ReportFragmentandroid.arch.lifecycle組件中的類,lifecycle組件中的ProcessLifecycleOwnerInitializer繼承自ContentProvider,利用ContentProvider#onCreate()方法初始化。

在初始化時,通過Application#registerActivityLifecycleCallbacks()註冊Activity的生命週期監聽,在onActivityCreated()回調中爲每一個Activity注入一個ReportFragment

public static void injectIfNeededIn(Activity activity) {
        // ProcessLifecycleOwner should always correctly work and some activities may not extend
        // FragmentActivity from support lib, so we use framework fragments for activities
        android.app.FragmentManager manager = activity.getFragmentManager();
        if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
            manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
            // Hopefully, we are the first to make a transaction.
            manager.executePendingTransactions();
        }
    }

注入ReportFragment之後,ProcessLifecycleOwner中也會監聽Activity的生命週期,並且在onActivityCreated()回調中通過ReportFragment.get()方法獲取ReportFragment

static ReportFragment get(Activity activity) {
        return (ReportFragment) activity.getFragmentManager().findFragmentByTag(
                REPORT_FRAGMENT_TAG);
    }

出錯的代碼就是get()方法中的轉型。

ReportFragment爲什麼不能被轉型成ReportFragment呢?因爲它們的ClassLoader不同,但是從ReportFragment的注入方法:injectIfNeededIn()和使用方法:get()來看,這兩處代碼都是ProcessLifecycleOwnerInitializer在初始化時註冊的Activity的生命週期監聽中回調的。又因爲在類A中通過new關鍵字創建類B,類B的ClassLoader就是類A的ClassLoader,那麼這兩處代碼中的ReportFragment的Classloader應該是同一個,爲什麼會出現ClassLoader不一致的情況呢?

這就要從FragmentActivity的狀態保存說起,來看代碼,FragmentActivity#onSaveInstanceState()中,this.mFragments.saveAllState會保存Fragment

protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        this.markFragmentsCreated();
        Parcelable p = this.mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable("android:support:fragments", p);
        }
        ...
        }
    }

最終通過new FragmentState()Fragment序列化。

 Parcelable saveAllState() {
        ...
        if (this.mActive != null && this.mActive.size() > 0) {
            int N = this.mActive.size();
            FragmentState[] active = new FragmentState[N];
            boolean haveFragments = false;

            for(int i = 0; i < N; ++i) {
                Fragment f = (Fragment)this.mActive.valueAt(i);
                if (f != null) {
                    if (f.mIndex < 0) {
                        this.throwException(new IllegalStateException("Failure saving state: active " + f + " has cleared index: " + f.mIndex));
                    }

                    haveFragments = true;
                    FragmentState fs = new FragmentState(f);
                    active[i] = fs;
                    ...

而序列化時,只保存了Fragment的類名,ClassLoader信息就丟失了。

FragmentState(Fragment frag) {
        this.mClassName = frag.getClass().getName();
        this.mIndex = frag.mIndex;
        this.mFromLayout = frag.mFromLayout;
        this.mFragmentId = frag.mFragmentId;
        this.mContainerId = frag.mContainerId;
        this.mTag = frag.mTag;
        this.mRetainInstance = frag.mRetainInstance;
        this.mDetached = frag.mDetached;
        this.mArguments = frag.mArguments;
        this.mHidden = frag.mHidden;
    }

當後臺App由於內存不足被kill後,從最近使用的App列表中返回App,系統會恢復切後臺時所處的Activity,在Activity#onCreate()中通過this.mFragments.restoreAllState恢復Fragment

protected void onCreate(@Nullable Bundle savedInstanceState) {
        this.mFragments.attachHost((Fragment)null);
        super.onCreate(savedInstanceState);
        FragmentActivity.NonConfigurationInstances nc = (FragmentActivity.NonConfigurationInstances)this.getLastNonConfigurationInstance();
        if (nc != null && nc.viewModelStore != null && this.mViewModelStore == null) {
            this.mViewModelStore = nc.viewModelStore;
        }

        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable("android:support:fragments");
            this.mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
            if (savedInstanceState.containsKey("android:support:next_request_index")) {
            ...

之前序列化了Fragment信息的FragmentState類,通過FragmentState#instantiate()方法,將Context, ClassName傳遞給Fragment#instantiate()方法,最終通過ContextClassLoader加載FragmentClass

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
        try {
            Class<?> clazz = sClassMap.get(fname);
            if (clazz == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = context.getClassLoader().loadClass(fname);
                if (!Fragment.class.isAssignableFrom(clazz)) {
                    throw new InstantiationException("Trying to instantiate a class " + fname
                            + " that is not a Fragment", new ClassCastException());
                }
                sClassMap.put(fname, clazz);
            }
           ...

這個ContextActivity#mFragments中的HostCallbacks通過onGetHost獲取的,就是Activity自身。

final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

class HostCallbacks extends FragmentHostCallback<Activity> {
        @Override
        public Activity onGetHost() {
            return Activity.this;
        }
    }

下面瞭解一下Replugin的ClassLoader機制,Replugin中有宿主和插件之分,通過RePluginClassLoader加載宿主的類,通過PluginDexClassLoader加載插件的類,插件中的Activity就是PluginDexClassLoader加載的。如上所述,在內存不足的情況下,Activity中的ReportFragment是在狀態恢復時,由PluginDexClassLoader加載的。

再回到出問題的代碼,此時findfragmentByTag得到的ReportFragment就是由PluginDexClassLoader加載的,而轉型後的ReportFragment是由系統的ClassLoader加載的,ClassLoader不同,自然就不能轉型。

static ReportFragment get(Activity activity) {
        return (ReportFragment) activity.getFragmentManager().findFragmentByTag(
                REPORT_FRAGMENT_TAG);
    }

解決方案

知道了問題的來龍去脈,就好找解決方案了,只要保證LifeCycle使用的ReportFragment都是由LifeCycle組件加載,就不會有轉型問題,但是Activity的狀態保存步驟又不能去除,綜上得到的方案就是:在Activity創建了ReportFragment之後,LifeCycle插入ReportFragment之前,移除Activity創建的ReportFragment,這樣LifeCycle就會自行創建ReportFragment,就能保證LifeCycle使用的ReportFragment都是自己加載的。

方案一

在所有Activity的公共父類的onCreate中執行removeReportTag方法移除ReportFragment。

    private static void removeReportTag(Activity activity) {
            try {
                XLLog.d(TAG, "removeReportTag");
                FragmentManager manager = activity.getFragmentManager();
                if (manager != null) {
                    Fragment fragment = manager.findFragmentByTag(REPORT_FRAGMENT_TAG);
                    XLLog.d(TAG, "removeReportTag--fragment=" + fragment);
                    if (fragment != null) {
                        XLLog.d(TAG, "removeReportTag--fragment.classLoader=" + fragment.getClass().getClassLoader());
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            manager.beginTransaction().remove(fragment).commitNow();
                        }
                    }
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
    }

這種方法的缺點是,第三方的Activity不會繼承我們的公共父類,無法執行這段代碼,那就看方案二。

方案二

Application#onCreate中執行HookActivityLifecycle#hook方法,通過反射在Activity生命週期的監聽器list的第一位中插入一個監聽器,在監聽器中的onActivityCreated回調中移除ReportFragment。這個方案的缺點是,Activity中的FragmentManager中的commitNow方法在api 24及以上版本才實現,如果不用commitNow方法,在LifeCycle判斷是否插入ReportFragment時,ReportFragment還沒來得及被移除。

public class HookActivityLifeCycle {
    private static final String TAG = "HookActivityLifeCycle";
    private static final String REPORT_FRAGMENT_TAG = "android.arch.lifecycle"
            + ".LifecycleDispatcher.report_fragment_tag";

    public static void hook(Application application) {
        try {
            Field[] fields = Application.class.getDeclaredFields();
            for (Field field : fields) {
                XLLog.d(TAG, "field=" + field.getName());
            }
            Field mActivityLifecycleCallbacksField = Application.class.getDeclaredField("mActivityLifecycleCallbacks");
            mActivityLifecycleCallbacksField.setAccessible(true);
            List list = (List) mActivityLifecycleCallbacksField.get(application);
            if (list != null) {
                list.add(0, new hook());
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    static class hook implements Application.ActivityLifecycleCallbacks {
        @Override
        public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
            removeReportTag(activity);
        }

        @Override
        public void onActivityStarted(@NonNull Activity activity) {

        }

        @Override
        public void onActivityResumed(@NonNull Activity activity) {

        }

        @Override
        public void onActivityPaused(@NonNull Activity activity) {

        }

        @Override
        public void onActivityStopped(@NonNull Activity activity) {

        }

        @Override
        public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {

        }

        @Override
        public void onActivityDestroyed(@NonNull Activity activity) {

        }
    }

 
    private static void removeReportTag(Activity activity) {
        if (activity instanceof FragmentActivity) {
            removeReportTagV4((FragmentActivity) activity);
        } else {
            try {
                XLLog.d(TAG, "removeReportTag");
                FragmentManager manager = activity.getFragmentManager();
                if (manager != null) {
                    Fragment fragment = manager.findFragmentByTag(REPORT_FRAGMENT_TAG);
                    XLLog.d(TAG, "removeReportTag--fragment=" + fragment);
                    if (fragment != null) {
                        XLLog.d(TAG, "removeReportTag--fragment.classLoader=" + fragment.getClass().getClassLoader());
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            manager.beginTransaction().remove(fragment).commitNow();
                        }
                    }
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * FragmentActivity中的FragmentManager的commitNow方法沒有版本限制。
     */
    private static void removeReportTagV4(FragmentActivity activity) {
        try {
            XLLog.d(TAG, "removeReportTagV4");
            android.support.v4.app.FragmentManager manager = activity.getSupportFragmentManager();
            if (manager != null) {
                android.support.v4.app.Fragment fragment = manager.findFragmentByTag(REPORT_FRAGMENT_TAG);
                XLLog.d(TAG, "removeReportTagV4--fragment=" + fragment);
                if (fragment != null) {
                    XLLog.d(TAG, "removeReportTagV4--fragment.classLoader=" + fragment.getClass().getClassLoader());
                    manager.beginTransaction().remove(fragment).commitNow();
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

測試驗證

這個問題的復現場景就是內存不足,那麼如果模擬內存不足的情況呢,參考模擬內存不足

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章