Blog


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

字符串匹配算法

发表于 2019-10-17 | 分类于 数据结构

字符串的匹配算法有:
1、单模式串匹配算法(BF算法,RK算法,BM算法,KMP算法)
2、多模式串匹配算法有(Trie树,AC自动机)

BF(Brute Force)算法

BF算法就是拿模式串m,从主串n的第0位开始匹配,如果匹配不成功,则后移一位继续匹配。
是比较简单的一种字符串匹配算法,在处理简单的数据时候就可以用这种算法,完全匹配,速度很慢,时间复杂度最坏情况O(M*N)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static int bf(String str,String sub){
int i = 0;
int j = 0;
while(i < str.length()){
while(j < sub.length()){
if(str.charAt(i) == sub.charAt(j)){
i++;
j++;
}else{
i = i-j+1;
j =0;
}
}
break;
}
return i-j;
}

RK(Rabin-Karp)算法

RK算法:数字的匹配比字符串快速,就把主串中的这n-m+1的子串分别求哈希值,然后在分别跟模式串的哈希值进行比较。如果哈希值不一样那肯定不匹配,如果哈希值一样,因为哈希算法存在哈希冲突,这时候在拿模式串跟该子串对比一下就好了。

虽然模式串跟子串的对比速度提高了,但是我们事先需要遍历主串,逐个求子串的哈希值,这部分也挺耗时的,所以需要设计比较高效的哈希算法尽量的减少哈希冲突的产生。

BM(Boyer-Moore)算法

上面两种字符串匹配算法都有缺点,BF算法在极端情况下效率会很低,RK算法需要有一个很好的哈希算法,而设计一个好的哈希算法并不简单,有没有尽可能的高效,极端情况下效率退化也不大的算法呢,下面看看BM算法。

BM算法是一种非常高效的算法,各种记事本的查找功能一般都是采用的这种算法。该算法从模式串的尾部开始匹配,且拥有在最坏情况下 O(N) 的时间复杂度。在实践中,比 KMP 算法的实际效能高。

BM 算法定义了两个规则:

  • 坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 坏字符在模式串中最右出现的位置。此外,如果”坏字符”不包含在模式串之中,则最右出现位置为 -1。
  • 好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为 -1。

通过坏字符算法与好后缀算法分别获取位移值,取两者中的最大值进行位移操作。

案例可参考:http://wiki.jikexueyuan.com/project/kmp-algorithm/bm.html

BM算法完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// a,b 表示主串和模式串;n,m 表示主串和模式串的长度。
public int bm(char[] a, int n, char[] b, int m) {
int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
generateBC(b, m, bc); // 构建坏字符哈希表
int[] suffix = new int[m];
boolean[] prefix = new boolean[m];
generateGS(b, m, suffix, prefix);
int i = 0; // j 表示主串与模式串匹配的第一个字符
while (i <= n - m) {
int j;
for (j = m - 1; j >= 0; --j) { // 模式串从后往前匹配
if (a[i+j] != b[j]) break; // 坏字符对应模式串中的下标是 j
}
if (j < 0) {
return i; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
}
int x = j - bc[(int)a[i+j]];
int y = 0;
if (j < m-1) { // 如果有好后缀的话
y = moveByGS(j, m, suffix, prefix);
}
i = i + Math.max(x, y);
}
return -1;
}

// j 表示坏字符对应的模式串中的字符下标 ; m 表示模式串长度
private int moveByGS(int j, int m, int[] suffix, boolean[] prefix) {
int k = m - 1 - j; // 好后缀长度
if (suffix[k] != -1) return j - suffix[k] +1;
for (int r = j+2; r <= m-1; ++r) {
if (prefix[m-r] == true) {
return r;
}
}
return m;
}

private void generateBC(char[] b, int m, int[] bc) {
for (int i = 0; i < m; ++i) {
bc[i] = -1;
}

for (int i = 0; i < m; ++i) {
int ascii = (int) b[i];
bc[ascii] = i; // 如果ascii相同只需要存储 bc[ascii] = 最后一个
}
}

private void generateGS(char[] b, int m, int[] suffix, boolean[] prefix) {
for (int i = 0; i < m; ++i) { //初始化
suffix[i] = -1;
prefix[i] = false;
}

for (int i = 0; i < m - 1; ++i) { // b[0,i]
int j = i;
int k = 0;
while (j >= 0 && b[j] == b[m - 1 - k]) {
--j;
++k;
suffix[k] = j + 1;//记录模式串每个可以匹配前缀子串的长度 等于 最大下标值
}

if (j == -1) prefix[k] = true;
}
}

KMP(Knuth Morris Pratt)算法

模式串跟主串左端对齐,先比较第一个字符,如果不一样就,模式串后移,直到第一个字符相等

第一个字符匹配上之后在匹配第二个,直到有不相等的为止,比如下面

主串:cdababaeabac

模式串:ababacd

e和c不匹配,e就可以理解为坏字符,ababa可以理解为好前缀,那移动几位呢?

我们拿好前缀本身,在它的后缀子串中查找最长的那个可以跟好前缀的前缀子串匹配的。

移动位数 = 已匹配的字符数 - 对应的部分匹配值的长度

如何求这个对应的匹配值呢?这个不涉及到主串只需要根据模式串就可以求出来。

比如这里的模式串是 ababacd

a的前缀和后缀都是空,共有元素为0
ab的前缀是[a]后缀是[b],共有元素为0
aba的前缀是[a,ab]后缀是[ba,a],共有元素a的长度是1
abab的前缀是[a,ab,aba]后缀是[bab,ab,b],共有元素是[ab]长度为2
ababa的前缀是[a,ab,aba,abab]后缀是[baba,aba,ba,a],最长共有元素是[aba]长度是3
ababac的前缀是[a,ab,aba,abab,ababa]后缀是[babac,abac,bac,ac,c]共有元素为0
ababacd的前缀是[a,ab,aba,abab,ababa,ababac]后缀是[babacd,abacd,bacd,acd,cd,d]共有元素是0

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// a, b 分别是主串和模式串;n, m 分别是主串和模式串的长度。
public static int kmp(char[] a, int n, char[] b, int m) {
int[] next = getNexts(b, m);
int j = 0;
for (int i = 0; i < n; ++i) {
while (j > 0 && a[i] != b[j]) { // 一直找到 a[i] 和 b[j]
j = next[j - 1] + 1;
}
if (a[i] == b[j]) {
++j;
}
if (j == m) { // 找到匹配模式串的了
return i - m + 1;
}
}
return -1;
}
// b 表示模式串,m 表示模式串的长度
private static int[] getNexts(char[] b, int m) {
int[] next = new int[m];
next[0] = -1;
int k = -1;
for (int i = 1; i < m; ++i) {
while (k != -1 && b[k + 1] != b[i]) {
k = next[k];
}
if (b[k + 1] == b[i]) {
++k;
}
next[i] = k;
}
return next;
}

参考资料

https://blog.csdn.net/mingyunxiaohai/article/details/87563292

单例模式

发表于 2019-10-17 | 分类于 设计模式

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效,下面会讨论。)

主要优点:
1、提供了对唯一实例的受控访问。
2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
3、允许可变数目的实例。

主要缺点:
1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

以下列出单例模式的几种写法及单例模式的漏洞解决方案:

饿汉式

类初始化时创建对象,不管需不需要实例对象,都会创建。不存在线程安全问题,因为实例是在类创建和初始化时创建,是由类加载器完成的,类加载器是线程安全的。

缺点,无法延时加载,没有使用就已经加载了

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static final Singleton mInstance = new Singleton();

private Singleton(){

}

public static Singleton getInstance(){
return mInstance;
}

}

懒汉式

优化了恶汉式无法延迟加载的问题。
缺点:存在同步问题,多线程并发的时候会失效,getInstance不同步。比如:一个线程在创建mInstance时,还未创建完成,另一个线程访问mInstance此时还是为空,又创建了一次。

对懒汉式的优化,主要是在线程安全方面,使用synchronized关键字或同步代码块修饰,使得同时只能有一个线程访问。但存在性能缺陷的,因为使用了synchronized关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {

private static Singleton mInstance;

private Singleton(){

}

public static synchronized Singleton getInstance(){
if(mInstance == null){
mInstance = new Singleton();
}
return mInstance;
}

}

DCL双重检查锁

DCL双重检查锁,是对第二种方法性能缺陷的优化。

DCL双重检查锁仅在真正创建mInstance实例的时候加上了synchronized关键字。而且使用volatile关键字修饰,是为了禁止编译器对volatile变量重排序,并且保证volatile变量的读操作发生在写操作之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {

private static volatile Singleton mInstance =null; //volatile关键字是为了禁止编译器对 volatile关键字修饰的变量进行重排序,并保证volatile变量的读操作发生在写操作之后

private Singleton(){

}

public static Singleton getInstance(){
if(mInstance == null){ //第一次检查
synchronized (Singleton.class){ //同步代码块
if(mInstance == null){ //第二次检查
mInstance = new Singleton();
}
}
}
return mInstance;
}

}

静态内部类

利用static final关键字的同步机制,初始化后就无法修改保证了线程安全。使用静态内部类的方式保证了延迟加载,不使用不会被加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton { //完成了懒汉式的延迟加载,同时static保证了线程安全。

private Singleton(){

}

public static Singleton getIntance(){
return SingletonHolder.mIntance;
}

private static class SingletonHolder{ //私有的,初始化的时候,没有调用getIntance方法则不会加载
private static final Singleton mIntance = new Singleton(); //static,final是jvm提供的同步机制,初始化后就无法修改了
}

}

枚举(推荐用法)

1.简洁
2.线程安全
3.可以防止反射注入,反序列化它也不会重新生成新的实例

所有的枚举类型隐性地继承自java.lang.Enum 。枚举实质上还是类!而每个枚举的成员实质就是一个枚举类型的实例,他们默认都是public static final 修饰的。可以直接通过枚举类型名使用它们。

1
2
3
4
5
6
7
8
9
public enum Singleton {

INSTANCE;

public void doSomething(){
System.out.println("do sth.");
}

}

单例模式存在的漏洞

1、 通过反射获取单例对象

我们观察反射获取单例的代码,发现它还是调用了私有的构造方法获取对象【声明为私有的构造方法就是为了不让类外直接new对象】。如果只让私有的构造器只能调用一次就可以避免反射。

2、 反序列化获得单例模式对象

传统的单例模式的另外一个问题是一旦你实现了serializable接口,他们就不再是单例的了,因为readObject()方法总是返回一个 新的实例对象,就像java中的构造器一样。如果定义了readResolve()则直接返回此方法指定的对象,而不需要再创建新的对象!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 序列化必须实现Serializable接口,否则序列化时会报错
*/
public class Singleton implements Serializable{

private static final long serialVersionUID = 1L;

private static Singleton sl;


private Singleton() {
//如果sl不为空即这不是第一次调用该构造器
if(sl != null)
throw new RuntimeException();
}

public static Singleton getInstance() {
if(sl == null) {
sl = new Singleton();
}
return sl;
}

/**
* 反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!
* @return
*/
private Object readResolve() {
return getInstance();
}
}

参考资料

为什么说枚举是最好的Java单例实现方法?

面向对象7大原则

发表于 2019-10-17 | 分类于 设计模式

这 7 种设计原则是软件设计模式必须尽量遵循的原则,各种原则要求的侧重点不同。其中,开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;单一职责原则告诉我们实现类要职责单一;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合度;合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。

开闭原则(Open Close Principle)

开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,对于修改是封闭的。意思就是说当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。

开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下:
1、软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
2、粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
3、遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

实现方法:
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

里式替换原则(Liskov Substitution Principle)

继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

它包含以下4层含义:
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2、子类中可以增加自己特有的方法。
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。

依赖倒置原则(Dependence Inversion Principle)

依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向接口编程,不要面向实现编程。

依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。

由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。

主要作用如下:
1、可以降低类间的耦合性。
2、可以提高系统的稳定性。
3、可以减少并行开发引起的风险。
4、可以提高代码的可读性和可维护性。

实现方法:
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则:
1、每个类尽量提供接口或抽象类,或者两者都具备。
2、变量的声明类型尽量是接口或者是抽象类。
3、任何类都不应该从具体类派生。
4、使用继承时尽量遵循里氏替换原则。

单一职责原则(Single Responsibility Principle)

这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。简单来说,一个类中应该是一组相关性很高的函数、数据的封装。

该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
1、一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
2、当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点:
1、降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
2、提高类的可读性。复杂性降低,自然其可读性会提高。
3、提高系统的可维护性。可读性提高,那自然更容易维护了。
4、变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

实现方法:
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。

注意:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。

接口隔离原则(Interface Segregation Principles)

定义是:客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methods they do not use)。该原则还有另外一个定义:一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。

以上两个定义的含义是:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
1、单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
2、单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点:
1、将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
2、接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
3、如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
4、使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
5、能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

实现方法:
在具体应用接口隔离原则时,应该根据以下几个规则来衡量:
1、接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
2、为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
3、了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
4、提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

迪米特原则(Law of Demeter)

迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。又称为最少知识原则:一个对象应该对其他对象有最少的了解。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点:
1、降低了类之间的耦合度,提高了模块的相对独立性。
2、由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

实现方法:
从迪米特法则的定义和特点可知,它强调以下两点:
1、从依赖者的角度来说,只依赖应该依赖的对象。
2、从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点:
1、在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
2、在类的结构设计上,尽量降低类成员的访问权限。
3、在类的设计上,优先考虑将一个类设置成不变类。
4、在对其他类的引用上,将引用其他对象的次数降到最低。
5、不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
6、谨慎使用序列化(Serializable)功能。

合成复用原则(Composite Reuse Principle)

合成复用原则又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
1.继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
2.子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
3.它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
1.它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
2.新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
3.复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

实现方法:
合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。

HashMap实现原理

发表于 2019-10-16 | 分类于 数据结构

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表。本文会对java集合框架中的对应实现HashMap的实现原理进行讲解,然后会对JDK8的HashMap源码进行分析。

什么是哈希表

  在讨论哈希表之前,我们先大概了解下其他数据结构在新增,查找等基础操作执行性能

   数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
  线性链表:对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
  二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
  哈希表:相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1),接下来我们就来看看哈希表是如何实现达到惊艳的常数阶O(1)的。

  我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。

  比如我们要新增或查找某个元素,我们通过把当前元素的关键字 通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。

        存储位置 = f(关键字)

  其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:

  查找操作同理,先通过哈希函数计算出实际存储地址,然后从数组中对应地址取出即可。

哈希冲突

然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表+红黑树的方式。

HashMap底层存储结构

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做一个Entry。这些Entry分散存储在一个数组当中,这个数组就是HashMap的主干。

1
2
3
4
5
6
7
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value);}
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}

因为table数组的长度是有限的,再好的hash函数也会出现index冲突的情况,所以我们用链表来解决这个问题,table数组的每一个元素不只是一个Entry对象,也是一个链表的头节点,每一个Entry对象通过Next指针指向下一个Entry节点。当新来的Entry映射到冲突数组位置时,只需要插入对应的链表即可。

需要注意的是:新来的Entry节点插入链表时,会插在链表的头部,因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。

HashMap中的table数组如下所示:

所以,HashMap是数组+链表+红黑树(在Java 8中为了优化Entry的查找性能,新加了红黑树部分)实现的。

Put方法原理

调用hashMap.put(“str”, 1),将会在HashMap的table数组中插入一个Key为“str”的元素,这时候需要我们用一个hash()函数来确定Entry的插入位置,而每种数据类型有自己的hashCode()函数,比如String类型的hashCode()函数如下所示:

1
2
3
4
5
6
7
public static int hashCode(byte[] value) {
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff);
}
return h;
}

所以,put()函数的执行路径是这样的:

  1. 首先put(“str”, 1)会调用HashMap的hash(“str”)方法。
  2. 在hash()内部,会调用String(Latin1)内部的hashcode()获取字符串”str”的hashcode。
  3. “str”的hashcode被返回给put(),put()通过一定计算得到最终的插入位置index。
  4. 最后将这个Entry插入到table的index位置。

这里就出现了两个问题,问题1: 在put()里怎样得到插入位置index?问题2: 为什么会调用HashMap的hash()函数,直接调用String的hashcode()不好吗?

问题1: 在put()里怎样得到插入位置index?

对于不同的hash码我们希望它被插入到不同的位置,所以我们首先会想到对数组长度的取模运算,但是由于取模运算的效率很低,所以HashMap的发明者用位运算替代了取模运算。

在put()里是通过如下的语句得到插入位置的(key的哈希值与map长度-1相与):

1
index = hash(key) & (Length - 1)

其中Length是table数组的长度。为了实现和取模运算相同的功能,这里要求(Length - 1)这部分的二进制表示全为1,我们用HashMap的默认初始长度16举例说明:

1
2
3
4
5
假设"str"的hash吗为: 1001 0110 1011 1110 1101 0010 1001 0101

Length - 1 = 15 : 1111

hash("str") & (Length - 1) = 0101

如果(Length - 1)这部分不全为1,假如Length是10,那么Length - 1 = 9 :1001 那么无论再和任何hash码做与操作,中间两位数都会是0,这样就会出现大量不同的hash码被映射到相同位置的情况。

所以,在HashMap中table数组的默认长度是16,并且要求每次自动扩容或者手动扩容时,长度都必须是2的幂。

问题2: 为什么会调用HashMap的hash()函数,直接调用String的hashcode()不好吗?

HashMap中的hash()函数如下所示:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap中的hash()函数是将得到hashcode做进一步处理,它将hashcode的高16位和低16位进行异或操作,这样做的目的是:在table的长度比较小的情况下,也能保证hashcode的高位参与到地址映射的计算当中,同时不会有太大的开销。

综上所述:从hashcode计算得到table索引的计算过程如下所示:

put()方法的执行过程如下所示:

HashMap的扩容机制

在HashMap中有一下两个属性和扩容相关:

1
2
int threshold;
final float loadFactor;

其中threshold = Length * loadFactor,Length表示table数组的长度(默认值是16),loadFactor为负载因子(默认值是0.75),阀值threshold表示当table数组中存储的元素超过这个阀值的时候,就需要扩容了。以默认长度16,和默认负载因子0.75为例,threshold = 16 * 0.75 = 12,即当table数组中存储的元素个数超过12个的时候,table数组就该扩容了。

当然Java中的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,然后将旧数组中的元素经过重新计算放到新数组中,那么怎样对旧元素进行重新映射呢?

其实很简单,由于我们在扩容时,是使用2的幂扩展,即数组的长度扩大到原来的2倍, 4倍, 8倍…,因此在resize时(Length - 1)这部分相当于在高位新增一个或多个1bit,我们以扩大到原来的两倍为例说明:

