android日记(四)

上一篇:android日记(三)

1.java关键字transient

  • 只能修饰变量,不能修饰类和方法。
  • 作用:在对象序列化时,被transient修饰过的变量不会参与到序列化过程。
  • 验证:Activity1携带一个序列化的对象,跳转到Activity2。
    private void testTransient() {
            Intent intent = new Intent(getContext(), Activity2.class);
            Model model = new Model();
            model.setName("transient");
            model.setNumber(123);
            intent.putExtra("model", model);
            startActivity(intent);
        }
    class Model implements Serializable {
    
        private transient String name;
        private int number;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getNumber() {
            return number;
        }
    
        public void setNumber(int number) {
            this.number = number;
        }
    }

    在Activity2中,从Intent中获取Activity1中传过来的对象,实现反序列化过程。

     结果显示,被transient修饰过的name变量,其值是null,表明name变量确实没有参与到序列化过程。

     验证:Activity1携带一个序列化的对象,跳转到Activity2。

  • 那transient用于Parceable的效果一样吗?
    class Model implements Parcelable {
    
        private transient String name;
        private int number;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getNumber() {
            return number;
        }
    
        public void setNumber(int number) {
            this.number = number;
        }
    
    
        @Override
        public int describeContents() {
            return 0;
        }
    
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(this.number);
        }
    
        public Model() {
        }
    
        protected Model(Parcel in) {
            this.number = in.readInt();
        }
    
        public static final Parcelable.Creator<Model> CREATOR = new Parcelable.Creator<Model>() {
            @Override
            public Model createFromParcel(Parcel source) {
                return new Model(source);
            }
    
            @Override
            public Model[] newArray(int size) {
                return new Model[size];
            }
        };
    }

     结果显示,在Parceable中,transient同样不会参与到序列化。

  • 听说只要是static修饰的变量,都不会被序列化?没毛病!如果你发现反序列化取出的静态变量不会空,那只是因为,会拿到JVM中对应的static值。
    class Model implements Parcelable {
        static String name = "transient";//static变量原始值
        private int number;
    }
    
     private void testTransient() {
            Intent intent = new Intent(getContext(), ViewBindingActivity.class);
            Model model = new Model();
            model.setNumber(123);
            intent.putExtra("model", model);
    
            model.setName("static");//修改静态变量
            startActivity(intent);
        }

    运行结果:

    name不是填入序列化时的原始值“transient”,而是修改后的“static”。这说明反序列化后类中static型变量username的值为当前JVM中对应static变量的值,而不是序列化时的值Alexia

