0%

Android多线程技术

本章学习Android多线程技术,多线程更新UI,用Handler方式解决两个线程之间的通信,同时理解学习Handler的实现原理(Handler、Lopper、MesageQueue三者的关系),以及AsyncTask异步任务处理类,最后学习线程池的,多个异步任务时,合理利用线程池可以减少系统资源的使用,增加程序的流畅性。

5.1 多线程的建立

每个应用启动都会启动一个线程处理UI相关的事物,例如处理用户的点击事件、屏幕绘制,并把相关的事件分发到相应的组件中进行处理,所以主线程又被称为UI线程。

耗时任务也在UI线程处理的话会影响用户体验,阻塞线程,所以在这种一般都是新建一个子线程来处理耗时任务。Android中与java中新建线程方式一样,例如在onCreate()方法中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 100;i++) {
Log.i("MainActivity", "当前值是" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();

5.2 子线程更新UI的方法

Android中是不能在子线程中更新UI的,否则会抛出异常,我们一般是在子线程中执行耗时任务,执行任务完后发送消息给主线程,通知主线程更新UI。例如,下载文件,下载完成后发送一个消息通知主线程更新界面,提示用户文件已经下载完成。

Android中子线程更新UI的方式有以下四种:

  • 1,用activity对象的runOnUiThread方法
  • 2,View.post(Runnable r)
  • 3, AsyncTask系统SDK提供的处理耗时任务的类
  • 4,Handler解决多线程间的通信

5.2.1 用activity对象的runOnUiThread方法

5.3 Handler的使用

handler可以发送和处理消息对象或Runnable对象,这些消息对象和Runnable对象与一个线程相关联。

Handler类有两种主要用途:

  • 1,执行Runnable对象,还可设置延迟
  • 2,两个线程之间发送消息,主要用来给主线程发送消息更新UI

5.3.1 为什么要用Handler

解决多线程并发问题,假设有多个线程去更新Activity的UI,并且都没有加锁,那么界面会显示不正常,这是不被允许的。Android提供了一套更新UI的机制,也可以使用Handler来实现多个线程之间的消息发送。

1,使用Handler的sendMessage发送消息
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
private Handler handler = new Handler() {

public void handleMessage(Message msg) {
switch (msg.what) {
case UPDATE_TEXT:
textView.setText("Nice to meet you");
break;
default:
break;
}
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

textView = findViewById(R.id.text);
Button changeText = findViewById(R.id.change_text);
changeText.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
Message message =new Message();
message.what = UPDATE_TEXT;
handler.sendMessage(message);
}
}).start();
break;
default:
break;
}
}

如上代码:这就是Handler消息机制,我们可以通过这个机制来更新UI,也就是异步消息处理机制,解决了在子线程中进行UI操作的问题,下面我们来分析一下Handler消息处理机制。

如上在MainActivity中的代码,我们定义了一个整型常量,表示更新UI操作(在这里是更新textview),我们新建了一个Handler对象,并重写了handleMessage()方法,在这里对具体的message进行处理,若发现Message的what字段的值等于UPDATE_TEXT,就将TextView显示的内容改成Nice to meet you。

我们再看点击事件的代码,我们并没有在子线程中直接进行UI操作,而是创建了一个Message对象,并将它的what字段值指定为UPDATE_TEXT,然后调用Handler的sendMessage方法将这条Message发送出去。Handler收到这条消息,并在handleMessage()对它进行处理,而此时handlerMessage方法的则是运行在主线程中,我们可以在这里进行UI操作,接下来对Message的what字段进行判断,如果等于UPDATE_TEXT,就将TextView的显示内容改成Nice to meet you。

这就是Android异步消息处理的基本用法,下面分析一下它的原理。

5.4 解析异步消息处理机制

