TabLayout踩坑之IllegalArgumentException: Tab belongs to a different TabLayout.

最近在開發中使用TabLayout的時候遇到了這個bug。bug就長下面這樣(內容有點囉嗦,解決辦法在最下面):

05-24 21:54:36.989 17175-17175/com.testW/System.err: java.lang.IllegalArgumentException: Tab belongs to a different TabLayout.
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at android.support.design.widget.TabLayout.addTab(TabLayout.java:433)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at android.support.design.widget.TabLayout.addTab(TabLayout.java:411)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at com.kaopu.buss.home.CouponsFragment.initTabData(CouponsFragment.java:333)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at com.kaopu.buss.home.CouponsFragment.access$900(CouponsFragment.java:42)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at com.kaopu.buss.home.CouponsFragment$3.onSuccess(CouponsFragment.java:213)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at com.kaopu.datamanager.HttpManager$1.onResponse(HttpManager.java:180)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at retrofit2.ExecutorCallAdapterFactory$ExecutorCallbackCall$1$1.run(ExecutorCallAdapterFactory.java:68)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at android.os.Handler.handleCallback(Handler.java:739)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at android.os.Handler.dispatchMessage(Handler.java:95)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at android.os.Looper.loop(Looper.java:179)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at android.app.ActivityThread.main(ActivityThread.java:5491)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at java.lang.reflect.Method.invoke(Native Method)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at java.lang.reflect.Method.invoke(Method.java:372)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:961)
05-24 21:54:36.989 17175-17175/com.testW/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)

那麼我們來看看這個bug是什麼意思?Log中可以看到Tab belongs to a different TabLayout.這句話。 翻譯成中文就是說這個tab已經屬於一個TabLayout了,不能再添加到別的TabLayout了。已經屬於一個Tablayout了是怎麼回事?帶着這個疑問我們來看下報錯的地方。從Log中看到報錯的源頭是:CouponsFragment.initTabData(CouponsFragment.java:333),好那麼我們來找到這個地方。代碼如下:

tabLayout.addTab(tabLayout.newTab().setText(newsClass.getTitle));

我明明是通過tabLayout.newTab()的方式添加的啊也是new出來的怎麼就會已經屬於一個TabLayout了?帶着這個疑問我們來看看TabLayout中是哪裏報錯了?

    /**
     * Add a tab to this layout. The tab will be added at the end of the list.
     *
     * @param tab Tab to add
     * @param setSelected True if the added tab should become the selected tab.
     */
    public void addTab(@NonNull Tab tab, boolean setSelected) {
        if (tab.mParent != this) {
            throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
        }

        addTabView(tab, setSelected);
        configureTab(tab, mTabs.size());
        if (setSelected) {
            tab.select();
        }
    }

從上面的源碼中可以看出是對Tab的成員變量mParent 做了判斷如果說這個mParent 的值不是this的話就拋出了這個異常。可以我明明是通過tabLayout.newTab()的方式創建的Tab啊,在來看看通過TabLayout的newTab()的方法裏面是幹了什麼事。

    /**
     * Create and return a new {@link Tab}. You need to manually add this using
     * {@link #addTab(Tab)} or a related method.
     *
     * @return A new Tab
     * @see #addTab(Tab)
     */
    @NonNull
    public Tab newTab() {
        Tab tab = sTabPool.acquire();
        if (tab == null) {
            tab = new Tab();
        }
        tab.mParent = this;
        tab.mView = createTabView(tab);
        return tab;
    }

我們看到源碼中顯示調用了sTabPool成員的acquire()方法。那這個方法中有幹了什麼事?最終我找到了下面的代碼:

        @Override
        @SuppressWarnings("unchecked")
        public T acquire() {
            if (mPoolSize > 0) {
                final int lastPooledIndex = mPoolSize - 1;
                T instance = (T) mPool[lastPooledIndex];
                mPool[lastPooledIndex] = null;
                mPoolSize--;
                return instance;
            }
            return null;
        }

原來TabLayout給Tab做了緩存,但是做緩存了也不應該把其他的TabLayout中的Tab緩存起來吧,除非TabLayout中的sTabPool是靜態的,哎~(這裏度二聲,表示驚喜)還真是,看下面的代碼:

    private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16);

我認爲就算是google這樣做了也沒有什麼不可以,只要在適當的時候把Tab的成員mParent給置爲null就可以了。但是不知道出於什麼原因並沒有這麼做。我把它理解爲Google的bug。

在網上找了很多辦法都不可行,於是只能自己動手了想辦法了。(說了這麼多廢話終於要開始進入正題了,哈哈,別罵我啊!)

先來分析一下上面的一段源碼,(怎麼又來了?哈哈因爲這段代碼是解決問題的關鍵,別急。)

        @Override
        @SuppressWarnings("unchecked")
        public T acquire() {
            if (mPoolSize > 0) {
                final int lastPooledIndex = mPoolSize - 1;
                T instance = (T) mPool[lastPooledIndex];
                mPool[lastPooledIndex] = null;
                mPoolSize--;
                return instance;
            }
            return null;
        }

從上面的代碼中可以看出acquire方法其實是從對象池中去除最後一個對象返回給調用者並把最後一個對象置爲null,對象池中的對象就減少了一個。這樣下次在取的時候就沒有這個Tab對象了。(廢話,大家都看出來了。)

下面是解決辦法(這裏用大字,省的大家看內容太長找不到答案了( ̄▽ ̄))。


那好吧,我的思路是 重寫TabLayou的public void addTab(@NonNull Tab tab)方法,在方法中對參數tab進行檢測,檢測tab的成員變量mParent是否是this。如果不是就從新調用newTab()方法在獲取一個Tab,newTab方法是先從對象池中獲取如果取到了直接返回沒取到就直接new一個。但是在取到的還有可能mParent!=this。所以這裏我要用遞歸的方式去不斷獲取不斷檢測直到拿到了可以用的Tab爲止。而且mParent成員是私有的而且是常量所以要用到反射。下面直接上代碼。總於囉嗦完了,再不上代碼就要捱罵了。。。(已經在罵了)。

    @Override
    public void addTab(@NonNull Tab tab) {
        Tab newTab = checkTabParent(tab);//addTab前檢查是否可用,如果不可用就獲取可用的Tab。
        super.addTab(newTab);
    }

    private Tab checkTabParent(@NonNull Tab tab) {
        try {
            Field mParent = Tab.class.getDeclaredField("mParent");
            mParent.setAccessible(true);
            Object o = mParent.get(tab);
            if (o != this) { //檢測tab的mParent是不是this。
                return checkTabParent(newTab());//如果不是從新獲取並重新檢查。
            } else {
                return tab;//如果是直接返回。
            } 
        } catch (Exception e) {
            throw new RuntimeException("創建Tab失敗!", e);
        }
    }
}

其實上面囉嗦了這麼多主要也是想告訴大家一種解決bug的思路。


如果大家有用請幫忙點贊,有更好的解決辦法也歡迎留言交流。如果沒有用也別來打我啊。(就算想打你們也找不到我,哈哈)。最後謝謝大家的支持。

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