2.将List转为Map需要注意什么

  • java.util.stream.Collections类中提供了toMap()方法,可以将list转为map。 
    private void convertList2Map() {
            List<Person> list = new ArrayList<>();
            list.add(new Person(1, "Peder"));
            list.add(new Person(2, "Bob"));
            list.add(new Person(3, "Hanhan"));
            Map<Integer, String> map = list.stream()
                    .collect(java.util.stream.Collectors.toMap(person -> person.id, person -> person.name));
        }
  • lis中不能有重复key,否则会遇到illegalStateException,
    private void convertList2Map1() {
            List<Person> list = new ArrayList<>();
            list.add(new Person(1, "Peder"));
            list.add(new Person(2, "Bob"));
            list.add(new Person(2, "Hanhan"));//出现重复key
            try {
                Map<Integer, String> map = list.stream()
                        .collect(java.util.stream.Collectors.toMap(person -> person.id, person -> person.name));
            } catch (IllegalStateException e) {
                e.printStackTrace();//java.lang.IllegalStateException: Duplicate key Bob
            }
        }
  • 使用mergeFunction解决重复key问题,其作用在于出现重复key时,自定义对value的选择策略。比如下面(v1,v2)->v2的mergeFunction,定义当出现重复key时,选择新的value。
    private void convertList2Map3() {
            List<Person> list = new ArrayList<>();
            list.add(new Person(1, "Peder"));
            list.add(new Person(2, "Bob"));
            list.add(new Person(2, "Hanhan"));
            list.add(new Person(2, "Panpan"));
            try {
                Map<Integer, String> map = list.stream()
                        .collect(java.util.stream.Collectors.toMap(person -> person.id, person -> person.name, (v1, v2) -> v2));
                String name = map.get(1);//name = Panpan
            } catch (IllegalStateException e) {
                e.printStackTrace();
            }
        }

    类似的,也可以使用(v1,v2)->v1自定义重复key时,取旧的value。又比如定义(v1,v2)->v1+v2,将新旧value拼接起来。

  • 转为map的list中不能有null值,否则会遇到NullPointerException
    private void convertList2Map() {
            List<Person> list = new ArrayList<>();
            list.add(new Person(1, "Peder"));
            list.add(new Person(2, "Bob"));
            list.add(new Person(3, "Hanhan"));
            list.add(new Person(4, null));//将会导致NullPointer
            try {
                Map<Integer, String> map = list.stream()
                        .collect(java.util.stream.Collectors.toMap(person -> person.id, person -> person.name, (v1, v2) -> v1 + v2));
                String name = map.get(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    为什么会出现空指针呢?来看看HashMap的merge()方法的源码就知道了。

     @Override
        public V merge(K key, V value,
                       BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
            if (value == null)
                throw new NullPointerException();
            if (remappingFunction == null)
                throw new NullPointerException();
           ...
    }

3.如何遍历一个Map

  • 使用keySet(),遍历key
    //keySet遍历
        private void traverseMap(HashMap<String, String> map) {
            long start = System.currentTimeMillis();
            for (String key : map.keySet()) {
                String value = map.get(key);
            }
        }
  • 使用keyEntry(),遍历Entry<Key,Value>
    //entrySet遍历
        private void traverseMap(HashMap<String, String> map) {
            long start = System.currentTimeMillis();
            for (Map.Entry<String, String> entry : map.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
            }
        }
  • 使用迭代器Ietrator
    //iterator keySet遍历
    private void traverseMap(HashMap<String, String> map) { long start = System.currentTimeMillis(); Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); String value = map.get(key); } }
    //iterator entrySet遍历
    private void traverseMap(HashMap<String, String> map) { long start = System.currentTimeMillis(); Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); String key = entry.getKey(); String value = entry.getValue(); } }
  • 使用Java_8的forEach()
    //forEach   
    private void traverseMap(HashMap<String, String> map) { map.forEach((k, v) -> { String key = k; String value = v; }); }
  • entrySet()和keySet()效率谁高?
    public void traverseMap() {
            Map<Integer, Integer> map = new HashMap<>();
            for (int i = 0; i < 5000000; i++) {
                map.put(i, i);
            }
    
            //keySet遍历
            long start = System.currentTimeMillis();
            Iterator<Integer> iterator = map.keySet().iterator();
            while (iterator.hasNext()) {
                int key = iterator.next();
                int value = map.get(key); //效率低下原因在此,因为此处会再次遍历Map ,取得key对应的value值。
            }
            long end = System.currentTimeMillis();
            long spendTime = end - start;
            Log.d(TAG, "keySet consume time = " + spendTime);
    
    
            //entrySet遍历
            start = System.currentTimeMillis();
            Iterator<Map.Entry<Integer, Integer>> iterator2 = map.entrySet().iterator();
            Map.Entry<Integer, Integer> entry;
            while (iterator2.hasNext()) {
                entry = iterator2.next();
                int key = entry.getKey();
                int value = entry.getValue();
            }
            end = System.currentTimeMillis();
            spendTime = end - start;
            Log.d(TAG, "entrySet consume time = " + spendTime);
        }

    keSet()遍历过程中,通过map.get(key)实际又进行一次遍历取值,因此效率会比entrySet()低。

  • 结论:keySet()会遍历两遍,遍历map时尽量用entrySet(),而不是keySet()。

