安卓-通信

通信

《Android开发艺术探索》

  • HandlerAndroid消息机制的上层接口,使得在开发过程中只需要和Handler交互即可。
  • Handler的运行需要底层的MessageQueue(消息队列:存储消息数据)【单链表】和Looper(循环:无限循环地去遍历)支撑。
  • Looper中有特殊概念ThreadLocal【不是线程,在每个线程中存储数据】。
    Handler创建时会采用当前线程的Looper来构造消息循环系统。
    Handler内部通过ThreadLocal获取到当前线程的Looper
  • ThreadLocal可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLocal可轻松获取每个线程的Looper
  • 线程默认没有Looper,若需要使用Handler就必须为线程创建Looper
  • UI线程(ActivityThread)在创建时就会初始化Looper,也即Handler可以在主线程中默认使用的原因。

Android只能主线程访问UI

ViewRootImplcheckThread方法对UI操作做了验证(限制UI只能在主线程中访问)

1
2
3
4
5
void checkThread(){
if(mThread != Thread.currentThread()){
throw new CalledFromWrongThreadExeption("Only the original thread that created a view hierarchy can touch its views.");
}
}

Handler切线程的作用,让耗时操作放到子线程中,更新UI时切到主线程。

Android不允许在子线程中访问UI

原因:AndroidUI控件不是线程安全的,若多线程中并发访问可能会导致UI控件处于不可预期的状态。

若给UI控件加上锁机制,缺点:

  1. 加上锁机制会让UI访问的逻辑变复杂
  2. 加上锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行

消息机制

相关概念

概念 定义 作用 备注
主线程
(UI线程、Main Thread)
当应用程序第1次启动时,会同时自动开启1条主线程 处理与UI相关的事件
(如更新、操作等)
主线程与子线程通信媒介=Handler
子线程
(工作线程)
认为手动开启的线程 执行耗时操作
(如网络请求、数据加载等)
主线程与子线程通信媒介=Handler
消息
(Message)
线程间通讯的数据单元
(即Handler接受 & 处理的消息对象)
存储需操作的通信信息 /
消息队列
(Message Queue)
一种数据结构
(存储特点:先进先出)
存储 Handler 发送过来的消息(Message) /
处理者
(Handler)
* 主线程与子线程的通信媒介
* 线程消息的主要处理者
* 添加消息(Message)到消息队列(Message Queue)
* 处理循环器(Looper)分派过来的消息(Message)
/
循环器
(Looper)
消息队列(Message Queue)与处理者(Handler)的通信媒介 消息循环,即
* 消息获取:循环取出消息队列(Message Queue)的消息(Message)
* 消息分发:将取出的消息(Message)发送给对应的处理者(Handler)
* 每个线程中只能拥有1个Looper
* 1个Looper可绑定多个线程的Handler
* 即多个线程可往1个Looper所持有的MessageQueue中发送消息,提供了线程间通信的可能
  • 主线程(UI线程、Main Thread)
    • 定义:当应用程序第一次启动时,会同时自动开启一条主线程
    • 作用:处理与UI相关的事件
  • 子线程(工作线程)
    • 定义:人为手动开启的线程
    • 作用:执行耗时操作(如网络请求、数据加载等)
  • 消息(Message)
    • 定义:线程间通讯的数据单元(即Handler接受&处理的消息对象)
    • 作用:

Handler、Looper、MessegeQueue关系

Handler、Looper、MessegeQueue关系

Looper运行模型

Looper运行模型

用Handler的原因即其作用

  • 为什么用Handler消息传递机制
    能保证 :多个线程并发更新UI;同时保证线程安全
  • Handler作用
    即工作线程需要更新UI时,通过Handler通知主线程,从而在主线程中更新UI操作

