安卓SparseArray的indexOfValue方法遇到的坑

本文探讨了在Android开发中SparseArray与SparseIntArray的使用区别,特别是针对indexOfValue方法在不同数据范围内的表现差异。揭示了问题根源在于Integer自动装箱机制及“==”操作符的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在数据量比较少的情况下使用SparseArray可以提高效率,最近一个项目利用它存储int型数据,利用indexOfValue去获取value的索引,本以为value大小范围可以是int的范围大小,结果不是,数据可以存储但是利用indexOfValue方法得到的索引就是不对返回-1检索不到,特别坑,当value范围是-128到127,indexOfValue才能得到正确的索引,查阅官方文档也没有找到原因,如果有大佬发现原因麻烦指出了,一起交流。解决这个问题可以用SparseIntArray代替,value的值是int的范围通过indexOfValue方法也能获得正确的索引。实验代码如下: 

	SparseArray<Integer> abc = new SparseArray<Integer>();
	int gpuIndex = 0;
	abc.put(gpuIndex++, 127);
	abc.put(gpuIndex++, 128);
	abc.put(gpuIndex++, -128);
        int a, a1, a2;
        a = a1 = a2 = Integer.MIN_VALUE;
        a = abc.indexOfValue(127);
        a1 = abc.indexOfValue(128);
        a2 = abc.indexOfValue(-128);
        Log.i("SparseArray", "SparseArrayIndex127: " + a);
        Log.i("SparseArray", "SparseArrayIndex128: " + a1);
        Log.i("SparseArray", "SparseArrayIndex-128: " + a2);
        
        SparseIntArray def = new SparseIntArray();
        int index = 0;
        def.put(index++, 2147483647);
        int b = def.indexOfValue(2147483647);
        Log.i("SparseIntArray", "SparseIntArrayIndex: " + b);
输出的log日志为:

07-20 16:53:45.611: I/SparseArray(13173): SparseArrayIndex127: 0
07-20 16:53:45.611: I/SparseArray(13173): SparseArrayIndex128: -1
07-20 16:53:45.611: I/SparseArray(13173): SparseArrayIndex-128: 2
07-20 16:53:45.611: I/SparseIntArray(13173): SparseIntArrayIndex: 0

如果大佬知道原因还希望指出来一起讨论!

------------------------------------------------------------------2017.8.9更新----------------------------------------------------------------------------------------------

查看java的int的自动装箱机制发现了问题的原因,是由于自动装箱机制和“==”比较引起的

首先看一下SparseArray的indexOfValue方法源码

 /**
     * Returns an index for which {@link #valueAt} would return the
     * specified key, or a negative number if no keys map to the
     * specified value.
     * <p>Beware that this is a linear search, unlike lookups by key,
     * and that multiple keys can map to the same value and this will
     * find only one of them.
     * <p>Note also that unlike most collections' {@code indexOf} methods,
     * this method compares values using {@code ==} rather than {@code equals}.
     */
    public int indexOfValue(E value) {
        if (mGarbage) {
            gc();
        }

        for (int i = 0; i < mSize; i++)
            if (mValues[i] == value)
                return i;

        return -1;
    }
注释已经提示了是利用“==”来进行比较,比较的是对象引用的地址,而传进来的类型E是是Integer的对象,我们知道在-128至127的数自动装箱时,获得的是同一个对象,而不在这个范围的自动装箱获得的是不同的对象,例子如下:

Integer num1 = 666; Integer num2 = 666; Integer num3 = 88; Integer num4 = 88;

System.out.println("num1==num2: "+(num1==num2));

System.out.println("num3==num4: "+(num3==num4)); 

输出结果:num1==num2: false num3==num4: true 

所以indexOfValue(128)时,128自动装箱成Integer对象,利用“==”与原来put进去的128是不同的对象,所以检索不到,返回-1;

再看SparseIntArray的indexOfValue源码:

    /**
     * Returns an index for which {@link #valueAt} would return the
     * specified key, or a negative number if no keys map to the
     * specified value.
     * Beware that this is a linear search, unlike lookups by key,
     * and that multiple keys can map to the same value and this will
     * find only one of them.
     */
    public int indexOfValue(int value) {
        for (int i = 0; i < mSize; i++)
            if (mValues[i] == value)
                return i;

        return -1;
    }
用的也是“==”,但是比较的类型是基本数据类型int,而不是Integer对象,所以能够检索到之前的数据。问题根源解决!