4.关于ConcurrentModifiedException应该注意些什么

  • 在对集合进行相关操作时,常常会遇到ConcurrentModifiedException异常。无一例外的,异常都是当modCount不等于expectedModCount时抛出。
    if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }

    那么modCount和expectedModCount分别是什么?在各个集合类中,都定义着变量modCount,其初始值是0,注释说明其值表示当前集合结构变更的次数。

     /**
       * The number of times this list has been <i>structurally modified</i>.*/
    protected transient int modCount = 0;

    当集合的结构变更时modCount就会加1,以ArrayList#sort()为例,

     @Override
        @SuppressWarnings("unchecked")
        public void sort(Comparator<? super E> c) {
            final int expectedModCount = modCount;
            Arrays.sort((E[]) elementData, 0, size, c);
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            modCount++;
        }

    expectedModCount是操作方法内的局部变量,在具体操作执行前,expectedModCount被赋值为modCount本身,而操作执行完成后,会检查modCount与expectedModCount是否相等。如果发现不相等,就抛了ConcurrentModificationException。

  • 什么情况下modCount会不等于expectedModCount呢?当集合操作方法执行期间,如果发生了结构变更,使modCount++得到执行,比如多线程执行arrayList.sort(),就可能出现一个线程执行完sort()后使modCount++,这时另一个线程来比较modCount与expectedModCount,就出现不相等的情况。
  • 如果是单线程,ConcurrentModificationException常常出现在迭代器中。比如下面的代码,
    private void testForeach() {
            List<Integer> list = new ArrayList<>();
            list.add(1);
            list.add(2);
            try {
                for (Integer i : list) {
                    if (i == 1) {
                        list.remove(i);
                    }
                }
            } catch (ConcurrentModificationException e) {
                e.printStackTrace();
            }
        }
    public boolean remove(Object o) {
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }
    private void fastRemove(int index) {
            modCount++;
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
        }

    上面的代码引申出好几个问题:1)foreach内部怎么实现循环?2)为什么遭遇ConcurrentModificationException异常?3)数据结构上如何实现删除操作?

  • foreach内部如何实现循环?按照惯例,androidStudio build/javac目录下查看对应的字节码,可以看到foreach实际使用迭代器Iterator来实现。
    private void testForeach() {
            List<Integer> list = new ArrayList();
            list.add(1);
            list.add(2);
    
            try {
                Iterator var2 = list.iterator();
                while(var2.hasNext()) {
                    Integer i = (Integer)var2.next();
                    if (i == 1) {
                        list.remove(i);
                    }
                }
            } catch (Exception var4) {
                var4.printStackTrace();
            }
        }
  • 为什么循环会遭遇ConcurrentModificationException异常?看下面ArrayList.Itr的源码,遍历时总是以hashNext()为条件,通过next()方法取下一条数据。
    private class Itr implements Iterator<E> {
            // Android-changed: Add "limit" field to detect end of iteration.
            // The "limit" of this iterator. This is the size of the list at the time the
            // iterator was created. Adding & removing elements will invalidate the iteration
            // anyway (and cause next() to throw) so saving this value will guarantee that the
            // value of hasNext() remains stable and won't flap between true and false when elements
            // are added and removed from the list.
            protected int limit = ArrayList.this.size;
    
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
            int expectedModCount = modCount;
    
            public boolean hasNext() {
                return cursor < limit;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                int i = cursor;
                if (i >= limit)
                    throw new NoSuchElementException();
                Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length)
                    throw new ConcurrentModificationException();
                cursor = i + 1;
                return (E) elementData[lastRet = i];
            }
      public void remove() {
    if (lastRet < 0)
    throw new IllegalStateException();
    if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

    try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    lastRet = -1;
    expectedModCount = modCount;
    limit--;
    } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
    }
    }

    在创建迭代器的时候,赋值了变量expectedModCount = modCount。在而next()方法中,会比较modCount和expectedModCount,一旦在循环过程中,发生了改变集合结果的操作,比如上面代码中的remove()操作执行时会modCount++。从而进入到下一次迭代时,modCount > expectedModCount,抛出异常。哦对了,这个就是大名鼎鼎的fast-fail机制。

  • 数据结构上如何实现删除操作?上面fastRemove()方法中,remove的操作是通过System.arrayCopy()完成的,这是个native方法,顾名思义,就是通过数组复制。
    System.arraycopy(int[] arr, int star,int[] arr2, int start2, length);
    
    5个参数,
    #第一个参数,是要被复制的数组
    #第二个参数,是被复制的数字开始复制的下标
    #第三个参数,是目标数组,也就是要把数据放进来的数组
    #第四个参数,是从目标数据第几个下标开始放入数据
    #第五个参数,表示从被复制的数组中拿几个数值放到目标数组中

    比如: 数组1:
    int[] arr = { 1, 2, 3, 4, 5 }; 数组2:int[] arr2 = { 5, 6,7, 8, 9 }; 运行:System.arraycopy(arr, 1, arr2, 0, 3); 得到: int[] arr2 = { 2, 3, 4, 8, 9 };

    因此,remove操作的过程为,先计算需要移动的长度(从删除点index到数组末尾)

    int numMoved = size - index - 1;

    然后,数组自身复制到自身,将[index+1, size-1]复制到[index, size-2]

    System.arraycopy(elementData, index+1, elementData, index, numMoved);

    可见,remove完数组的物理长度没有改变的,改变的是数据长度,在 elementData[--size] = null中执行了--size操作。

    /**
         * Returns the number of elements in this list.
         *
         * @return the number of elements in this list
         */
        public int size() {
            return size; 
        }
  • 如何防范ConcurrentModificationException?
    • 在for循环中,如果在集合操作后,不需要再遍历,应该使用break结束循环。
    • 使用Iterator内的remove()操作代替List本身的remove(),每次remove操作完,会重置expectedModCount的值。
      private void testForeach() {
              List<Integer> list = new ArrayList<>();
              list.add(1);
              list.add(2);
              Iterator<Integer> iterator = list.iterator();
              while (iterator.hasNext()) {
                  Integer integer = iterator.next();
                  if (integer == 1) {
                      iterator.remove();
                  }
              }
          }
      private class Itr implements Iterator<E> {
              public void remove() {
                  if (lastRet < 0)
                      throw new IllegalStateException();
                  if (modCount != expectedModCount)
                      throw new ConcurrentModificationException();
      
                  try {
                      ArrayList.this.remove(lastRet);
                      cursor = lastRet;
                      lastRet = -1;
                      expectedModCount = modCount;//每次remove操作完,会重置expectedModCount的值
                      limit--;
                  } catch (IndexOutOfBoundsException ex) {
                      throw new ConcurrentModificationException();
                  }
          }

5.关于集合转换时发生的UnsportedOperationException

  • AarryList.asList(array)实现将数组转为List,对转换得到的list进行add/remove操作,就会遇到UnsportedOperationException
    private void aboutUnsupportedOperateException() {
            String[] array = {"1", "3", "5"};
            List<String> list = Arrays.asList(array);
            try {
                list.set(0, "11");
                array[1] = "33";
    
                list.remove(2);//throw UnsupportedOperationException
                list.add("77");//throw UnsupportedOperationException
            } catch (UnsupportedOperationException e) {
                e.printStackTrace();
            }
        }

    上面代码将array转为list后,有两个注意事项:

    • list和array发生变更时,会相互之间映射影响,原因在第二点分析源码时一起说。

    • list内部是定长数组,进行add/remove就会抛遇到UnsportedOperationException。
      public static <T> List<T> asList(T... a) {
              return new ArrayList<>(a);
          }
      
      //Arrays内中的静态内部类,是java.util.Arrays.ArrayList,而不是java.util.ArrayList
      private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable { private static final long serialVersionUID = -2764017481108945198L; private final E[] a; ArrayList(E[] array) { a = Objects.requireNonNull(array);//数组引用赋值 } }

      Arrays.asList()得到的是Arrays的静态内部类java.util.Arrays.ArrayList,并不是java.util.ArrayList,这个同名内部类也是继承于AbstractList,但是并没有重写实现add/remove方法,故而上面的list的remove和add操作遇到了UnsuportedOperationException。另外,Arrays.ArrayList内部实际也是一个数组,在创建Arrays.ArrayList的方法中,其实是把转换的数组引用赋值给了内部数组变量,它们都指向了同一个数组,因此array与list在变更时会相互影响。所谓数组转成集合,只是提供了一个集合的视图,本质还是数组。

解决方法:用Arrays.asList()的结果,去new新的java.util.ArrayList。

       List<String> list = new ArrayList<>(Arrays.asList(array));
  • java.util.Collections类中提供了一些生成immutable列表的方法,比如生成一个空列表Collections.emptyList(),又比如生成一个单元素的列表Collects.singletonList(),这些方法返回的都是Collections旗下的内部类,比如EmptyList、SingletonList,他们都是继承于AbstractList,但是并没有实现父类的remove()和add()方法,也就是不可变的集合。如果对他们使用add()、remove()操作,就会遭遇UnsupportedOperationException。类似的操作还有Collections.emptyMap(),Collections.emptySet(),Collections.singletonMap()等。
     //生成一个空的immutable列表,java.util.Collections.EmptyList
     List<String> emptyList = Collections.emptyList();
    
     //生成一个就包含1个元素的immutable列表,java.util.Collections.SingletonList
     List<String> singletonList = Collections.singletonList("7");
    
    public static <T> List<T> singletonList(T o) {
       return new SingletonList<>(o);
    }
    private static class SingletonList<E>
      extends AbstractList<E>
      implements RandomAccess, Serializable {
        //未实现add()、remove()
    }
  • 使用Map的keySet()/values()/entrySet()方法,返回的集合对象时,有些也没有实现,或者全部实现add()和remove(),使用不当也会报UnSupportedOperationException。以HashMap中的实现为例:
    //hashMap
    public Set<K> keySet() {
            Set<K> ks = keySet;
            if (ks == null) {
                ks = new KeySet();//KeySet是HashMap内部集合类,只实现了remove()没有实现add()
                keySet = ks;
            }
            return ks;
        }
    //hashMap
    public Collection<V> values() {
            Collection<V> vs = values;
            if (vs == null) {
                vs = new Values();//Vaules是HashMap内部immutable集合类
                values = vs;
            }
            return vs;
        }
    //hashMap
    //KeySet是HashMap内部集合类,只实现了remove(),没有实现add()
    public Set<Map.Entry<K,V>> entrySet() {
            Set<Map.Entry<K,V>> es;
            return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
        }
  • 想想为什么转换得到的集合要做immutable限制呢?一个显然易见的解释是,转换结果只是被转对象的一个视图,其本质还是那个转换前的对象。而转换前的集合结构,比如一个数组,本身可能就不支持remove()或者add()操作。

6.RandomAccess接口的作用

  • 翻看ArrayList源码注意到,它实现了RandomAccess接口。这是个空架子接口,注释写道,只是用于标记实现类,具有快速随机访问的能力。
    /**
     * Marker interface used by <tt>List</tt> implementatons to indicate that
     * they support fast (generally constant time) random access.  The primary
     * purpose of this interface is to allow generic algorithms to alter their
     * behavior to provide good performance when applied to either random or
     * sequential access lists.
     * 
     */
    public interface RandomAccess {
    }

    看完好像还是不太清晰它的作用,或者说要具体要怎么用。这时候,可以先检索下,看看系统源码中是怎么使用RandomAccess的。发现Collections类中有使用到它。

  • 从Collections.binarySearch()源码说起,方法用于二分查找,其具体实现逻辑前,有list instanceof RandomAccess的判断。根据判断结果,分别执行indexBinarySearch()和iteratorBinarySearch()。
    public static <T>
        int binarySearch(List<? extends Comparable<? super T>> list, T key) {
            if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
                return Collections.indexedBinarySearch(list, key);
            else
                return Collections.iteratorBinarySearch(list, key);
        }
    private static <T>
        int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
            int low = 0;
            int high = list.size()-1;
    
            while (low <= high) {
                int mid = (low + high) >>> 1;
                Comparable<? super T> midVal = list.get(mid);
                int cmp = midVal.compareTo(key);
    
                if (cmp < 0)
                    low = mid + 1;
                else if (cmp > 0)
                    high = mid - 1;
                else
                    return mid; // key found
            }
            return -(low + 1);  // key not found
        }
    private static <T>
        int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
        {
            int low = 0;
            int high = list.size()-1;
            ListIterator<? extends Comparable<? super T>> i = list.listIterator();
    
            while (low <= high) {
                int mid = (low + high) >>> 1;
                Comparable<? super T> midVal = get(i, mid);
                int cmp = midVal.compareTo(key);
    
                if (cmp < 0)
                    low = mid + 1;
                else if (cmp > 0)
                    high = mid - 1;
                else
                    return mid; // key found
            }
            return -(low + 1);  // key not found
        }

    也就是说,如果是RandomAccess,遍历时那就直接list.get(mid);否则,使用迭代器get(iterator.get(i,mid))。

  • 由此联想到,同样是List家族成员,ArrayList实现了RandomAccess,而LinkedList则没有。ArrayList内部是一个数组来说,遍历取值时,直接通过get(index)更快。而LinkedList通过iterator的next()方法取值效率更高。
  • 拿实验数据佐证一下,让ArrayList和LinkedList,都分别执行for循环遍历取值和迭代器遍历取值。记录算法执行耗时。
    private void testRandomAccess() {
            List arrayList = createList(new ArrayList<Integer>(), 20000);
            List linkedList = createList(new LinkedList<Integer>(), 20000);
    
            long loopArrayListTime = traverseByLoop(arrayList);
            long iteratorArrayListTime = traverseByIterator(arrayList);
    
            long loopLinkedListTime = traverseByLoop(linkedList);
            long iteratorLinkedListTime = traverseByIterator(linkedList);
        }
    
        private List createList(List list, int size) {
            for (int i = 0; i < size; i++) {
                list.add(i);
            }
            return list;
        }
    
        //使用for循环遍历
        private long traverseByLoop(List list) {
            long startTime = System.currentTimeMillis();
            for (int i = 0; i < list.size(); i++) {
                list.get(i);
            }
            long endTime = System.currentTimeMillis();
            return endTime - startTime;
        }
    
        //使用迭代器遍历
        private long traverseByIterator(List list) {
            Iterator iterator = list.iterator();
            long startTime = System.currentTimeMillis();
            while (iterator.hasNext()) {
                iterator.next();
            }
            long endTime = System.currentTimeMillis();
            return endTime - startTime;
        }

     从实验结果上看,对ArrayLisy使用for循环取值更快,对LinkedList使用迭代器遍历取值更快。而一旦对List用了不合适的遍历方式,会引起严重的性能损耗。

  • 从而,当实际coding中,遇到需要对一个List遍历取值的场景,应该先判断是否是RandomAccess,根据List具体的类型,选择合适遍历方式。一个科学的遍历方法如下。
    private void traverse(List<Integer> list) {
            if (list instanceof RandomAccess) {
                System.out.println("实现了RandomAccess接口,不使用迭代器");
                for (int i = 0; i < list.size(); i++) {
                    System.out.println(list.get(i));
                }
            } else {
                System.out.println("没实现RandomAccess接口,使用迭代器");
                Iterator<Integer> it = list.iterator();
                while (it.hasNext()) {
                    System.out.println(it.next());
                }
            }
        }

7.StateLoss带来的Exception:Can not perform this action after onSaveInstanceState

  • 最近在做权限申请时,需要先在dialog中申请到打电话权限,获取权限成功就跳转页,同时dismiss当前dialog,使用DialogFragment来实现。
    private void startCallActivity() {
            if (account == null) {
                return;
            }
            CallActivity.start(mContext, number, account);//跳转到电话页
            try {
                dismiss();
                Log.d("tag", "dismissAllowingStateLoss() no error ");
            } catch (Exception e) {//Can not perform this action after onSaveInstanceState
                Log.d("tag", "error: " + e.getMessage());
                e.printStackTrace();
            }
        }

    遭遇了异常:Can not perform this action after onSaveInstanceState

  • 查阅dismiss源码发现,对应的还有一个dismissAllowingStateLoss()方法,两个方法的核心区别在于,内部通过fragmentTransaction来remove fragment的操作,是选择commit()还是commitAllowingStateLoss()来完成。
     public void dismiss() {
            dismissInternal(false, false);
        }
    
        /**
         * Version of {@link #dismiss()} that uses
         * {@link FragmentTransaction#commitAllowingStateLoss()
         * FragmentTransaction.commitAllowingStateLoss()}. See linked
         * documentation for further details.
         */
        public void dismissAllowingStateLoss() {
            dismissInternal(true, false);
        }
    
        void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) {
    ...

    FragmentTransaction ft = requireFragmentManager().beginTransaction();
         ft.remove(this);
    if (allowStateLoss) {
    ft.commitAllowingStateLoss();
    } else {
    ft.commit();
    }
    ...

    }
  • 从而,问题等价于在add fragment时,commit()和commitAllowingStateLoss()的区别问题。
     /**
         * Schedules a commit of this transaction.  The commit does
         * not happen immediately; it will be scheduled as work on the main thread
         * to be done the next time that thread is ready.
         *
         * <p class="note">A transaction can only be committed with this method
         * prior to its containing activity saving its state.  If the commit is
         * attempted after that point, an exception will be thrown.  This is
         * because the state after the commit can be lost if the activity needs to
         * be restored from its state.  See {@link #commitAllowingStateLoss()} for
         * situations where it may be okay to lose the commit.</p>
         *
         * @return Returns the identifier of this transaction's back stack entry,
         * if {@link #addToBackStack(String)} had been called.  Otherwise, returns
         * a negative number.
         */
        public abstract int commit();
    
        /**
         * Like {@link #commit} but allows the commit to be executed after an
         * activity's state is saved.  This is dangerous because the commit can
         * be lost if the activity needs to later be restored from its state, so
         * this should only be used for cases where it is okay for the UI state
         * to change unexpectedly on the user.
         */
        public abstract int commitAllowingStateLoss();
  • 也就是说,commit()允许fragment宿主保存状态信息,在onSaveInstanceState()之前发起操作,等到onSaveInstanceState()之后再完成commit操作;而commitAllowingLossState()可以在activity执行onSaveInstance()之前就完成操作。从而,一旦当onSaveInstanceState()方法已经执行完后,再来执行commit()方法,就会报“Can not perform this action after onSaveInstanceState”,因为这时候系统无法再保存fragment的状态了。
  • commit操作的检测入口在FragmentManager.checkStateLoss()方法中,当onSaveInstanceState()方法调用后,会触发saveAllState()方法,改变mStateSave值为true。
    #FragmentManager
    private void checkStateLoss() { if (isStateSaved()) { throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } if (mNoTransactionsBecause != null) { throw new IllegalStateException( "Can not perform this action inside of " + mNoTransactionsBecause); } } @Override public boolean isStateSaved() { // See saveAllState() for the explanation of this. We do this for // all platform versions, to keep our behavior more consistent between // them. return mStateSaved || mStopped;//当onSaveInstanceState()方法调用后,会触发saveAllState()方法,改变mStateSave值为true }
  • 页面跳转时Activity的onSaveInstanceState()方法的执行时机:在activity跳转的时,activity就会调用onSaveInstanceState()保存当前activity的状态。因为当前activity被切到后台,有内存不足被回收的风险。系统一旦预感到activity有被动销毁的风险,就会先行onSaveInstanceState(),而不会等到activity真正开始销毁时才执行。当新activity按返回键,又回到当前activity时,如果activity确实是被回收过,就会重建activity,并且调用onRestoreInstanceState()恢复页面状态。如果activity在后台没有被回收,那就不用onRestoreInstanceState,直接onResume()重现视图即可。
  • 实际业务中,点击dialog中的拨号按钮,触发跳转到拨号页,同时需要dismiss()当前dialog。这时,当前activity的onSaveInstanceState()生命周期,可能在dismiss()操作进行checkState()时先执行,使得检查发现mStateSaved = true,从而抛出异常。
    class TestDialogFragment : DialogFragment() {
      ...
        private fun leaveAndDismiss() {
            val intent = Intent(context, Main3Activity::class.java)
            intent.putExtra("f", "fff")
            startActivity(intent)
    //        dismiss()
            
            Thread(Runnable {
                //为了稳定复现,可以等待activity跳转完成后,再执行dismiss
                Thread.sleep(1000)
                val message = Message()
                message.what = 1
                handler.sendMessage(message)
            }).start()
        }
    
        private val handler: Handler = object : Handler() {
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    1 -> {
                        dismiss()
                    }
                }
            }
        }
    ... }
  • 解决办法:使用dismissAllowingLoss()代替dismiss()即可。