Handler机制

  • Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper,那么就会报错。解决:为当前线程创建Looper,或者在一个有Looper的线程中创建Handler也行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class LooperThread extends Thread{
    public Handler mHandler;
    public void run(){
    //为Thread创建Looper
    Looper.prepare();
    mHandler = new Handler(){
    public void handleMessage(Message msg){
    //process incoming message here
    }
    };
    //looper开始循环遍历messageQueue工作
    Looper.loop();
    }
    }
  • Handler创建完毕后,其内部Looper以及MessageQueue即可与Handler一起协同工作了
    可以通过Handlerpost方法(最终也是通过send方法来完成的)将一个Runnable投递到Handler内部的Looper中去处理;
    也可以通过Handlersend方法发送个消息,这个消息同样会在Looper中去处理;

  • send工作过程:Handlersend被调用时,它会调用MessageQueueenqueueMessage方法将这个消息放入消息队列中,而后Looper发现新消息到来会去处理这个消息,最终消息中的Runnable或者HandlerhandleMessage方法会被调用。
    注意:Looper是运行在创建Handler所在的线程中的,这样Handler中的业务逻辑就会被切换到创建Handler所在的线程中去执行了。
    Handler的工作过程.png

  • Handler有两个主要用途:
    (1)安排Messagerunnables在将来的某个时刻执行;
    (2)将要在不同于自己的线程上执行的操作排入队列。(在多个线程并发更新UI的同时保证线程安全。)

  • Android规定访问UI只能在主线程中进行,因为Android的UI控件不是线程安全的,多线程并发访问会导致UI控件处于不可预期的状态。为什么系统不对UI控件的访问加上锁机制?缺点有两个:

    1. 加锁会让UI访问的逻辑变得复杂;
    2. 其次锁机制会降低UI访问的效率。
  • 如果子线程访问UI,那么程序就会抛出异常。ViewRootImpl对UI操作做了验证,这个验证工作是由ViewRootImplcheckThread方法完成:
    ViewRootImpl.java

    1
    2
    3
    4
    5
    6
    void checkThread() {
    if (mThread != Thread.currentThread()) {
    throw new CalledFromWrongThreadException(
    "Only the original thread that created a view hierarchy can touch its views.");
    }
    }
  • MessageHandler接收和处理的消息对象
  • MessageQueueMessage的队列,先进先出,每一个线程最多可以拥有一个
  • Looper:消息泵,是MessageQueue的管理者,会不断从MessageQueue中取出消息,并将消息分给对应的Handler处理,**每个线程只有一个Looper**。

Handler创建的时候会采用当前线程的Looper来构造消息循环系统,需要注意的是,线程默认是没有Looper的,直接使用Handler会报错,如果需要使用Handler就必须为线程创建Looper,因为默认的UI主线程,也就是ActivityThreadActivityThread被创建的时候就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。

Handler源码

Handler源码

IdleHandler

IdleHandler

工作原理

ThreadLocal

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,其他线程则无法获取。LooperActivityThread以及AMS中都用到了ThreadLocal。当不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLcoal的索引去查找对应的value值。
在指定的线程中存储数据,只有在这个线程中才可以获取到存储的数据,其他线程无法获取到。【数据为线程私有】

ThreadLocal.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

···
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

应用场景:
1. 场景1:当某些数据是以线程为作用域切不同线程有不同的数据副本时,考虑采用ThreadLocal。若不用ThreadLocal,那么就得提供个类似于LooperManager的全局哈希表供Handler查找指定线程的Looper来管理数据。
2. 场景2:复杂逻辑下的对象传递,比如监听器的传递,

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>();
//主线程设置ThreadLocal值为true
mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());

new Thread("Thread#1"){
@Override
public void run(){
//子线程1中设置ThreadLocal值为false
mBooleanThreadLocal.set(false);
Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
};
}.start();