(a)中n为16,(b)中n扩大到两倍为32,相当于(n - 1)这部分的高位多了一个1, 然后和原hash码作与操作,这样元素在数组中映射的位置要么不变,要不向后移动16个位置:

因此,我们在扩充HashMap的时候,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中resize的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

JDK 1.8 以后哈希表的 添加、删除、查找、扩容方法都增加了一种 节点为 TreeNode 的情况:

  • 添加时,当桶中链表个数超过 8 时会转换成红黑树;
  • 删除、扩容时,如果桶中结构为红黑树,并且树中元素个数太少的话,会进行修剪或者直接还原成链表结构;
  • 查找时即使哈希函数不优,大量元素集中在一个桶中,由于有红黑树结构,性能也不会差。

HashMap死锁形成原理

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用线程安全的ConcurrentHashMap。

要理解HashMap死锁形成的原理,我们要对HashMap的resize里的transfer过程有所了解,transfer过程是将旧数组中的元素复制到新数组中,在Java 8之前,复制过程会导致链表倒置,这也是形成死锁的重要原因(Java 8中已经不会倒置),transfer的基本过程如下:

1
2
3
4
1. 新建节点e指向当前节点,新建节点next指向e.next
2. 将e.next指向新数组中指定位置newTable[i]
3. newTable[i] = e
4. e = next

举个例子:

1
2
3
4
5
6
7
现在有链表1->2->3,要将它复制到新数组的newTable[i]位置
1. Node e = 1, next = e.next;
2. e.next = newTable[i];
3. newTable[i] = e;
4. e = next, next = e.next;
执行完后会得到这样的结果:
newTable[i]=3->2->1

死锁会在这种情况产生:两个线程同时往HashMap里放Entry,同时HashMap正好需要扩容,如果一个线程已经完成了transfer过程,而另一个线程不知道,并且又要进行transfer的时候,死锁就会形成。

1
2
3
4
5
现在Thread1已将完成了transfer,newTable[i]=3->2->1
在Thread2中:
Node e = 1, next = e.next;
e.next = newTable[i] : 1 -> newTable[i]=3
newTable[i] = e : newTable[i] = 1->3->2->1 //这时候链表换已经形成了

在形成链表换以后再对HashMap进行Get操作时,就会形成死循环。

在Java 8中对这里进行了优化,链表复制到新数组时并不会倒置,不会因为多个线程put导致死循环,但是还有很多弊端,比如数据丢失等,因此多线程情况下还是建议使用ConcurrentHashMap。

HashMap和Hashtable有什么区别

Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:

Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

总结

  1. 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
  2. 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
  3. HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
  4. JDK1.8引入红黑树大程度优化了HashMap的性能。

参考资料

HashMap底层实现原理
HashMap实现原理及源码分析
ConcurrentHashMap实现原理及源码分析

Android WebView 性能优化

发表于 2019-09-26 | 分类于 Android知识点

离线缓存

这个比较容易,开启webView的缓存功能就可以了。

1
2
3
4
5
6
WebSettings settings = webView.getSettings();
settings.setAppCacheEnabled(true);
settings.setDatabaseEnabled(true);
settings.setDomStorageEnabled(true);//开启DOM缓存,关闭的话H5自身的一些操作是无效的
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
settings.setJavaScriptEnabled(true);

这边我们通过setCacheMode方法来设置WebView的缓存策略,WebSettings.LOAD_DEFAULT是默认的缓存策略,它在缓存可获取并且没有过期的情况下加载缓存,否则通过网络获取资源。这样的话可以减少页面的网络请求次数,那我们如何在离线的情况下也能打开页面呢,这里我们在加载页面的时候可以通过判断网络状态,在无网络的情况下更改webview的缓存策略。

1
2
3
4
5
6
7
8
9
ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if(info.isAvailable())
{
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
}else
{
settings.setCacheMode(WebSettings.LOAD_CACHE_ONLY);//不使用网络,只加载缓存
}

这样我们就可以使我们的混合应用在没有网络的情况下也能使用一部分的功能,不至于什么都显示不了了,当然如果我们将缓存做的更好一些,在网络好的时候,比如说在WIFI状态下,去后台加载一些网页缓存起来,这样处理的话,即使在无网络情况下第一次打开某些页面的时候,也能将该页面显示出来。
当然缓存资源后随之会带来一个问题,那就是资源无法及时更新,WebSettings.LOAD_DEFAULT中的页面中的缓存版本好像不是很起作用,所以我们这边可能需要自己做一个缓存版本控制。这个缓存版本控制可以放在APP版本更新中。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (upgrade.cacheControl > cacheControl)
{
webView.clearCache(true);//删除DOM缓存
VersionUtils.clearCache(mContext.getCacheDir());//删除APP缓存
try
{
mContext.deleteDatabase("webview.db");//删除数据库缓存
mContext.deleteDatabase("webviewCache.db");
}
catch (Exception e)
{
}
}

内存泄露

1.可以将 Webview 的 Activity 新起一个进程,结束的时候直接System.exit(0);退出当前进程;
启动新进程,主要代码: AndroidManifest.xml 配置文件代码如下

1
2
3
4
5
6
7
8
<activity
android:name=".ui.activity.Html5Activity"
android:process=":lyl.boon.process.web">
<intent-filter>
<action android:name="com.lyl.boon.ui.activity.htmlactivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

在新进程中启动 Activity ,里面传了 一个 Url:

1
2
3
4
5
Intent intent = new Intent("com.lyl.boon.ui.activity.htmlactivity");
Bundle bundle = new Bundle();
bundle.putString("url", gankDataEntity.getUrl());
intent.putExtra("bundle",bundle);
startActivity(intent);

然后在 Html5Activity 的onDestory() 最后加上 System.exit(0); 杀死当前进程。

Android7.0系统以后,WebView相对来说是比较稳定的,无论承载WebView的容器是否在主进程,都不需要担心WebView崩溃导致应用也跟着崩溃。然后7.0以下的系统就没有这么幸运了,特别是低版本的WebView。考虑应用的稳定性,我们可以把7.0以下系统的WebView使用一个独立进程的Activity来包装,这样即使WebView崩溃了,也只是WebView所在的进程发生了崩溃,主进程还是不受影响的。

2.不在xml中定义 Webview ,而是在需要的时候在Activity中创建,并且Context使用 getApplicationgContext()

1
2
3
4
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
mLayout.addView(mWebView);

3.在 Activity 销毁( WebView )的时候,先让 WebView 加载null内容,然后移除 WebView,再销毁 WebView,最后置空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onDestroy() {
if (mWebView != null) {
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearFormData();
mWebView.clearHistory();
mWebView.stopLoading();

((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}

预加载

有时候一个页面资源比较多,图片,CSS,js比较多,还引用了JQuery这种庞然巨兽,从加载到页面渲染完成需要比较长的时间,有一个解决方案是将这些资源打包进APK里面,然后当页面加载这些资源的时候让它从本地获取,这样可以提升加载速度也能减少服务器压力。重写WebClient类中的shouldInterceptRequest方法,再将这个类设置给WebView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
webView.setWebViewClient(new WebViewClient()
{
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url)
{
if (url.contains("[tag]"))
{
String localPath = url.replaceFirst("^http.*[tag]\\]", "");
try
{
InputStream is = getApplicationContext().getAssets().open(localPath);
Log.d(TAG, "shouldInterceptRequest: localPath " + localPath);
String mimeType = "text/javascript";
if (localPath.endsWith("css"))
{
mimeType = "text/css";
}
return new WebResourceResponse(mimeType, "UTF-8", is);
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
else
{
return null;
}

}
});

这里我们队页面中带有特殊标记的请求进行过滤替换,也就是上面代码中的[tag],这个可以跟做后台开发的同事约定好来就行了。对图片资源或者其他资源进行替换也是可以的。补充一个小点可以通过settings.setLoadsImagesAutomatically(true);来设置在页面装载完成之后再去加载图片。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
webView.getSettings().setBlockNetworkImage(true);  
webView.setWebChromeClient(new WebChromeClient() {  
            @Override  
            public void onProgressChanged(WebView view, int newProgress) {  
                if (newProgress == 100) {  
                    // 网页加载完成  
                    loadDialog.dismiss();  
                    webView.getSettings().setBlockNetworkImage(false);  
                } else {  
                    // 网页加载中  
                    loadDialog.show();  
                }  
            }  
        });

白屏

SSL问题

通常情况下,通过WebView的loadUrl(String url)方法,可以顺利加载页面。但是,当load通过SSL加密的HTTPS页面时,如果这个页面的安全证书无法得到认证,WebView就会显示成空白页。

解决方式:
通过重写WebViewClient的onReceivedSslError方法来接受所有网站的证书,忽略SSL错误。

1
2
3
4
5
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
super.onReceivedSslError(view, handler, error);
}

低版本兼容问题

由于现在h5大部分都是 vue的形式打包,(可能过个一两年就变了,但是万变不离其中),这个时候要注意了, 由于是webv加载的h5,在Android老的机型上 webview 内核可能不支持 最新的h5 框架,这时候 就需要我们找h5 的同学 搞事情了, 对于 vue,想要老机器不出现白屏其实也很简单,让h5的同学 做一下老版本的兼容,具体方法:

虽然vue-cli引入了babel对js语法进行降级,但是还是有些老旧的机型会发生各种各样的问题,这里需要引入一个叫babel-polyfill的包。所以你只需只在你引入import vue之前 import babel-polyfill进来就可以了,主要是为了让es6对个别机型做到兼容。

301/302重定向问题

WebView的301/302重定向问题,绝对在踩坑排行榜里名列前茅。。。随便搜了几个解决方案,要么不能满足业务需求,要么清一色没有彻底解决问题。

https://stackoverflow.com/questions/4066438/android-webview-how-to-handle-redirects-in-app-instead-of-opening-a-browser
http://blog.csdn.net/jdsjlzx/article/details/51698250
http://www.cnblogs.com/pedro-neer/p/5318354.html
http://www.jianshu.com/p/c01769ababfa

301/302业务场景及白屏问题

先来分析一下业务场景。对于需要对url进行拦截以及在url中需要拼接特定参数的WebView来说,301和302发生的情景主要有以下几种:

  • 首次进入,有重定向,然后直接加载H5页面,如http跳转https
  • 首次进入,有重定向,然后跳转到native页面,如扫一扫短链,然后跳转到native
  • 二次加载,有重定向,跳转到native页面
  • 对于考拉业务来说,还有类似登录后跳转到某个页面的需求。如我的拼团,未登录状态下点击我的拼团跳转到登录页面,登录完成后再加载我的拼团页。

第一种情况属于正常情况,暂时没遇到什么坑。

第二种情况,会遇到WebView空白页问题,属于原始url不能拦截到native页面,但301/302后的url拦截到native页面的情况,当遇到这种情况时,需要把WebView对应的Activity结束,否则当用户从拦截后的页面返回上一个页面时,是一个WebView空白页。

第三种情况,也会遇到WebView空白页问题,原因在于加载的第一个页面发生了重定向到了第二个页面,第二个页面被客户端拦截跳转到native页面,那么WebView就停留在第一个页面的状态了,第一个页面显然是空白页。

第四种情况,会遇到无限加载登录页面的问题。考拉的登录链接是类似下面这种格式:

1
https://m.kaola.com/login.html?target=登录后跳转的url

如果登录成功后还重新加载这个url,那么就会循环跳转到登录页面。第四点解决起来比较简单,登录成功以后拿到target后的跳转url再重新加载即可。

301/302回退栈问题

无论是哪种重定向场景,都不可避免地会遇到回退栈的处理问题,如果处理不当,用户按返回键的时候不一定能回到重定向之前的那个页面。很多开发者在覆写WebViewClient.shouldOverrideUrlLoading()方法时,会简单地使用以下方式粗暴处理:

1
2
3
4
5
6
7
8
WebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
...
)

这种方法最致命的弱点就是如果不经过特殊处理,那么按返回键是没有效果的,还会停留在302之前的页面。现有的解决方案无非就几种:

  1. 手动管理回退栈,遇到重定向时回退两次^6。
  2. 通过HitTestResult判断是否是重定向,从而决定是否自己加载url^7。
  3. 通过设置标记位,在onPageStarted和onPageFinished分别标记变量避免重定向^9。

可以说,这几种解决方案都不是完美的,都有缺陷。以下给出301/302较优解决方案:

解决301/302回退栈问题

能否结合上面的几种方案,来更加准确地判断301/302的情况呢?下面说一下本文的解决思路。在提供解决方案之前,我们需要了解一下shouldOverrideUrlLoading方法的返回值代表什么意思。

Give the host application a chance to take over the control when a new url is about to be loaded in the current WebView. If WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the url. If WebViewClient is provided, return true means the host application handles the url, while return false means the current WebView handles the url.

简单地说,就是返回true,那么url就已经由客户端处理了,WebView就不管了,如果返回false,那么当前的WebView实现就会去处理这个url。

WebView能否知道某个url是不是301/302呢?当然知道,WebView能够拿到url的请求信息和响应信息,根据header里的code很轻松就可以实现,事实正是如此,交给WebView来处理重定向(return false),这时候按返回键,是可以正常地回到重定向之前的那个页面的。(PS:WebView在5.0以后是一个独立的apk,可以单独升级,新版本的WebView实现肯定处理了重定向问题)

但是,业务对url拦截有需求,肯定不能把所有的情况都交给系统WebView处理。为了解决url拦截问题,本文引入了另一种思想——通过用户的touch事件来判断重定向。下面通过代码来说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* WebView基础类,处理一些基础的公有操作
*
* @author xingli
* @time 2017-12-06
*/
public class BaseWebView extends WebView {
private boolean mTouchByUser;
public BaseWebView(Context context) {
super(context);
}
public BaseWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public BaseWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public final void loadUrl(String url, Map<String, String> additionalHttpHeaders) {
super.loadUrl(url, additionalHttpHeaders);
resetAllStateInternal(url);
}
@Override
public void loadUrl(String url) {
super.loadUrl(url);
resetAllStateInternal(url);
}
@Override
public final void postUrl(String url, byte[] postData) {
super.postUrl(url, postData);
resetAllStateInternal(url);
}
@Override
public final void loadData(String data, String mimeType, String encoding) {
super.loadData(data, mimeType, encoding);
resetAllStateInternal(getUrl());
}
@Override
public final void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding,
String historyUrl) {
super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
resetAllStateInternal(getUrl());
}
@Override
public void reload() {
super.reload();
resetAllStateInternal(getUrl());
}
public boolean isTouchByUser() {
return mTouchByUser;
}
private void resetAllStateInternal(String url) {
if (!TextUtils.isEmpty(url) && url.startsWith("javascript:")) {
return;
}
resetAllState();
}
// 加载url时重置touch状态
protected void resetAllState() {
mTouchByUser = false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//用户按下到下一个链接加载之前,置为true
mTouchByUser = true;
break;
}
return super.onTouchEvent(event);
}
@Override
public void setWebViewClient(final WebViewClient client) {
super.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, url);
if (handleByChild) {
// 开放client接口给上层业务调用,如果返回true,表示业务已处理。
return true;
} else if (!isTouchByUser()) {
// 如果业务没有处理,并且在加载过程中用户没有再次触摸屏幕,认为是301/302事件,直接交由系统处理。
return super.shouldOverrideUrlLoading(view, url);
} else {
//否则,属于二次加载某个链接的情况,为了解决拼接参数丢失问题,重新调用loadUrl方法添加固有参数。
loadUrl(url);
return true;
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
boolean handleByChild = null != client && client.shouldOverrideUrlLoading(view, request);
if (handleByChild) {
return true;
} else if (!isTouchByUser()) {
return super.shouldOverrideUrlLoading(view, request);
} else {
loadUrl(request.getUrl().toString());
return true;
}
}
});
}
}

上述代码解决了正常情况下的回退栈问题。

解决业务白屏问题

为了解决白屏问题,考拉目前的解决思路和上面的回退栈问题思路有些类似,通过监听touch事件分发以及onPageFinished事件来判断是否产生白屏,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class KaolaWebview extends BaseWebView implements DownloadListener, Lifeful, OnActivityResultListener {
private boolean mIsBlankPageRedirect; //是否因重定向导致的空白页面。
public KaolaWebview(Context context) {
super(context);
init();
}
public KaolaWebview(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public KaolaWebview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
protected void back() {
if (mBackStep < 1) {
mJsApi.trigger2("kaolaGoback");
} else {
realBack();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
mIsBlankPageRedirect = true;
}
return super.dispatchTouchEvent(ev);
}
private WebViewClient mWebViewClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
url = WebViewUtils.removeBlank(url);
//允许启动第三方应用客户端
if (WebViewUtils.canHandleUrl(url)) {
boolean handleByCaller = false;
// 如果不是用户触发的操作,就没有必要交给上层处理了,直接走url拦截规则。
if (null != mIWebViewClient && isTouchByUser()) {
handleByCaller = mIWebViewClient.shouldOverrideUrlLoading(view, url);
}
if (!handleByCaller) {
handleByCaller = handleOverrideUrl(url);
}
return handleByCaller || super.shouldOverrideUrlLoading(view, url);
} else {
try {
notifyBeforeLoadUrl(url);
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
mContext.startActivity(intent);
if (!mIsBlankPageRedirect) {
// 如果遇到白屏问题,手动后退
back();
}
} catch (Exception e) {
ExceptionUtils.printExceptionTrace(e);
}
return true;
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return shouldOverrideUrlLoading(view, request.getUrl().toString());
}

private boolean handleOverrideUrl(final String url) {
RouterResult result = WebActivityRouter.startFromWeb(
new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
@Override
public void onActivityFound() {
if (!mIsBlankPageRedirect) {
// 路由已经拦截到跳转到native页面,但此时可能发生了
// 301/302跳转,那么执行后退动作,防止白屏。
back();
}
}
@Override
public void onActivityNotFound() {
if (mIWebViewClient != null) {
mIWebViewClient.onActivityNotFound();
}
}
}));
return result.isSuccess();
}
};
@Override
public void onPageFinished(WebView view, String url) {
mIsBlankPageRedirect = true;
if (null != mIWebViewClient) {
mIWebViewClient.onPageReallyFinish(view, url);
}
super.onPageFinished(view, url);
}
}

本来上面的两个问题可以用同一个变量控制解决的,但由于历史代码遗留问题,目前还没有时间优化测试,这也是代码暂不公布的原因之一。