8.关于DialogFragment显示原理

  • 显示一个dialogFragment的方法 new DialogFragment().show(fragmentManager, tag)。
    new DialogFragment().show(fragmentManager, tag)
  • show方法的中添加fragment的原理,源码如下,内部实际上就是fragmentTransaction.add()后发起commit()操作。
    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
            mDismissed = false;
            mShownByMe = true;
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        }
  • ft.add(this, tag)方式不会把fragment添加到任何ViewGroup中,常常用于添加一个无视图的fragment,比如android申请权限的操作需要依赖fragment的生命周期,但是不需要任何视图。那问题在于DialogFragment是需要显示视图的,而且还支持在onCreateView()中加载自定义布局,这是怎么回事呢?
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
            return inflater.inflate(R.layout.layout_dialog, container, false)
        }

    1)首先明确DialogFragment本质是对Dialog视图的包装,其内部一定有创建Dialog的地方。很容易找到,只要外部没有设置mShowsDialog为false,那么在DialogFragment创建完成后,就会执行onCreateDialog(),创建Dialog。

    @Override
        public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
            if (!mShowsDialog) {
                return super.onGetLayoutInflater(savedInstanceState);
            }
    
            mDialog = onCreateDialog(savedInstanceState);
    
            if (mDialog != null) {
                setupDialog(mDialog, mStyle);
    
             ...
        }
    @NonNull
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return new Dialog(getActivity(), getTheme());
        }
    @Override
        public void onStart() {
            super.onStart();
    
            if (mDialog != null) {
                mViewDestroyed = false;
                mDialog.show();
            }
        }

    然后,当dialogFragment生命周期执行到onStart()时,就执行了dialog.show(),展示出了弹窗,并不依赖当前fragment的视图container容器ViewGroup,因此即便添加fragment时不指定container,也不会影响到dialog的显示。

    2)其次要弄清楚内部Dialog的自定义布局是如何添加。很明显,onCreateView()返回的布局View被添加进了Dialog中,Dialog是通过setContentView()添加布局的,简单的查找该方法调用处就能发现,在onActivityCreated()方法内部,有操作dialog.setContentView(),添加的view通过getView()得到。

    @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
    
            if (!mShowsDialog) {
                return;
            }
    
            View view = getView();
            if (view != null) {
                if (view.getParent() != null) {
                    throw new IllegalStateException(
                            "DialogFragment can not be attached to a container view");
                }
                mDialog.setContentView(view);
            }
            final Activity activity = getActivity();
            if (activity != null) {
                mDialog.setOwnerActivity(activity);
            }
            ...
        }

    getView()拿到的是啥,不就是onCreateView()的返回值么。

     @Nullable
        public View getView() {
            return mView;
        }
    void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                @Nullable Bundle savedInstanceState) {
            mChildFragmentManager.noteStateNotSaved();
            mPerformedCreateView = true;
            mViewLifecycleOwner = new FragmentViewLifecycleOwner();
            mView = onCreateView(inflater, container, savedInstanceState);
           ...
            }
        }

    所以,通过onCreateView()给内部Dialog添加布局的操作实锤了。

