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在变更时会相互影响。所谓数组转成集合,只是提供了一个集合的视图,本质还是数组。
- list内部是定长数组,进行add/remove就会抛遇到UnsportedOperationException。
解决方法:用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日记(五)