loadUrl(url,map)方法加载带hash(带#号)的url导致刷新问题或请求头缓存问题

1.如果调用loadUrl(url,map)方法去加载资源,那么在此调用loadUrl(ur),reload,loadUrl(url,map)造成无法刷新的问题。这个现象主要出现在Android 8.0的系统中。
可尝试调用如下url尝试:

https://baike.baidu.com/item/%E9%83%8E%E5%B9%B3/58857#/
https://baike.baidu.com/item/%E9%83%8E%E5%B9%B3/58857#/?a=123
https://baike.baidu.com/item/%E9%83%8E%E5%B9%B3/58857#3

2.loadUrl(url,map) 第二个参数map中传入的数据用于请求头,此外这个请求头数据会被webview缓存下来,刷新时,请求头中的数据还是原来的,因此,不适用传入需要进程变化的“状态”信息。

解决方法:不要使用loadUrl(url,map),推荐使用loadUrl(url),如果非要传输参数,还不如在url中添加参数。

是否应该开启硬件加速

由于碎片化问题太多,建议保持默认状态【默认表示由系统决定,不要手动设置】,否则可能产生问题。

Cookie同步导致的内存泄漏

使用CookieSyncManager同步时,会永久引用第一个acitivity的的Context,为了避免此种情况,请使用ApplicationContext

1
2
3
 if (Build.VERSION.SDK_INT < 21) {
android.webkit.CookieSyncManager.createInstance(context.getApplicationContext());
}

Android5.0 WebView中Http和Https混合问题

在Android 5.0上 Webview 默认不允许加载 Http 与 Https 混合内容:

解决办法:

1
2
3
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

参数类型说明:
MIXED_CONTENT_ALWAYS_ALLOW:允许从任何来源加载内容,即使起源是不安全的;
MIXED_CONTENT_NEVER_ALLOW:不允许Https加载Http的内容,即不允许从安全的起源去加载一个不安全的资源;
MIXED_CONTENT_COMPATIBILITY_MODE:当涉及到混合式内容时,WebView 会尝试去兼容最新Web浏览器的风格。

在5.0以下 Android 默认是 全允许,但是到了5.0以上,就是不允许,实际情况下很我们很难确定所有的网页都是https的,所以就需要这一步的操作。

onPageFinished被调用多次

使用onPageProgressChanged代替

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void  handleProgress(WebView view, int newProgress){
if(progressPending.get()!=newProgress){
progressPending.set(newProgress);
onProgressChanged(newProgress);

}
}
@Override
public final void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
handleProgress(view,newProgress);
}

public void onProgressChanged(int newProgress){
Log.i("WebChromeClient","progress="+newProgress+"%");
if(newProgress==100){
Log.i("WebChromeClient","加载完成");
}
}

H5优化

Android的OnPageFinished事件会在Javascript脚本执行完成之后才会触发。如果在页面中使 用JQuery,会在处理完DOM对象,执行完$(document).ready(function() {});事件自会后才会渲染并显示页面。而同样的页面在iPhone上却是载入相当的快,因为iPhone是显示完页面才会触发脚本的执行。所以我们这边的解决方案延迟JS脚本的载入,这个方面的问题是需要Web前端工程师帮忙优化的,网上应该有比较多LazyLoad插件,这里放一个比较老的链接Painless JavaScript lazy loading with LazyLoad,同样也放上一小段前端代码,仅供参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script src="/css/j/lazyload-min.js" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
loadComplete() {
//instead of document.read();
}
function loadscript() {
LazyLoad.loadOnce([
'/css/j/jquery-1.6.2.min.js',
'/css/j/flow/jquery.flow.1.1.min.js',
'/css/j/min.js?v=2011100852'
], loadComplete);
}
setTimeout(loadscript,10);
</script>

参考资料

https://www.jianshu.com/p/427600ca2107
https://my.oschina.net/ososchina/blog/1799575
https://iluhcm.com/2017/12/10/design-an-elegant-and-powerful-android-webview-part-one/

Android ListView原理完全解析

发表于 2019-09-05 | 分类于 Android知识点

ListView的结构,如下图所示:

Adapter作用

Adapter是适配器的意思,它在ListView和数据源之间起到了一个桥梁的作用,ListView并不会直接和数据源打交道,而是会借助Adapter这个桥梁来去访问真正的数据源,与之前不同的是,Adapter的接口都是统一的,因此ListView不用再去担心任何适配方面的问题。而Adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说ArrayAdapter可以用于数组和List类型的数据源适配,SimpleCursorAdapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示:

当然Adapter的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我们在Adapter当中去重写,就是getView()方法

RecycleBin机制

在开始分析ListView的源码之前,还有一个东西是我们提前需要了解的,就是RecycleBin机制,这个机制也是ListView能够实现成百上千条数据都不会OOM最重要的一个原因。其实RecycleBin的代码并不多,它是写在AbsListView中的一个内部类,所以所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。那我们来看一下RecycleBin中的主要代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin
* has two levels of storage: ActiveViews and ScrapViews. ActiveViews are
* those views which were onscreen at the start of a layout. By
* construction, they are displaying current information. At the end of
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews
* are old views that could potentially be used by the adapter to avoid
* allocating views unnecessarily.
*
* @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
* @see android.widget.AbsListView.RecyclerListener
*/
class RecycleBin {
private RecyclerListener mRecyclerListener;

/**
* The position of the first view stored in mActiveViews.
*/
private int mFirstActivePosition;

/**
* Views that were on screen at the start of layout. This array is
* populated at the start of layout, and at the end of layout all view
* in mActiveViews are moved to mScrapViews. Views in mActiveViews
* represent a contiguous range of Views, with position of the first
* view store in mFirstActivePosition.
*/
private View[] mActiveViews = new View[0];

/**
* Unsorted views that can be used by the adapter as a convert view.
*/
private ArrayList<View>[] mScrapViews;

private int mViewTypeCount;

private ArrayList<View> mCurrentScrap;

/**
* Fill ActiveViews with all of the children of the AbsListView.
*
* @param childCount
* The minimum number of views mActiveViews should hold
* @param firstActivePosition
* The position of the first view that will be stored in
* mActiveViews
*/
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in
// active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
}
}
}

/**
* Get the view corresponding to the specified position. The view will
* be removed from mActiveViews if it is found.
*
* @param position
* The position to look up in mActiveViews
* @return The view if it is found, null otherwise
*/
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >= 0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}

/**
* Put a view into the ScapViews list. These views are unordered.
*
* @param scrap
* The view to add
*/
void addScrapView(View scrap) {
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
// Don't put header or footer views or views that should be ignored
// into the scrap heap
int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(scrap, false);
}
return;
}
if (mViewTypeCount == 1) {
dispatchFinishTemporaryDetach(scrap);
mCurrentScrap.add(scrap);
} else {
dispatchFinishTemporaryDetach(scrap);
mScrapViews[viewType].add(scrap);
}

if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}

/**
* @return A view from the ScrapViews collection. These are unordered.
*/
View getScrapView(int position) {
ArrayList<View> scrapViews;
if (mViewTypeCount == 1) {
scrapViews = mCurrentScrap;
int size = scrapViews.size();
if (size > 0) {
return scrapViews.remove(size - 1);
} else {
return null;
}
} else {
int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
scrapViews = mScrapViews[whichScrap];
int size = scrapViews.size();
if (size > 0) {
return scrapViews.remove(size - 1);
}
}
}
return null;
}

public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
// noinspection unchecked
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}

}

这里的RecycleBin代码并不全,只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析ListView的工作原理将会有很大的帮助:

  • fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。

  • getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。

  • addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃View。

  • getScrapView() 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。

  • setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。

第一次Layout

不管怎么说,ListView即使再特殊最终还是继承自View的,因此它的执行流程还将会按照View的规则来执行。

View的执行流程无非就分为三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。而在ListView当中,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了,因此我们本篇文章也是主要分析的这个方法里的内容。

如果你到ListView源码中去找一找,你会发现ListView中是没有onLayout()这个方法的,这是因为这个方法是在ListView的父类AbsListView中实现的,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Subclasses should NOT override this method but {@link #layoutChildren()}
* instead.
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
if (changed) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren();
mInLayout = false;
}

可以看到,onLayout()方法中主要就是一个判断,如果ListView的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutChildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的。那么进入ListView的layoutChildren()方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (!blockLayoutRequests) {
mBlockLayoutRequests = true;
} else {
return;
}
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
int childrenTop = mListPadding.top;
int childrenBottom = getBottom() - getTop() - mListPadding.bottom;
int childCount = getChildCount();
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
View focusLayoutRestoreView = null;
// Remember stuff we will need down below
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
index = mNextSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
newSel = getChildAt(index);
}
break;
case LAYOUT_FORCE_TOP:
case LAYOUT_FORCE_BOTTOM:
case LAYOUT_SPECIFIC:
case LAYOUT_SYNC:
break;
case LAYOUT_MOVE_SELECTION:
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition;
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index);
}
// Remember the previous first child
oldFirst = getChildAt(0);
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition;
}
// Caution: newSel might be null
newSel = getChildAt(index + delta);
}
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only "
+ "from the UI thread. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
setSelectedPositionInt(mNextSelectedPosition);
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
// reset the focus restoration
View focusLayoutRestoreDirectChild = null;
// Don't put header or footer views into the Recycler. Those are
// already cached in mHeaderViews;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i));
if (ViewDebug.TRACE_RECYCLER) {
ViewDebug.trace(getChildAt(i),
ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
}
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
// take focus back to us temporarily to avoid the eventual
// call to clear focus when removing the focused child below
// from messing things up when ViewRoot assigns focus back
// to someone else
final View focusedChild = getFocusedChild();
if (focusedChild != null) {
// TODO: in some cases focusedChild.getParent() == null
// we can remember the focused view to restore after relayout if the
// data hasn't changed, or if the focused position is a header or footer
if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
focusLayoutRestoreDirectChild = focusedChild;
// remember the specific view that had focus
focusLayoutRestoreView = findFocus();
if (focusLayoutRestoreView != null) {
// tell it we are going to mess with it
focusLayoutRestoreView.onStartTemporaryDetach();
}
}
requestFocus();
}
// Clear out old views
detachAllViewsFromParent();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
if (newSel != null) {
sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
} else {
sel = fillFromMiddle(childrenTop, childrenBottom);
}
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown();
break;
case LAYOUT_SPECIFIC:
sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else {
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews();
if (sel != null) {
// the current selected item should get focus if items
// are focusable
if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
if (!focusWasTaken) {
// selected item didn't take focus, fine, but still want
// to make sure something else outside of the selected view
// has focus
final View focused = getFocusedChild();
if (focused != null) {
focused.clearFocus();
}
positionSelector(sel);
} else {
sel.setSelected(false);
mSelectorRect.setEmpty();
}
} else {
positionSelector(sel);
}
mSelectedTop = sel.getTop();
} else {
if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
View child = getChildAt(mMotionPosition - mFirstPosition);
if (child != null) positionSelector(child);
} else {
mSelectedTop = 0;
mSelectorRect.setEmpty();
}
// even if there is not selected position, we may need to restore
// focus (i.e. something focusable in touch mode)
if (hasFocus() && focusLayoutRestoreView != null) {
focusLayoutRestoreView.requestFocus();
}
}
// tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focusLayoutRestoreView != null
&& focusLayoutRestoreView.getWindowToken() != null) {
focusLayoutRestoreView.onFinishTemporaryDetach();
}
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
updateScrollIndicators();
if (mItemCount > 0) {
checkSelectionChanged();
}
invokeOnItemScrollListener();
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}

这段代码比较长,我们挑重点的看。首先可以确定的是,ListView当中目前还没有任何子View,数据都还是由Adapter管理的,并没有展示到界面上,因此第19行getChildCount()方法得到的值肯定是0。接着在第81行会根据dataChanged这个布尔型的值来判断执行逻辑,dataChanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入到第90行的执行逻辑,调用RecycleBin的fillActiveViews()方法。按理来说,调用fillActiveViews()方法是为了将ListView的子View进行缓存的,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用。

接下来在第114行会根据mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,因此会进入到第140行的default语句当中。而下面又会紧接着进行两次if判断,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到第145行的fillFromTop()方法,我们跟进去瞧一瞧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Fills the list from top to bottom, starting with mFirstPosition
*
* @param nextTop The location where the top of the first item should be
* drawn
*
* @return The view that is currently selected
*/
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
return fillDown(mFirstPosition, nextTop);
}

从这个方法的注释中可以看出,它所负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView。而这个方法本身并没有什么逻辑,就是判断了一下mFirstPosition值的合法性,然后调用fillDown()方法,那么我们就有理由可以猜测,填充ListView的操作是在fillDown()方法中完成的。进入fillDown()方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Fills the list from pos down to the end of the list view.
*
* @param pos The first position to put in the list
*
* @param nextTop The location where the top of the item associated with pos
* should be drawn
*
* @return The view that is currently selected, if it happens to be in the
* range that we draw.
*/
private View fillDown(int pos, int nextTop) {
View selectedView = null;
int end = (getBottom() - getTop()) - mListPadding.bottom;
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
return selectedView;
}

可以看到,这里使用了一个while循环来执行重复逻辑,一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。

那么while循环当中又做了什么事情呢?值得让人留意的就是第18行调用的makeAndAddView()方法,进入到这个方法当中,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

这里在第19行尝试从RecycleBin当中快速获取一个active view,不过很遗憾的是目前RecycleBin当中还没有缓存任何的View,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行,到第28行会调用obtainView()方法来再次尝试获取一个View,这次的obtainView()方法是可以保证一定返回一个View的,于是下面立刻将获取到的View传入到了setupChild()方法当中。那么obtainView()内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}

obtainView()方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个ListView中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在第19行代码中调用了RecycleBin的getScrapView()方法来尝试获取一个废弃缓存中的View,同样的道理,这里肯定是获取不到的,getScrapView()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用mAdapter的getView()方法来去获取一个View。那么mAdapter是什么呢?当然就是当前ListView关联的适配器了。而getView()方法又是什么呢?还用说吗,这个就是我们平时使用ListView时最最经常重写的一个方法了,这里getView()方法中传入了三个参数,分别是position,null和this。

那么我们平时写ListView的Adapter时,getView()方法通常会怎么写呢?这里我举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}

getView()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertView,刚才传入的是null,说明没有convertView可以利用,因此我们会调用LayoutInflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。

那么这个View也会作为obtainView()的结果进行返回,并最终传入到setupChild()方法当中。其实也就是说,第一次layout过程当中,所有的子View都是调用LayoutInflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* Add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child The view to add
* @param position The position of this child
* @param y The y position relative to which this view will be positioned
* @param flowDown If true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @param recycled Has this view been pulled from the recycle bin? If so it
* does not need to be remeasured.
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
final boolean isSelected = selected && shouldShowSelector();
final boolean updateChildSelected = isSelected != child.isSelected();
final int mode = mTouchMode;
final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
mMotionPosition == position;
final boolean updateChildPressed = isPressed != child.isPressed();
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
p.viewType = mAdapter.getItemViewType(position);
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (needToMeasure) {
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
}

setupChild()方法当中的代码虽然比较多,但是我们只看核心代码的话就非常简单了,刚才调用obtainView()方法获取到的子元素View,这里在第40行调用了addViewInLayout()方法将它添加到了ListView当中。那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上。

那么到此为止,第一次Layout过程结束。

第二次Layout

虽然我在源码中并没有找出具体的原因,但如果你自己做一下实验的话就会发现,即使是一个再简单的View,在展示到界面上之前都会经历至少两次onMeasure()和两次onLayout()的过程。其实这只是一个很小的细节,平时对我们影响并不大,因为不管是onMeasure()或者onLayout()几次,反正都是执行的相同的逻辑,我们并不需要进行过多关心。但是在ListView中情况就不一样了,因为这就意味着layoutChildren()过程会执行两次,而这个过程当中涉及到向ListView中添加子元素,如果相同的逻辑执行两遍的话,那么ListView中就会存在一份重复的数据了。因此ListView在layoutChildren()过程当中做了第二次Layout的逻辑处理,非常巧妙地解决了这个问题,下面我们就来分析一下第二次Layout的过程。

其实第二次Layout和第一次Layout的基本流程是差不多的,那么我们还是从layoutChildren()方法开始看起:

同样还是在第19行,调用getChildCount()方法来获取子View的数量,只不过现在得到的值不会再是0了,而是ListView中一屏可以显示的子View数量,因为我们刚刚在第一次Layout过程当中向ListView添加了这么多的子View。下面在第90行调用了RecycleBin的fillActiveViews()方法,这次效果可就不一样了,因为目前ListView中已经有子View了,这样所有的子View都会被缓存到RecycleBin的mActiveViews数组当中,后面将会用到它们。

接下来将会是非常非常重要的一个操作,在第113行调用了detachAllViewsFromParent()方法。这个方法会将所有ListView当中的子View全部清除掉,从而保证第二次Layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View吗,待会儿将会直接使用这些缓存好的View来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。

那么我们接着看,在第141行的判断逻辑当中,由于不再等于0了,因此会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mSelectedPosition应该等于-1。第二个逻辑判断通常是成立的,因为mFirstPosition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillSpecific()方法当中,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position The reference view to use as the starting point
* @param top Pixel offset from the top of this view to the top of the
* reference view.
*
* @return The selected view, or null if the selected view is outside the
* visible area.
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) {
above = fillUp(position - 1, temp.getTop() - dividerHeight);
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown();
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooHigh(childCount);
}
} else {
below = fillDown(position + 1, temp.getBottom() + dividerHeight);
// This will correct for the bottom of the last view not touching the bottom of the list
adjustViewsUpOrDown();
above = fillUp(position - 1, temp.getTop() - dividerHeight);
int childCount = getChildCount();
if (childCount > 0) {
correctTooLow(childCount);
}
}
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}

fillSpecific()这算是一个新方法了,不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。那么由于这里我们传入的position就是第一个子View的位置,于是fillSpecific()方法的作用就基本上和fillDown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeAndAddView()方法上面。再次回到makeAndAddView()方法,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

仍然还是在第19行尝试从RecycleBin当中获取Active View,然而这次就一定可以获取到了,因为前面我们调用了RecycleBin的fillActiveViews()方法来缓存子View。那么既然如此,就不会再进入到第28行的obtainView()方法,而是会直接进入setupChild()方法当中,这样也省去了很多时间,因为如果在obtainView()方法中又要去infalte布局的话,那么ListView的初始加载效率就大大降低了。

