最近在开发中使用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的思路。
如果大家有用请帮忙点赞,有更好的解决办法也欢迎留言交流。如果没有用也别来打我啊。(就算想打你们也找不到我,哈哈)。最后谢谢大家的支持。