Android异步消息主要分为4个部分组成:Message、Handler、MessageQueue、Looper。下面对这4部分进行介绍。

  • 1,Message
    它是线程之间传递的消息,它可以在内部传递少量的消息,用于在不同的线程之间交换数据。比如我们刚刚用到的what字段,除此之外还可以使用args1和arg2字段来携带一些整型数据,obj携带一个Object对象。
  • 2,Handler
    处理者,它主要用于发送和处理消息,发送消息一般是使用Handler的sendMessage方法,而发出的消息经过一系列的处理后,最终会传递到Handler的handleMessage方法中。
  • 3,MessageQueue
    消息队列,主要用于存放所有通过Handler发送的消息,这部分消息会一直存在于消息队列中,等待被处理,每个线程只会有一个消息队列。
  • 4,Looper
    它是MessageQueue的管家,调用Looper的loop方法后,就会进入一个无限循环中,每当发现MessageQueue中有一条消息后,就会将它取出,并传递到Handler的handleMessage方法中,每个线程只能有一个Looper对象。

问题:主线程的Looper.loop()死循环为什么不会导致ANR?

回答这个问题先看ActivityThread的源码:

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
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);

Environment.initForCurrentUser();

// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());

// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);

Process.setArgV0("<pre-initialized>");

Looper.prepareMainLooper();//1

ActivityThread thread = new ActivityThread();
thread.attach(false);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}

// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();//2

throw new RuntimeException("Main thread loop unexpectedly exited");
}

// ------------------ Regular JNI ------------------------

private native void nDumpGraphicsInfo(FileDescriptor fd);
}

在java程序中main方法是入口,当main方法的代码结束后程序也结束了,所以我们要让一个程序一直或长时间运行知道用户退出才结束程序,那么就要阻塞这个线程,如果不阻塞,那么一个APP刚刚启动main方法结束后,程序直接退出,这是不合适的。

这个阻塞就是通过Looper.loop()来实现的,再看loop()的源码:

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
 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 1 注意此处
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
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
msg.target.dispatchMessage(msg);// 2注意此处
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (slowDispatchThresholdMs > 0) {
final long time = end - start;
if (time > slowDispatchThresholdMs) {
Slog.w(TAG, "Dispatch took " + time + "ms on "
+ Thread.currentThread().getName() + ", h=" +
msg.target + " cb=" + msg.callback + " msg=" + msg.what);
}
}

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();
}
}

这个方法在获取到调用这个方法的线程(即主线程)的looper后,再通过looper获取到了messageQueue,然后进入一个死循环,我们看官方注释,”might block”(当MessageQueue为空时),所以此时主线程就阻塞在这个地方了,从而导致main方法不会结束而退出而因此避免APP一启动就退出的状况。

那么问题来了,既然阻塞了主线程,那又是如何响应用户操作和回调activity生命周期方法的呢?

答:

  • 1,这就涉及到了Android中的Handler机制原理和IPC机制,简单概括如下:首先启动一个APP时,此时应该还有其他两个Binder线程(用来和系统进行通信操作,接收系统进程发送的通知)。在thread.attach(false)就创建了新binder线程(具体指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,而ActivityThread并非线程,它没有继承Thread类。其次,主线程的MeassageQueue没有消息时,便阻塞在loop的queue.next()的nativePollOnce()方法里,此时主线程慧释放cpu资源进入休眠状态,直到下个消息到达或有事务发生,通过pipe管道写端写入数据来唤醒主线程工作。这里采用epoll机制,是一种IO多复用机制,可以监听多个描述符,当某个描述符就绪(读或写就绪),就立即通知相应的程序进行读或者写操作,同步IO,即读写是阻塞的。所以主线程大部分时间是阻塞的,并不会消耗大量CPU资源。

  • 2,Activity的生命周期是怎么实现在死循环体外能够执行起来?
    ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。
    Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:
    在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。
    比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法; 再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。
    主线程的消息又是哪来的呢?当然是App进程中的其他线程通过Handler发送给主线程,请看接下来的内容:

    system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。

App进程则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程 ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程,比如signal catcher线程等,这里就不一一列举。

Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。

结合图说说Activity生命周期,比如暂停Activity,流程如下:
  • 1,线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)

  • 2,线程2通过binder传输到App进程的线程4;

  • 3,线程4通过handler消息机制,将暂停Activity的消息发送给主线程;

  • 4,主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。

    参考:https://www.zhihu.com/search?type=content&q=Android%E4%B8%AD%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%B8%8D%E4%BC%9A%E5%9B%A0%E4%B8%BALooper.loop

    另附另外两个知乎答案

    ActivityThread类是Android App进程的初始类。它里面有我们常见的javamain函数,我们都知道这是java程序的入口。