注意在第23行,setupChild()方法的最后一个参数传入的是true,这个参数表明当前的View是之前被回收过的,那么我们再次回到setupChild()方法当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* Add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child The view to add
* @param position The position of this child
* @param y The y position relative to which this view will be positioned
* @param flowDown If true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @param recycled Has this view been pulled from the recycle bin? If so it
* does not need to be remeasured.
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
final boolean isSelected = selected && shouldShowSelector();
final boolean updateChildSelected = isSelected != child.isSelected();
final int mode = mTouchMode;
final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
mMotionPosition == position;
final boolean updateChildPressed = isPressed != child.isPressed();
final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}
p.viewType = mAdapter.getItemViewType(position);
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (needToMeasure) {
int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
final int w = child.getMeasuredWidth();
final int h = child.getMeasuredHeight();
final int childTop = flowDown ? y : y - h;
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
}

可以看到,setupChild()方法的最后一个参数是recycled,然后在第32行会对这个变量进行判断,由于recycled现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。那么由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。

经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。

滑动加载更多数据

经历了两次Layout过程,虽说我们已经可以在ListView中看到内容了,然而关于ListView最神奇的部分我们却还没有接触到,因为目前ListView中只是加载并显示了第一屏的数据而已。比如说我们的Adapter当中有1000条数据,但是第一屏只显示了10条,ListView中也只有10个子View而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下ListView滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。

由于滑动部分的机制是属于通用型的,即ListView和GridView都会使用同样的机制,因此这部分代码就肯定是写在AbsListView当中的了。那么监听触控事件是在onTouchEvent()方法当中进行的,我们就来看一下AbsListView中的这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!isEnabled()) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isClickable() || isLongClickable();
}
final int action = ev.getAction();
View v;
int deltaY;
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mActivePointerId = ev.getPointerId(0);
final int x = (int) ev.getX();
final int y = (int) ev.getY();
int motionPosition = pointToPosition(x, y);
if (!mDataChanged) {
if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
&& (getAdapter().isEnabled(motionPosition))) {
// User clicked on an actual view (and was not stopping a
// fling). It might be a
// click or a scroll. Assume it is a click until proven
// otherwise
mTouchMode = TOUCH_MODE_DOWN;
// FIXME Debounce
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
// If we couldn't find a view to click on, but the down
// event was touching
// the edge, we will bail out and try again. This allows
// the edge correcting
// code in ViewRoot to try to find a nearby view to
// select
return false;
}

if (mTouchMode == TOUCH_MODE_FLING) {
// Stopped a fling. It is a scroll.
createScrollingCache();
mTouchMode = TOUCH_MODE_SCROLL;
mMotionCorrection = 0;
motionPosition = findMotionRow(y);
reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
}
}
}
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
}
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mLastY = Integer.MIN_VALUE;
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final int y = (int) ev.getY(pointerIndex);
deltaY = y - mMotionY;
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
// Check if we have moved far enough that it looks more like a
// scroll than a tap
startScrollIfNeeded(deltaY);
break;
case TOUCH_MODE_SCROLL:
if (PROFILE_SCROLLING) {
if (!mScrollProfilingStarted) {
Debug.startMethodTracing("AbsListViewScroll");
mScrollProfilingStarted = true;
}
}
if (y != mLastY) {
deltaY -= mMotionCorrection;
int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
// No need to do all this work if we're not going to move
// anyway
boolean atEdge = false;
if (incrementalDeltaY != 0) {
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
if (atEdge && getChildCount() > 0) {
// Treat this like we're starting a new scroll from the
// current
// position. This will let the user start scrolling back
// into
// content immediately rather than needing to scroll
// back to the
// point where they hit the limit first.
int motionPosition = findMotionRow(y);
if (motionPosition >= 0) {
final View motionView = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = motionView.getTop();
}
mMotionY = y;
mMotionPosition = motionPosition;
invalidate();
}
mLastY = y;
}
break;
}
break;
}
case MotionEvent.ACTION_UP: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
case TOUCH_MODE_TAP:
case TOUCH_MODE_DONE_WAITING:
final int motionPosition = mMotionPosition;
final View child = getChildAt(motionPosition - mFirstPosition);
if (child != null && !child.hasFocusable()) {
if (mTouchMode != TOUCH_MODE_DOWN) {
child.setPressed(false);
}
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
final AbsListView.PerformClick performClick = mPerformClick;
performClick.mChild = child;
performClick.mClickMotionPosition = motionPosition;
performClick.rememberWindowAttachCount();
mResurrectToPosition = motionPosition;
if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? mPendingCheckForTap
: mPendingCheckForLongPress);
}
mLayoutMode = LAYOUT_NORMAL;
if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
mTouchMode = TOUCH_MODE_TAP;
setSelectedPositionInt(mMotionPosition);
layoutChildren();
child.setPressed(true);
positionSelector(child);
setPressed(true);
if (mSelector != null) {
Drawable d = mSelector.getCurrent();
if (d != null && d instanceof TransitionDrawable) {
((TransitionDrawable) d).resetTransition();
}
}
postDelayed(new Runnable() {
public void run() {
child.setPressed(false);
setPressed(false);
if (!mDataChanged) {
post(performClick);
}
mTouchMode = TOUCH_MODE_REST;
}
}, ViewConfiguration.getPressedStateDuration());
} else {
mTouchMode = TOUCH_MODE_REST;
}
return true;
} else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
post(performClick);
}
}
mTouchMode = TOUCH_MODE_REST;
break;
case TOUCH_MODE_SCROLL:
final int childCount = getChildCount();
if (childCount > 0) {
if (mFirstPosition == 0
&& getChildAt(0).getTop() >= mListPadding.top
&& mFirstPosition + childCount < mItemCount
&& getChildAt(childCount - 1).getBottom() <= getHeight()
- mListPadding.bottom) {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final int initialVelocity = (int) velocityTracker
.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinimumVelocity) {
if (mFlingRunnable == null) {
mFlingRunnable = new FlingRunnable();
}
reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
mFlingRunnable.start(-initialVelocity);
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
}
} else {
mTouchMode = TOUCH_MODE_REST;
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
break;
}
setPressed(false);
// Need to redraw since we probably aren't drawing the selector
// anymore
invalidate();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
if (PROFILE_SCROLLING) {
if (mScrollProfilingStarted) {
Debug.stopMethodTracing();
mScrollProfilingStarted = false;
}
}
break;
}
case MotionEvent.ACTION_CANCEL: {
mTouchMode = TOUCH_MODE_REST;
setPressed(false);
View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
if (motionView != null) {
motionView.setPressed(false);
}
clearScrollingCache();
final Handler handler = getHandler();
if (handler != null) {
handler.removeCallbacks(mPendingCheckForLongPress);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mActivePointerId = INVALID_POINTER;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);
final int x = mMotionX;
final int y = mMotionY;
final int motionPosition = pointToPosition(x, y);
if (motionPosition >= 0) {
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
mMotionPosition = motionPosition;
}
mLastY = y;
break;
}
}
return true;
}

这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是ACTION_MOVE这个动作,那么我们就只看这部分代码就可以了。可以看到,ACTION_MOVE这个case里面又嵌套了一个switch语句,是根据当前的TouchMode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,TouchMode是等于TOUCH_MODE_SCROLL这个值的。

这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并没有什么太多需要注意的东西,唯一一点非常重要的就是第92行调用的trackMotionScroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。那么我们进入到这个方法中瞧一瞧,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
final int childCount = getChildCount();
if (childCount == 0) {
return true;
}
final int firstTop = getChildAt(0).getTop();
final int lastBottom = getChildAt(childCount - 1).getBottom();
final Rect listPadding = mListPadding;
final int spaceAbove = listPadding.top - firstTop;
final int end = getHeight() - listPadding.bottom;
final int spaceBelow = lastBottom - end;
final int height = getHeight() - getPaddingBottom() - getPaddingTop();
if (deltaY < 0) {
deltaY = Math.max(-(height - 1), deltaY);
} else {
deltaY = Math.min(height - 1, deltaY);
}
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
final int firstPosition = mFirstPosition;
if (firstPosition == 0 && firstTop >= listPadding.top && deltaY >= 0) {
// Don't need to move views down if the top of the first position
// is already visible
return true;
}
if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY <= 0) {
// Don't need to move views up if the bottom of the last position
// is already visible
return true;
}
final boolean down = incrementalDeltaY < 0;
final boolean inTouchMode = isInTouchMode();
if (inTouchMode) {
hideSelector();
}
final int headerViewsCount = getHeaderViewsCount();
final int footerViewsStart = mItemCount - getFooterViewsCount();
int start = 0;
int count = 0;
if (down) {
final int top = listPadding.top - incrementalDeltaY;
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
} else {
final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
for (int i = childCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getTop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
mRecycler.addScrapView(child);
}
}
}
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
invalidate();
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
final int childIndex = mSelectedPosition - mFirstPosition;
if (childIndex >= 0 && childIndex < getChildCount()) {
positionSelector(getChildAt(childIndex));
}
}
mBlockLayoutRequests = false;
invokeOnItemScrollListener();
awakenScrollBars();
return false;
}

这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实我们就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了。如第34行代码所示,如果incrementalDeltaY小于0,说明是向下滑动,否则就是向上滑动。

下面将会进行一个边界值检测的过程,可以看到,从第43行开始,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,第47行当中,如果该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。

接下来在第76行,会根据当前计数器的值来进行一个detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念当中,所有看不到的View就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。紧接着在第78行调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作为参数传入,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。

然后在第84行会进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Fills the gap left open by a touch-scroll. During a touch scroll,
* children that remain on screen are shifted and the other ones are
* discarded. The role of this method is to fill the gap thus created by
* performing a partial layout in the empty space.
*
* @param down
* true if the scroll is going down, false if it is going up
*/
abstract void fillGap(boolean down);
OK,AbsListView中的fillGap()是一个抽象方法,那么我们立刻就能够想到,它的具体实现肯定是在ListView中完成的了。回到ListView当中,fillGap()方法的代码如下所示:
void fillGap(boolean down) {
final int count = getChildCount();
if (down) {
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
getListPaddingTop();
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - getListPaddingBottom();
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}

down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* Obtain the view and add it to our list of children. The view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position Logical position in the list
* @param y Top or bottom edge of the view to add
* @param flow If flow is true, align top edge to y. If false, align bottom
* edge to y.
* @param childrenLeft Left edge where children should be positioned
* @param selected Is this position selected?
* @return View that was added
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an exsiting view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}

不管怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。

既然getActiveView()方法返回的值是null,那么就还是会走到第28行的obtainView()方法当中,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position
* The position to display
* @param isScrap
* Array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
isScrap[0] = false;
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
child = mAdapter.getView(position, scrapView, this);
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
}
return child;
}

这里在第19行会调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。

那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,然后我们在第22行将它作为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getView()方法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position);
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
fruitImage.setImageResource(fruit.getImageId());
fruitName.setText(fruit.getName());
return view;
}

第二个参数就是我们最熟悉的convertView呀,难怪平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?

之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。

为了方便大家理解,这里我再附上一张图解说明:

参考资料

https://blog.csdn.net/guolin_blog/article/details/44996879

NDK之CMake

发表于 2019-08-16 | 分类于 Android NDK

1、简介

JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。

NDK(native development kit)提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so,该动态库可以兼容各个平台。

2、CMake

CMake是一个跨平台的安装(编译)工具,通过编写CMakeLists.txt,可以生成对应的makefile或project文件,再调用底层的编译。AS 2.2之后工具中增加了对CMake的支持,官方也推荐用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去构建JNI项目.

创建使用CMake构建的项目

开始前AS要先在SDK Manager中安装SDK Tools->CMake,只要勾选Include C++ Support。其中会提示配置C++支持的功能.

一般默认就可以了,各个选项的具体含义:

C++ Standard:指定编译库的环境。
Exception Support:当前项目支持C++异常处理
Runtime Type Information Support:除异常处理外,还支持动态转类型(dynamic casting) 、模块集成、以及对象I/O

工程的目录结构

创建好的工程主Module下直接就有.externalNativeBuild,多出一个CMakeLists.txt,相当于以前的配置文件。并且在src/main目录下多了一个cpp文件夹,里面存放的是C++文件,相当于以前的jni文件夹。这个是工程创建后AS生成的示例JNI方法,返回了一个字符串。后面开发JNI就可以按照这个目录结构。

相应的,build.gradle下也增加了一些配置:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -frtti -fexceptions"
                abiFilters "x86", "arm64-v8a", "armeabi-v7a"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

defaultConfig中的externalNativeBuild各项属性和前面创建项目时的选项配置有关,外部的externalNativeBuild则定义了CMakeLists.txt的存放路径。
如果只是在自己的项目中使用,CMake的方式在打包APK的时候会自动将cpp文件编译成so文件拷贝进去。如果要提供给外部使用时,Make Project,之后在libs目录下就可以看到生成的对应配置的相关CPU平台的.so文件。

CMakeLists.txt

CMakeLists.txt可以自定义命令、查找文件、头文件包含、设置变量,具体可见 官方文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 编译本地库时我们需要的最小的cmake版本
cmake_minimum_required(VERSION 3.4.1)

#设置生成的so动态库最后输出的路径,一定要在add_library之前设置,否则不会生效
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})

# 相当于Android.mk。如果是多次使用add_library,则会生成多个so库;
# 如果想将多个本地文件编译到一个so库中,只要最后一个参数添加多个C/C++文件的相对路径就可以
add_library( # Sets the name of the library.设置编译生成本地库的名字
native-lib

# Sets the library as a shared library.库的类型
SHARED

# Provides a relative path to your source file(s).编译文件的路径
src/main/cpp/native-lib.cpp )

# 添加一些我们在编译我们的本地库的时候需要依赖的一些库,这里是用来打log的库。可以写多个find_library
find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log )

# 关联自己生成的库和一些第三方库或者系统库
target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib} )

#设置头文件搜索路径(和此txt同个路径的头文件无需设置),可选
#INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/common)

#指定用到的系统库或者NDK库或者第三方库的搜索路径,可选。
#LINK_DIRECTORIES(/usr/local/lib)

#添加子目录,将会调用子目录中的CMakeLists.txt
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)

  • cmake_minimum_required(VERSION 3.4.1):指定CMake的最小版本
  • add_library:创建一个静态或者动态库,并提供其关联的源文件路径,开发者可以定义多个库,CMake会自动去构建它们。Gradle可以自动将它们打包进APK中。
    第一个参数——native-lib:是库的名称
    第二个参数——SHARED:是库的类别,是动态的还是静态的
    第三个参数——src/main/cpp/native-lib.cpp:是库的源文件的路径
  • find_library:找到一个预编译的库,并作为一个变量保存起来。由于CMake在搜索库路径的时候会包含系统库,并且CMake会检查它自己之前编译的库的名字,所以开发者需要保证开发者自行添加的库的名字的独特性。
    第一个参数——log-lib:设置路径变量的名称
    第一个参数—— log:指定NDK库的名子,这样CMake就可以找到这个库
  • target_link_libraries:指定CMake链接到目标库。开发者可以链接多个库,比如开发者可以在此定义库的构建脚本,并且预编译第三方库或者系统库。
    第一个参数——native-lib:指定的目标库
    第一个参数——${log-lib}:将目标库链接到NDK中的日志库,

如果想要配置so库的目标CPU平台,可以在build.gradle中设置

1
2
3
4
5
6
7
8
9
10
android {
...
defaultConfig {
...
ndk{
abiFilters "x86","armeabi","armeabi-v7a"
}
}
...
}

set_target_properties

设置目标的一些属性来改变它们构建的方式。

1
2
3
set_target_properties(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)

使用示例为:

1
2
3
4
5
6
set_target_properties(cocos2d
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
VERSION "${COCOS2D_X_VERSION}"
)

为一个目标设置属性。该命令的语法是列出所有你想要变更的文件,然后提供你想要设置的值。你能够使用任何你想要的属性/值对,并且在随后的代码中调用GET_TARGET_PROPERTY命令取出属性的值。

大家在用cmake时,应该经常会用到第三方so库,导入第三方so库中需要使用到set_target_properties,例如这样写:

1
2
3
4
5
6
7
8
9
set_target_properties(

Thirdlib

PROPERTIES IMPORTED_LOCATION

${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/libThirdlib.so

)

CMAKE_CURRENT_SOURCE_DIR 这个变量是系统自定义的,表示CMakeLists.txt文件的绝对路径

包含其他 CMake 项目

如果想要编译多个 CMake 项目并在 Android 项目中包含它们的输出,您可以使用一个 CMakeLists.txt 文件(即您关联到 Gradle 的那个文件)作为顶级 CMake 编译脚本,并添加其他 CMake 项目作为此编译脚本的依赖项。以下顶级 CMake 编译脚本使用 add_subdirectory() 命令将另一个 CMakeLists.txt 文件指定为编译依赖项,然后关联其输出,就像处理任何其他预编译库一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Sets lib_src_DIR to the path of the target CMake project.
set( lib_src_DIR ../gmath )

# Sets lib_build_DIR to the path of the desired output directory.
set( lib_build_DIR ../gmath/outputs )
file(MAKE_DIRECTORY ${lib_build_DIR})

# Adds the CMakeLists.txt file located in the specified directory
# as a build dependency.
add_subdirectory( # Specifies the directory of the CMakeLists.txt file.
${lib_src_DIR}

# Specifies the directory for the build outputs.
${lib_build_DIR} )

# Adds the output of the additional CMake build as a prebuilt static
# library and names it lib_gmath.
add_library( lib_gmath STATIC IMPORTED )
set_target_properties( lib_gmath PROPERTIES IMPORTED_LOCATION
${lib_build_DIR}/${ANDROID_ABI}/lib_gmath.a )
include_directories( ${lib_src_DIR}/include )

# Links the top-level CMake build output against lib_gmath.
target_link_libraries( native-lib ... lib_gmath )

3、Android.mk

Android.mk内容示例如下:

1
2
3
4
5
6
7
8
9
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ndkdemotest-jni

LOCAL_SRC_FILES := ndkdemotest.c

include $(BUILD_SHARED_LIBRARY)

  • LOCAL_PATH := $(call my-dir):每个Android.mk文件必须以定义开始。它用于在开发tree中查找源文件。宏my-dir则由Build System 提供。返回包含Android.mk目录路径。

  • include $(CLEAR_VARS) :CLEAR_VARS变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx。例如LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_STATIC_LIBRARIES等等。但不是清理LOCAL_PATH。这个清理是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能便面相互影响。

  • LOCAL_MODULE := ndkdemotest-jni:LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。Build System 会自动添加适当的前缀和后缀。例如,demo,要生成动态库,则生成libdemo.so。但请注意:如果模块名字被定义为libabd,则生成libabc.so。不再添加前缀。

  • LOCAL_SRC_FILES := ndkdemotest.c:这行代码表示将要打包的C/C++源码。不必列出头文件,build System 会自动帮我们找出依赖文件。缺省的C++ 源码的扩展名为.cpp。

  • include $(BUILD_SHARED_LIBRARY):BUILD_SHARED_LIBRARY是Build System提供的一个变量,指向一个GUN Makefile Script。它负责收集自从上次调用include $(CLEAR_VARS)后的所有LOCAL_xxxxinx。并决定编译什么类型:
    1)BUILD_STATIC_LIBRARY:编译为静态库
    2)BUILD_SHARED_LIBRARY:编译为动态库
    3)BUILD_EXECUTABLE:编译为Native C 可执行程序
    4)BUILD_PREBUILT:该模块已经预先编译