9.解决DialogFragment.show()方法抛出的IIegalException:Can not perform this action after onSaveInstanceState

  • 使用FragmentTranscation的相关操作时,常常遭遇IIegalException:Can not perform this action after onSaveInstanceState。面对这个臭名昭著的StateLoss问题,系统一般是提供了一个AllowingStateLoss方法予以避免。
  • 那DialogFragment.show()为什么也会遇到这个异常呢。查看源码可知,show()内部实际是FragmentTransaction.commit()操作。这就不难解释了,一旦dialogFragment.show()的commit操作实际完成的时机,走在宿主onSaveInstanceState()之后的话,就会遇到StateLoss Exception。
    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
            mDismissed = false;
            mShownByMe = true;
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        }
  • 那用commitAllowingStateLoss()方法去替换commit()方法,完成fragment添加,不就好了吗。然而,DialogFragment并未提供show()对应的showAllowingStateLoss()方法。这该如何是好尼呢?
  • 既然系统没有提供showAllowingStateLoss()方法,那自力更生,手动给实现一个嘛。弄个BaseDialogFragment基类,重写show()方法,把内部的commit()方法替换成commitAllowingStateLoss(),保留方法内其他的操作不变,只是对private变量通过反射获取,就大功告成了。
    open class BaseDialogFragment : DialogFragment() {
        /**
         * 重写show方法,使用commitAllowingStateLoss代替commit
         * to fix: [java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState with DialogFragment]
         */
        override fun show(manager: FragmentManager, tag: String?) {
            try {
                val mDismissed = DialogFragment::class.java.getDeclaredField("mDismissed")
                val mShownByMe = DialogFragment::class.java.getDeclaredField("mShownByMe")
                mDismissed.isAccessible = true
                mShownByMe.isAccessible = true
                mDismissed.set(this, false)
                mShownByMe.set(this, true)
                val ft = manager.beginTransaction()
                ft.add(this, tag)
                ft.commitAllowingStateLoss()
            } catch (e: java.lang.Exception) {
                super.show(manager, tag)
            }
        }
    }