------------------------------------------------------------------------------------------------------------------------------------------------

后记:细节决定成败啊


 

<think>我们遇到了一个 `java.lang.IllegalStateException`,异常信息为: "Can't change tag of fragment HomeFragment{d73edad} (b6be2337-cfd8-4fe8-8258-f443ebca7e23 id=0x7f08020c tag=android:switcher:2131231244:0): was android:switcher:2131231244:0 now android:switcher:2131231244:1" 这个异常通常在使用 `FragmentPagerAdapter` 或 `FragmentStatePagerAdapter` 时发生。异常信息表明,我们试图改变一个已经附加的Fragment的tag。在ViewPager中,每个页面的Fragment都有一个由系统生成的tag,格式为 `"android:switcher:" + viewId + ":" + index`。当ViewPager尝试重新使用或重新附加一个Fragment时,如果发现tag与期望的不匹配,就会抛出这个异常。 常见原因: 1. 在FragmentPagerAdapter中,当我们重写 `getItem(int position)` 方法时,每次调用都应该返回一个新的Fragment实例,而不是返回一个已经存在的实例(或者缓存的实例)。如果我们在某个位置返回了之前已经附加过的Fragment,就会导致这个问题。 2. 在FragmentStatePagerAdapter中,虽然会保存状态,但是同样在`getItem`中不应该返回已经存在的Fragment。 3. 可能是在使用FragmentPagerAdapter时,我们在`getItem`方法中返回了同一个Fragment实例(比如通过一个静态列表缓存Fragment),然后当ViewPager尝试重新实例化Fragment时,发现该Fragment已经被附加并且tag已经改变。 解决方案: 1. 确保在 `getItem(int position)` 方法中每次都返回一个新的Fragment实例(除非你确实知道如何正确复用Fragment,但通常不建议复用,因为ViewPager内部会管理Fragment的恢复)。 2. 不要缓存Fragment实例在数组或列表中然后从缓存中返回,因为ViewPager会自己管理Fragment的生命周期和状态恢复。 3. 如果确实需要复用Fragment(例如在Fragment数量固定且不多的情况下),可以考虑使用`FragmentPagerAdapter`并重写`getItemId`方法,确保每个位置返回一个唯一的id,同时确保在`getItem`中根据id返回正确的Fragment。但是,这种方法需要谨慎处理。 下面是一个典型的错误示例和正确示例: 错误示例(缓存Fragment实例): ```java public class MyPagerAdapter extends FragmentPagerAdapter { private Fragment[] fragments = new Fragment[2]; public MyPagerAdapter(FragmentManager fm) { super(fm); fragments[0] = new HomeFragment(); fragments[1] = new ProfileFragment(); } @Override public Fragment getItem(int position) { return fragments[position]; // 每次都返回同一个实例 } @Override public int getCount() { return fragments.length; } } ``` 正确示例(每次返回新实例): ```java public class MyPagerAdapter extends FragmentPagerAdapter { public MyPagerAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { // 每次都创建新的Fragment实例 switch (position) { case 0: return new HomeFragment(); case 1: return new ProfileFragment(); default: return null; } } @Override public int getCount() { return 2; } } ``` 但是,如果我们的Fragment需要传递参数,我们可以这样做: ```java @Override public Fragment getItem(int position) { if (position == 0) { return HomeFragment.newInstance(param1, param2); } else if (position == 1) { return ProfileFragment.newInstance(param3); } return null; } ``` 注意:使用`newInstance`模式(静态工厂方法)来创建Fragment并传递参数。 另外,如果我们在使用`FragmentStatePagerAdapter`,同样遵循上述规则。 但是,如果我们确实需要缓存Fragment(例如创建开销很大),我们可以尝试以下方法: 1. 使用`FragmentPagerAdapter`(它会将Fragment保留在内存中,不会销毁)。 2. 在适配器中维护一个`SparseArray<Fragment>`,但是要确保在`getItem`中不是每次都返回缓存的Fragment,而是只在第一次创建,之后返回缓存的。同时,我们需要重写`getItemId`来确保每个位置有一个唯一的id,这样当位置改变时,ViewPager不会尝试重新使用旧的Fragment。 但是,这种方法比较复杂,而且容易出错。通常,不推荐缓存Fragment,因为ViewPager内部已经有一个机制来管理Fragment的恢复(通过FragmentManager)。如果我们使用`FragmentStatePagerAdapter`,它会在不需要时销毁Fragment,所以缓存也没有意义。 如果问题仍然存在,检查一下是否在代码中其他地方手动修改了Fragment的tag?比如调用了`fragment.setTag()`?如果有,应该避免这样做,因为ViewPager会自己设置tag。 总结:这个异常的根本原因是我们在`getItem`方法中返回了已经附加到Activity的Fragment实例(可能是之前返回过的同一个实例),而ViewPager在试图重新附加这个Fragment时发现它的tag已经改变(因为之前已经附加过,tag已经被设置过)。所以,最简单的解决方案就是确保每次`getItem`都返回一个新的Fragment实例。 如果我们的Fragment需要传递数据,我们可以通过`Bundle`传递参数,而不是复用Fragment实例。 下面是一个正确传递参数的示例: ```java public class HomeFragment extends Fragment { private static final String ARG_PARAM1 = "param1"; private static final String ARG_PARAM2 = "param2"; public static HomeFragment newInstance(String param1, String param2) { HomeFragment fragment = new HomeFragment(); Bundle args = new Bundle(); args.putString(ARG_PARAM1, param1); args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } // ... rest of the fragment code } ``` 然后在适配器中: ```java @Override public Fragment getItem(int position) { if (position == 0) { return HomeFragment.newInstance("Value1", "Value2"); } else { return OtherFragment.newInstance(); } } ``` 这样,每次调用`getItem`都会创建一个新的Fragment实例,并且传递参数。 如果问题解决了,那么我们就避免了Fragment的复用。如果问题仍然存在,请检查是否在Fragment内部修改了tag?或者是否有其他地方持有了这个Fragment并重新附加? 希望这个解释和解决方案能帮助你解决问题。</think>### 解决 `IllegalStateException: Can't change tag of fragment` 错误 这个错误发生在使用 `ViewPager` 或 `FragmentStatePagerAdapter` 时,尝试重用已附加的 `Fragment` 实例但标签(tag)不匹配。核心原因是 **Fragment 的 tag 被系统分配后不可更改**,而你的代码可能尝试重用已附加的 Fragment 实例。 #### 解决方案代码(Kotlin) ```kotlin class SafeFragmentStateAdapter( fragmentManager: FragmentManager, private val fragmentCreator: (Int) -> Fragment ) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { // 缓存已创建的 Fragment 实例 private val fragments = SparseArray<Fragment>() override fun getItem(position: Int): Fragment { // 优先使用缓存,无缓存则创建新实例 return fragments[position] ?: fragmentCreator(position).also { fragments.put(position, it) } } override fun getItemId(position: Int): Long { // 为每个位置生成唯一 ID(解决 position 相同但内容不同的情况) return position.toLong() } override fun getItemPosition(`object`: Any): Int { // 强制刷新所有 Fragment(可选,按需使用) return POSITION_NONE } override fun getCount(): Int = ... // 返回你的页面数量 // 清理缓存(在不需要时调用) fun clearFragments() { fragments.clear() } } ``` #### 关键点解释 1. **唯一 ID 机制**: - 重写 `getItemId(position)` 为每个位置返回唯一 ID - 解决 ViewPager 内部复用机制导致的 tag 冲突 2. **Fragment 缓存**: - 使用 `SparseArray` 按位置缓存 Fragment - 避免重复创建实例,同时保证标签一致性 3. **动态刷新**: - `getItemPosition()` 返回 `POSITION_NONE` 强制刷新 - 适用于数据动态变化的场景(需配合 `notifyDataSetChanged()`) #### 使用示例 ```kotlin // 初始化 Adapter val adapter = SafeFragmentStateAdapter(childFragmentManager) { position -> when (position) { 0 -> HomeFragment.newInstance() 1 -> ProfileFragment.newInstance() else -> throw IllegalArgumentException() } } viewPager.adapter = adapter // 数据更新时刷新(可选) fun updateData() { adapter.notifyDataSetChanged() } ``` #### 注意事项 1. **不可复用 Fragment 实例**: - 不要在不同位置返回同一个 Fragment 实例 - 不要手动修改 Fragment 的 tag 2. **生命周期管理**: - 使用 `childFragmentManager` 嵌套 Fragment - 在 Activity/Fragment 销毁时调用 `adapter.clearFragments()` 3. **数据传递**: - 通过 `newInstance()` 工厂方法传递参数 - 避免在 Fragment 构造函数中传参 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值