5.5 使用AsyncTask创建后台线程

使用Handler类来发送消息,然后更新UI,这种方式对于整个过程灵活控制,但是也存在缺点,就是代码比较臃肿,当多个任务同时执行时,不易于对线程进行精确控制。为了简化操作,Android提供了工具类android.os.AsyncTask,在代码上要比Handler轻量级。AsyncTask底层是一个线程池,执行多任务时消耗比较少。

AsyncTask的三个参数:Params、Progress和Result。

  • 1,Params:启动任务是需要的参数,例如下载链接地址
  • 2,Progress:执行的进度
  • 3,Result:执行结果

需要重写四个方法:onPreExecute、doInbackground、onProgressUpdate和onPostExecute。

  • 1,onPreExecute:执行任务之前调用
  • 2,doInbackground:执行任务的方法,该方法是多线程调用,所以耗时操作都写在这个方法中,通过调用publishProgress方法更新进度
  • 3,onProgressUpdate:更新任务进度,在调用publishProgress时,这个方法才会被调用,该方法可以把数据直接更新到UI空间上
  • 4,onPostExecute:任务执行完毕调用

以我们使用AsyncTask模拟从网上下载文件为例:

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
private class DownloadFilesTask extends AsyncTask<String,Integer,Long> {
@Override
protected void onPreExxcute() {
Log.i("DownloadFilesTask","执行任务之前");
}

protected Long doInbackground (String... url) {
int count = url[0].length();//第一个字符串
long totalSize = 0;
for (int i = 0;i < count;i++) {
totalSize += i;
publishProgress(i);//此方法会调用onProgressUpdate更新下载进度
//如果取消就结束任务
if (isCancled)
break;
}
return totalSize;
}
protected void onProgressUpdate(Integer... progress) {
Log.i("DownloadFilesTask","当前下载进度" + progress[0].intValue());
}

protected void onPostExecute(Long result) {
Log.i("DownloadFilesTask","下载完成" + result);
}

}

这是在MainActivity中定义的一个类并让它继承自AsyncTask,定义泛型,重写方法,接下来在onCreate()方法调用这个类。

1
new DownloadFilesTask().execute("www.downloadfile.com);

5.6 线程池的使用

线程池是一种多处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。

线程池应用的场景?

比如说一个音乐类App,用户需要下载歌曲,下载歌曲很耗时,需要启动一个新的线程进行下载,之前可能会使用如下代码:

1
2
3
4
5
new Thread(new Runnable() {
public void run() {
//下载歌曲的代码
}
}).start();

如果要下载1000首歌曲呢?开启1000个线程吗?这会导致什么问题?

  • 每下载一首歌曲就要新建一个线程,导致频繁的新建销毁线程,会使程序卡主或崩溃
  • 这样创建的线程无法统一管理
  • 不方便统计(例如已下载歌曲的数量)

而线程池能解决以上问题,它的优点如下:

  • 重用已创建线程,不会频繁地创建与销毁线程
  • 对线程统一管理、分配、调优和监控
  • 控制线程数量,合理使用系统资源,这样不会造成程序卡顿或崩溃

Android中的线程操作是通过ThreadPoolExecutor类实现。最长的构造函数有7个参数。

  • corePoolSize:核心线程数
  • maximumPoolSize:线程池最大线程数
  • keepAliveTime:线程空闲保持时间
  • unit
  • workQueue:线程池中的队列任务,这个队列保存线程池提交的任务,它的使用与线程池中的线程数量有关
  • threadFactory:
  • handler