参考资料

https://blog.csdn.net/wzhseu/article/details/79683045
CMake学习

Android小知识点笔记

发表于 2019-08-12 | 分类于 Android知识点

FP函数式编程

函数式编程(functional programming),是一种编程范式,它将计算机运算视为函数运算 ,并且避免使用程序状态以及不可变对象。

即一种把程序看作数学方程式的编程风格,并避免可变状态和副作用。

FP核心思想强调:

  1. 声明式代码 —— 程序员应该关心是什么,让编译器和运行环境去关心怎样做。

  2. 明确性 —— 代码应该尽可能的明显。尤其是要隔离副作用避免意外。要明确定义数据流和错误处理,要避免 GOTO 语句和异常,因为它们会将应用置于意外的状态。

  3. 并发 —— 因为纯函数的概念,大多数函数式代码默认都是并行的。由于CPU运行速度没有像以前那样逐年加快((详见 摩尔定律)), 普遍看来这个特点导致函数式编程渐受欢迎。以及我们也必须利用多核架构的优点,让代码尽量的可并行。

  4. 高阶函数 —— 和其他的基本语言元素一样,函数是一等公民。你可以像使用 string 和 int 一样的去传递函数。

  5. 不变性 —— 变量一经初始化将不能修改。一经创建,永不改变。如果需要改变,需要创建新的。这是明确性和避免副作用之外的另一方面。如果你知道一个变量不能改变,当你使用时会对它的状态更有信心。

声明式、明确性和可并发的代码,难道不是更易推导以及从设计上就避免了意外吗?不可变性和纯粹性是帮助我们写出安全的并发代码的强力组合。

参考链接:https://blog.csdn.net/u010144805/article/details/81416355

λ表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

lambda 表达式的语法格式如下:

(parameters) -> expression
或
(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

λ表达式主要用于精简广泛使用的内部匿名类,各种回调,比如事件响应器、传入Thread类的Runnable等。Java8有一个短期目标和一个长期目标。短期目标是:配合“集合类批处理操作”的内部迭代和并行处理;长期目标是将Java向函数式编程语言这个方向引导(并不是要完全变成一门函数式编程语言,只是让它有更多的函数式编程语言的特性)。

Java8为集合类引入了另一个重要概念:流(stream)。一个流通常以一个集合类实例为其数据源,然后在其上定义各种操作。流的API设计使用了管道(pipelines)模式。对流的一次操作会返回另一个流。你可能会觉得List 被迭代了好多次,map,filter,distinct都分别是一次循环,效率会不好。实际并非如此。这些返回另一个Stream的方法都是“懒(lazy)”的,而最后返回最终结果的方法则是“急(eager)”的。在遇到eager方法之前,lazy的方法不会执行。

内部类总是持有一个其外部类对象的引用。而λ表达式呢,除非在它内部用到了其外部类(包围类)对象的方法或者成员,否则它就不持有这个对象的引用。在Java8以前,如果要在内部类访问外部对象的一个本地变量,那么这个变量必须声明为final才行。在Java8中,这种限制被去掉了,代之以一个新的概念,“effectively final”。它的意思是你可以声明为final,也可以不声明final但是按照final来用,也就是一次赋值永不改变。

任何一个λ表达式都可以代表某个函数接口的唯一方法的匿名描述符。我们也可以使用某个类的某个具体方法来代表这个描述符,叫做方法引用。

匿名内部类

外部类创建匿名内部类后,有可能匿名内部类还在使用,而外部类实例(或者创建内部类的方法)已经被回收了。如果此时匿名内部类用到了外部类的成员变量,那么就会出现匿名内部类要去访问一个不存在的变量的这种荒唐情况,为了延长局部变量的生命周期,于是匿名内部类使用的局部变量会被复制一份,从而使得局部变量的生命周期看起来变长了。但是这样又会引出另一个问题:数据一致性的问题!为了保证局部变量和 内部类中复制品 的数据一致性,于是要求内部类使用的局部变量是final的。

String & StringBuffer & StringBuilder

1、长度是否可变
String 是被 final 修饰的,他的长度是不可变的,就算调用 String 的concat 方法,那也是把字符串拼接起来并重新创建一个对象,把拼接后的 String 的值赋给新创建的对象
StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象,StringBuffer 与 StringBuilder 中的方法和功能完全是等价的。调用StringBuffer 的 append 方法,来改变 StringBuffer 的长度,并且,相比较于 StringBuffer,String 一旦发生长度变化,是非常耗费内存的!

2、执行效率
三者在执行速度方面的比较:StringBuilder > StringBuffer > String

3、应用场景
如果要操作少量的数据用 = String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。

Java中Vector和ArrayList的区别

首先看这两类都实现List接口,而List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。3个具体实现类的相关区别如下:

  1. ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
  2. Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
  3. LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

View绘制与事件

绘制

onMeasure()、onLayout():
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。
getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

onDraw()步骤:
1.第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制
2.第三步是在第34行执行的,这一步的作用是对视图的内容进行绘制
3.第四步的作用是对当前视图的所有子视图进行绘制
4.第六步的作用是对视图的滚动条进行绘制。其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。
部分源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}

重绘

调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用invalidate()方法来实现的。

invalidate()方法中,当ViewParent不等于空的时候就会一直循环下去。在这个while循环当中会不断地获取当前布局的父布局,并调用它的invalidateChildInParent()方法,在ViewGroup的invalidateChildInParent()方法中主要是来计算需要重绘的矩形区域,当循环到最外层的根布局后,就会调用ViewRoot的invalidateChildInParent()方法了, 最终会调用到performTraversals()方法。
invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。

参考:https://blog.csdn.net/guolin_blog/article/details/17045157

事件传递

Android中触摸事件传递过程中最重要的是dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()方法。这里记录一下他们的处理过程,以供记忆:
1.dispatchTouchEvent是处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。执行super.dispatchTouchEvent(ev),事件向下分发。dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。
2.onInterceptTouchEvent是ViewGroup提供的方法,默认返回false,返回true表示拦截。
3.onTouchEvent是View中提供的方法,ViewGroup也有这个方法,view中不提供onInterceptTouchEvent。view中默认返回true,表示消费了这个事件。

事件传递流程如下:
1.ACTION_DOWN首先会传递到onInterceptTouchEvent()方法
2.如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
3.如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
4.如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
5.如果最终需要处理事件的view的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。


当触摸事件ACTION_DOWN发生之后,先调用Activity中的dispatchTouchEvent函数进行处理,紧接着ACTION_DOWN事件传递给ViewGroup中的dispatchTouchEvent函数,接着viewGroup中的dispatchTouchEvent中的ACTION_DOWN事件传递到调用ViewGroup中的onInterceptTouchEvent函数,此函数负责拦截ACTION_DOWN事件。由于viewGroup下还包含子View,所以默认返回值为false,即不拦截此ACTION_DOWN事件。如果返回false,则ACTION_DOWN事件继续传递给其子view。由于子view不是viewGroup的控件,所以ACTION_DOWN事件接着传递到onTouchEvent进行处理事件。此时消息的传递基本上结束。从上可以分析,motionEvent事件的传递是采用隧道方式传递。隧道方式,即从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递。

参考:https://blog.csdn.net/qiushuiqifei/article/details/9918527

Android属性动画优化

android的三种动画:

  • View Animation(视图动画,平移、缩放、透明等)View的属性没有改变,其位置与大小都不变
  • Drawable Animation(帧动画)
  • Property Animation(属性动画)对象自己的属性会被真的改变,而且属性动画不止用于View,还可以用于任何对象。

现在项目的动画问题最主要出在动画部分临时变量多,GC触发频繁,内存泄漏。属性动画优化思路:

  1. 硬件加速
    在开始动画时调用View.setLayerType(View.LAYER_TYPE_HARDWARE, null)
    运行动画
    动画结束时调用View.setLayerType(View.LAYER_TYPE_NONE, null).
  2. 减少临时变量,使用PropertyValuesHolder(可以用在多属性动画同时工作管理) ,一个view同时发生多种属性效果时,建议这种写法。
  3. 使用Keyframe
    Keyframe是PropertyValuesHolder的成员,用来管理每一个关键帧的出现时间。一个view的单个属性先后发生一系列变化时,建议使用Keyframe达到效果。
    总的来说就是:ObjectAnimator把属性值的更新委托给PropertyValuesHolder执行,PropertyValuesHolder再把关键帧的时序性计算委托给Keyframe。
    最后,不同的view再用不同的ObjectAnimator管理。
  4. 内存泄漏: animator.setRepeatCount(ValueAnimator.INFINITE)及时cancel()
  5. 动画卡顿,可以考虑使用自定义控件实现,如果一个自定义不行,那就是两个

ART和Dalvik

Android4.4版本以前是Dalvik虚拟机,4.4版本开始引入ART虚拟机(Android Runtime)。在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0版本以后,Dalvik虚拟机则被彻底的丢弃,全部采用ART。

JIT,Just-in-time,即时编译,边运行边编译;AOT,Ahead Of Time,提前编译,指运行前编译。区别:这两种编译方式的主要区别在于是否在“运行时”进行编译

ART

ART 是一种执行效率更高且更省电的运行机制,执行的是本地机器码,这些本地机器码是从dex字节码转换而来。ART采用的是AOT(Ahead-Of-Time)编译,应用在第一次安装的时候,字节码就会预先编译成机器码存储在本地。在App运行时,ART模式就较Dalvik模式少了解释字节码的过程,所以App的运行效率会有所提高,占用内存也会相应减少。

Dalvik

Dalvik 虚拟机采用的是JIT(Just-In-Time)编译模式,意思为即时编译,我们知道apk被安装到手机中时,对应目录会有dex或odex和apk文件,apk文件存储的是资源文件,而dex或odex(经过优化后的dex文件内部存储class文件:加快软件的启动速度,odex可预先提取、应用保护)内部存储class文件,每次运行app时虚拟机会将dex文件解释翻译成机器码,这样才算是本地可执行代码,之后被系统运行。

Dalvik虚拟机可以看做是一个Java VM,他负责解释dex文件为机器码,如果我们不做处理的话,每次执行代码,都需要Dalvik将dex代码翻译为微处理器指令,然后交给系统处理,这样效率不高。为了解决这个问题,Google在2.2版本添加了JIT编译器,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。JIT代表运行时编译策略,也可以理解成一种运行时编译器,是为了加快Dalvik虚拟机解释dex速度提出的一种技术方案,来缓存频繁使用的本地机器码。

两者的区别

Dalvik每次都要编译再运行,Art只会安装时启动编译
Art占用空间比Dalvik大(原生代码占用的存储空间更大),就是用“空间换时间”
Art减少编译,减少了CPU使用频率,使用明显改善电池续航
Art应用启动更快、运行更快、体验更流畅、触感反馈更及时

dexopt 与 dex2oat 的区别

通过上图可以很明显的看出 dexopt 与 dex2oat 的区别,前者针对 Dalvik 虚拟机,后者针对 Art 虚拟机。

  • dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。

  • dex2oat 是对 dex 文件的 AOT 提前编译操作,其需要一个 dex 文件,然后对其进行编译,结果是一个本地可执行的 ELF 文件,可以直接被本地处理器执行。

除此之外在上图还可以看到 Dalvik 虚拟机中有使用 JIT 编译器,也就是说其也能将程序运行的热点 java 字节码编译成本地 code 执行,所以其与 Art 虚拟机还是有区别的。Art 虚拟机的 dex2oat 是提前编译所有 dex 字节码,而 Dalvik 虚拟机只编译使用启发式检测中最频繁执行的热点字节码。

CLASS_ISPREVERIFIED

  1. 在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
  2. 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
  3. 被打上这个标记的类不能引用其他dex中的类,否则就会报错
  4. 在我们的Demo中,MainActivity和Cat本身是在同一个dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我们修复bug的时候却引用了另外一个dex的Cat.class,所以这里就报错了
  5. 而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED
  6. 补充一下第二条:A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。

方案:
根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。在所有类的构造函数中插入这行代码(AOP):

System.out.println(AntilazyLoad.class);

这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作

UncaughtExceptionHandler

如果给一个线程设置了UncaughtExceptionHandler 这个接口:
1、这个线程中,所有未处理或者说未捕获的异常都将会由这个接口处理,也就说被这个接口给try…catch了。
2、在这个线程中抛出异常时,java虚拟机将会忽略,也就是说,java虚拟机不会让程序崩溃了。
3、如果没有设置,那么最终会调用getDefaultUncaughtExceptionHandler 获取默认的UncaughtExceptionHandler 来处理异常。

注意:
1、即使我们用这种方式捕获到了异常,保证程序不会闪退,如果是子线程出现了异常,那么还好,并不会影响UI线程的正常流程,但是如果是UI线程中出现了异常,那么程序就不会继续往下走,处于没有响应的状态,所以,我们处理异常的时候,应该给用户一个有好的提示,让程序优雅地退出。
2、Thread.setDefaultUncaughtExceptionHandler(handler)方法,如果多次调用的话,会以最后一次调用时,传递的handler为准,之前设置的handler都没用。所以,这也是如果用了第三方的统计模块后,可能会出现失灵的情况。(这种情况其实也好解决,就是只设置一个handler,以这handler为主,然后在这个handler的uncaughtException 方法中,调用其他的handler的uncaughtException 方法,保证都会收到异常信息)

参考资料:
https://blog.csdn.net/bai981002/article/details/78717069

ANR

ANR问题发生时,系统会收集ANR相关的日志信息,CPU使用情况,trace日志也就是各线程执行情况等信息,生成一个traces.txt的文件并且放在/data/anr/路径下。

ANR的发生原因,总结出三个典型场景

1、主线程被其他线程锁(占比57%):调用了thread的sleep()、wait()等方法,导致的主线程等待超时。

2、系统资源被占用(占比14%):其他进程系统资源(CPU/RAM/IO)占用率高,导致该进程无法抢占到足够的系统资源。

3、主线程耗时工作导致线程卡死(占比9%):例如大量的数据库读写,耗时的网络情况,高强度的硬件计算等。

解决ANR问题方法总体思路

1、导出ANR日志信息,根据日志信息,判断确认发生ANR的包名类名,进程号,发生时间,导致ANR原因类型等。

2、关注系统资源信息,包括ANR发生前后的CPU,内存,IO等系统资源的使用情况。

3、查看主线程状态,关注主线程是否存在耗时、死锁、等锁等问题,判断该ANR是App导致还是系统导致的。

4、结合应用日志,代码或源码等,分析ANR问题发生前,应用是否有异常,其中具体问题具体分析。

掉帧检测方案Looper

Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。

Looper源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
//处理消息前,打印开始日志
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

msg.target.dispatchMessage(msg);

//处理完消息后,打印结束日志
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}

我们可以根据消息处理前后的日志输出作为检测点,计算出消息处理的耗时,如果超出16ms,说明发生了卡顿,此时就可以把UI线程的堆栈日志打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";

@Override
public void println(String x) {
if (x.startsWith(START)) {
UiBlockLogMonitor.getInstance().startMonitor();
}
if (x.startsWith(END)) {
UiBlockLogMonitor.getInstance().stopMonitor();
}
}
});

不过,由于系统定制的原因,打印出来的日志标识不一定标准,所以可以改为判断第一次日志输出和第二次日志输出。

常见卡顿原因及解决方案

一、过度绘制,去除不必要的背景色(开发者选项 -> 调试GPU过度绘制):

  1. 设置窗口背景色为通用背景色,去除根布局背景色。
  2. 若页面背景色与通用背景色不一致,在页面渲染完成后移除窗口背景色
  3. 去除和列表背景色相同的Item背景色

二、布局视图树扁平化

  1. 移除嵌套布局
  2. 使用merge、include标签
  3. 使用性能消耗更小布局(TableLayout、ConstraintLayout)

三、减少透明色,即alpha属性的使用
将alpha设置为半透明值(0<alpha<1)会很耗性能,尤其是对大视图。所以最好短暂地使用alpha属性。通过使用半透明颜色值(#77000000)代替

四、其他

  1. 使用ViewStub标签,延迟加载不必要的视图
  2. 使用AsyncLayoutInflater异步解析视图

五、主线程耗时操作

  1. Json数据解析耗时(Cache类)
  2. 文件操作(获取所属渠道名称)
  3. Binder通信(获取系统属性(mac地址))
  4. 正则匹配(Hybird 通信)
  5. 相机操作:初始化、预览、停止预览、释放(反扫)
  6. 组件初始化(推送)
  7. 循环删除、创建View(更多页面)
  8. WebView首次初始化

设置异步线程优先级为Process.THREAD_PRIORITY_BACKGROUND,减少与主线程的竞争。

参考资料:
https://www.cnblogs.com/developer-huawei/p/13856252.html
https://blog.csdn.net/joye123/article/details/79425398

@aar

@aar的方式关闭传递依赖

// 只下载该库,其他所依赖的所有库不下载
compile 'io.reactivex.rxjava2:rxandroid:2.0.1@aar'
// 在使用@aar的前提下还能下载其他依赖库,则需要添加transitive=true的条件
compile ("io.reactivex.rxjava2:rxandroid::2.0.1@aar") {
    transitive=true
}

Groovy

Groovy语言=Java语言的扩展+众多脚本语言的语法。运行在JVM虚拟机上。Gradle项目构框架使用groovy语言实现。 基于Gradle框架为我们实现了一些项目构件框架。

Groovy是一门jvm语言,它最终是要编译成class文件然后在jvm上执行,所以Java语言的特性Groovy都支持,我们完全可以混写Java和Groovy。

在Groovy中,数据类型有:
1) Java中的基本数据类型
2) Java中的对象
3) Closure(闭包)
4) 加强的List、Map等集合类型
5) 加强的File、Stream等IO类型

类型可以显示声明,也可以用 def 来声明,用 def 声明的类型Groovy将会进行类型推断。

gradle插件

插件会扩展项目的功能,帮助我们在项目的构建的过程中做很多事情:

  • 可以添加任务到项目中,帮助完成诸如 测试、编译、打包等事情。
  • 可以添加依赖配置到项目中,通过它们来配置我们在构建过程中的依赖。
  • 可以向项目中现有的对象类型添加新的扩展属性、方法等,可以使用它们来配置优化构建。例如:android{}这个配置块就是Android Gradle插件为Project对象添加的一个扩展。
  • 可以对项目进行一些约定,比如应用Java插件后,可以约定src/main/java目录下是我们的源代码的存放地址,在编译的时候也是编译这个目录下的Java源代码文件。

