Android进阶——性能优化之多线程总结及简单应用(一)

时间:2022-12-05 17:29:06

引言

总所周知,我们的Android手机系统是一种多任务操作系统。而多线程就是实现多任务的一种基本方式。在Android系统中一个app程序就是一个运行在虚拟机里的一个进程。

一、进程和线程

1、进程

进程是一个正在执行中的程序,每一个进程执行都是依据一个执行顺序,该顺序被称为一个执行路径或者叫一个控制单元,用于封装每一个程序的控制单元。比如在Windows系统中,一个运行的exe就是一个进程。而Android中当某个组件第一次运行的时候,Android自动启动了一个进程,默认情况下其所有的组件和程序运行在这个进程和线程里。
最终组件运行的进程由manifest file控制。组件的节点 —— activity, service, receiver, 和 provider 都包含一个 process 属性用于指定组件运行的进程可以配置组件在一个独立进程运行,或者多个组件在同一个进程运行。甚至可以多个程序在一个进程中运行——如果这些程序共享一个User ID并给定同样的权限。当然application 节点也包含 process 属性用于设置程序中所有组件的默认进程。默认情况下,所有组件均在此进程的主线程中实例化,但系统对这些组件的调用会从主线程中特意分离出来,然而并非所有对象都会从主线程中分离。一般而言,响应用户交互的方法和通知的方法依然在主线程中运行。这也是组件被系统调用的时候不宜长时间运行或者阻塞操作(如网络操作、IO操作、大数据量的计算等耗时操作)的根本原因,因为这样导致阻塞进程中的其他组件,进而导致ANR,所以必须把这类操作从主线程中单独分离出来。

2、线程

线程就是进程中的一个独立的控制单元,线程在控制着进程的执行
一个进程中至少有一个线程。比如Java中的JVM,JVM启动都不是单线程,因为除了主线程之外还有负责垃圾回收GC的一个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。总而言之,CPU真正执行是线程,在CPU某一时刻永远都是在执行一个程序,即永远只有一个控制单元在执行(多核除外),实际情况cpu总是在极短的时间内不停地切换执行路径即线程,是因为切换时间极短所以根本感受不到,这也是我们在线程里常常主动sleep若干时间的原因之一,避免阻塞其他线程。最好,即使我们在前面为组件配置了不同的进程,有时候也需要再配置线程。尤其是当用户界面需要快速对用户交互操作进行响应的情况下,因此譬如网络操作、IO操作、大数据量的计算等耗时操作都应该放到其他线程。

二、多线程的意义

正如前面所说,我们的app就是一个进程,而实际是线程去完成app各项功能,默认情况下,所有的功能都是由主线程mainThread(又称UI Thread)去完成的,而我们知道在一个线程内部程序程序是有序的运行的,这就导致了在某些情况下发生ANR造成不好的体验。所以以笔者的项目经验来说多线程的意义有:

1、提高用户体验

提高用户体验这是最最根本的原因,没啥好解释的。

2、避免ANR

ANR全称Application not response,程序未响应,一般是在操作一些耗时操作时,比如I/O读写的大文件读写,数据库操作以及网络下载需要很长时间,为了不阻塞用户界面,出现ANR的响应提示窗口,因为默认情况下所有的组件都是在主线程中实例化的,包含Service、Activity以及Broadcastrecever均是由主线程处理的。

2.1、ANR的三种类型

  1. KeyDispatchTimeout(5 seconds) –主要是类型按键或触摸事件在特定时间内(5s)无响应

  2. BroadcastTimeout–BroadcastReceiver在特定时间内(10s) 无法处理完成

  3. ServiceTimeout –小概率事件 Service在特定的时间内(20 s)无法处理完成

以上的5s、10s、20s都是Android系统机制规定的,实际情况下,即使你的程序某个事件响应不超过5s,在2s以上再响应也是会有卡顿的现象造成不好的体验的。

2.2、在主线程中进行以下操作易导致ANR

磁盘读写、数据库读写操作 、大量的创建新对象、网络操作等耗时操作都有可能导致ANR。