10.解决DialogFragment的窗口大小不受控制

  • 明明添加的根布局是设置了固定宽高、四周margin的,但是显示出来的视图大小始终是刚刚包裹住内容。
  • 这是因为,DialogFragment在添加时,不会指定container,其在onCreateView()阶段,传入的container = null。
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? {
            Log.d("TestDialogFragment", "container = $container")//结果container = null
            return inflater.inflate( R.layout.layout_call_dialog, container, false )
        }

  • 然后在执行LayouterInflater.inflate()时,会因为root为空,跳过了setLayoutParams(),不会设置布局参数。

    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    ViewGroup.LayoutParams params = null;
    if (root != null) {// Create layout params that match root, if supplied
      params = root.generateLayoutParams(attrs);
      if (!attachToRoot) {
           // Set the layout params for temp if we are not
           // attaching. (If we are, we use addView, below)
           temp.setLayoutParams(params);
        }
     }
  • 因此,只能通过手动设置Window的大小,来控制dialog的大小。注意了:window.setLayout()一定要在onActivityCreated()之后执行,才能生效。
     override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            dialog.window?.setLayout(MATCH_PARENT, WRAP_CONTENT)
        }

  • 窗口大小被控制住了,然而还是有问题,明明设置的窗口宽度是MATCH_PAENT,但是窗口宽度并没有占满屏幕,而是在四周有一定的留白。这是因为dialog默认设置的带有padding的windowsBackground造成的。这个样式受内部的mTheme变量控制,在创建Dialog时传入mTheme。
     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
            return new Dialog(getActivity(), getTheme());
     }
    @StyleRes
     public int getTheme() {
        return mTheme;
     }
  • 默认情况下mTheme = 0,外部可以通过setStyle()方法来自定义样式。当设置style为STYELE_NO_FRAME或者STYLE_NO_INPUT时,就会把dialog设置成R.style.Theme_panel样式,这个样式是没有留白的。注意:setStyle(STYLE_NO_TITLE, 0)需要在onGetLayoutInflater()之前执行,比如在onCreate()中执行,因为onGetLayoutInflater()中会执行onCreateDialog()创建,之后dialog已经创建好了,再设置mTheme当然就没有用了。
    override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setStyle(STYLE_NO_TITLE, 0)
        }
    public void setStyle(@DialogStyle int style, @StyleRes int theme) {
            mStyle = style;
            if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
                mTheme = android.R.style.Theme_Panel;
            }
            if (theme != 0) {
                mTheme = theme;
            }
        }

  • 如果想进一步控制dialog在屏幕上的位置,该怎么做呢?设置window的gravity即可。
    override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            dialog?.window?.let {
                //设置dialog大小
                it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
                //设置dialog在底部
                val lp = it.attributes
                lp.gravity = Gravity.BOTTOM
                it.attributes = lp
            }
        }

     

     

 下一篇:android日记(五)

 

posted @ 2020-06-21 23:06  是个写代码的  阅读(192)  评论(0编辑  收藏  举报