这就是插件,我们只需要按照它约定的方式,使用它提供的任务、方法或者扩展,就可以对我们的项目进行构建。

因为是基于groovy开发,所有代码文件要以.groovy结尾

1.定义插件入口: implements Plugin

2.XXXTask必须要继承DefautTask,并使用@TaskAction来定义Task的入口函数。

3.gradle配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//bulid.gradle
buildscript {

repositories {
google()
jcenter()
maven {
url 'file:///Users/xinyu/work/maven-lib/'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath 'com.zxy.plugin:plugin:1.0.0'
}
}

//plugin gradle
apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
// gradle sdk
compile gradleApi()
// groovy sdk
compile localGroovy()
// 可以引用其它库
compile fileTree(dir: 'libs'2 include: ['*.jar'])
}

uploadArchives {
repositories {
mavenDeployer {
//本地的Maven地址
repository(url: 'file:///Users/xinyu/work/maven-lib/')
}
}
}

group='com.zxy.plugin'
version='1.0.0'

参考资料:https://www.jianshu.com/p/eda0bfd692e6

AOP

AOP(Aspect Oriented Programming 面向切面编程)则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。能将同一个关注点聚焦到同一个方法中解决。

AspectJ

由于AndroidStudio默认是没有ajc编译器的,所以在Android中使用@AspectJ来编写(包括SpringAOP也是如此)。它在代码的编译期间扫描目标程序,根据切点(PointCut)匹配,将开发者编写的Aspect程序编织(Weave)到目标程序的.class文件中,对目标程序作了重构(重构单位是JoinPoint),目的就是建立目标程序与Aspect程序的连接(获得执行的对象、方法、参数等上下文信息),从而达到AOP的目的。

AspectJ是通过对目标工程的.class文件进行代码注入的方式将通知(Advise)插入到目标代码中:
第一步:根据pointCut切点规则匹配的joinPoint;
第二步:将Advise插入到目标JoinPoint中。
这样在程序运行时被重构的连接点将会回调Advise方法,就实现了AspectJ代码与目标代码之间的连接。

Gradle Transform

Gradle Transform是Android官方提供给开发者在项目构建阶段即由class到dex转换期间修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。Gradle Transform更多的是提供一种可以让开发者参与项目构建的机制,而诸如修改字节码等更加具体的细节则需要开发者去实现。

Hook

代理Hook:如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。

hook,又叫钩子,通常是指对一些方法进行拦截。这样当这些方法被调用时,也能够执行我们自己的代码,这也是面向切面编程的思想(AOP)。

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

大致思路:
1.找到需要Hook方法的系统类
2.利用代理模式来代理系统类的运行拦截我们需要拦截的方法
3.使用反射的方法把这个系统类替换成你的代理类

案例可参考:
https://blog.csdn.net/yulong0809/article/details/56842027

应用的启动过程

1.首先我们要启动的Activity会去ActivityManagerService中去校检是否合法

2.通过回调ActivityThread中内部类ApplicationThread的scheduleLaunchActivity去发送一个消息到ActivityThread中的内部类H中,H继承于Handler

3.然后会通过反射创建Activity对象及Application对象,并回调响应生命周期方法

这里说一点ActivityManagerService和我们应用间沟通几乎都是ActivityThread,ApplicationThread,H这几个类之间来回调用,而且不只是Activity,我们Android的四大组件几乎都用了这种模式

参考:
https://blog.csdn.net/yulong0809/article/details/58589715

Watchdog

能通过关闭FinalizerWatchdogDaemon来减少TimeoutException的触发。需要注意的是,此种方法并不是去解决问题,而是为了避免上报异常采取的一种 hack 方案,并没有真正的解决引起 finialize() 超时的问题。

// Android P 以后不能反射FinalizerWatchdogDaemon
if (Build.VERSION.SDK_INT >= 28) {
    Log.w(TAG, "stopWatchDog, do not support after Android P, just return");
    return;
}

try {
    final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
    final Field field = clazz.getDeclaredField("INSTANCE");
    field.setAccessible(true);
    final Object watchdog = field.get(null);
    try {
        final Field thread = clazz.getSuperclass().getDeclaredField("thread");
        thread.setAccessible(true);
        thread.set(watchdog, null);
    } catch (final Throwable t) {
        Log.e(TAG, "stopWatchDog, set null occur error:" + t);

        t.printStackTrace();
        try {
            // 直接调用stop方法,在Android 6.0之前会有线程安全问题
            final Method method = clazz.getSuperclass().getDeclaredMethod("stop");
            method.setAccessible(true);
            method.invoke(watchdog);
        } catch (final Throwable e) {
            Log.e(TAG, "stopWatchDog, stop occur error:" + t);
            t.printStackTrace();
        }
    }
} catch (final Throwable t) {
    Log.e(TAG, "stopWatchDog, get object occur error:" + t);
    t.printStackTrace();
}

NFC

Near Field Communication (NFC) 为一短距离无线通信技术,通常有效通讯距离为4厘米以内。NFC工作频率为13.65 兆赫兹,通信速率为106 kbit/秒到 848kbit/秒。

NFC支持如下3种工作模式:
读卡器模式(Reader/writer mode)、
仿真卡模式(Card Emulation Mode)、
点对点模式(P2P mode)。

Android SDK API主要支持NFC论坛标准(Forum Standard),这种标准被称为NDEF(NFC Data Exchange Format,NFC数据交换格式),类似传感器。

线程池

线程池使用的好处:
1:对多个线程进行统一地管理,避免资源竞争中出现的问题。
2:(重点):对线程进行复用,线程在执行完任务后不会立刻销毁,而会等待另外的任务,这样就不会频繁地创建、销毁线程和调用GC。
3:JAVA提供了一套完整的ExecutorService线程池创建的api,可创建多种功能不一的线程池,使用起来很方便。

创建线程池,主要是利用ThreadPoolExecutor这个类,而这个类有几种构造方法,其中参数最多的一种构造方法如下:

1
2
3
4
5
6
7
8
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
...
}

corePoolSize: 该线程池中核心线程的数量。
maximumPoolSize:该线程池中最大线程数量。(区别于corePoolSize)
keepAliveTime:从字面上就可以理解,是非核心线程空闲时要等待下一个任务到来的时间,当任务很多,每个任务执行时间很短的情况下调大该值有助于提高线程利用率。注意:当allowCoreThreadTimeOut属性设为true时,该属性也可用于核心线程。
unit:上面时间属性的单位
workQueue:任务队列
threadFactory:线程工厂,可用于设置线程名字等等,一般无须设置该参数。

进程保活

android进程优先级:前台进程 > 可见进程 > 服务进程 > 后台进程 > 空进程

android进程的回收策略:主要依靠LMK ( Low Memory Killer )机制来完成。LMK机制通过 oom_adj 这个阀值来判断进程的优先级,oom_adj 的值越高,优先级越低,越容易被杀死。

拓展:LMK ( Low Memory Killer ) 机制基于Linux的OOM(Out Of Memery)机制,通过一些比较复杂的评分机制,对进程进行打分,将分数高的进程判定为bad进程,杀死并释放内存。LMS机制和OOM机制的不同之处在于:OOM只有当系统内存不足时才会启动检查,而LMS机制是定时进行检查。

方案:

1、利用系统广播拉活:

系统广播事件是不可控制的,只有在发生事件时才能进行拉活,无法保证进程被杀死后立即被拉活

2、利用系统Service机制拉活:

将Service中的onStartCommand()回调方法的返回值设为START_STICKY,就可以利用系统机制在Service挂掉后自动拉活。
拓展:onStartCommand()的返回值表明当Service由于系统内存不足而被系统杀掉之后,在未来的某个时间段内当系统内存足够的情况下,系统会尝试创建这个Service,一旦创建成功就又会回调onStartCommand()方法。
缺点(无法拉活的情形):Service第一次被异常杀死后会在5s内重启,第二次会在10s内重启,第三次会在20s内重启,若Service在短时间内被杀死的次数超过3次以上系统就会不惊醒拉活;进程被取得root权限的管理工具或系统工具通过强制stop时,通过Service机制无法重启进程。

3、利用JobScheduler机制拉活

说明:android在5.0后提供了JobScheduler接口,这个接口能够监听主进程的存活,然后拉活进程。

4、利用Native进程拉活

思想:利用Linux中的fork机制创建一个Native进程,在Native进程可以监控主进程的存活,当主进程挂掉之后,Native进程可以立即对主进程进行拉活。
在Native进程中如何监听主进程被杀死:可在Native进程中通过死循环或定时器,轮询地判断主进程被杀死,但是此方案会耗时耗资源;在主线程中创建一个监控文件,并且在主进程中持有文件锁,在拉活进程启动后申请文件锁将会被阻塞,一旦成功获取到锁说明主进程挂掉了。
如何在Native进程中拉活主进程:主要通过一个am命令即可拉活。
说明:android5.0后系统对Native进程加强了管理,利用Native进程拉活的方式已失效。

RxJava2原理解析

发表于 2019-08-09 | 分类于 Android知识点

本篇主要对以下三个方面进行讲解:

  • RxJava是流式编程,在每一条流中,都至少包含三个要素:源头/被订阅者(Observable或Flowable)、订阅者(Observer或subscriber)、触发时机(subscribe()方法);
  • 其次就是线程切换(subscribeOn()和observeOn());
  • 最后就是数据操作(如map()、flatMap()等)。

1、订阅(subscribe)

首先看下最简单的情况:

Observable.create(new ObservableOnSubscribe<String>() {
    @Override
    public void subscribe(ObservableEmitter<String> emitter) throws Exception {
        // doSomething, eg: 
        // emitter.onNext("onNext");
        // emitter.onComplete();
    }
}).subscribe();

一条RxJava流若是没有调用subscribe()方法,该流便无法执行,即必须由subscribe()确定了订阅关系后这条流才能生效,原因如下:

// Observable.java
public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
    ObjectHelper.requireNonNull(source, "source is null");
    // 可以直接忽略RxJavaPlugins的相关方法,不影响我们理解原理
    return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source)); 
 }
// 无论调用subscribe的哪个重载方法,最终都会走到这个方法
public final void subscribe(Observer<? super T> observer) {
    ... // 省去不重要代码
    subscribeActual(observer);
    ...
}
protected abstract void subscribeActual(Observer<? super T> observer);

可以看到subscribe()里面主要是调用了subscribeActual,而subscribeActual是一个抽象方法,所以具体实现在子类中,这里的子类便是ObservableCreate,再来看它的实现:

// ObservableCreate.java
public ObservableCreate(ObservableOnSubscribe<T> source) {
    this.source = source;
}
protected void subscribeActual(Observer<? super T> observer) {
    CreateEmitter<T> parent = new CreateEmitter<T>(observer);
    observer.onSubscribe(parent);
    try {
        source.subscribe(parent);
    } catch (Throwable ex) {
        Exceptions.throwIfFatal(ex);
        parent.onError(ex);
    }
}

ObservableCreate中subscribeActual的实现就是将我们的observer封装成CreateEmitter(ObservableEmitter的实现类),再执行observer.onSubscribe,确保onSubscribe总能在onNext等其他订阅行为之前执行,接着就是我们的核心代码:source.subscribe(parent),source便是我们一开始创建流时新建的ObservableOnSubscribe对象,而parent则是封装后的CreateEmitter,所以其实此时执行的便是在创建ObservableOnSubscribe时实现的public void subscribe(ObservableEmitter emitter) throws Exception方法,此时整条流的订阅关系便成立了。

现在我们知道,事件流的执行实际上是由子类实现的subscribeActual控制的,所以其他的Observable创建方式也是一样的道理,这里再以fromIterable为例看一下:

// Observable.java
public static <T> Observable<T> fromIterable(Iterable<? extends T> source) {
    ObjectHelper.requireNonNull(source, "source is null");
    return RxJavaPlugins.onAssembly(new ObservableFromIterable<T>(source));
}

// ObservableFromIterable.java
public ObservableFromIterable(Iterable<? extends T> source) {
    this.source = source;
}
@Override
public void subscribeActual(Observer<? super T> s) {
    Iterator<? extends T> it;
    try {
        it = source.iterator();
    } catch (Throwable e) {
        Exceptions.throwIfFatal(e);
        EmptyDisposable.error(e, s);
        return;
    }
    boolean hasNext;
    try {
        hasNext = it.hasNext();
    } catch (Throwable e) {
        Exceptions.throwIfFatal(e);
        EmptyDisposable.error(e, s);
        return;
    }
    if (!hasNext) {
        EmptyDisposable.complete(s);
        return;
    }
    FromIterableDisposable<T> d = new FromIterableDisposable<T>(s, it);
    s.onSubscribe(d);
    if (!d.fusionMode) {
        d.run();
    }
}    

可以看到,这里是将observer和我们的数据源列表封装为FromIterableDisposable,然后执行d.run(),下面看下run的实现:

// FromIterableDisposable.java
FromIterableDisposable(Observer<? super T> actual, Iterator<? extends T> it) {
    this.actual = actual;
    this.it = it;
}
void run() {
    boolean hasNext;
    do {
        if (isDisposed()) {
            return;
        }
        T v;
        try {
            v = ObjectHelper.requireNonNull(it.next(), "The iterator returned a null value");
        } catch (Throwable e) {
            Exceptions.throwIfFatal(e);
            actual.onError(e);
            return;
        }
        actual.onNext(v);
        if (isDisposed()) {
            return;
        }
        try {
            hasNext = it.hasNext();
        } catch (Throwable e) {
            Exceptions.throwIfFatal(e);
            actual.onError(e);
            return;
        }
    } while (hasNext);
    if (!isDisposed()) {
        actual.onComplete();
    }
}

在run方法中不停地遍历数据源列表,然后根据实际情况执行对应的事件处理方法(actual.onNext(v);等,actual即为我们传进来的observer),这便完成了RxJava流的处理。

总结:RxJava2的订阅原理其实便是在subcribe时执行子类中实现的subscribeActual方法,该方法最终会去调用observer相关的订阅方法,可理解为观察者模式的一种变形。

2、线程切换

subscribeOn

// Observable.java
public final Observable<T> subscribeOn(Scheduler scheduler) {
    ObjectHelper.requireNonNull(scheduler, "scheduler is null");
    return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
    }

subscribeOn时其实也是创建了一个Observable子类去实现subscribeActual方法:

// ObservableSubscribeOn.java
public void subscribeActual(final Observer<? super T> s) {
    final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);
    s.onSubscribe(parent);
    parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
}

这里将下游的Observer s封装成SubscribeOnObserver后又封装成Runnable的实现类SubscribeTask,在run方法中source.subscribe(parent):

// ObservableSubscribeOn.java
final class SubscribeTask implements Runnable {
    private final SubscribeOnObserver<T> parent;
    SubscribeTask(SubscribeOnObserver<T> parent) {
        this.parent = parent;
    }
    @Override
    public void run() {
        source.subscribe(parent);
    }
}

然后通过scheduler.scheduleDirect(new SubscribeTask(parent))将这个Task放到Worker中:

// Scheduler.java
public Disposable scheduleDirect(@NonNull Runnable run) {
    return scheduleDirect(run, 0L, TimeUnit.NANOSECONDS);
}
public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
    final Worker w = createWorker();
    final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);
    DisposeTask task = new DisposeTask(decoratedRun, w);
    w.schedule(task, delay, unit);
    return task;
}
public abstract Worker createWorker();

而createWorker由Scheduler子类实现,即我们执行的线程类型,如AndroidSchedulers.mainThread()、Schedulers.io(),这里以mainThread()为例,

// AndroidSchedulers.java
public static Scheduler mainThread() {
    return RxAndroidPlugins.onMainThreadScheduler(MAIN_THREAD);
}
private static final Scheduler MAIN_THREAD = RxAndroidPlugins.initMainThreadScheduler(new Callable<Scheduler>() {
    @Override 
    public Scheduler call() throws Exception {
        return MainHolder.DEFAULT;
    }
});
private static final class MainHolder {
    static final Scheduler DEFAULT = new HandlerScheduler(new Handler(Looper.getMainLooper()));
}

RxAndroidPlugins里面的非常简单,这里最终返回的就是HandlerScheduler,因此来看下它实现的createWorker和schedule方法:

// HandlerScheduler.java
public Worker createWorker() { return new HandlerWorker(handler); }

在HandlerWorker中实现schedule(task, delay, unit):

// HandlerWorker.java
public Disposable schedule(Runnable run, long delay, TimeUnit unit) {
    ...
    ScheduledRunnable scheduled = new ScheduledRunnable(handler, run);
    Message message = Message.obtain(handler, scheduled);
    message.obj = this; // Used as token for batch disposal of this worker's runnables.
    handler.sendMessageDelayed(message, unit.toMillis(delay));
    ....
    return scheduled;
}

这里将handler(由上面创建DEFAULT Scheduler时可知,该handler为UI线程的handler)和run又再次封装为ScheduledRunnable,然后通过handler发送到主线程处理,因此便保证了订阅操作(source.subscribe(parent))执行在主线程。

同时,由上面可知,rx流由subscribe开始触发,然后执行source.subscribe(observer),然而source可能也是上游操作后的产物(如map),因此便会触发上游内部的subscribe,直到源头,即rx流由subscribe开始触发,然后逆向向上寻找直到源头,才开始真正的执行。因此,若是有多个subscribeOn,最终的subscribe也是被放到最上面的subscribeOn(即第一个)指定的线程中执行,这便是指定多个subscribeOn只有第一个生效的原因。

总结:其实就是将订阅操作放到Runnable中执行,并结合handler机制将Runnable发送到主线程,对于其他线程(不同的线程模式会创建不同的Scheduler,并持有对应的线程池)则是将Runnable交给指定线程池处理。这便保证了在指定线程获取/处理数据源(observable)。

observeOn

// Observable.java
public final Observable<T> observeOn(Scheduler scheduler) {
    return observeOn(scheduler, false, bufferSize());
}
public final Observable<T> observeOn(Scheduler scheduler, boolean delayError, int bufferSize) {
    ObjectHelper.requireNonNull(scheduler, "scheduler is null");
    ObjectHelper.verifyPositive(bufferSize, "bufferSize");
    return RxJavaPlugins.onAssembly(new ObservableObserveOn<T>(this, scheduler, delayError, bufferSize));
}

observeOn的线程切换则是在ObservableObserveOn中处理的:

// ObservableObserveOn.java
protected void subscribeActual(Observer<? super T> observer) {
    if (scheduler instanceof TrampolineScheduler) {
         source.subscribe(observer);
    } else {
        Scheduler.Worker w = scheduler.createWorker();
        source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
    }
}

这里也是根据指定线程类型创建Worker(可参考上面subscribeOn原理),并将observer和w一同放到ObserveOnObserver中:

// ObserveOnObserver.java
public void onNext(T t) {
    ....
    schedule();
}
void schedule() {
    if (getAndIncrement() == 0) {
        worker.schedule(this);
    }
}
public void run() {
    if (outputFused) {
        drainFused();
    } else {
         drainNormal();
    }
}
void drainNormal() {
    ...
    for (;;) {
        ...
        for (;;) {
            ...
            a.onNext(v);
        }
        ...
    }
}

可以看到,onNext执行的是schedule,而schedule则是将该对象直接放到指定线程的Worker中,然后在run中去执行对应的事件处理方法(onNext等),因此便实现了将下游的observer放到指定线程执行的目的,当然,这里只是将其直接下游的observer放到指定线程而已,对于其下游的下游则不一定。也就是说,observeOn可以有多个,每个都是对其直接下游做线程切换,若是下游不再切换,则所有下游都在该指定线程中执行。

总结:observeOn其实就是将下游的observer放到指定线程里面去执行。

3、数据变换

在事件从源头流到最终观察者的过程中,我们可以对事件进行操作转换,这里以map和flatMap为例进行解析。

map

// Observable.java
public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
    ObjectHelper.requireNonNull(mapper, "mapper is null");
    return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
}

执行map操作时其实是将上游的observable和我们自己实现的mapper封住成一个ObservableMap,所以具体实现就是在ObservableMap的subscribeActual中:

// ObservableMap.java
public void subscribeActual(Observer<? super U> t) {
    source.subscribe(new MapObserver<T, U>(t, function));
}

看到这句是不是很熟悉,从上面的订阅原理可知,到这里其实真正执行的便是MapObserver中的onNext等方法了,而其onNext里便是先执行map转换,再将转换结果交给下游的observer执行:

// MapObserver.java
@Override
public void onNext(T t) {
    if (done) { // error or complete
        return;
    }
    if (sourceMode != NONE) {
        actual.onNext(null);
        return;
    }
    U v;
    try {
    v = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper function returned a null value.");
    } catch (Throwable ex) {
        fail(ex);
    return;
    }
    actual.onNext(v);
}

flatMap

我们调用的flatMap最终执行的是该重载方法:

// Observable.java
public final <R> Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency, int bufferSize) {
     ...
     return RxJavaPlugins.onAssembly(new ObservableFlatMap<T, R>(this, mapper, delayErrors, maxConcurrency, bufferSize));
}

因此看下ObservableFlatMap:

// ObservableFlatMap.java
public ObservableFlatMap(ObservableSource<T> source, Function<? super T, ? extends ObservableSource<? extends U>> mapper, boolean delayErrors, int maxConcurrency, int bufferSize) {
    super(source);
    this.mapper = mapper;
    this.delayErrors = delayErrors;
    this.maxConcurrency = maxConcurrency;
    this.bufferSize = bufferSize;
}
@Override
public void subscribeActual(Observer<? super U> t) {
    if (ObservableScalarXMap.tryScalarXMapSubscribe(source, t, mapper)) {
    return;
    }
    source.subscribe(new MergeObserver<T, U>(t, mapper, delayErrors, maxConcurrency, bufferSize));
}

所以实际的操作是在MergeObserver,这里我们就看下onNext就好了:

// MergeObserver.java
public void onNext(T t) {
    // safeguard against misbehaving sources
    if (done) {
        return;
    }
    ObservableSource<? extends U> p;
    try {
        p = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper returned a null ObservableSource");
    } catch (Throwable e) {
        Exceptions.throwIfFatal(e);
        s.dispose();
        onError(e);
        return;
    }
    if (maxConcurrency != Integer.MAX_VALUE) {
        synchronized (this) {
            if (wip == maxConcurrency) {
                sources.offer(p);
                return;
            }
            wip++;
        }
    }
    subscribeInner(p);
}

可以看到,这里是想将上游传进来的对象通过我们自己实现的mapper进行转换,然后再执行subscribeInner(p)(maxConcurrency默认就是Integer.MAX_VALUE),因此看下subscribeInner(p):

// MergeObserver.java
void subscribeInner(ObservableSource<? extends U> p) {
    for (;;) {
        if (p instanceof Callable) {
            if (tryEmitScalar(((Callable<? extends U>)p)) && maxConcurrency != Integer.MAX_VALUE) {
                boolean empty = false;
                synchronized (this) {
                    p = sources.poll();
                    if (p == null) {
                        wip--;
                        empty = true;
                    }
                }
                if (empty) {
                    drain();
                    break;
                }
            } else {
                break;
            }
        } else {
            InnerObserver<T, U> inner = new InnerObserver<T, U>(this, uniqueId++);
            if (addInner(inner)) {
                p.subscribe(inner);
            }
            break;
        }
    }
}

这里是个无线循环,根据是否为Callable类型执行不同的逻辑,一般Observable.just为Callable,而from类型的则不是。这里看下from的逻辑,毕竟Callable类型又没指定maxConcurrency的话,是直接break,所以没什么好看的。而非Callable类型的,可以看到这里又封装成了InnerObserver,而for循环并没有什么用。

总结:在执行操作符方法(如map、flatMap等)时,会生成对应的Observable对象,在该对象中实现具体业务逻辑,对上游流下来的数据进行操作,再将处理后的结果交给下游的的订阅者继续处理。

4、总结

RxJava2事件流的产生由subscribe方法调用subscribeActual(observer)触发,而subscribeActual由Observable子类实现,每个子类里的实现逻辑不同,可能会先执行自己的操作(如map或flatMap等 ),但最终都会调用source.subscribe,source即为该节点的上游数据源,因此需要上游操作执行完才能拿到source,最终便形成逐级逆向向上获取数据源(Observable或Flowable),即形成了从最开始的源头发射数据一路向下经过各个节点的操作后交给最终观察者的链式模型。

而对于线程切换,subscribeOn即是将订阅操作(observer)放到Runnable中执行,并将Runnable放到指定线程里操作;observeOn则是将下游的observer放到指定线程里面去执行。

5、参考资料

https://blog.csdn.net/reakingf/article/details/84845705
What’s different in 3.0

RxJava2五种被观察者及背压

发表于 2019-08-07 | 分类于 Android知识点

RxJava五种被观察者为Flowable, Observable,Single, Completable, Maybe。

五种被观察者可通过toFlowable, toObservable,toSingle, toCompletable, toMaybe相互转换。

1、Flowable

1.1、Flowable简介

Flowable类,用于实现Reactive-Streams模式,并提供工厂方法,中间运算符以及使用反应式数据流的能力。

Reactive-Streams使用Flowable运行,Flowable实现了Publishers。因此,许多运算符直接接受Publishers,并允许与其他Reactive-Streams的实现进行直接交互操作

public abstract class Flowable<T> implements Publisher<T>

Flowable为操作符提供128个元素的默认缓冲区大小,可通过bufferSize() 方法获取,可通过系统参数rx2.buffer-size全局覆盖。但是大多数运算符都有重载,允许显式设置其内部缓冲区大小。

/** The default buffer size. */
static final int BUFFER_SIZE;
static {
    BUFFER_SIZE = Math.max(16, Integer.getInteger("rx2.buffer-size", 128));
}

/**
 * Returns the default internal buffer size used by most async operators.
 * <p>The value can be overridden via system parameter {@code rx2.buffer-size}
 * <em>before</em> the Flowable class is loaded.
 * @return the default internal buffer size.
 */
public static int bufferSize() {
    return BUFFER_SIZE;
}

1.2、Flowable官方图解


1)看到上图有点疑问,不是在说Flowable嘛,怎么图解里的说明是Observable呢?

2)其实在官方文档里面Flowable和Observable都使用的是上面这个图解,因此这两个类肯定是提供相似功能,既然是相似,那么这幅图就是他们的共性,那不同的地方是什么呢?

不同之处是:Flowable支持Backpressure,Observable不支持Backpressure;只有在需要处理背压问题时,才需要使用Flowable。

由于只有在上下游运行在不同的线程中,且上游发射数据的速度大于下游接收处理数据的速度时,才会产生背压问题;
所以,如果能够确定:
1、上下游运行在同一个线程中,
2、上下游工作在不同的线程中,但是下游处理数据的速度不慢于上游发射数据的速度,
3、上下游工作在不同的线程中,但是数据流中只有一条数据
则不会产生背压问题,就没有必要使用Flowable,以免影响性能。

类似于Observable,在使用Flowable时,也可以通过create操作符创建发射数据流,代码如下:

Flowable.create(new FlowableOnSubscribe<Integer>() {
            @Override
            public void subscribe(FlowableEmitter<Integer> e) throws Exception {
                e.onNext(1);
                e.onNext(2);
                e.onNext(3);
                e.onComplete();
            }
        }, BackpressureStrategy.BUFFER) //create方法中多了一个BackpressureStrategy类型的参数
        .subscribeOn(Schedulers.newThread())//为上下游分别指定各自的线程
        .observeOn(Schedulers.newThread())
        .subscribe(new Subscriber<Integer>() {
            @Override
            public void onSubscribe(Subscription s) {   //onSubscribe回调的参数不是Disposable而是Subscription
                s.request(Long.MAX_VALUE);            //注意此处,暂时先这么设置
            }

            @Override
            public void onNext(Integer integer) {
                System.out.println("接收----> " + integer);
            }

            @Override
            public void onError(Throwable t) {
            }

            @Override
            public void onComplete() {
                System.out.println("接收----> 完成");
            }
        });

运行结果如下:

System.out: 接收----> 1
System.out: 接收----> 2
System.out: 接收----> 3
System.out: 接收----> 完成

发射与处理数据流在形式上与Observable大同小异,发射器中均有onNext,onError,onComplete方法,订阅器中也均有onSubscribe,onNext,onError,onComplete方法。
但是在细节方面还是有三点不同:
一、create方法中多了一个BackpressureStrategy类型的参数。
二、订阅器Subscriber中,方法onSubscribe回调的参数不是Disposable而是Subscription,多了行代码:s.request(Long.MAX_VALUE);
三、Flowable发射数据时,使用特有的发射器FlowableEmitter,不同于Observable的ObservableEmitter

1.3、Backpressure

在通过create操作符创建Flowable时,多了一个BackpressureStrategy类型的参数,BackpressureStrategy是个枚举,源码如下:

/**
 * Represents the options for applying backpressure to a source sequence.
 */
public enum BackpressureStrategy {
    /**
     * OnNext events are written without any buffering or dropping.
     * Downstream has to deal with any overflow.
     * <p>Useful when one applies one of the custom-parameter onBackpressureXXX operators.
     */
    MISSING,
    /**
     * Signals a MissingBackpressureException in case the downstream can't keep up.
     */
    ERROR,
    /**
     * Buffers <em>all</em> onNext values until the downstream consumes it.
     */
    BUFFER,
    /**
     * Drops the most recent onNext value if the downstream can't keep up.
     */
    DROP,
    /**
     * Keeps only the latest onNext value, overwriting any previous value if the
     * downstream can't keep up.
     */
    LATEST
}

当上游发送数据的速度快于下游接收数据的速度,且运行在不同的线程中时,Flowable通过自身特有的异步缓存池,来缓存没来得及处理的数据,缓存池的容量上限为128

BackpressureStrategy的作用便是用来设置Flowable通过异步缓存池缓存数据的策略。在源码FlowableCreate类中,可以看到五个泛型分别对应五个java类,通过代理模式对原始的发射器进行了包装:

Override
public void subscribeActual(Subscriber<? super T> t) {
    BaseEmitter<T> emitter;

    switch (backpressure) {
        case MISSING: {
            emitter = new MissingEmitter<T>(t);
            break;
        }
        case ERROR: {
            emitter = new ErrorAsyncEmitter<T>(t);
            break;
        }
        case DROP: {
            emitter = new DropAsyncEmitter<T>(t);
            break;
        }
        case LATEST: {
            emitter = new LatestAsyncEmitter<T>(t);
            break;
        }
        default: {
            emitter = new BufferAsyncEmitter<T>(t, bufferSize());
            break;
        }
    }

    t.onSubscribe(emitter);
    try {
        source.subscribe(emitter);
    } catch (Throwable ex) {
        Exceptions.throwIfFatal(ex);
        emitter.onError(ex);
    }
}

ERROR

对应于ErrorAsyncEmitter类,在其源码

static final class ErrorAsyncEmitter<T> extends NoOverflowBaseAsyncEmitter<T> {
        private static final long serialVersionUID = 338953216916120960L;

        ErrorAsyncEmitter(Subscriber<? super T> actual) {
            super(actual);
        }

        @Override
        void onOverflow() {
            onError(new MissingBackpressureException("create: could not emit value due to lack of requests"));
        }

    }

abstract static class NoOverflowBaseAsyncEmitter<T> extends BaseEmitter<T> {

        private static final long serialVersionUID = 4127754106204442833L;

        NoOverflowBaseAsyncEmitter(Subscriber<? super T> actual) {
            super(actual);
        }

        @Override
        public final void onNext(T t) {
            if (isCancelled()) {
                return;
            }

            if (t == null) {
                onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
                return;
            }

            if (get() != 0) {
                actual.onNext(t);
                BackpressureHelper.produced(this, 1);
            } else {
                onOverflow();
            }
        }

        abstract void onOverflow();
    }

onOverflow方法中可以看到,在此策略下,如果放入Flowable的异步缓存池中的数据超限了,则会抛出MissingBackpressureException异常。父类的onNext中,在判断get() != 0,即缓存池未满的情况下,才会让被代理类调用onNext方法。运行如下代码:

Flowable.create(new FlowableOnSubscribe<Integer>() {
            @Override
            public void subscribe(FlowableEmitter<Integer> e) throws Exception {
                for (int i = 1; i <= 129; i++) {
                    e.onNext(i);
                }
                e.onComplete();
            }
        }, BackpressureStrategy.ERROR)
        .subscribeOn(Schedulers.newThread())
        .observeOn(Schedulers.newThread())
        .subscribe(new Subscriber<Integer>() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(Long.MAX_VALUE);            //注意此处,暂时先这么设置
            }

            @Override
            public void onNext(Integer integer) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException ignore) {
                }
                System.out.println(integer);
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("接收----> 完成");
            }
        });

创建并通过Flowable发射129条数据,Subscriber的onNext方法睡10秒之后再开始接收,运行后会发现控制台打印如下异常:

W/System.err: io.reactivex.exceptions.MissingBackpressureException: create: could not emit value due to lack of requests
W/System.err:     at io.reactivex.internal.operators.flowable.FlowableCreate$ErrorAsyncEmitter.onOverflow(FlowableCreate.java:411)
W/System.err:     at io.reactivex.internal.operators.flowable.FlowableCreate$NoOverflowBaseAsyncEmitter.onNext(FlowableCreate.java:377)

如果将Flowable发射数据的条数改为128,则不会出现此异常。

DROP

对应于DropAsyncEmitter类,通过DropAsyncEmitter类和它父类NoOverflowBaseAsyncEmitter的源码

static final class DropAsyncEmitter<T> extends NoOverflowBaseAsyncEmitter<T> {
    private static final long serialVersionUID = 8360058422307496563L;

    DropAsyncEmitter(Subscriber<? super T> actual) {
        super(actual);
    }

    @Override
    void onOverflow() {
        // nothing to do
    }
}

可以看到,DropAsyncEmitter的onOverflow是个空方法,没有执行任何操作。所以在此策略下,如果Flowable的异步缓存池满了,会丢掉上游发送的数据。

存池中数据的清理,并不是Subscriber接收一条,便清理一条,而是存在一个延迟,等累积一段时间后统一清理一次。也就是Subscriber接收到第96条数据时,缓存池才开始清理数据,之后Flowable发射的数据才得以放入。如果数据处于缓存池存满的状态时,则被丢弃。

LATEST

对应于LatestAsyncEmitter类
与Drop策略一样,如果缓存池满了,会丢掉将要放入缓存池中的数据,不同的是,不管缓存池的状态如何,LATEST都会将最后一条数据强行放入缓存池中,来保证观察者在接收到完成通知之前,能够接收到Flowable最新发射的一条数据。

BUFFER

Flowable处理背压的默认策略,对应于BufferAsyncEmitter类

其部分源码为:

static final class BufferAsyncEmitter<T> extends BaseEmitter<T> {
        private static final long serialVersionUID = 2427151001689639875L;
        final SpscLinkedArrayQueue<T> queue;
        . . . . . .
        final AtomicInteger wip;
        BufferAsyncEmitter(Subscriber<? super T> actual, int capacityHint) {
            super(actual);
            this.queue = new SpscLinkedArrayQueue<T>(capacityHint);
            this.wip = new AtomicInteger();
        }
        . . . . . .
}

在其构造方法中可以发现,其内部维护了一个缓存池SpscLinkedArrayQueue,其大小不限,此策略下,如果Flowable默认的异步缓存池满了,会通过此缓存池暂存数据,它与Observable的异步缓存池一样,可以无限制向里添加数据,不会抛出MissingBackpressureException异常,但会导致OOM。

和使用Observable时一样,都会导致内存剧增,最后导致OOM,不同的是使用Flowable内存增长的速度要慢得多,那是因为基于Flowable发射的数据流,以及对数据加工处理的各操作符都添加了背压支持,附加了额外的逻辑,其运行效率要比Observable低得多。

MISSING

对应于MissingEmitter类,通过其源码:

static final class MissingEmitter<T> extends BaseEmitter<T> {


        private static final long serialVersionUID = 3776720187248809713L;

        MissingEmitter(Subscriber<? super T> actual) {
            super(actual);
        }

        @Override
        public void onNext(T t) {
            if (isCancelled()) {
                return;
            }

            if (t != null) {
                actual.onNext(t);
            } else {
                onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
                return;
            }

            for (;;) {
                long r = get();
                if (r == 0L || compareAndSet(r, r - 1)) {
                    return;
                }
            }
        }

    }

可以发现,在传递数据时

actual.onNext(t);

并没有对缓存池的状态进行判断,所以在此策略下,通过Create方法创建的Flowable相当于没有指定背压策略,不会对通过onNext发射的数据做缓存或丢弃处理,需要下游通过背压操作符(onBackpressureBuffer()/onBackpressureDrop()/onBackpressureLatest())指定背压策略。

onBackpressureXXX背压操作符:Flowable除了通过create创建的时候指定背压策略,也可以在通过其它创建操作符just,fromArray等创建后通过背压操作符指定背压策略。