2.3、尽量避免ANR可以采取的措施

  1. 主线程尽量只做跟UI相关的工作

  2. 单独开辟子线程处理耗时操作

  3. 尽量用Handler来处理主线程和别的Thread之间的通信,在子线程中处理耗时操作 ,处理完成之后,通过handler.sendMessage()传递处理结果,在handler的handleMessage()方法中更新UI 或者使用handler.post()方法将消息放到Looper中

  4. 合理使用AsyncTask ,在doInBackground()方法中执行耗时操作 ,在onPostExecuted()更新UI ,这个在某些情况下会有坑,集合具体业务使用

总之,我们的处理原则就是所有可能耗时的操作都放到其他线程中处理,因为Activity的生命周期方法、事件处理方法等Activity基类中以on为前缀的方法都是在主线程中被回调的,一般而论,Activity的onCreate、onStart、onResume方法执行的时间直接决定了你的应用界面打开的时间。

3、完成一些持续性长周期的后台操作

三、创建多线程

1、构造Thread对象

Android的多线程其实也是基于Java的,Thread 继承自Obejct基类并实现了Runnable接口,虽然在他的成员方法run里没有任何实现,但是它是一个实体类,依然是通过其构造方法来创建对象。

部分构造方法 说明
Thread()
Thread(Runnable target) 通过传入一个Runnable对象构造
Thread(String name) 传入指定线程名
Thread(Runnable target, String name) 传入指定线程名和一个Runnable对象

2、创建多线程的形式

其实创建Thread本质上都是实现了Runnable接口的一个实例,这个Runnable的实例也就是一个线程的实例,启动线程的唯一方法就是通过调用Thread的成员方法start(),其中start方法是一个native方法,它将启动一个新的子线程并执行run方法。

2.1、继承Thread类,然后重写run方法

继承Thread,重写run的方法,因为父类Thread的run方法中并没有任何操作代码,而我们需要执行自己的功能就应该复写父类的run。
创建线程对象,调用线程的start方法启动线程

class PrimeThread extends Thread {
/**
因为虚拟机定义时,Thread类的run方法就是用于存储线程要运行的代码,如果不重写run方法,虚拟机加载run方法的时候,找不到执行代码。
*/

public void run() {
. . .
}
}

//调用:
PrimeThread p = new PrimeThread();
p.start();

2.2.实现Runnable接口,然后实现其run方法。

**实现Runnable 的接口并实现run方法,最后
通过Thread类的构造方法Thread(Runable r)创建Thread对象。因为自定义Runnable接口的**run方法所属对象是Runnable接口的子类对象,要想让线程去执行指定对象的run方法,就必须明确该run方法的所属对象,通过将Runnable接口的子类对象传递到Thread的构造函数来明确。

 class PrimeRun implements Runnable {
public void run() {
. . .
}
}
//调用
PrimeRun p = new PrimeRun(143);
new Thread(p).start();

2.3、通过线程池技术

后面再单独总结。

四、多线程的简单应用

1、继承Thread方式

    /**
* 继承Thread方式,实现自定义的线程
*/

