日期: 2021 年 5 月 28 日

关于MySQL的lock wait timeout exceeded解决方案

关于MySQL出现 lock wait timeout exceeded; try restarting transaction 的解决方案。

一、问题抛出

在做查询语句时,MySQL 抛出了这样的异常:

  1. MySQL server error report:Array
  2. (
  3. [0] => Array
  4. (
  5. [message] => MySQL Query Error
  6. )
  7. [1] => Array
  8. (
  9. [sql] => SELECT * FROM taobao_trade WHERE order_status = 1 and orderID =‘2018061812306547’ AND is_tran_success=0 for update
  10. )
  11. [2] => Array
  12. (
  13. [error] => Lock wait timeout exceeded; try restarting transaction
  14. )
  15. [3] => Array
  16. (
  17. [errno] => 1205
  18. )
  19. )

Lock wait timeout exceeded; try restarting transaction的异常,错误提示的意思,很明显,是因为这条语句被锁住了,所以释放这个锁。

二、解决方案

我们可以通过到information_schema 中来进行查找被锁的语句。

解释: information_schema这张数据表保存了MySQL服务器所有数据库的信息。如数据库名,数据库的表,表栏的数据类型与访问权限等。再简单点,这台MySQL服务器上,到底有哪些数据库、各个数据库有哪些表,每张表的字段类型是什么,各个数据库要什么权限才能访问,等等信息都保存在information_schema表里面。

我们可以用下面三张表来查原因:

  • innodb_trx 当前运行的所有事务
  • innodb_locks 当前出现的锁
  • innodb_lock_waits 锁等待的对应关系

如果数据库中有锁的话,我们可以使用这条语句来查看:

select * from information_schema.innodb_trx

 

%title插图%num

图中红色语句 LOCK WAIT为占用系统资源的语句,我们需要杀掉这个锁,执行 kill 线程id号。上面这条记录的id为199120823069, trx_mysql_thread_id 为 738178711, 所以我们执行:kill 738178711杀掉这个MySQL语句的线程即可。

执行之后:

  1. kill 738178711
  2. // 查询线程
  3. // SELECT * from information_schema.processlist WHERE id = 738178711;
  4. // show full processlist;

 

%title插图%num

其他的记录不需要关注,因为其他的记录状态为“RUNNING” 即正在执行的事务,并没有锁。

三、三张表字段说明

innodb_trx

desc information_schema.innodb_trx;

 

%title插图%num

innodb_locks

desc information_schema.innodb_locks;

 

%title插图%num

innodb_lock_waits

desc information_schema.innodb_lock_waits

 

%title插图%num

四、终*方法

如果以上方法杀掉线程,但还是不能解决,则我们就可以查找执行线程用时比较久的用户,然后直接干掉。

  1. SELECT * from information_schema.`PROCESSLIST` WHERE Time > 1000 AND USER = ‘wonguser’ ORDER BY TIME desc;
  2. kill 740097562

这样把所有耗时比较久的任务干掉,就可以解决这个问题了。

小结

关于我的那个问题,我通过这个方法 select * from information_schema.innodb_trx 已经杀掉了线程,但通过表直接修改那个id对应的数据,还是会弹出Lock wait timeout exceeded; try restarting transaction这样的异常,在网上找了许多未找出具体的解决方法,后来自己灵光一现,可以找出那些好事比较久的线程,然后把那些可疑的线程杀掉,没想到这个问题就解决了,可以正常对这行数据进行操作了

为什么说Java中只有值传递

对于初学者来说,要想把这个问题回答正确,是比较难的。在第二天整理答案的时候,我发现我竟然无法通过简单的语言把这个事情描述的很容易理解,遗憾的是,我也没有在网上找到哪篇文章可以把这个事情讲解的通俗易懂。所以,就有了我写这篇文章的初衷。这篇文章中,我从什么是方法的实际参数和形式参数开始,给你讲解为什么说Java中只有值传递。

辟谣时间

关于这个问题,在StackOverflow上也引发过广泛的讨论,看来很多程序员对于这个问题的理解都不尽相同,甚至很多人理解的是错误的。还有的人可能知道Java中的参数传递是值传递,但是说不出来为什么。

在开始深入讲解之前,有必要纠正一下大家以前的那些错误看法了。如果你有以下想法,那么你有必要好好阅读本文。

错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。

错误理解二:Java是引用传递。

错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。

实参与形参

我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法,public static void main(String[] args),这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。

形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。

简单举个例子:

public static void main(String[] args) { ParamTest pt = new ParamTest(); pt.sout(“Hollis”);//实际参数为 Hollis }

public void sout(String name) { //形式参数为 name System.out.println(name); }

实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。

值传递与引用传递

上面提到了,当我们调用一个有参函数的时候,会把实际参数传递给形式参数。但是,在程序语言中,这个传递过程中传递的两种情况,即值传递和引用传递。我们来看下程序语言中是如何定义和区分值传递和引用传递的。

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

有了上面的概念,然后大家就可以写代码实践了,来看看Java中到底是值传递还是引用传递 ,于是,*简单的一段代码出来了:

public static void main(String[] args) { ParamTest pt = new ParamTest();

int i = 10; pt.pass(10); System.out.println(“print in main , i is ” + i); }

public void pass(int j) { j = 20; System.out.println(“print in pass , j is ” + j); }

上面的代码中,我们在pass方法中修改了参数j的值,然后分别在pass方法和main方法中打印参数的值。输出结果如下:

print in pass , j is 20 print in main , i is 10

可见,pass方法内部对name的值的修改并没有改变实际参数i的值。那么,按照上面的定义,有人得到结论:Java的方法传递是值传递。

但是,很快就有人提出质疑了(哈哈,所以,不要轻易下结论咯。)。然后,他们会搬出以下代码:

public static void main(String[] args) { ParamTest pt = new ParamTest();

User hollis = new User(); hollis.setName(“Hollis”); hollis.setGender(“Male”); pt.pass(hollis); System.out.println(“print in main , user is ” + hollis); }

public void pass(User user) { user.setName(“hollischuang”); System.out.println(“print in pass , user is ” + user); }

同样是一个pass方法,同样是在pass方法内修改参数的值。输出结果如下:

print in pass , user is User{name=’hollischuang’, gender=’Male’} print in main , user is User{name=’hollischuang’, gender=’Male’}

经过pass方法执行后,实参的值竟然被改变了,那按照上面的引用传递的定义,实际参数的值被改变了,这不就是引用传递了么。于是,根据上面的两段代码,有人得出一个新的结论:Java的方法中,在传递普通类型的时候是值传递,在传递对象类型的时候是引用传递。

但是,这种表述仍然是错误的。不信你看下面这个参数类型为对象的参数传递:

public static void main(String[] args) { ParamTest pt = new ParamTest();

String name = “Hollis”; pt.pass(name); System.out.println(“print in main , name is ” + name); }

public void pass(String name) { name = “hollischuang”; System.out.println(“print in pass , name is ” + name); }

上面的代码输出结果为

print in pass , name is hollischuang print in main , name is Hollis

这又作何解释呢?同样传递了一个对象,但是原始参数的值并没有被修改,难道传递对象又变成值传递了?

Java中的值传递

上面,我们举了三个例子,表现的结果却不一样,这也是导致很多初学者,甚至很多高级程序员对于Java的传递类型有困惑的原因。

其实,我想告诉大家的是,上面的概念没有错,只是代码的例子有问题。来,我再来给大家画一下概念中的重点,然后再举几个真正恰当的例子。

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

那么,我来给大家总结一下,值传递和引用传递之前的区别的重点是什么。

pass

我们上面看过的几个pass的例子中,都只关注了实际参数内容是否有改变。如传递的是User对象,我们试着改变他的name属性的值,然后检查是否有改变。其实,在实验方法上就错了,当然得到的结论也就有问题了。

为什么说实验方法错了呢?这里我们来举一个形象的例子。再来深入理解一下值传递和引用传递,然后你就知道为啥错了。

你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。

你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把钥匙做什么都不会影响你手里的这把钥匙。

但是,不管上面那种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。那你说你会不会受到影响?而我们在pass方法中,改变user对象的name属性的值的时候,不就是在“砸电视”么。

还拿上面的一个例子来举例,我们真正的改变参数,看看会发生什么?

public static void main(String[] args) { ParamTest pt = new ParamTest();

User hollis = new User(); hollis.setName(“Hollis”); hollis.setGender(“Male”); pt.pass(hollis); System.out.println(“print in main , user is ” + hollis); }

public void pass(User user) { user = new User(); user.setName(“hollischuang”); user.setGender(“Male”); System.out.println(“print in pass , user is ” + user); }

上面的代码中,我们在pass方法中,改变了user对象,输出结果如下:

print in pass , user is User{name=’hollischuang’, gender=’Male’} print in main , user is User{name=’Hollis’, gender=’Male’}

我们来画一张图,看一下整个过程中发生了什么,然后我再告诉你,为啥Java中只有值传递。

pass1

稍微解释下这张图,当我们在main中创建一个User对象的时候,在堆中开辟一块内存,其中保存了name和gender等数据。然后hollis持有该内存的地址0x123456(图1)。当尝试调用pass方法,并且hollis作为实际参数传递给形式参数user的时候,会把这个地址0x123456交给user,这时,user也指向了这个地址(图2)。然后在pass方法内对参数进行修改的时候,即user = new User();,会重新开辟一块0X456789的内存,赋值给user。后面对user的任何修改都不会改变内存0X123456的内容(图3)。

上面这种传递是什么传递?肯定不是引用传递,如果是引用传递的话,在user=new User()的时候,实际参数的引用也应该改为指向0X456789,但是实际上并没有。

通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以,上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。

我们再来回顾下之前的那个“砸电视”的例子,看那个例子中的传递过程发生了什么。

pass2

同样的,在参数传递的过程中,实际参数的地址0X1213456被拷贝给了形参,只是,在这个方法中,并没有对形参本身进行修改,而是修改的形参持有的地址中存储的内容。

所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。在判断实参内容有没有受影响的时候,要看传的的是什么,如果你传递的是个地址,那么就看这个地址的变化会不会有影响,而不是看地址指向的对象的变化。就像钥匙和房子的关系。

那么,既然这样,为啥上面同样是传递对象,传递的String对象和User对象的表现结果不一样呢?我们在pass方法中使用name = "hollischuang";试着去更改name的值,阴差阳错的直接改变了name的引用的地址。因为这段代码,会new一个String,在把引用交给name,即等价于name = new String("hollischuang");。而原来的那个”Hollis”字符串还是由实参持有着的,所以,并没有修改到实际参数的值。

pass3

所以说,Java中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用。

总结

无论是值传递还是引用传递,其实都是一种求值策略(Evaluation strategy)。在求值策略中,还有一种叫做按共享传递(call by sharing)。其实Java中的参数传递严格意义上说应该是按共享传递。

按共享传递,是指在调用函数时,传递给函数的是实参的地址的拷贝(如果实参在栈中,则直接拷贝该值)。在函数内部对参数进行操作时,需要先拷贝的地址寻找到具体的值,再进行操作。如果该值在栈中,那么因为是直接拷贝的值,所以函数内部对参数进行操作不会对外部变量产生影响。如果原来拷贝的是原值在堆中的地址,那么需要先根据该地址找到堆中对应的位置,再进行操作。因为传递的是地址的拷贝所以函数内对值的操作对外部变量是可见的。

简单点说,Java中的传递,是值传递,而这个值,实际上是对象的引用。

而按共享传递其实只是按值传递的一个特例罢了。所以我们可以说Java的传递是按共享传递,或者说Java中的传递是值传递。

JVM内幕:Java虚拟机详解

转载自:https://www.cnblogs.com/yhl-yh/p/7145218.html

这篇文章解释了Java 虚拟机(JVM)的内部架构。下图显示了遵守 Java SE 7 规范的典型的 JVM 核心内部组件。

%title插图%num

上图显示的组件分两个章节解释。*章讨论针对每个线程创建的组件,第二章节讨论了线程无关组件。

  • 线程
    • JVM 系统线程
    • 每个线程相关的
    • 程序计数器
    • 本地栈
    • 栈限制
    • 栈帧
    • 局部变量数组
    • 操作数栈
    • 动态链接
  • 线程共享
    • 内存管理
    • 非堆内存
    • 即时编译
    • 方法区
    • 类文件结构
    • 类加载器
    • 更快的类加载
    • 方法区在哪里
    • 类加载器参考
    • 运行时常量池
    • 异常表
    • 符号表
    • Interned 字符串

线程

这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。run() 返回时,被处理未捕获异常,原生线程将确认由于它的结束是否要终止 JVM 进程(比如这个线程是*后一个非守护线程)。当线程结束时,会释放原生线程和 Java 线程的所有资源。

JVM 系统线程

如果使用 jconsole 或者其它调试器,你会看到很多线程在后台运行。这些后台线程与触发 public static void main(String[]) 函数的主线程以及主线程创建的其他线程一起运行。Hotspot JVM 后台运行的系统线程主要有下面几个:

虚拟机线程(VM thread) 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。
周期性任务线程 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。
GC 线程 这些线程支持 JVM 中不同的垃圾回收活动。
编译器线程 这些线程在运行时将字节码动态编译成本地平台相关的机器码。
信号分发线程 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。

线程相关组件

每个运行的线程都包含下面这些组件:

程序计数器(PC)

PC 指当前指令(或操作码)的地址,本地指令除外。如果当前方法是 native 方法,那么PC 的值为 undefined。所有的 CPU 都有一个 PC,典型状态下,每执行一条指令 PC 都会自增,因此 PC 存储了指向下一条要被执行的指令地址。JVM 用 PC 来跟踪指令执行的位置,PC 将实际上是指向方法区(Method Area)的一个内存地址。

栈(Stack)

每个线程拥有自己的栈,栈包含每个方法执行的栈帧。栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。每次方法调用时,一个新的栈帧创建并压栈到栈顶。当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。除了栈帧的压栈和出栈,栈不能被直接操作。所以可以在堆上分配栈帧,并且不需要连续内存。

Native栈

并非所有的 JVM 实现都支持本地(native)方法,那些提供支持的 JVM 一般都会为每个线程创建本地方法栈。如果 JVM 用 C-linkage 模型实现 JNI(Java Native Invocation),那么本地栈就是一个 C 的栈。在这种情况下,本地方法栈的参数顺序、返回值和典型的 C 程序相同。本地方法一般来说可以(依赖 JVM 的实现)反过来调用 JVM 中的 Java 方法。这种 native 方法调用 Java 会发生在栈(一般是 Java 栈)上;线程将离开本地方法栈,并在 Java 栈上开辟一个新的栈帧。

栈的限制

栈可以是动态分配也可以固定大小。如果线程请求一个超过允许范围的空间,就会抛出一个StackOverflowError。如果线程需要一个新的栈帧,但是没有足够的内存可以分配,就会抛出一个 OutOfMemoryError。

栈帧(Frame)

每次方法调用都会新建一个新的栈帧并把它压栈到栈顶。当方法正常返回或者调用过程中抛出未捕获的异常时,栈帧将出栈。更多关于异常处理的细节,可以参考下面的异常信息表章节。

每个栈帧包含:

  • 局部变量数组
  • 返回值
  • 操作数栈
  • 类当前方法的运行时常量池引用

局部变量数组

局部变量数组包含了方法执行过程中的所有变量,包括 this 引用、所有方法参数、其他局部变量。对于类方法(也就是静态方法),方法参数从下标 0 开始,对于对象方法,位置0保留为 this。

有下面这些局部变量:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了 long 和 double 类型以外,所有的变量类型都占用局部变量数组的一个位置。long 和 double 需要占用局部变量数组两个连续的位置,因为它们是 64 位双精度,其它类型都是 32 位单精度。

操作数栈

操作数栈在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。

int i;

 

被编译成下面的字节码:

iconst_0    // Push 0 to top of the operand stack

istore_1    // Pop value from top of operand stack and store as local variable 1

 

更多关于局部变量数组、操作数栈和运行时常量池之间交互的详细信息,可以在类文件结构部分找到。

动态链接

每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池。通过这个引用支持动态链接(dynamic linking)。

C/C++ 代码一般被编译成对象文件,然后多个对象文件被链接到一起产生可执行文件或者 dll。在链接阶段,每个对象文件的符号引用被替换成了*终执行文件的相对偏移内存地址。在 Java中,链接阶段是运行时动态完成的。

当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。符号引用是一个逻辑引用,实际上并不指向物理内存地址。JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在*次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在*次使用符号引用时完成解析并抛出可能发生的解析错误。绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。

线程间共享

堆被用来在运行时分配类实例、数组。不能在栈上存储数组和对象。因为栈帧被设计为创建以后无法调整大小。栈帧只存储指向堆中对象或数组的引用。与局部变量数组(每个栈帧中的)中的原始类型和引用类型不同,对象总是存储在堆上以便在方法结束时不会被移除。对象只能由垃圾回收器移除。

为了支持垃圾回收机制,堆被分为了下面三个区域:

  • 新生代
    • 经常被分为 Eden 和 Survivor
  • 老年代
  • 永久代

内存管理

对象和数组永远不会显式回收,而是由垃圾回收器自动回收。通常,过程是这样的:

  1. 新的对象和数组被创建并放入老年代。
  2. Minor垃圾回收将发生在新生代。依旧存活的对象将从 eden 区移到 survivor 区。
  3. Major垃圾回收一般会导致应用进程暂停,它将在三个区内移动对象。仍然存活的对象将被从新生代移动到老年代。
  4. 每次进行老年代回收时也会进行永久代回收。它们之中任何一个变满时,都会进行回收。

非堆内存

非堆内存指的是那些逻辑上属于 JVM 一部分对象,但实际上不在堆上创建。

非堆内存包括:

  • 永久代,包括:
    • 方法区
    • 驻留字符串(interned strings)
  • 代码缓存(Code Cache):用于编译和存储那些被 JIT 编译器编译成原生代码的方法。

即时编译(JIT)

Java 字节码是解释执行的,但是没有直接在 JVM 宿主执行原生代码快。为了提高性能,Oracle Hotspot 虚拟机会找到执行*频繁的字节码片段并把它们编译成原生机器码。编译出的原生机器码被存储在非堆内存的代码缓存中。通过这种方法,Hotspot 虚拟机将权衡下面两种时间消耗:将字节码编译成本地代码需要的额外时间和解释执行字节码消耗更多的时间。

方法区

方法区存储了每个类的信息,比如:

  • Classloader 引用
  • 运行时常量池
    • 数值型常量
    • 字段引用
    • 方法引用
    • 属性
  • 字段数据
    • 针对每个字段的信息
      • 字段名
      • 类型
      • 修饰符
      • 属性(Attribute)
  • 方法数据
    • 每个方法
      • 方法名
      • 返回值类型
      • 参数类型(按顺序)
      • 修饰符
      • 属性
  • 方法代码
    • 每个方法
      • 字节码
      • 操作数栈大小
      • 局部变量大小
      • 局部变量表
      • 异常表
      • 每个异常处理器
      • 开始点
      • 结束点
      • 异常处理代码的程序计数器(PC)偏移量
      • 被捕获的异常类对应的常量池下标

所有线程共享同一个方法区,因此访问方法区数据的和动态链接的进程必须线程安全。如果两个线程试图访问一个还未加载的类的字段或方法,必须只加载一次,而且两个线程必须等它加载完毕才能继续执行。

类文件结构

一个编译后的类文件包含下面的结构:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

ClassFile {

    u4            magic;

    u2            minor_version;

    u2            major_version;

    u2            constant_pool_count;

    cp_info        contant_pool[constant_pool_count – 1];

    u2            access_flags;

    u2            this_class;

    u2            super_class;

    u2            interfaces_count;

    u2            interfaces[interfaces_count];

    u2            fields_count;

    field_info        fields[fields_count];

    u2            methods_count;

    method_info        methods[methods_count];

    u2            attributes_count;

    attribute_info    attributes[attributes_count];

}

magic, minor_version, major_version 类文件的版本信息和用于编译这个类的 JDK 版本。
constant_pool 类似于符号表,尽管它包含更多数据。下面有更多的详细描述。
access_flags 提供这个类的描述符列表。
this_class 提供这个类全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。
super_class 提供这个类的父类符号引用的常量池索引。
interfaces 指向常量池的索引数组,提供那些被实现的接口的符号引用。
fields 提供每个字段完整描述的常量池索引数组。
methods 指向constant_pool的索引数组,用于表示每个方法签名的完整描述。如果这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码。
attributes 不同值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。

可以用 javap 查看编译后的 java class 文件字节码。

如果你编译下面这个简单的类:

1

2

3

4

5

6

package org.jvminternals;

public class SimpleClass {

    public void sayHello() {

        System.out.println("Hello");

    }

}

运行下面的命令,就可以得到下面的结果输出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。

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

public class org.jvminternals.SimpleClass

  SourceFile: "SimpleClass.java"

  minor version: 0

  major version: 51

  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V

   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;

   #3 = String             #20            //  "Hello"

   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V

   #5 = Class              #23            //  org/jvminternals/SimpleClass

   #6 = Class              #24            //  java/lang/Object

   #7 = Utf8               <init>

   #8 = Utf8               ()V

   #9 = Utf8               Code

  #10 = Utf8               LineNumberTable

  #11 = Utf8               LocalVariableTable

  #12 = Utf8               this

  #13 = Utf8               Lorg/jvminternals/SimpleClass;

  #14 = Utf8               sayHello

  #15 = Utf8               SourceFile

  #16 = Utf8               SimpleClass.java

  #17 = NameAndType        #7:#8          //  "<init>":()V

  #18 = Class              #25            //  java/lang/System

  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;

  #20 = Utf8               Hello

  #21 = Class              #28            //  java/io/PrintStream

  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V

  #23 = Utf8               org/jvminternals/SimpleClass

  #24 = Utf8               java/lang/Object

  #25 = Utf8               java/lang/System

  #26 = Utf8               out

  #27 = Utf8               Ljava/io/PrintStream;

  #28 = Utf8               java/io/PrintStream

  #29 = Utf8               println

  #30 = Utf8               (Ljava/lang/String;)V

{

  public org.jvminternals.SimpleClass();

    Signature: ()V

    flags: ACC_PUBLIC

    Code:

      stack=1, locals=1, args_size=1

        0: aload_0

        1: invokespecial #1    // Method java/lang/Object."<init>":()V

        4: return

      LineNumberTable:

        line 3: 0

      LocalVariableTable:

        Start  Length  Slot  Name   Signature

          0      5      0    this   Lorg/jvminternals/SimpleClass;

 

  public void sayHello();

    Signature: ()V

    flags: ACC_PUBLIC

    Code:

      stack=2, locals=1, args_size=1

        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;

        3: ldc            #3    // String "Hello"

        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V

        8: return

      LineNumberTable:

        line 6: 0

        line 7: 8

      LocalVariableTable:

        Start  Length  Slot  Name   Signature

          0      9      0    this   Lorg/jvminternals/SimpleClass;

}

这个 class 文件展示了三个主要部分:常量池、构造器方法和 sayHello 方法。

  • 常量池:提供了通常由符号表提供的相同信息,详细描述见下文。
  • 方法:每一个方法包含四个区域,
    • 签名和访问标签
    • 字节码
    • LineNumberTable:为调试器提供源码中的每一行对应的字节码信息。上面的例子中,Java 源码里的第 6 行与 sayHello 函数字节码序号 0 相关,第 7 行与字节码序号 8 相关。
    • LocalVariableTable:列出了所有栈帧中的局部变量。上面两个例子中,唯一的局部变量就是 this。

这个 class 文件用到下面这些字节码操作符:

aload0 这个操作码是aload格式操作码中的一个。它们用来把对象引用加载到操作码栈。 表示正在被访问的局部变量数组的位置,但只能是0、1、2、3 中的一个。还有一些其它类似的操作码用来载入非对象引用的数据,如iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。局部变量数组位置大于 3 的局部变量可以用 iload, lload, float, dload 和 aload 载入。这些操作码都只需要一个操作数,即数组中的位置
ldc 这个操作码用来将常量从运行时常量池压栈到操作数栈
getstatic 这个操作码用来把一个静态变量从运行时常量池的静态变量列表中压栈到操作数栈
invokespecial, invokevirtual 这些操作码属于一组函数调用的操作码,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在这个 class 文件中,invokespecial 和 invokevirutal 两个指令都用到了,两者的区别是,invokevirutal 指令调用一个对象的实例方法,invokespecial 指令调用实例初始化方法、私有方法、父类方法。
return 这个操作码属于ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组。每个操作码返回一种类型的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void

跟任何典型的字节码一样,操作数与局部变量、操作数栈、运行时常量池的主要交互如下所示。

构造器函数包含两个指令。首先,this 变量被压栈到操作数栈,然后父类的构造器函数被调用,而这个构造器会消费 this,之后 this 被弹出操作数栈。

%title插图%num

 

sayHello() 方法更加复杂,正如之前解释的那样,因为它需要用运行时常量池中的指向符号引用的真实引用。*个操作码 getstatic 从System类中将out静态变量压到操作数栈。下一个操作码 ldc 把字符串 “Hello” 压栈到操作数栈。*后 invokevirtual 操作符会调用 System.out 变量的 println 方法,从操作数栈作弹出”Hello” 变量作为 println 的一个参数,并在当前线程开辟一个新栈帧。

%title插图%num

 

类加载器

JVM 启动时会用 bootstrap 类加载器加载一个初始化类,然后这个类会在public static void main(String[])调用之前完成链接和初始化。执行这个方法会执行加载、链接、初始化需要的额外类和接口。

加载(Loading)是这样一个过程,找到代表这个类的 class 文件或根据特定的名字找到接口类型,然后读取到一个字节数组中。接着,这些字节会被解析检验它们是否代表一个 Class 对象并包含正确的 major、minor 版本信息。直接父类的类和接口也会被加载进来。这些操作一旦完成,类或者接口对象就从二进制表示中创建出来了。

链接(Linking)是校验类或接口并准备类型和父类父接口的过程。链接过程包含三步:校验(verifying)、准备(preparing)、部分解析(optionally resolving)。

校验会确认类或者接口表示是否结构正确,以及是否遵循 Java 语言和 JVM 的语义要求,比如会进行下面的检查:

  1. 格式一致且格式化正确的符号表
  2. final 方法和类没有被重载
  3. 方法遵循访问控制关键词
  4. 方法参数的数量、类型正确
  5. 字节码没有不当的操作栈数据
  6. 变量在读取之前被初始化过
  7. 变量值的类型正确

在验证阶段做这些检查意味着不需要在运行阶段做这些检查。链接阶段的检查减慢了类加载的速度,但是它避免了执行这些字节码时的多次检查。

准备过程包括为静态存储和 JVM 使用的数据结构(比如方法表)分配内存空间。静态变量创建并初始化为默认值,但是初始化代码不在这个阶段执行,因为这是初始化过程的一部分。

解析是可选的阶段。它包括通过加载引用的类和接口来检查这些符号引用是否正确。如果不是发生在这个阶段,符号引用的解析要等到字节码指令使用这个引用的时候才会进行。

类或者接口初始化由类或接口初始化方法<clinit>的执行组成。

%title插图%num

 

JVM 中有多个类加载器,分饰不同的角色。每个类加载器由它的父加载器加载。bootstrap 加载器除外,它是所有*顶层的类加载器。

  • Bootstrap 加载器一般由本地代码实现,因为它在 JVM 加载以后的早期阶段就被初始化了。bootstrap 加载器负责载入基础的 Java API,比如包含 rt.jar。它只加载拥有较高信任级别的启动路径下找到的类,因此跳过了很多普通类需要做的校验工作。
  • Extension 加载器加载了标准 Java 扩展 API 中的类,比如 security 的扩展函数。
  • System 加载器是应用的默认类加载器,比如从 classpath 中加载应用类。
  • 用户自定义类加载器也可以用来加载应用类。使用自定义的类加载器有很多特殊的原因:运行时重新加载类或者把加载的类分隔为不同的组,典型的用法比如 web 服务器 Tomcat。

%title插图%num

 

加速类加载

共享类数据(CDS)是Hotspot JVM 5.0 的时候引入的新特性。在 JVM 安装过程中,安装进程会加载一系列核心 JVM 类(比如 rt.jar)到一个共享的内存映射区域。CDS 减少了加载这些类需要的时间,提高了 JVM 启动的速度,允许这些类被不同的 JVM 实例共享,同时也减少了内存消耗。

方法区在哪里

The Java Virtual Machine Specification Java SE 7 Edition 中写得很清楚:“尽管方法区逻辑上属于堆的一部分,简单的实现可以选择不对它进行回收和压缩。”。Oracle JVM 的 jconsle 显示方法区和 code cache 区被当做为非堆内存,而 OpenJDK 则显示 CodeCache 被当做 VM 中对象堆(ObjectHeap)的一个独立的域。

Classloader 引用

所有的类加载之后都包含一个加载自身的加载器的引用,反过来每个类加载器都包含它们加载的所有类的引用。

运行时常量池

JVM 维护了一个按类型区分的常量池,一个类似于符号表的运行时数据结构。尽管它包含更多数据。Java 字节码需要数据。这个数据经常因为太大不能直接存储在字节码中,取而代之的是存储在常量池中,字节码包含这个常量池的引用。运行时常量池被用来上面介绍过的动态链接。

常量池中可以存储多种类型的数据:

  • 数字型
  • 字符串型
  • 类引用型
  • 域引用型
  • 方法引用

示例代码如下:

1 Object foo = new Object();

写成字节码将是下面这样:

1

2

3

0:     new #2             // Class java/lang/Object

1:    dup

2:    invokespecial #3    // Method java/ lang/Object "&lt;init&gt;"( ) V

new 操作码的后面紧跟着操作数 #2 。这个操作数是常量池的一个索引,表示它指向常量池的第二个实体。第二个实体是一个类的引用,这个实体反过来引用了另一个在常量池中包含 UTF8 编码的字符串类名的实体(// Class java/lang/Object)。然后,这个符号引用被用来寻找 java.lang.Object 类。new 操作码创建一个类实例并初始化变量。新类实例的引用则被添加到操作数栈。dup 操作码创建一个操作数栈顶元素引用的额外拷贝。*后用 invokespecial 来调用第 2 行的实例初始化方法。操作码也包含一个指向常量池的引用。初始化方法把操作数栈出栈的顶部引用当做此方法的一个参数。*后这个新对象只有一个引用,这个对象已经完成了创建及初始化。

如果你编译下面的类:

1

2

3

4

5

6

7

8

package org.jvminternals;

public class SimpleClass {

 

    public void sayHello() {

        System.out.println("Hello");

    }

 

}

生成的类文件常量池将是这个样子:

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

Constant pool:

   #1 = Methodref          #6.#17         //  java/lang/Object."&lt;init&gt;":()V

   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;

   #3 = String             #20            //  "Hello"

   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V

   #5 = Class              #23            //  org/jvminternals/SimpleClass

   #6 = Class              #24            //  java/lang/Object

   #7 = Utf8               &lt;init&gt;

   #8 = Utf8               ()V

   #9 = Utf8               Code

  #10 = Utf8               LineNumberTable

  #11 = Utf8               LocalVariableTable

  #12 = Utf8               this

  #13 = Utf8               Lorg/jvminternals/SimpleClass;

  #14 = Utf8               sayHello

  #15 = Utf8               SourceFile

  #16 = Utf8               SimpleClass.java

  #17 = NameAndType        #7:#8          //  "&lt;init&gt;":()V

  #18 = Class              #25            //  java/lang/System

  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;

  #20 = Utf8               Hello

  #21 = Class              #28            //  java/io/PrintStream

  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V

  #23 = Utf8               org/jvminternals/SimpleClass

  #24 = Utf8               java/lang/Object

  #25 = Utf8               java/lang/System

  #26 = Utf8               out

  #27 = Utf8               Ljava/io/PrintStream;

  #28 = Utf8               java/io/PrintStream

  #29 = Utf8               println

  #30 = Utf8               (Ljava/lang/String;)V

这个常量池包含了下面的类型:

Integer 4 字节常量
Long 8 字节常量
Float 4 字节常量
Double 8 字节常量
String 字符串常量指向常量池的另外一个包含真正字节 Utf8 编码的实体
Utf8 Utf8 编码的字符序列字节流
Class 一个 Class 常量,指向常量池的另一个 Utf8 实体,这个实体包含了符合 JVM 内部格式的类的全名(动态链接过程需要用到)
NameAndType 冒号(:)分隔的一组值,这些值都指向常量池中的其它实体。*个值(“:”之前的)指向一个 Utf8 字符串实体,它是一个方法名或者字段名。第二个值指向表示类型的 Utf8 实体。对于字段类型,这个值是类的全名,对于方法类型,这个值是每个参数类型类的类全名的列表。
Fieldref, Methodref, InterfaceMethodref 点号(.)分隔的一组值,每个值都指向常量池中的其它的实体。*个值(“.”号之前的)指向类实体,第二个值指向 NameAndType 实体。

异常表

异常表像这样存储每个异常处理信息:

  • 起始点(Start point)
  • 结束点(End point)
  • 异常处理代码的 PC 偏移量
  • 被捕获异常的常量池索引

如果一个方法有定义 try-catch 或者 try-finally 异常处理器,那么就会创建一个异常表。它为每个异常处理器和 finally 代码块存储必要的信息,包括处理器覆盖的代码块区域和处理异常的类型。

当方法抛出异常时,JVM 会寻找匹配的异常处理器。如果没有找到,那么方法会立即结束并弹出当前栈帧,这个异常会被重新抛到调用这个方法的方法中(在新的栈帧中)。如果所有的栈帧都被弹出还没有找到匹配的异常处理器,那么这个线程就会终止。如果这个异常在*后一个非守护进程抛出(比如这个线程是主线程),那么也有会导致 JVM 进程终止。

Finally 异常处理器匹配所有的异常类型,且不管什么异常抛出 finally 代码块都会执行。在这种情况下,当没有异常抛出时,finally 代码块还是会在方法*后执行。这种靠在代码 return 之前跳转到 finally 代码块来实现。

符号表

除了按类型来分的运行时常量池,Hotspot JVM 在永久代还包含一个符号表。这个符号表是一个哈希表,保存了符号指针到符号的映射关系(也就是 Hashtable<Symbol*, Symbol>),它拥有指向所有符号(包括在每个类运行时常量池中的符号)的指针。

引用计数被用来控制一个符号从符号表从移除的过程。比如当一个类被卸载时,它拥有的在常量池中所有符号的引用计数将减少。当符号表中的符号引用计数为 0 时,符号表会认为这个符号不再被引用,将从符号表中卸载。符号表和后面介绍的字符串表都被保存在一个规范化的结构中,以便提高效率并保证每个实例只出现一次。

字符串表

Java 语言规范要求相同的(即包含相同序列的 Unicode 指针序列)字符串字面量必须指向相同的 String 实例。除此之外,在一个字符串实例上调用 String.intern() 方法的返回引用必须与字符串是字面量时的一样。因此,下面的代码返回 true:

1 ("j" "v" "m").intern() == "jvm"

Hotspot JVM 中 interned 字符串保存在字符串表中。字符串表是一个哈希表,保存着对象指针到符号的映射关系(也就是Hashtable<oop, Symbol>),它被保存到永久代中。符号表和字符串表的实体都以规范的格式保存,保证每个实体都只出现一次。

当类加载时,字符串字面量被编译器自动 intern 并加入到符号表。除此之外,String 类的实例可以调用 String.intern() 显式地 intern。当调用 String.intern() 方法时,如果符号表已经包含了这个字符串,那么就会返回符号表里的这个引用,如果不是,那么这个字符串就被加入到字符串表中同时返回这个引用。

Eclipse中设置作者日期等Java注释模板

转载自:https://jingyan.baidu.com/article/1612d500856cb1e20e1eeed5.html

Eclipse作为JavaIDE(Integrated Development Environment,集成开发环境),可以通过设置自动添加Javadoc注释信息,如@author 作者名、@version 版本标识、@date 日期等,在创建类或新增方法时会自动添加注释信息。关于java如何生成javadoc文档可参考下文。下面将会为大家介绍如何在Eclipse中设置Java注释模板。

4Eclipse规范注释及注释文档的生成

工具/原料

  • Eclipse Oxygen Release (4.7.0)

方法/步骤

  1. 1

    首先介绍几个常用的注解:

    @author 作者名

    @date 日期

    @version 版本标识

    @parameter 参数及其意义

    @return 返回值

    @throws 异常类及抛出条件

    @deprecated 引起不推荐使用的警告

    @override 重写

    这个注解我们在java代码中经常可以看到。

  2. 2

    设置注释模板的步骤:点击菜单栏上的Window –>Preferences–>Java–>Code Style –>Code Templates,对右侧Comments选项中具体的注释信息进行编辑即可。可勾选上自动添加注释信息,在生成java文件时便会自动生成注释,当然也可以手动插入注释。设置的界面如下:

    Eclipse中设置作者日期等Java注释模板

  3. 3

    点击Edit按钮,进入编辑页面,全部编辑完成后点击“Apply And Close”即可设置完成,并关闭设置页面。

    Eclipse中设置作者日期等Java注释模板

    Eclipse中设置作者日期等Java注释模板

  4. 4

    下面介绍具体的Comment如何设置:

    1. 点击Comments下的Files可对整个Java文件进行注释:包括公司名称,版权所属,作者信息,日期等。

     

    /**

    * <p>Title: ${file_name}</p>

    * <p>Description: </p>

    * <p>Copyright: Copyright (c) 2017</p>

    * <p>Company: www.baidudu.com</p>

    * @author shenlan

    * @date ${date}

    * @version 1.0

    */

  5. 5

    2. 点击Types对类进行注释:

     

    /**

    * <p>Title: ${type_name}</p>

    * <p>Description: </p>

    * @author shenlan

    * @date ${date}

    */

     

    3. 点击Fields对字段进行注释:

     

    /** ${field}*/

     

    4. 点击Constructors对构造方法进行注释:

     

    /**

    * <p>Title: </p>

    * <p>Description: </p>

    * ${tags}

    */

     

    5. 点击Methods对方法进行注释:

     

    /**

     

    * <p>Title: ${enclosing_method}</p>

     

    * <p>Description: </p>

     

    * ${tags}

     

    */

  6. 6

    6. 点击Overriding Methods对重写方法进行注释:

     

    /* (non-Javadoc)

     

    * <p>Title: ${enclosing_method}</p>

     

    * <p>Description: </p>

     

    * ${tags}

     

    * ${see_to_overridden}

     

    */

     

    7. Delegate methods对代表方法进行注释:

     

    /**

    * ${tags}

    * ${see_to_target}

    */

     

    8. Getters对get方法进行注释:

     

    /**

     

    * @return the ${bare_field_name}

     

    */

     

    9. Setters对set方法进行注释:

     

    /**

     

    * @param ${param} the ${bare_field_name} to set

     

    */

  7. 7

    注释模板的导入和导出:点击Import和Export按钮即可。

    Eclipse中设置作者日期等Java注释模板

    END

Java注释模板的使用

  1. 1

    在设置模板时如果勾选了自动添加注释信息,则在创建Java文件时会自动生成文档和类的注释信息,若没有勾选,按Shift+Alt+J快捷键也可生成。

    Eclipse中设置作者日期等Java注释模板

  2. 2

    在对类中的方法进行注释是:在方法上方输入/** 后点击回车,即可生成方法注释;或将光标放在方法名上,按住Shift+Alt+J快捷键也可;或在方法上右击,source》Generate Element Comment也可生成注释。

    Eclipse中设置作者日期等Java注释模板

    END

注意事项

  • 注释模板设置完一定要点击Appy保存

RxJava 从入门到全解析

转载自:http://www.apkbus.com/blog-873055-77431.html

前言

使用了RxJava有一段时间了,深深感受到了其“牛逼”之处。下面,就从RxJava的基础开始,一步一步与大家分享一下这个强大的异步库的用法!

RxJava 概念初步

RxJava 在Github Repo上给的解释是:

“RxJava is a Java VM implementation of Reactive Extensions: 

a library for composing asynchronous and event-based programs by using observable sequences.”

大概就是说RxJava是Java VM上一个灵活的、使用可观测序列来组成的一个异步的、基于事件的库。咋一看好像不知道是啥东西… … 没事,往下看~

作用 – 异步

上面 这段解释,重点就在于异步!但是它又不像 AsyncTask 这样用法简单,所以刚接触RxJava的童鞋,可能会觉得特别难,无从下手,没事,相信通过这篇文章,大伙儿可以有一个比较深刻的理解!

RxJava精华可以浓缩为异步两个字,其核心的东西不外乎两个:

1.  Observable(被观察者) 

2.  Observer/Subscriber(观察者)

Observables可以发出一系列的 事件,这里的事件可以是任何东西,例如网络请求、复杂计算处理、数据库操作、文件操作等等,事件执行结束后交给 Observer/Subscriber 的回调处理。

模式 – 观察者模式

观察者模式是一种对象的行为模式,是 Java 设计模式中很常用的一个模式。观察者模式也常称为:

发布-订阅模式(Publish/Subscribe)

模型-视图模式(Model/View)

源-监听器模式(Source/Listener)

从属者模式(Dependents)

例如用过事件总线 EventBus 库的童鞋就知道,EventBus 属于发布-订阅模式(Publish/Subscribe)。

// 事件订阅@Subscribe(threadMode = ThreadMode.MAIN)public void showDownProgress(MyEvent event) {     // TODO}// 事件发布EventBus.getDefault().post(new MyEvent());

实际上,使用 RxJava 也可以设计出一套事件总线的库,这个称为 RxBus。有兴趣的话可以在学完 RxJava 之后,可以尝试写一个。这里就不细说了~

为啥说这个呢?因为,RxJava 也是一种扩展的观察者模式!

举个栗子,Android 中 View 的点击监听器的实现,View 是被观察者,OnClickListener 对象是观察者,Activity 要如何知道 View 被点击了?那就是构造一个 OnClickListener 对象,通过 setOnClickListener 与View达成一个订阅关系,一旦 View 被点击了,就通过OnClickListener对象的 OnClick 方法传达给 Activity 。采用观察者模式可以避免去轮询检查,节约有限的cpu资源。

结构 – 响应式编程

响应式?顾名思义,就是“你变化,我响应”。举个栗子,a = b + c; 这句代码将b+c的值赋给a,而之后如果b和c的值改变了不会影响到a,然而,对于响应式编程,之后b和c的值的改变也动态影响着a,意味着a会随着b和c的变化而变化。

响应式编程的组成为Observable/Operator/Subscriber,RxJava在响应式编程中的基本流程如下:

Observable -> Operator 1 -> Operator 2 -> Operator 3 -> Subscriber

这个流程,可以简单的理解为:

  1. Observable 发出一系列事件,他是事件的产生者;
  2. Subscriber 负责处理事件,他是事件的消费者;
  3. Operator 是对 Observable 发出的事件进行修改和变换;
  4. 若事件从产生到消费不需要其他处理,则可以省略掉中间的 Operator,从而流程变为 Obsevable -> Subscriber
  5. Subscriber 通常在主线程执行,所以原则上不要去处理太多的事务,而这些复杂的事务处理则交给 Operator;

优势 – 逻辑简洁

Rx 优势可以概括为四个字,那就是 逻辑简洁。然而,逻辑简洁并不意味着代码简洁,但是,由于链式结构,一条龙,你可以从头到尾,从上到下,很清楚的看到这个连式结构的执行顺序。对于开发人员来说,代码质量并不在于代码量,而在于逻辑是否清晰简洁,可维护性如何,代码是否健壮!

另外,熟悉lambda的,还可以进一步提高代码的简洁性。举个简单栗子对比一下,暂时不需要过多理解,后面会一一道来:

// 不使用lambdaObservable.just("Hello World!")
     .map(new Func1<String, String>() {         @Override
         public String call(String s) {             return s + "I am kyrie!";
         }
     })
     .subscribeOn(Schedulers.io())
     .observeOn(AndroidSchedulers.mainThread())
     .subscribe(new Action1<String>() {         @Override
         public void call(String s) {
             Log.i(TAG, s);
         }
     });// 使用lambdaObservable.just("Hello World!")
    .map(s -> s + "I am kyrie!")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(s -> {
        Log.i(TAG, s);
    });

RxJava 依赖

在 Android Studio 项目下,为 module 增加 Gradle 依赖。

// Android 平台下须引入的一个依赖,主要用于线程控制compile 'io.reactivex:rxandroid:1.1.0'// RxJavacompile 'io.reactivex:rxjava:1.1.5'

这是我项目里面用的版本,也可以到Maven/RxJava下获取*新版本。

RxJava 入门

前面讲了那么多,大家在概念上对RxJava有一个初步的认识就好,接下来,将为您解开RxJava神秘的面纱~~

无需过分纠结于“事件”这个词,暂时可以简单的把“事件”看成是一个值,或者一个对象。

  1. 事件产生,就是构造要传递的对象;
  2. 事件处理变换,就是改变传递的对象,可以改变对象的值,或是干脆创建个新对象,新对象类型也可以与源对象不一样;
  3. 事件处理,就是接收到对象后要做的事;

事件产生

RxJava创建一个事件比较简单,由 Observable 通过 create 操作符来创建。举个栗子,还是经典的 HelloWorld~~

// 创建一个ObservableObservable<String> observable = Observable.create(new Observable.OnSubscribe<String>() {    @Override
    public void call(Subscriber<? super String> subscriber) {        // 发送一个 Hello World 事件
        subscriber.onNext("Hello World!");        // 事件发送完成
        subscriber.onCompleted();
    }
});

这段代码可以理解为, Observable 发出了一个类型为 String ,值为 “Hello World!” 的事件,仅此而已。

对于 Subscriber 来说,通常onNext()可以多次调用,*后调用onCompleted()表示事件发送完成。

上面这段代码,也可以通过just操作符进行简化。RxJava常用操作符后面会详细介绍,这里先有个了解。

// 创建对象,just里面的每一个参数,相当于调用一次Subscriber#OnNext()Observable<String> observable = Observable.just("Hello World!");

这样,是不是简单了许多?

事件消费

有事件产生,自然也要有事件消费。RxJava 可以通过 subscribe 操作符,对上述事件进行消费。首先,先创建一个观察者。

// 创建一个ObserverObserver<String> observer = new Observer<String>() {    @Override
    public void onCompleted() {
        Log.i(TAG, "complete");
    }    @Override
    public void onError(Throwable e) {

    }    @Override
    public void onNext(String s) {
        Log.i(TAG, s);
    }
};

或者

// 创建一个SubscriberSubscriber<String> subscriber = new Subscriber<String>() {    @Override
    public void onCompleted() {
        Log.i(TAG, "complete");
    }    @Override
    public void onError(Throwable e) {

    }    @Override
    public void onNext(String s) {
        Log.i(TAG, s);
    }
};
  1. Observer 是观察者, Subscriber 也是观察者,Subscriber 是一个实现了Observer接口的抽象类,对 Observer 进行了部分扩展,在使用上基本没有区别;
  2. Subscriber 多了发送之前调用的 onStart() 和解除订阅关系的 unsubscribe() 方法。
  3. 并且,在 RxJava 的 subscribe 过程中,Observer 也总是会先被转换成一个 Subscriber 再使用。所以在这之后的示例代码,都使用 Subscriber 来作为观察者。

事件订阅

*后,我们可以调用 subscribe 操作符, 进行事件订阅。

// 订阅事件observable.subscribe(subscriber);

在 Subscriber 实现的三个方法中,顾名思义,对应三种不同状态:
1. onComplete(): 事件全部处理完成后回调
2. onError(Throwable t): 事件处理异常回调
3. onNext(T t): 每接收到一个事件,回调一次

区分回调动作

对于事件消费事件订阅来说,好像为了打印一个“Hello World!”要费好大的劲… 其实,RxJava 自身提供了精简回调方式,我们可以为 Subscriber 中的三种状态根据自身需要分别创建一个回调动作 Action

// onComplete()Action0 onCompleteAction = new Action0() {    @Override
    public void call() {
        Log.i(TAG, "complete");
    }
};// onNext(T t)Action1<String> onNextAction = new Action1<String>() {    @Override
    public void call(String s) {
        Log.i(TAG, s);
    }
};// onError(Throwable t)Action1<Throwable> onErrorAction = new Action1<Throwable>() {    @Override
    public void call(Throwable throwable) {

    }
};

那么,RxJava 的事件订阅支持以下三种不完整定义的回调。

observable.subscribe(onNextAction);

observable.subscribe(onNextAction, onErrorAction);

observable.subscribe(onNextAction, onErrorAction, onCompleteAction);

我们可以根据当前需要,传入对应的 Action, RxJava 会相应的自动创建 Subscriber。

  1. Action0 表示一个无回调参数的Action;
  2. Action1 表示一个含有一个回调参数的Action;
  3. 当然,还有Action2 ~ Action9,分别对应2~9个参数的Action;
  4. 每个Action,都有一个 call() 方法,通过泛型T,来指定对应参数的类型;

入门示例

前面讲解了事件的产生到消费、订阅的过程,下面就举个完整的例子。从res/mipmap中取出一张图片,显示在ImageView上。

final ImageView ivLogo = (ImageView) findViewById(R.id.ivLogo);

Observable.create(new Observable.OnSubscribe<Drawable>() {        @Override
        public void call(Subscriber<? super Drawable> subscriber) {            // 从mipmap取出一张图片作为Drawable对象
            Drawable drawable = ContextCompat.getDrawable(mContext, R.mipmap.ic_launcher);            // 把Drawable对象发送出去
            subscriber.onNext(drawable);

            subscriber.onCompleted();
        }
    })
    .subscribe(new Subscriber<Drawable>() {        @Override
        public void onCompleted() {

        }        @Override
        public void onError(Throwable e) {
            Log.i(TAG, e.toString());
        }        @Override
        public void onNext(Drawable drawable) {            // 接收到Drawable对象,显示在ImageView上
            ivLogo.setImageDrawable(drawable);
        }
    });

上面示例是RxJava*基本的一个用法。稍微消化一下,继续~~

RxJava 进阶

Scheduler线程控制

默认情况下,RxJava事件产生和消费均在同一个线程中,例如在主线程中调用,那么事件的产生和消费都在主线程。

那么问题来了,假如事件产生的过程是耗时操作,比如网络请求,结果显示在UI中,这个时候在主线程执行对于网络请求就不合适了,而在子线程执行,显示结果需要进行UI操作,同样不合适~~

所以,RxJava 的*个牛逼之处在于可以自由切换线程!那么,如何做?

在 RxJava 中,提供了一个名为 Scheduler 的线程调度器,RxJava 内部提供了4个调度器,分别是:

  1. Schedulers.io(): I/O 操作(读写文件、数据库、网络请求等),与newThread()差不多,区别在于io() 的内部实现是是用一个无数量上限的线程池,可以重用空闲的线程,因此多数情况下 io() 效率比 newThread() 更高。值得注意的是,在 io() 下,不要进行大量的计算,以免产生不必要的线程;
  2. Schedulers.newThread(): 开启新线程操作;
  3. Schedulers.immediate(): 默认指定的线程,也就是当前线程;
  4. Schedulers.computation():计算所使用的调度器。这个计算指的是 CPU 密集型计算,即不会被 I/O等操作限制性能的操作,例如图形的计算。这个 Scheduler 使用的固定的线程池,大小为 CPU 核数。值得注意的是,不要把 I/O 操作放在 computation() 中,否则 I/O 操作的等待时间会浪费 CPU;
  5. AndroidSchedulers.mainThread(): RxJava 扩展的 Android 主线程;

我们可以通过 subscribeOn() 和 observeOn() 这两个方法来进行线程调度。举个栗子:

依然还是显示一张图片,不同的是,这次是从网络上加载图片

final ImageView ivLogo = (ImageView) findViewById(R.id.ivLogo);

Observable.create(new Observable.OnSubscribe<Drawable>() {    @Override
    public void call(Subscriber<? super Drawable> subscriber) {        try {
            Drawable drawable = Drawable.createFromStream(new URL("https://ss2.baidu.com/6ONYsjip0QIZ8tyhnq/it/u=2502144641,437990411&fm=80&w=179&h=119&img.JPEG").openStream(), "src");
            subscriber.onNext(drawable);
        } catch (IOException e) {
            subscriber.onError(e);
        }
    }
})        // 指定 subscribe() 所在的线程,也就是上面call()方法调用的线程
        .subscribeOn(Schedulers.io())        // 指定 Subscriber 回调方法所在的线程,也就是onCompleted, onError, onNext回调的线程
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Drawable>() {            @Override
            public void onCompleted() {

            }            @Override
            public void onError(Throwable e) {
                Log.e(TAG, e.toString());
            }            @Override
            public void onNext(Drawable drawable) {
                ivLogo.setImageDrawable(drawable);
            }
        });

所以,这段代码就做一件事,在 io 线程加载一张网络图片,加载完毕之后在主线程中显示到ImageView上。

变换

RxJava的又一牛逼之处,在于 变换。啥意思呢? 就是将发送的事件或事件序列,加工后转换成不同的事件或事件序列。

map操作符

变换的概念不好理解吧?举个简单的栗子,我们对上述示例 进行改写。

final ImageView ivLogo = (ImageView) findViewById(R.id.ivLogo);

Observable.create(new Observable.OnSubscribe<String>() {    @Override
    public void call(Subscriber<? super String> subscriber) {

        subscriber.onNext("https://ss2.baidu.com/-vo3dSag_xI4khGko9WTAnF6hhy/image/h%3D200/sign=4db5130a073b5bb5a1d727fe06d2d523/cf1b9d16fdfaaf51965f931e885494eef11f7ad6.jpg");
    }
}).map(new Func1<String, Drawable>() {    @Override
    public Drawable call(String url) {        try {
            Drawable drawable = Drawable.createFromStream(new URL(url).openStream(), "src");            return drawable;
        } catch (IOException e) {

        }        return null;
    }
})        // 指定 subscribe() 所在的线程,也就是call()方法调用的线程
        .subscribeOn(Schedulers.io())        // 指定 Subscriber 回调方法所在的线程,也就是onCompleted, onError, onNext回调的线程
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Drawable>() {            @Override
            public void onCompleted() {

            }            @Override
            public void onError(Throwable e) {
                Log.e(TAG, e.toString());
            }            @Override
            public void onNext(Drawable drawable) {                if (drawable != null) {
                    ivLogo.setImageDrawable(drawable);
                }
            }
        });

经过改写代码后,有什么变化呢? Observable 创建了一个 String 事件,也就是产生一个url,通过 map 操作符进行变换,返回Drawable对象,这个变换指的就是通过url进行网络图片请求,返回一个Drawable。所以简单的来说就是把String事件,转换为Drawable事件。逻辑表示就是:

Observable<String> --> map变换 --> Observable<Drawable>

那么,Func1 是什么呢?与 Action1 类似,不同的是 FuncX 有返回值,而 ActionX 没有。为什么需要返回值呢?目的就在于对象的变换,由String对象转换为Drawable对象。同样,也有Func0 ~ Func9,对应不同的参数个数。

当然了,RxJava 的变换,可不止于map这么简单,继续往下!

flatMap操作符

不难发现,上述的 map 操作符,是一对一的变换,并且返回的是变换后的对象。而 flatMap 操作符可以适应一对多,并且返回的是一个 Observable 。应用场景举例:例如一个员工负责多个任务,现在要打印所有员工的所有任务。

final List<Employee> list = new ArrayList<Employee>() {
    {
        add(new Employee("jackson", mission_list1));
        add(new Employee("sunny", mission_list2));
    }
};
Observable.from(list)
        .flatMap(new Func1<Employee, Observable<Employee.Mission>>() {            @Override
            public Observable<Employee.Mission> call(Employee employee) {                return Observable.from(employee.missions);
            }
        })
        .subscribe(new Subscriber<Employee.Mission>() {            @Override
            public void onCompleted() {

            }            @Override
            public void onError(Throwable e) {

            }            @Override
            public void onNext(Employee.Mission mission) {
                Log.i(TAG, mission.desc);
            }
        });

执行结果为顺序打印出两位员工的所有任务列表。

通过上面的代码可以看出,map 与 flatMap 这两个操作符的共同点在于,他们都是把一个对象转换为另一个对象,但须注意以下这些特点:

  1. flatMap 返回的是一个Observable对象,而 map 返回的是一个普通转换后的对象;
  2. flatMap 返回的Observable对象并不是直接发送到Subscriber的回调中,而是重新创建一个Observable对象,并激活这个Observable对象,使之开始发送事件;而 map 变换后返回的对象直接发到Subscriber回调中;
  3. flatMap 变换后产生的每一个Observable对象发送的事件,*后都汇入同一个Observable,进而发送给Subscriber回调;
  4. map返回类型 与 flatMap 返回的Observable事件类型,可以与原来的事件类型一样;
  5. 可以对一个Observable多次使用 map 和 flatMap

鉴于 flatMap 自身强大的功能,这常常被用于 嵌套的异步操作,例如嵌套网络请求。传统的嵌套请求,一般都是在前一个请求的 onSuccess() 回调里面发起新的请求,这样一旦嵌套多个的话,缩进就是大问题了,而且严重的影响代码的可读性。而RxJava嵌套网络请求仍然通过链式结构,保持代码逻辑的清晰!举个栗子:

Github上的 README.md 文件,通常是 MarkDown 语法。我们要获取 README.md 内容并按 MarkDown 风格显示在UI上,就可以通过以下方式(Retrofit2 + RxJava,稍后会介绍):

new ReadmeContentClient()    // 获取md语法的Readme内容, 返回的是一个Observable<String>对象
    .getReadme()
    .flatMap(new Func1<String, Observable<String>>() {        @Override
        public Observable<String> call(String md) {            // 由于Readme的内容是md语法,需要转成html字符串通过WebView显示到UI
            // 返回的也是Observable<String>对象
            return new MarkDownStyleClient(md)
                            .formatMarkStyle();
        }
    })
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<String>() {        @Override
        public void onCompleted() {

        }        @Override
        public void onError(Throwable e) {
            Log.e(TAG, "readme:" + e.toString());
        }        @Override
        public void onNext(String html) {            // html就是根据readme md格式内容,生成的html代码
            view.showReadme(html);
        }
    });

RxJava 其他常用操作符

  1. from
    接收一个集合作为输入,然后每次输出一个元素给subscriber。

    // Observable.from(T[] params)Observable.from(new Integer[]{1, 2, 3, 4, 5})
        .subscribe(new Action1<Integer>() {        @Override
            public void call(Integer number) {
                Log.i(TAG, "number:" + number);
            }
        });

    注意:如果from()里面执行了耗时操作,即使使用了subscribeOn(Schedulers.io()),仍然是在主线程执行,可能会造成界面卡顿甚至崩溃,所以耗时操作还是使用Observable.create(…);

  2. just
    接收一个可变参数作为输入,*终也是生成数组,调用from(),然后每次输出一个元素给subscriber。

    // Observable.just(T... params),params的个数为1 ~ 10Observable.just(1, 2, 3, 4, 5)
        .subscribe(new Action1<Integer>() {        @Override
            public void call(Integer number) {
                Log.i(TAG, "number:" + number);
            }
        });
  3. filter
    条件过滤,去除不符合某些条件的事件。举个栗子:

    Observable.from(new Integer[]{1, 2, 3, 4, 5})
        .filter(new Func1<Integer, Boolean>() {        @Override
            public Boolean call(Integer number) {            // 偶数返回true,则表示剔除奇数,留下偶数
                return number % 2 == 0;
            }
        })
        .subscribe(new Action1<Integer>() {        @Override
            public void call(Integer number) {
                Log.i(TAG, "number:" + number);
            }
        });
  4. take
    *多保留的事件数。
  5. doOnNext
    在处理下一个事件之前要做的事。

    Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})
        .filter(new Func1<Integer, Boolean>() {        @Override
            public Boolean call(Integer number) {            // 偶数返回true,则表示剔除奇数
                return number % 2 == 0;
            }
        })    // *多保留三个,也就是*后剩三个偶数
        .take(3)
        .doOnNext(new Action1<Integer>() {        @Override
            public void call(Integer number) {            // 在输出偶数之前输出它的hashCode
                Log.i(TAG, "hahcode = " + number.hashCode() + "");
            }
        })
        .subscribe(new Action1<Integer>() {        @Override
            public void call(Integer number) {
                Log.i(TAG, "number = " + number);
            }
        });

    输出如下:

    hahcode = 2number = 2hahcode = 4number = 4hahcode = 6number = 6
  6. debounce
    通俗点讲,就是N个事件发生的时间间隔太近,就过滤掉前N-1个事件,保留*后一个事件。debounce可以指定这个时间间隔!可以用在SearchEditText请求关键词的地方,SearchEditText的内容变化太快,可以抵制频繁请求关键词,后面第15条15.Subject会介绍这个。为了演示效果,先举个简单栗子:

    Observable
        .create(new Observable.OnSubscribe<Integer>() {        @Override
            public void call(Subscriber<? super Integer> subscriber) {            int i = 0;            int[] times = new int[]{100, 1000};            while (true) {
                    i++;                if (i >= 100)                    break;
                    subscriber.onNext(i);                try {                    // 注意!!!!
                        // 当i为奇数时,休眠1000ms,然后才发送i+1,这时i不会被过滤掉
                        // 当i为偶数时,只休眠100ms,便发送i+1,这时i会被过滤掉
                        Thread.sleep(times[i % 2]);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                subscriber.onCompleted();
            }
        })    // 间隔400ms以内的事件将被丢弃
        .debounce(400, TimeUnit.MILLISECONDS)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Integer>() {        @Override
            public void onCompleted() {
                Log.i(TAG, "complete");
            }        @Override
            public void onError(Throwable e) {
                Log.e(TAG, e.toString());
            }        @Override
            public void onNext(Integer integer) {
                Log.i(TAG, "integer = " + integer);
            }
        });

    输出结果:

    11-23 10:44:45.167 MainActivity: integer = 111-23 10:44:46.270 MainActivity: integer = 311-23 10:44:47.373 MainActivity: integer = 511-23 10:44:48.470 MainActivity: integer = 711-23 10:44:49.570 MainActivity: integer = 911-23 10:44:50.671 MainActivity: integer = 1111-23 10:44:51.772 MainActivity: integer = 1311-23 10:44:52.872 MainActivity: integer = 1511-23 10:44:53.973 MainActivity: integer = 17...

    我们设置过滤条件为400ms,可以发现,奇数正常输出,因为在它的下一个事件事件隔了1000ms,所以它不会被过滤掉;偶数被过滤掉,是因为它距离下一个事件(奇数)只隔了100ms。并且,输出的两个事件相隔大约为 100ms + 1000ms = 1100ms

  7. merge
    用于合并两个Observable为一个Observable。较为简单。

    Observable.merge(Observable1, Observable2)
        .subscribe(subscriber);
  8. concat
    顺序执行多个Observable,个数为1 ~ 9。例子稍后与first操作符一起~~
  9. compose
    与 flatMap 类似,都是进行变换,返回Observable对象,激活并发送事件。

    1. compose 是唯一一个能够从数据流中得到原始Observable的操作符,所以,那些需要对整个数据流产生作用的操作(比如,subscribeOn()和observeOn())需要使用 compose 来实现。相较而言,如果在flatMap()中使用subscribeOn()或者observeOn(),那么它仅仅对在 flatMap 中创建的Observable起作用,而不会对剩下的流产生影响。这样就可以简化subscribeOn()以及observeOn()的调用次数了。
    2. compose 是对 Observable 整体的变换,换句话说, flatMap 转换Observable里的每一个事件,而 compose 转换的是整个Observable数据流。
    3. flatMap 每发送一个事件都创建一个 Observable,所以效率较低。而 compose 操作符只在主干数据流上执行操作。
    4. 建议使用 compose 代替 flatMap
  10. first
    只发送符合条件的*个事件。可以与前面的contact操作符,做网络缓存。举个栗子:依次检查Disk与Network,如果Disk存在缓存,则不做网络请求,否则进行网络请求。

    // 从缓存获取Observable<BookList> fromDisk = Observable.create(new Observable.OnSubscribe<BookList>() {    @Override
        public void call(Subscriber<? super BookList> subscriber) {
            BookList list = getFromDisk();        if (list != null) {
                subscriber.onNext(list);
            } else {
                subscriber.onCompleted();
            }
        }
    });// 从网络获取Observable<BookList> fromNetWork = bookApi.getBookDetailDisscussionList();
    
    Observable.concat(fromDisk, fromNetWork)        // 如果缓存不为null,则不再进行网络请求。反之
            .first()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Subscriber<BookList>() {            @Override
                public void onCompleted() {
    
                }            @Override
                public void onError(Throwable e) {
    
                }            @Override
                public void onNext(BookList discussionList) {
    
                }
            });

    网络缓存用法,具体可参见我的项目:https://github.com/JustWayward/BookReader

  11. timer
    可以做定时操作,换句话讲,就是延迟执行。事件间隔由timer控制。举个栗子:两秒后输出“Hello World!”

    Observable.timer(2, TimeUnit.SECONDS)
        .subscribe(new Subscriber<Long>() {        @Override
            public void onCompleted() {
    
            }        @Override
            public void onError(Throwable e) {
    
            }        @Override
            public void onNext(Long aLong) {
                Log.i(TAG, "Hello World!");
            }
        });
  12. interval
    定时的周期性操作,与timer的区别就在于它可以重复操作。事件间隔由interval控制。举个栗子:每隔两秒输出“Hello World!”

    Observable.interval(2, TimeUnit.SECONDS)
        .subscribe(new Subscriber<Long>() {        @Override
            public void onCompleted() {
    
            }        @Override
            public void onError(Throwable e) {
    
            }        @Override
            public void onNext(Long aLong) {
                Log.i(TAG, "Hello World!");
            }
        });
  13. throttleFirst
    与debounce类似,也是时间间隔太短,就丢弃事件。可以用于防抖操作,比如防止双击。

    RxView.clicks(button)
      .throttleFirst(1, TimeUnit.SECONDS)
      .subscribe(new Observer<Object>() {      @Override
          public void onCompleted() {
    
          }      @Override
          public void onError(Throwable e) {
    
          }      @Override
          public void onNext(Object o) {
               Log.i(TAG, "do clicked!");
          }
      });

    上面这个RxView详见:https://github.com/JakeWharton/RxBinding, 主要与RxJava结合用于一些View的事件绑定,JakeWharton大神的项目,厉害。

  14. Single
    Single与Observable类似,相当于是他的精简版。订阅者回调的不是OnNext/OnError/onCompleted,而是回调OnSuccess/OnError。

    Single.create(new Single.OnSubscribe<Object>() {    @Override
        public void call(SingleSubscriber<? super Object> subscriber) {
            subscriber.onSuccess("Hello");
        }
    }).subscribe(new SingleSubscriber<Object>() {    @Override
        public void onSuccess(Object value) {
            Log.i(TAG, value.toString());
        }    @Override
        public void onError(Throwable error) {
    
        }
    });
  15. Subject
    Subject这个类,既是Observable又是Observer,啥意思呢?就是它自身既是事件的生产者,又是事件的消费者,相当于自身是一条管道,从一端进,又从另一端出。举个栗子:PublishSubject

    Subject subject = PublishSubject.create();// 1.由于Subject是Observable,所以进行订阅subject.subscribe(new Subscriber<Object>() {    @Override
        public void onCompleted() {
    
        }    @Override
        public void onError(Throwable e) {
    
        }    @Override
        public void onNext(Object o) {
            Log.i(TAG, o.toString());
        }
    });// 2.由于Subject同时也是Observer,所以可以调用onNext发送数据subject.onNext("world");

    这个好像有点厉害的样子,哈哈。可以配合debounce,避免SearchEditText频繁请求。

    Subject subject = PublishSubject.create();
    
    subject.debounce(400, TimeUnit.MILLISECONDS)
            .subscribe(new Subscriber<Object>() {        @Override
            public void onCompleted() {
    
            }        @Override
            public void onError(Throwable e) {
    
            }        @Override
            public void onNext(Object o) {            // request
            }
        });
    
    edittext.addTextChangedListener(new TextWatcher() {    @Override 
        public void beforeTextChanged(CharSequence s, int start, int count, int after) { }    @Override 
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            subject.onNext(s.toString());
        }    @Override 
        public void afterTextChanged(Editable s) { } 
    });

RxJava 应用

RxJava+Retrofit 的网络请求方式

Retrofit是一个非常适合RestAPI的网络请求库。没用过的童鞋,还是推荐学一学的。

使用Callback的请求方式:

// 1. 定义一个请求接口@GET("/match/stat")
Call<String> getMatchStat(@Query("mid") String mid, @Query("tabType") String tabType);// 2. 创建Service对象Retrofit retrofit = new Retrofit.Builder()
                        .baseUrl(BuildConfig.TENCENT_SERVER)// 加入RxJava支持
                        .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 
                        .addConverterFactory(ScalarsConverterFactory.create())
                        .client(OkHttpHelper.getTecentClient()).build();

TencentApi api = retrofit.create(TencentApi.class);// 3. 调用Call<String> call = api.getMatchStat(mid, tabType);
call.enqueue(new Callback<String>() {    @Override
    public void onResponse(Call<String> call, Response<String> response) {        if(response != null && response.body()!=null)            // 成功
        } else {            // 无数据
        }
    }    @Override
    public void onFailure(Call<String> call, Throwable t) {        // 失败
    }
});

与 RxJava 结合的方式,则是

// 1. 定义请求接口,返回的是Observable对象@GET("/user/followers")
Observable<List<User>> followers();// 2. 同样是创建api对象...// 3. 请求api.followers()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Observer<List<User>>() {        @Override
        public void onCompleted() {

        }        @Override
        public void onError(Throwable e) {            // 请求出错。可能发生网络异常、Json解析异常等等
        }        @Override
        public void onNext(List<User> list) {            // 请求成功
            view.showMyFollowers(list);
        }
    });

若需嵌套请求,比如先获取Token再进行才能进行登录,可参考flatMap操作符*后的获取Readme内容显示在WebView上的例子。

Retrofit2 + RxJava + Dagger2: 具体可参见我的项目,里面有比较详细的用法。
https://github.com/JustWayward/BookReader

不难发现,Retrofit 把请求封装进 Observable ,在请求结束后调用 onNext() 以及 OnCompleted() 或在请求失败后调用 onError()

:RxJava形式的请求,并不能减少代码量,但是逻辑非常清晰。假如请求到数据之后需要对数据进行处理,并且是耗时操作,难道要再开一个线程,或者用AsyncTask再做一次异步?很显然,RxJava的变换很好的解决了这个问题,依然会使逻辑结构清晰。

RxBus

准确的来说,是一种基于RxJava实现事件总线的一种思想。可以替代EventBus/Otto,因为他们都依赖于观察者模式。可以参考https://github.com/AndroidKnife/RxBus这个库。

RxBinding

前面介绍过了,JakeWharton大神的项目,https://github.com/JakeWharton/RxBinding, 主要与RxJava结合用于一些View的事件绑定。

RxJava 的一些坑

未取消订阅而引起的内存泄漏

举个栗子,对于前面常用操作符12.interval做周期性操作的例子,并没有使之停下来的,没有去控制订阅的生命周期,这样,就有可能引发内存泄漏。所以,在Activity#onDestroy()的时候或者不需要继续执行的时候应该取消订阅。

Subscription subscription = Observable.interval(2, TimeUnit.SECONDS)
    .subscribe(new Subscriber<Long>() {        @Override
        public void onCompleted() {

        }        @Override
        public void onError(Throwable e) {

        }        @Override
        public void onNext(Long aLong) {
            Log.i(TAG, "Hello World!");
        }
    });// 调用unsubscribe();方法进行取消订阅subscription.unsubscribe();

但是,如果有很多个数据源,那岂不是要取消很多次?当然不是的,可以利用 CompositeSubscription, 相当于一个 Subscription 集合。

CompositeSubscription list = new CompositeSubscription();
list.add(subscription1);
list.add(subscription2);
list.add(subscription3);// 统一调用一次unsubscribe,就可以把所有的订阅都取消list.unsubscribe();

总结

相信到了这里,大家对RxJava应该有了一个比较清晰的理解。当然,实践出真知,还是要去尝试,才能更深层次的体会到其强大之处。

*后,总结一下RxJava的基本使用过程。

  1. 首先是创建事件源源,也就是被观察者,可以用Observable的create/just/from等方法来创建;
  2. 通过filter/debounce等操作符,进行自定义事件过滤;
  3. 通过Schedules进行事件发送和订阅的线程控制,也就是subscribeOn() 和 observeOn();
  4. 通过map/flatMap/compose等操作符,进行事件的变换;
  5. 调用subscribe进行事件订阅;
  6. *后,不要忘了对订阅者生命周期的控制,不用的时候,记得调用unsubscribe(),以免引发内存泄漏。

Android studio 如何查看 library 间的依赖关系

一、配置环境
Android Studio中使用的 gradle 版本一般不是*新版,所以在使用其自带的 Terminal 时容易报版本过低的错误,为了方便使用,我从 gradle
官网下载了*新版的 gradle ,然后配置好 gradle 的环境变量以方便使用。

二、gradle task 相关内容
gradle 本身不提供查看 library 依赖关系的命令,幸好 Android Studio 提供了可供查看的 task ,位于各个 module 的 help 任务堆中,如下图:

%title插图%num

%title插图%num

双击 dependencies 执行该任务,可以看到在 gradle console 中均没有得到依赖关系,结果如下图所示:

%title插图%num

从上图可以看出无论我们执行哪个 module 下的 dependencies 其结果都是相当于在 Root project 执行了该任务;那么在 app project 或者 test01library project 中执行该 dependencies 任务呢?此时就需要我们通过命令行的方式执行 dependencies 了。

1.查看 app project 的依赖关系
在该路径下打开命令行工具,输入

gradle dependencies

稍等一会,便可看到 library 的依赖关系,如下图所示:

%title插图%num

但是命令行中会生成大量内容,并且无法看到全部信息,为了方便查看,我们将输出信息写入文本文件中

gradle dependencies >log.txt

在当前目录下将生成一个 log.txt 文件里面包括所有 app project 所依赖 library 的所有依赖关系(test01library 同理) 。

2.查看指定类型的依赖关系
由于 dependencies 的配置类型太多这里仅以编译时 library 为例说明:

// 查看 compile 时的依赖关系
gradle dependencies –configuration compile

关于其他配置类型可以通过这条命令获得:

gradle dependencies –info

可以看到有很多参数

%title插图%num

3.在 Root project 下查看依赖关系
在项目根目录下我们可以通过下面命令达到和上面相同的效果,如:

// gradle :project name:dependencies [–configuration compile]
gradle :app:dependencies –configuration compile

三、总结
查看各 library 的依赖关系是为了避免出现java.util.zip.ZipException: duplicate entry exception android/support/vX/…/xxx.class 异常;由于 app project 必定会直接或间接引用其他所有 project, 所以,只查看这一个 project 的依赖关系即可得到全部信息。

Android 查看项目依赖关系

做android项目,module多的话,很容易遇到包冲突的问题。比如v4既有26.1.0版本,也有27.1.0版本。
这时候,要找出两个版本都是从哪个module 或者第三方包引入的,然后再用exclude命令,把不要的版本去除。
例如

implementation (‘com.google.firebase:firebase-core:15.0.0’) {
exclude group: ‘com.android.support’, module: ‘support-v4’
}

那么,怎么查真个工程的依赖呢?网上很多说,用命令行执行:gradle -q dependencies。
这个在windows下,很容易出错,原因是gradle 这个可执行文件,并不在当前工程下,而是在AndroidStudio的安装目录,没有加到环境变量,就不能执行。
其实没必要用命令行,可以用AndroidStudio集成好的入口:

在AndroidStudio的右边,以下入口,双击dependencies,就可以了。
如果是要整个app的关系,则双击的是module app下的help/dependencies。如果是只想看其他子模块,则在对应的模块,双击help/dependencies即可。

%title插图%num

依赖关系结果示例:

%title插图%num
如果duplicate的包,真的有多个版本,把以上依赖关系数据,复制到文本编辑器,搜module名字(如android-support),就能找到对应得包,以及是哪个包带入的。

异常、堆内存溢出、OOM的几种情况

1堆内存溢出
2Java异常
OOM
1、堆内存溢出
【情况一】:
java.lang.OutOfMemoryError: Java heap space:这种是java堆内存不够,一个原因是真不够,另一个原因是程序中有死循环;
如果是java堆内存不够的话,可以通过调整JVM下面的配置来解决:
< jvm-arg>-Xms3062m < / jvm-arg>
< jvm-arg>-Xmx3062m < / jvm-arg>

【情况二】
java.lang.OutOfMemoryError: GC overhead limit exceeded
【解释】:JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。
【解决方案】:
1、查看系统是否有使用大内存的代码或死循环;
2、通过添加JVM配置,来限制使用内存:
< jvm-arg>-XX:-UseGCOverheadLimit< /jvm-arg>

【情况三】:
java.lang.OutOfMemoryError: PermGen space:这种是P区内存不够,可通过调整JVM的配置:
< jvm-arg>-XX:MaxPermSize=128m< /jvm-arg>
< jvm-arg>-XXermSize=128m< /jvm-arg>
【注】:
JVM的Perm区主要用于存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space,这个区域成为年老代,GC在主程序运行期间不会对年老区进行清理,默认是64M大小,当程序需要加载的对象比较多时,超过64M就会报这部分内存溢出了,需要加大内存分配,一般128m足够。

【情况四】:
java.lang.OutOfMemoryError: Direct buffer memory
调整-XX:MaxDirectMemorySize= 参数,如添加JVM配置:
< jvm-arg>-XX:MaxDirectMemorySize=128m< /jvm-arg>

【情况五】:
java.lang.OutOfMemoryError: unable to create new native thread
【原因】:Stack空间不足以创建额外的线程,要么是创建的线程过多,要么是Stack空间确实小了。
【解决】:由于JVM没有提供参数设置总的stack空间大小,但可以设置单个线程栈的大小;而系统的用户空间一共是3G,除了Text/Data/BSS /MemoryMapping几个段之外,Heap和Stack空间的总量有限,是此消彼长的。因此遇到这个错误,可以通过两个途径解决:
1.通过 -Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError);
2.通过-Xms -Xmx 两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。

【情况六】:
java.lang.StackOverflowError
【原因】:这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。
【解决】:优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小。

2、Java异常

Throwable
Throwable是 Java 语言中所有错误或异常的超类。
Throwable包含两个子类: Error 和 Exception 。它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。

Exception
Exception及其子类是 Throwable 的一种形式,它指出了合理的应用程序想要捕获的条件。

RuntimeException
RuntimeException是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
编译器不会检查RuntimeException异常。 例如,除数为零时,抛出ArithmeticException异常。RuntimeException是ArithmeticException的超类。当代码发生除数为零的情况时,倘若既”没有通过throws声明抛出ArithmeticException异常”,也”没有通过try…catch…处理该异常”,也能通过编译。这就是我们所说的”编译器不会检查RuntimeException异常”!
如果代码会产生RuntimeException异常,则需要通过修改代码进行避免。 例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!

Error
和Exception一样, Error也是Throwable的子类。 它用于指示合理的应用程序不应该试图捕获的严重问题,大多数这样的错误都是异常条件。
和RuntimeException一样, 编译器也不会检查Error。

Java将可抛出(Throwable)的结构分为三种类型: 被检查的异常(Checked Exception),运行时异常(RuntimeException)和错误(Error)。

(01) 运行时异常
定义 : RuntimeException及其子类都被称为运行时异常。
特点 : Java编译器不会检查它。 也就是说,当程序中可能出现这类异常时,倘若既”没有通过throws声明抛出它”,也”没有用try-catch语句捕获它”,还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。
如果产生运行时异常,则需要通过修改代码来进行避免。 例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!

(02) 被检查的异常
定义 : Exception类本身,以及Exception的子类中除了”运行时异常”之外的其它子类都属于被检查异常。
特点 : Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。
被检查异常通常都是可以恢复的。

(03) 错误
定义 : Error类及其子类。
特点 : 和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。
按照Java惯例,我们是不应该是实现任何新的Error子类的!

对于上面的3种结构,我们在抛出异常或错误时,到底该哪一种?《Effective Java》中给出的建议是: 对于可以恢复的条件使用被检查异常,对于程序错误使用运行时异常。

OOM
1, OutOfMemoryError异常

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,

Java Heap 溢出

一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess

java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到*大堆容量限制后产生内存溢出异常。

出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。

如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

2, 虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的*大深度,将抛出StackOverflowError异常。

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

这里需要注意当栈的大小越大可分配的线程数就越少。

3, 运行时常量池溢出

异常信息:java.lang.OutOfMemoryError:PermGen space

如果要向运行时常量池中添加内容,*简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

4, 方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

异常信息:java.lang.OutOfMemoryError:PermGen space

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

 

几种可能导致OOM异常的情况

本文将讲述两种可能导致OOM的案例
注意:1、程序计数器不会发生OOM
2、在jdk1.8中已经取消了永久代概念,改由元空间取代,就算设置-XX:MetaspaceSize=1m;这种参数限制大小,实际操作时并没有起到多大用处,因此很难通过简单的demo复现以前老年代产生OOM的异常。

由于递归深度过长导致
jvm对递归深度有限制,具体深度由于jdk 版本等的不同有差异,下面这个案例使用jdk1.8测试,递归深度测试为7893,附测试案例:

static int stackDeep = 0;
public static void main(String[] args) {
try{
TestStackOverFlow testStakOverFlow = new TestStackOverFlow();
testStakOverFlow.foo();
}catch(Throwable t){
System.out.println(t);
System.out.println(“栈的深度为:” + stackDeep);
}
}

public void foo() {
stackDeep ++ ;
foo();
}

测试结果:

java.lang.StackOverflowError
栈的深度为:7893

堆溢出
运行时堆溢出是一种常见的溢出情况,下面展示其中的一种,有感兴趣的朋友可以使用Set来代替List重写该案例,异常堆栈信息将不同:
设置JVM参数:

-Xmx1m -XX:+PrintGCDetails

测试代码:

int i = 0;
while(true){
test.add(String.valueOf(i++).intern());
}

异常日志打印如下:

[Full GC (Allocation Failure) [Tenured: 1407K->1408K(1408K), 0.0080421 secs] 1855K->1820K(1984K), [Metaspace: 82K->82K(4480K)], 0.0080726 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)…

可以看到,*后一次GC,是full GC,异常报错在扩容的地方,由于空间不够,扩容时申请不到空间,引发OOM。

小结:OOM发生的情况很多,很多时候需要结合实际产生的问题来具体分析,比如:机器实际内存2g,设置堆大小:-Xmx与-Xms为1.8g,而使用NIO的情况很多,那么可能由于直接内存不够导致异常;另外大对象的使用不当也可能导致OOM;再比如,大量数据直接使用内存作为缓存,也可能导致OOM…很多时候,可以根据JVM参数(比如设置堆栈等的大小,GC收集器等)或者增加机器配置来进行调优。实际上,很多OOM,都是程序不当引起的,因此出现该情况,应首先考虑程序问题。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速