new Thread("Thread#2"){
@Override
public void run(){
//子线程2中不设置ThreadLocal的值,直接获取ThreadLocal中的值
Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
}
}.start();
1
2
3
4
//输出结果:
[Thread#main]mBooleanThreadLocal=true
[Thread#1]mBooleanThreadLocal=false
[Thread#2]mBooleanThreadLocal=null

不同线程访问同一个ThreadLocal对象,但是通过ThreadLocal获取到的值确实不一样的。

不同线程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值。显然,不同线程中的数组是不同的,这就是为何可通过ThreadLocal在不同线程中维护一套数据副本并且互不干扰的原因。

ThreadLocal

MessageQueue

MessageQueue主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作,插入和读取对应的方法分别是enqueueMessagenextMessageQueue内部实现并不是用的队列,实际上通过一个单链表的数据结构来维护消息列表。next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞。当有新消息到来时,next方法会放回这条消息并将其从单链表中移除。

MessageQueue.java

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
boolean enqueueMessage(Message msg, long when) {
···
synchronized (this) {
···
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}

// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
···
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
···
for (;;) {
···
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
···
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

MessageQueue

Looper

Looper会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则会一直阻塞。

Looper.java

1
2
3
4
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

可通过Looper.prepare()为当前线程创建一个Looper:

1
2
3
4
5
6
7
8
new Thread("Thread#2") {
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler();
Looper.loop();
}
}.start();

除了prepare方法外,Looper还提供了prepareMainLooper方法,主要是给ActivityThread创建Looper使用,本质也是通过prepare方法实现的。由于主线程的Looper比较特殊,所以Looper提供了一个getMainLooper方法来获取主线程的Looper。
Looper提供了quit和quitSafely来退出一个Looper,二者的区别是:quit会直接退出Looper,而quitSafly只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。Looper退出后,通过Handler发送的消息会失败,这个时候Handler的send方法会返回false。因此在不需要的时候应终止Looper。

Looper.java

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
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;
···
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
···
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
···
msg.recycleUnchecked();
}
}

loop方法是一个死循环,唯一跳出循环的方式是MessageQueue的next方法返回了null。当Looperquit方法被调用时,Looper就会调用MessageQueuequit或者qutiSafely方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null。loop方法会调用MessageQueuenext方法来获取新消息,而next是一个阻塞操作,当没有消息时,next会一直阻塞,导致loop方法一直阻塞。Looper处理这条消息:msg.target.dispatchMessage(msg),这里的msg.target是发送这条消息的Handler对象。

Looper源码

Looper

Handler

Handler的工作主要包含消息的发送和接收的过程。消息的发送可以通过post/send的一系列方法实现,post最终也是通过send来实现的。

Handler工作流程图

Message

Message

ThreadPoolExecutor

ThreadPoolExecutor

子线程与主线程通信

Activity.runOnUIThread(Runnable)

View.Post(Runnable)

View.PostDelayed(Runnable, long)

AsyncTask

安卓-线程AsyncTask

3个参数

  1. Params:后台线程所需的参数
  2. Progress:后台线程处理作业的进度
  3. Result:后台线程运行的结果,也就是需要提交给UI线程的信息

4个方法

  1. onPreExecute:运行在UI线程,主要目的是为后台线程的运行做准备
  2. doInBackground:运行在后台线程,它用来负责运行任务。它拥有参数Params,并且返回Result
  3. onProgressUpdate:运行在UI线程,主要目的是用来更新UI线程中显示进度的UI控件
  4. onPostExecute:运行在UI线程,doInBackground后被调用,在此方法中,就可以将Result更新到UI控件上

Handler.Post(Runnable)

Handler.PostDelayed(Runnable, long)

在子线程中创建Handler,但实例化在run()外=>handler内是主线程

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
private void showChildMessageQueue(){
new Thread(new Runnable(){
Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg){
super.handleMessage(msg);
//打印出的线程是主线程
System.out.println("当前线程名称:"+Thread.currentThread().getName() + " " + msg.obj.toString());
}
};

@Override
public void run(){
int i = 0;
while(i < 10){
Message msg = Message.obtain();
msg.obj = "this is message " + i;
mHandler.sendMessage(msg);
i++;

try{
Thread.sleep(1000L);
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}

在子线程中创建Handler,但实例化在run()内=>handler内是子线程

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
private void showChildMessageQueue(){
new Thread(new Runnable(){
Handler mHandler;

@Override
public void run(){
mHandler = new Handler(){
super.handleMessage(msg);
//打印出的线程是子线程
System.out.println("当前线程名称:"+Thread.currentThread().getName() + " " + msg.obj.toString());
};

//子线程中操作UI的话需要获取自己的Looper。
//创建属于子线程的Looper
Looper.prepare();
//操作UI控件
...
//开启looper
Looper.loop();

int i = 0;
while(i < 10){
Message msg = Message.obtain();
msg.obj = "this is message " + i;
mHandler.sendMessage(msg);
i++;

try{
Thread.sleep(1000L);
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
}).start();
}

Looper无限循环为啥不阻塞UI线程?

  • AcitivityThread是个final的类,不是主线程,主线程是Zygote fork出来的。
  • ActivityThread里有main方法,就是程序入口,main中有Looper.prepareMainLooper()looper.loop()
  • Looper无限循环的原因:ActivityThreadmain()主要是做消息循环(保证main()不会结束掉),一旦退出消息循环则App也会退出。
  • Looper.loop()不会阻塞UI线程原因:Looper.loop()功能是不断收发事件的消息(Activity的生命周期都依靠于主线程的Looper.loop()来调度)。在进入死循环前会创建个新binder线程(ActivityThread.main()thread.attach(false);就是建立Bindler通道–创建新线程)这个线程会接受系统服务AMS发送来的事件
    当没有事件需要处理时,主线程就会阻塞;当子线程往消息队列发送消息,并且往管道文件写数据时,主线程就被唤醒。
  • 主线程中的 Looper 从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此 loop 的循环并不会对 CPU 性能有过多的消耗。

通信相关问题

Can’t create handler inside thread that has not called Looper.prepare()

现象:报错Can't create handler inside thread that has not called Looper.prepare()
原因:在子线程中new Handler(加Toast也有这个问题)导致的
分析:因为Handler与其调用者在同一线程中,Handler中执行了延迟操作,会导致调用的线程阻塞。每个Hander对象都会绑定一个Looper对象,每个Looper对象对应一个消息队列(MessageQueue),若在创建Handler时不指定与其绑定的Looper对象则默认将当前线程的Looper绑定到该Handler上。
在主线程中直接使用new Hanlder()创建Handler对象,其将自动与主线程的Looper对象绑定;在子线程中直接new创建会报错,因为子线程中没有开启Looper,而Handler对象必须绑定Looper对象

解决:
方案一:需先在该子线程中手动开启LooperLooper.prepare()-->Looper.loop()),然后将其绑定到Handler对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final Runnable runnable = new Runnable(){
@Override
public void run(){
//执行耗时操作
try{
Log.e("bm", "runnable线程:"+ Thread.currentThread().getId() + " name:" + Thread.currentThread().getName());
Thread.sleep(2000L);
Log.e("bm", "执行完耗时操作了~");
} catch (InterruptedException e){
e.printStackTrace();
}
}
};

new Thread(){
public void run(){
Looper.prepare();
new Handler().post(runnable);//在子线程中直接去new一个handler
Looper.loop();//这种情况下,Runnable对象时运行在子线程中的,可以进行耗时操作,但是不能更新UI
}
}.start();

方案二:通过Looper.getMainLooper(),获得主线程的Looper,将其绑定到此Handler对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final Runnable runnable = new Runnable() {
  @Override
  public void run() {
    //执行耗时操作
    try {
      Log.e("bm", "runnable线程: " + Thread.currentThread().getId()+ " name:" + Thread.currentThread().getName());
      Thread.sleep(2000);
      Log.e("bm", "执行完耗时操作了~");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
  }
};
new Thread() {
  public void run() {
    new Handler(Looper.getMainLooper()).post(runnable);//在子线程中直接去new 一个handler

    //这种情况下,Runnable对象是运行在主线程中的,不可以进行联网操作,但是可以更新UI
  }
}.start();

sending message to a Handler on dead thread

toast报这个异常。解决:在主线程创建个Handler,toast写里面,需要吐司的地方去发送消息给这个handler