private static class TestThread extends Thread{

@Override
public void run() {
//耗时操作写在这
Log.e("CrazyMo", "run: "+Thread.currentThread().getName()+"is running");
try {
sleep(100);//休眠100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

启动线程

 public void runThread(View view){
TestThread ta=new TestThread();//实例化线程
ta.start();//启动线程
TestThread tb=new TestThread();
tb.start();
}

运行结果:

12-11 23:04:12.793 20739-20912/crazymo.com.testmutilethread E/CrazyMo: Thread-122933is running 
12-11 23:04:12.794 20739-20913/crazymo.com.testmutilethread E/CrazyMo: Thread-122934is running

2、实现Runnable接口

 /**
* 实现Runbable方式
*/

private static class TestRunnable implements Runnable{

@Override
public void run() {
//在这里做耗时操作
Log.e("CrazyMo", Thread.currentThread().getName() +"running by runnable");
try {
Thread.sleep(100);//休眠100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

启动线程

TestRunnable runnable=new TestRunnable();//实例化Runnable
Thread ta=new Thread(runnable);//实例化线程
ta.start();
TestRunnable runnableB=new TestRunnable();
Thread tb=new Thread(runnableB);
tb.start();

运行结果

12-11 22:58:49.998 17706-17985/crazymo.com.testmutilethread E/CrazyMo: Thread-122877running by runnable
12-11 22:58:49.998 17706-17986/crazymo.com.testmutilethread E/CrazyMo: Thread-122878running by runnable

3、匿名类的形式启动多线程

new Thead

运行结果

12-11 23:09:41.953 22747-23290/crazymo.com.testmutilethread E/CrazyMo: Thread-122984anonymous 

4、不允许重复启动已经start的线程

public void runThread(View view){
TestThread ta=new TestThread();//构造对象
ta.start();//启动线程
try {
ta.start();//特意做个小实验
}catch (Exception e){
Log.e("CrazyMo", "runThread: "+e.toString() );
}
}

运行结果

12-11 23:11:33.825 24183-24183/crazymo.com.testmutilethread E/CrazyMo: runThread: java.lang.IllegalThreadStateException: Thread already started
12-11 23:11:33.832 24183-24312/crazymo.com.testmutilethread E/CrazyMo: Thread-123001is running

正如运行结果所示,线程已经start了,就不能再次start了,否则会报IllegalThreadStateException异常,可是为什么不是先执行一次run方法之后再报异常呢?哈哈其实只是两次run执行间隔时间太短导致的。

 public void runThread(View view){
TestThread ta=new TestThread();//构造对象
ta.start();//启动线程
try {
TestThread tb=new TestThread();
Thread.sleep(10000);//加上休眠之后,两次start间隔时间足够先去执行run
ta.start();
}catch (Exception e){
Log.e("CrazyMo", "runThread: "+e.toString() );
}
}

运行结果

12-11 23:17:22.304 26925-27047/crazymo.com.testmutilethread E/CrazyMo: Thread-123030is running 
12-11 23:17:32.304 26925-26925/crazymo.com.testmutilethread E/CrazyMo: runThread: java.lang.IllegalThreadStateException: Thread already started

五、直接继承Thread和实现Runnable接口实现多线程区别

众所周知在Java中类仅支持单继承,当定义一个新的类的时候,它只能扩展一个外部类。假如创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。但继承Thread和实现Runnable重要区别并不是在于此,更重要的是实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享。先看一个例子,以售票系统售卖火车票的三种情况:

1、三个售票员负责分别销售20张学生票、普通票和特价票

这种情况下我们可以直接继承Thread,实现三个子线程模拟三个售票员分别销售三种类型的票

private static class SailTikectThread extends Thread{
private int ticket=20;//每个线程都拥有20张票
///private static int ticket=20;所有线程共享这20张票

public SailTikectThread (){}
public SailTikectThread (String name){
super(name);
}
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
if(ticket>0) {
Log.e("CrazyMo","第"+ticket + "张 is saled by " + Thread.currentThread().getName());
ticket--;
}else {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//开启三个线程,模拟三个售票员售票
SailTikectThread sailerA=new SailTikectThread("sailerA");
sailerA.start();
SailTikectThread sailerB=new SailTikectThread("sailerB");
sailerB.start();
SailTikectThread sailerC=new SailTikectThread("sailerC");
sailerC.start();

运行结果

12-11 23:46:24.528 18313-18381/crazymo.com.testmutilethread E/CrazyMo: 第20is saled by sailerB
12-11 23:46:24.528 18313-18380/crazymo.com.testmutilethread E/CrazyMo: 第20is saled by sailerA
12-11 23:46:24.536 18313-18382/crazymo.com.testmutilethread E/CrazyMo: 第20is saled by sailerC
12-11 23:46:24.628 18313-18381/crazymo.com.testmutilethread E/CrazyMo: 第19is saled by sailerB
12-11 23:46:24.628 18313-18380/crazymo.com.testmutilethread E/CrazyMo: 第19is saled by sailerA
12-11 23:46:24.636 18313-18382/crazymo.com.testmutilethread E/CrazyMo: 第19is saled by sailerC
。。。。。。
E/CrazyMo: 第2is saled by sailerA
12-11 23:46:26.332 18313-18381/crazymo.com.testmutilethread E/CrazyMo: 第2is saled by sailerB
12-11 23:46:26.340 18313-18382/crazymo.com.testmutilethread E/CrazyMo: 第2is saled by sailerC
12-11 23:46:26.432 18313-18380/crazymo.com.testmutilethread E/CrazyMo: 第1is saled by sailerA
12-11 23:46:26.432 18313-18381/crazymo.com.testmutilethread E/CrazyMo: 第1is saled by sailerB
12-11 23:46:26.440 18313-18382/crazymo.com.testmutilethread E/CrazyMo: 第1is saled by sailerC

从以上运行结果得知三个子线程之间并无任何关系,这就说明默认情况每个线程之间是平等的,没有优先级关系,因此都有机会得到CPU的处理。但是结果显示这三个线程并不是依次交替执行,而是在三个线程同时被执行的情况下,有的线程被分配时间片的机会多。

2、三个售票员协同一起销售20张票

这种情况下三个售票员相当于是共享这20张票,我们也有两种机制实现

2.1、直接继承Thread,并设置静态变量

只需要把上例的ticket从普通变量改为静态变量即可,因为我们知道静态变量是属于类的由所有的类的实例共享。

 ///private static int ticket=20;所有线程共享这20张票
12-11 23:53:33.602 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第20is saled by sailerA
12-11 23:53:33.603 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第19is saled by sailerC
12-11 23:53:33.605 23795-23959/crazymo.com.testmutilethread E/CrazyMo: 第18is saled by sailerB
12-11 23:53:33.702 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第17is saled by sailerA
12-11 23:53:33.703 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第16is saled by sailerC
12-11 23:53:33.706 23795-23959/crazymo.com.testmutilethread E/CrazyMo: 第15is saled by sailerB
12-11 23:53:33.803 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第14is saled by sailerA
12-11 23:53:33.803 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第13is saled by sailerC
12-11 23:53:33.806 23795-23959/crazymo.com.testmutilethread E/CrazyMo: 第12is saled by sailerB
12-11 23:53:33.903 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第11is saled by sailerA
12-11 23:53:33.904 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第10is saled by sailerC
12-11 23:53:33.906 23795-23959/crazymo.com.testmutilethread E/CrazyMo: 第9is saled by sailerB
12-11 23:53:34.003 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第8is saled by sailerA
12-11 23:53:34.004 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第7is saled by sailerC
12-11 23:53:34.006 23795-23959/crazymo.com.testmutilethread E/CrazyMo: 第6is saled by sailerB
12-11 23:53:34.103 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第5is saled by sailerA
12-11 23:53:34.104 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第4is saled by sailerC
12-11 23:53:34.106 23795-23959/crazymo.com.testmutilethread E/CrazyMo: 第3is saled by sailerB
12-11 23:53:34.203 23795-23958/crazymo.com.testmutilethread E/CrazyMo: 第2is saled by sailerA
12-11 23:53:34.205 23795-23960/crazymo.com.testmutilethread E/CrazyMo: 第1is saled by sailerC

2.2、使用实现Runnable接口的方式实现资源共享(推荐)

 private static class SailTikectRunnable implements Runnable {
private String name;
private int ticket = 20;//每个线程都拥有20张票

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
if (ticket > 0) {
Log.e("CrazyMo", "第" + ticket + "张 is saled by(runnable) " + Thread.currentThread().getName());
ticket--;
} else {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//启动线程,模拟三个售票员同时销售这20张票
SailTikectRunnable silerA=new SailTikectRunnable();

Thread sa=new Thread(silerA,"sailerA");
sa.start();
Thread sb=new Thread(silerA,"sailerB");
sb.start();
Thread sc=new Thread(silerA,"sailerC");
sc.start();

运行结果

12-12 00:15:18.959 11202-11377/crazymo.com.testmutilethread E/CrazyMo: 第20is saled by(runnable) sailerC
12-12 00:15:18.960 11202-11376/crazymo.com.testmutilethread E/CrazyMo: 第19is saled by(runnable) sailerB
。。。
E/CrazyMo: 第3is saled by(runnable) sailerA
12-12 00:15:19.561 11202-11377/crazymo.com.testmutilethread E/CrazyMo: 第2is saled by(runnable) sailerC
12-12 00:15:19.561 11202-11376/crazymo.com.testmutilethread E/CrazyMo: 第1is saled by(runnable) sailerB

实现Runnable接口相对于扩展Thread类来说,具有无可比拟的优势。此方式不仅有助于程序的健壮性,使代码能够被多个线程共享,而且代码和数据资源相对独立,从而特别适合多个具有相同代码的线程去处理同一资源的情况。使得线程、代码和数据资源三者有效分离,很好地体现了面向对象程序设计的思想。因此,几乎所有的多线程程序都是通过实现Runnable接口的方式来完成的。
PS:
以上的程序均未考虑同步处理,仅作为入门的基本知识介绍,后续的相关的文章中再对锁和同步、异步做总结。