onBackpressureBuffer()对应BackpressureStrategy.BUFFER
onBackpressureDrop()对应BackpressureStrategy.DROP
onBackpressureLatest()对应BackpressureStrategy.LATEST

例如代码

Flowable.range(0, 500)
        .onBackpressureDrop()
        .subscribeOn(Schedulers.newThread())
        .observeOn(Schedulers.newThread())
        .subscribe(new Consumer<Integer>() {
            @Override
            public void accept(@NonNull Integer integer) throws Exception {
                System.out.println(integer);
            }
        });

Subscription

Subscription与Disposable均是观察者与可观察对象建立订阅状态后回调回来的参数,如同通过Disposable的dispose()方法可以取消Observer与Oberverable的订阅关系一样,通过Subscription的cancel()方法也可以取消Subscriber与Flowable的订阅关系。
不同的是接口Subscription中多了一个方法request(long n),如上面代码中的:

s.request(Long.MAX_VALUE);   

Flowable在设计的时候,采用了一种新的思路——响应式拉取方式,来设置下游对数据的请求数量,上游可以根据下游的需求量,按需发送数据。如果不显示调用request则默认下游的需求量为零,上游Flowable发射的数据不会交给下游Subscriber处理。

多次调用request,数字会累积。

上游并没有根据下游的实际需求,发送数据,而是能发送多少,就发送多少,不管下游是否需要。而且超出下游需求之外的数据,仍然放到了异步缓存池中。这点我们可以通过以下代码来验证:

Flowable.create(new FlowableOnSubscribe<Integer>() {
            @Override
            public void subscribe(FlowableEmitter<Integer> e) throws Exception {
                for (int i = 1; i < 130; i++) {
                    System.out.println("发射---->" + i);
                    e.onNext(i);
                }
                e.onComplete();
            }
        }, BackpressureStrategy.ERROR)
        .subscribeOn(Schedulers.newThread())
        .observeOn(Schedulers.newThread())
        .subscribe(new Subscriber<Integer>() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(1);
            }

            @Override
            public void onNext(Integer integer) {
                System.out.println("接收------>" + integer);
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }

            @Override
            public void onComplete() {
                System.out.println("接收------>完成");
            }
        });

通过Flowable发射130条数据,通过s.request(1)设置下游的数据请求量为1条,设置缓存策略为BackpressureStrategy.ERROR,如果异步缓存池超限,会导致MissingBackpressureException异常。久违的异常出现了,所以超出下游需求之外的数据,仍然放到了异步缓存池中,并导致缓存池溢出。

那么上游如何才能按照下游的请求数量发送数据呢,虽然通过request可以设置下游的请求数量,但是上游并没有获取到这个数量,如何获取呢?这便需要用到Flowable与Observable的第三点区别,Flowable特有的发射器FlowableEmitter

FlowableEmitter

flowable的发射器FlowableEmitter与observable的发射器ObservableEmitter均继承自Emitter
比较两者源码可以发现:

public interface ObservableEmitter<T> extends Emitter<T> {

    void setDisposable(Disposable d);

    void setCancellable(Cancellable c);

    boolean isDisposed();

    ObservableEmitter<T> serialize();
}

与

public interface FlowableEmitter<T> extends Emitter<T> {

    void setDisposable(Disposable s);

    void setCancellable(Cancellable c);

    long requested();

    boolean isCancelled();

    FlowableEmitter<T> serialize();
}

接口FlowableEmitter中多了一个方法

long requested();

上游在发送数据的时候并不需要考虑下游需不需要,而只需要考虑异步缓存池中是否放得下,放得下便发,放不下便暂停。所以,通过e.requested()获取到的值,并不是下游真正的数据请求数量,而是异步缓存池中可放入数据的数量。数据放入缓存池中后,再由缓存池按照下游的数据请求量向下传递,待到传递完的数据累积到95条之后,将其清除,腾出空间存放新的数据。如果下游处理数据缓慢,则缓存池向下游传递数据的速度也相应变慢,进而没有传递完的数据可清除,也就没有足够的空间存放新的数据,上游通过e.requested()获取的值也就变成了0,如果此时,再发送数据的话,则会根据BackpressureStrategy背压策略的不同,抛出MissingBackpressureException异常,或者丢掉这条数据。

我们可以通过这个方法来获取当前未完成的请求数量,上游只需要在e.requested()等于0时,暂停发射数据,便可解决背压问题。

最终方案

下面,对其通过Flowable做些改进,让其既不会产生背压问题,也不会引起异常或者数据丢失。
代码如下:

Flowable.create(new FlowableOnSubscribe<Integer>() {
            @Override
            public void subscribe(FlowableEmitter<Integer> e) throws Exception {
                int i = 0;
                while (true) {
                    if (e.requested() == 0) continue;//此处添加代码,让flowable按需发送数据
                    System.out.println("发射---->" + i);
                    i++;
                    e.onNext(i);
                }
            }
        }, BackpressureStrategy.MISSING)
        .subscribeOn(Schedulers.newThread())
        .observeOn(Schedulers.newThread())
        .subscribe(new Subscriber<Integer>() {
            private Subscription mSubscription;

            @Override
            public void onSubscribe(Subscription s) {
                s.request(1);            //设置初始请求数据量为1
                mSubscription = s;
            }

            @Override
            public void onNext(Integer integer) {
                try {
                    Thread.sleep(50);
                    System.out.println("接收------>" + integer);
                    mSubscription.request(1);//每接收到一条数据增加一条请求量
                } catch (InterruptedException ignore) {
                }
            }

            @Override
            public void onError(Throwable t) {
            }

            @Override
            public void onComplete() {
            }
        });

下游处理数据的速度Thread.sleep(50)赶不上上游发射数据的速度,不同的是,我们在下游onNext(Integer integer) 方法中,每接收一条数据增加一条请求量,

mSubscription.request(1)

在上游添加代码

if(e.requested()==0)continue;

让上游按需发送数据,上游严格按照下游的需求量发送数据,不会产生MissingBackpressureException异常,或者丢失数据。

2、Observable

2.1、Observable简介

Observable类是不支持背压的,Observable是Reactive的一个抽象基类,Observable提供工厂方法,中间运算符以及消费同步和/或异步数据流的功能。

Observable类中的多数运算符接受一个或者多个ObservableSource,ObservableSource是非背压的基本接口,Observable实现了这个接口。

public abstract class Observable implements ObservableSource
默认情况下,Observable的为其运算符提供128个元素的缓冲区大小运行,可看考Flowable.bufferSize(),可以通过系统参数rx2.buffer-size全局覆盖。但是,大多数运算符都有重载,允许设置其内部缓冲区大小。

2.2、Flowable和Observable对比

在上面已经说明了二者最大的区别。

官方也给出的解释是:

The design of this class was derived from the Reactive-Streams design and specification by removing any backpressure-related infrastructure and implementation detail, replacing the org.reactivestreams.Subscription with Disposable as the primary means to dispose of a flow.

中文翻译:

该类的设计源自Reactive-Streams设计和规范,通过删除任何与背压相关的基本结构和实现细节,将Disposable替换为org.reactivestreams.Subscription作为处理流的主要方式。

从代码层面上做简单的说明,Flowable实现了Publisher接口,Publisher源码如下:

public interface Publisher<T> {

    /**
     * Request {@link Publisher} to start streaming data.
     * <p>
     * This is a "factory method" and can be called multiple times, each time starting a new {@link Subscription}.
     * <p>
     * Each {@link Subscription} will work for only a single {@link Subscriber}.
     * <p>
     * A {@link Subscriber} should only subscribe once to a single {@link Publisher}.
     * <p>
     * If the {@link Publisher} rejects the subscription attempt or otherwise fails it will
     * signal the error via {@link Subscriber#onError}.
     *
     * @param s the {@link Subscriber} that will consume signals from this {@link Publisher}
     */
    public void subscribe(Subscriber<? super T> s);
}

Observable实现了ObservableSource接口,ObservableSource源码如下

1
2
3
4
5
6
7
8
9
public interface ObservableSource<T> {

/**
* Subscribes the given Observer to this ObservableSource instance.
* @param observer the Observer, not null
* @throws NullPointerException if {@code observer} is null
*/
void subscribe(Observer<? super T> observer);
}

对比ObservableSource和Publisher,都有一个同名的接口subscribe()

2.4、形象理解ObservableSource和Publisher有何异同

ObservableSource:可观察源

Publisher:发布者

subscribe:订阅

Subscriber:订阅者

Observer:观察者

对于ObservableSource,可以将subscribe(Observer observer)理解为Observer通过subscribe订阅了ObservableSource

对于Publisher,可以将subscribe(Subscriber s)理解为Subscriber通过subscribe订阅了Publisher

上面的解释可能比较抽象,通俗的举个例子,来个角色扮演

第一组:报刊(ObservableSource)、报刊订阅者(Observer)、订阅报刊的行为(subscribe)

第二组:报刊发布人(Publisher)、报刊订阅者(Subscriber)、订阅报刊的行为(subscribe)

把这个场景串起来讲就是:报刊订阅者订阅了报刊,或者说报刊订阅者在报刊发布人手中订阅了报刊。

这其实是典型的观察者模式,所不同的是信息的发布者是ObservableSource还是Publisher,信息的订阅者是Observer还是Subscriber,统一的行为都是subscribe。

3、Single

3.1、Single简介

public abstract class Single<T> implements SingleSource<T> 

Single实现了SingleSource

/**
 * Represents a basic {@link Single} source base interface,
 * consumable via an {@link SingleObserver}.
 * <p>
 * This class also serves the base type for custom operators wrapped into
 * Single via {@link Single#create(SingleOnSubscribe)}.
 *
 * @param <T> the element type
 * @since 2.0
 */
public interface SingleSource<T> {

    /**
     * Subscribes the given SingleObserver to this SingleSource instance.
     * @param observer the SingleObserver, not null
     * @throws NullPointerException if {@code observer} is null
     */
    void subscribe(SingleObserver<? super T> observer);
}

Single类为单个值响应实现Reactive Pattern。

Single和Observable类似,所不同的是Single只能发出一个值,要么发射成功要么发射失败,也没有“onComplete”作为完成时的回调

Single类实现了基类SingleSource的接口,SingleObserver作为Single发出来的消息的默认消费者,SingleObserver通过subscribe(SingleObserver<? super T> observer)在Single中订阅消息

public interface SingleObserver<T> {

    /**
     * Provides the SingleObserver with the means of cancelling (disposing) the
     * connection (channel) with the Single in both
     * synchronous (from within {@code onSubscribe(Disposable)} itself) and asynchronous manner.
     * @param d the Disposable instance whose {@link Disposable#dispose()} can
     * be called anytime to cancel the connection
     * @since 2.0
     */
    void onSubscribe(Disposable d);

    /**
     * Notifies the SingleObserver with a single item and that the {@link Single} has finished sending
     * push-based notifications.
     * <p>
     * The {@link Single} will not call this method if it calls {@link #onError}.
     *
     * @param value
     *          the item emitted by the Single
     */
    void onSuccess(T value);

    /**
     * Notifies the SingleObserver that the {@link Single} has experienced an error condition.
     * <p>
     * If the {@link Single} calls this method, it will not thereafter call {@link #onSuccess}.
     *
     * @param e
     *          the exception encountered by the Single
     */
    void onError(Throwable e);
}

3.2、Single与Observable的区别

Single只能发送单个消息,不能发送消息流,而且观察者接收到消息也只有两种情况,要么接收成功,要么接收失败

3.3、Single官方图解

4、Completable

4.1、Completable简介

Completable类表示延迟计算,没有任何值,只表示完成或异常。

Completable的行为类似于Observable,在计算完成后只能发出完成或错误信号,由onComplete或onError接口来处理,没有onNext或onSuccess等回调接口

Completable实现了基类CompletableSource的接口,CompletableObserver通过subscribe()方法在Completable处订阅消息。

Completable遵循协议:onSubscribe (onComplete | onError)

public abstract class Completable implements CompletableSource


public interface CompletableSource {

    /**
     * Subscribes the given CompletableObserver to this CompletableSource instance.
     * @param cs the CompletableObserver, not null
     * @throws NullPointerException if {@code cs} is null
     */
    void subscribe(CompletableObserver cs);
}


public interface CompletableObserver {

    void onSubscribe(Disposable d);

    void onComplete();

    void onError(Throwable e);
}

从源码中我们可以看到CompletableObserver里面有三个接口:

1)onSubscribe中传入参数Disposable,由Completable调用一次,在CompletableObserver实例上设置Disposable,然后可以随时取消订阅。

2)onComplete一旦延迟计算正常完成将会被调用

3)onError 一旦延迟计算抛出异常将会被调用

4.3、Completable示例

注意: 通过Disposable调用dispose()取消订阅,后面的消息无法接收。

运行下面的例子,可以看到在调用dispose后,onStart被回调后,后续的消息就收不到了;去掉dispose,onStart回调后,三秒后onComplete将会被回调

 private void doCompletable() {
        Disposable d = Completable.complete()
                .delay(3, TimeUnit.SECONDS, Schedulers.io())
                .subscribeWith(new DisposableCompletableObserver() {
                    @Override
                    public void onStart() {
                        System.out.println("Started");
                    }

                    @Override
                    public void onError(Throwable error) {
                        error.printStackTrace();
                    }

                    @Override
                    public void onComplete() {
                        System.out.println("onComplete!");
                    }
                });


        d.dispose();

    }

运行完毕的结果是:
10-02 11:10:34.797 15565-15565/hq.demo.net I/System.out: Started

注释d.dispose()后在运行结果是:
10-02 14:34:19.490 23232-23232/hq.demo.net I/System.out: Started
10-02 14:34:22.492 23232-23483/hq.demo.net I/System.out: Done!

上面使用的是DisposableCompletableObserver通过subscribeWith来订阅消息,返回一个Disposable可以通过dispose来取消订阅关系,DisposableCompletableObserver是CompletableObserve的子类,只是增加了可取消订阅的功能。当然也能通过CompletableObserve方法操作,但是无法取消订阅关系,除此外没什么本质区别。

5、Maybe

5.1、Maybe简介

Maybe类表示延迟计算和单个值的发射,这个值可能根本没有或异常。

Maybe类实现MaybeSource的接口,MaybeObserver通过subscribe(MaybeObserver)在Maybe处订阅消息

Maybe遵循协议:onSubscribe (onSuccess | onError | onComplete),也就是Maybe发射消息后,可能会回调的接口是onSuccess | onError | onComplete

public abstract class Maybe<T> implements MaybeSource<T>

public interface MaybeSource<T> {

    /**
     * Subscribes the given MaybeObserver to this MaybeSource instance.
     * @param observer the MaybeObserver, not null
     * @throws NullPointerException if {@code observer} is null
     */
    void subscribe(MaybeObserver<? super T> observer);
}


public interface MaybeObserver<T> {
    void onSubscribe(Disposable d);
    void onSuccess(T value);
    void onError(Throwable e);
    void onComplete();
}

5.3、Maybe示例

下面是个例子,注意让线程睡多少秒可以修改测试dispose,与Completable类似,但是无论怎么onStart()都会被回调,为什么onStart()都会被回调呢?可以看DisposableMaybeObserver源码,在订阅消息的时候就会首先回调onSubscribe,这个时候dispose还没有运行了,这个动作发生在订阅的时候,没有订阅何来取消订阅呢。

public abstract class DisposableMaybeObserver<T> implements MaybeObserver<T>, Disposable {
    final AtomicReference<Disposable> s = new AtomicReference<Disposable>();

    @Override
    public final void onSubscribe(Disposable s) {
        if (DisposableHelper.setOnce(this.s, s)) {
            onStart();
        }
    }

    /**
     * Called once the single upstream Disposable is set via onSubscribe.
     */
    protected void onStart() {
    }

    @Override
    public final boolean isDisposed() {
        return s.get() == DisposableHelper.DISPOSED;
    }

    @Override
    public final void dispose() {
        DisposableHelper.dispose(s);
    }
}

下面是实例的运行和结果

private void doMaybe() {

        new Thread(new Runnable() {
            @Override
            public void run() {
                Disposable d = Maybe.just("Hello World")
                        .delay(3, TimeUnit.SECONDS, Schedulers.io())
                        .subscribeWith(new DisposableMaybeObserver<String>() {
                            @Override
                            public void onStart() {
                                System.out.println("Started");
                            }

                            @Override
                            public void onSuccess(String value) {
                                System.out.println("Success: " + value);
                            }

                            @Override
                            public void onError(Throwable error) {
                                error.printStackTrace();
                            }

                            @Override
                            public void onComplete() {
                                System.out.println("Done!");
                            }
                        });

                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                d.dispose();
            }
        }).start();

    }


运行结果是:
10-02 15:01:53.320 25573-25649/hq.demo.net I/System.out: Started
10-02 15:01:56.324 25573-25654/hq.demo.net I/System.out: Success: Hello World

如果把Thread.sleep(4000)修改为Thread.sleep(2000)运行结果是:
10-02 15:05:34.362 25840-25872/hq.demo.net I/System.out: Started

上面例子使用DisposableMaybeObserver通过subscribeWith在Maybe处订阅,并返回一个Disposable,可以通过Disposable调用dispose来取消订阅。当然我们也可以通过下面的方式来完成,但是无法取消订阅关系:

private void doMaybe() {
    Maybe.just("Hello World")
            .delay(3, TimeUnit.SECONDS, Schedulers.io())
            .subscribe(new MaybeObserver<String>() {
                @Override
                public void onSubscribe(Disposable d) {
                    System.out.println("Started");
                }

                @Override
                public void onSuccess(String value) {
                    System.out.println("Success: " + value);
                }

                @Override
                public void onError(Throwable e) {
                    e.printStackTrace();
                }

                @Override
                public void onComplete() {
                    System.out.println("Done!");
                }
            });

    }

6、总结

Single、Completable、Maybe是简化的Observable,只是具有少部分功能:

Single:只能发射一条单一数据或者一条异常通知,不能发射完成通知,数据与通知只能发射一个,二选一
Completable:只能发射一条完成通知或者一条异常通知,不能发射数据,要么发射完成通知要么发射异常通知,二选一
Maybe:只能发射一条单一数据,和发射一条完成通知,或者一条异常通知,完成通知和异常通知二选一,只能在发射完成通知或异常通知之前发射数据,否则发射数据无效

7、参考资料

https://blog.csdn.net/weixin_36709064/article/details/82911270
Flowable背压支持

1…456…8
Shuming Zhao

Shuming Zhao

78 日志
14 分类
31 标签

© 2021 Shuming Zhao
访客数人, 访问量次 |
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4