android handle 阻塞,android 多线程 — handle 学习

为啥要有 handle

首先 android UI 线程的类型是 ActivityThread,这可能在这里没什么用,凑凑字数吧......

android 的 UI 控件不是线程安全的, 多线程并发访问 UI 控件时可能会产生问题。

为什么不给 UI 控件加锁,一是加锁会复杂很多,二是加锁会阻塞其他访问 UI 的线程,有可能造成别的线程占用 UI 而把 UI 线程阻塞了,这就肯定会造成卡顿问题了。所以才采用了单线程更新 UI 的模式,使用 handle 来切换线程。

上面的解释是看:开发艺术探索 总结的......

handle 中几个角色:

ThreadLocal

每个线程中用来保存私有变量的容器

Looper

消息队列的管理容器,也可以叫轮询器

MessageQueue

消息队列

Message

消息本身

handle

消息发送器,和消息消费者

说下过程:

Looper.prepare();

looper 的初始化创建,looper 会创建自己,每个 Looper 对象的创建都会伴随创建一个消息队列 MessageQueue,并把自己保存在当前线程的 ThreadLocal 中,保证每个线程中 looper 的唯一性。

Looper.loop();

looper 开始一个无限循环,从内部的 MessageQueue 消息队列中循环取出数据挨个执行,消息队列没有数据了就会挂起。

message.getTarget().dispatchMessage(message);

消息最终就是这么被执行的, message.getTarget() 的返回的对象就是 handle ,所以这里由 handle 会出现内存泄露。

handler.sendMessage();

handle 发送数据,就是把自己传递给这个待处理的消息 message 中,然后添加到 MessageQueue 消息队列里面去。

Handler handler2 = new Handler(Looper.getMainLooper());

Looper里面有个静态的 looper ,就是当前进程中 UI 线程的 looper,通过这个 UI 的线程的 looper ,我们可以创建一个 handle 出来,然后添加消息到主进程中去执行。

为啥 looper 可以切换线程

looper 的都是要求我们在 Looper.prepare() looper 的初始化之后马上 Looper.loop() 让 looper 跑起来的,lopper 本身是一个无限循环,会一直在 looper 所在线程中执行,所以我们通过不同的 looper 对象,创建 handle 对象发送消失时都是把这个消息发送到了对应 looper 所以在的 MessageQueue 消息队列中去,这个消息队列自然的会在所诉的那个 looper 循环中执行,looper 在哪个线程,那么就是在哪个线程运行。所以线程切换就是这么做的。

MessageQueue 的 next() 方法内部会调用 nativePollOnce() 方法,该方法会阻塞线程。该方法的作用简单说,就是当消息队列中没消息时,阻塞掉当前执行的线程.避免过度的cpu消耗。

handle.post 干啥了

为什么特别说下 handle 的 post 方法,因为有的人面试会问,其实这个很简单,post 方法会生成一个 message 对象,然后把我们 post 方法里面的 runnable 对象存到 message 的 callback 里面去,message 在执行时,会判断有没有 callback 值,有的话直接执行消费本地信息了,没有的话才会把这个 message 交给 handle 去执行。

private static Message getPostMessage(Runnable r) {

// 1. 创建1个消息对象(Message)

Message m = Message.obtain();

// 注:创建Message对象可用关键字new 或 Message.obtain()

// 建议:使用Message.obtain()创建,

// 原因:因为Message内部维护了1个Message池,用于Message的复用,使用obtain()直接从池内获取,从而避免使用new重新分配内存

// 2. 将 Runable对象 赋值给消息对象(message)的callback属性

m.callback = r;

// 3. 返回该消息对象

return m;

} // 回到调用原处

对于 looper 的阻塞测试

其实看了上面对于 looper 阻塞的解释后,我被这个 Pipe 管道 hold 住了,原来用到了这么高大上的技术啊,此时我非常觉得我得做点什么才行,于是不管怎样样总得干点,干脆咱来看看这个阻塞是不是真的,我是不是有点失心疯啊...........

所以我做了个测试明确下思路:

起2个 thread 线程

thread1 里面跑一个 looper ,然后把这个 looper 抛出去,在 looper 启动之后加一个 打印循环

thread2 拿到 thread1 抛出的 looper 构建 handle 发送消息

最后看看这个在 looper 之后的打印循环有没有执行机会。

thread1

public class Thread1 extends Thread {

private Looper looper;

@Override

public void run() {

super.run();

Looper.prepare();

looper = Looper.myLooper();

Looper.loop();

int num = 1;

while (num <= 50) {

Log.d("AAA", Thread.currentThread().getName() + "_执行次数 / " + num);

num++;

try {

Thread.sleep(300);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

public Looper getLooper() {

return looper;

}

}

thread2

public class Thread2 extends Thread {

private Handler handler;

public void setHandler(Handler handler) {

this.handler = handler;

}

@Override

public void run() {

int num = 1;

while (num <= 3) {

try {

java.lang.Thread.sleep(1000);

if (handler != null) {

Message message = new Message();

message.what = 2;

handler.sendMessage(message);

Log.d("AAA", Thread.currentThread().getName() + "发送消息 / " + num);

}

num++;

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

UI 线程启动这 2 个测试线程

Thread1 t1 = new Thread1();

Thread2 t2 = new Thread2();

t1.start();

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

Handler handler = new Handler(t1.getLooper()) {

int num = 0;

@Override

public void handleMessage(Message msg) {

super.handleMessage(msg);

if (msg.what == 2) {

num++;

Log.d("AAA", Thread.currentThread().getName() + "接受到消息 / " + num);

}

}

};

t2.setHandler(handler);

t2.start();

}

UI 线程中中间 sleep 1秒,因为 looper 初始化需要一点时间,handle 中传入的 looper 要是 null 的话会抛异常。

2501d293c444

测试结果

测试结果是果然 thread1 后面的打印循环是出不来的,looper 的确是阻塞了当前线程的,looper 之后的代码都是没机会执行的

首先 looper 里面的队列是无限循环的,这个循环出不去后面的代码肯定不会执行

再次 looper 的阻塞会阻塞这个无限循环,进而阻塞当前线程。

我想说这个测试有点凑字数的嫌疑,但是我的确在 looper 无限循环,阻塞,唤醒的问题上找了好多资料,折腾了好几回,非常不爽啊

ThreadLocal

ThreadLocal 是一个存储器,作用域在单个线程对象内部,多线程间是无法共享的,相当于线程对象私有的变量存储器。这个存储器只能存一个数据,那么要是想存多个数据的话,就需要多个 ThreadLocal 对象了。

threadLocal.set("BB") -- > 存数据

threadLocal.get() -- > 取数据

通过这 2 个方法就知道了吧,只能存一个数据进去

一般我们认为,你在一个类里面声明的一个对象,这个对象的作用域肯定就是你这个类的对象啊。但是注意 ThreadLocal 的特别之处在于即使你在3个线程之内,操作同一个ThreadLocal 对象里面的值,那么你会发现 3个线程所属的 ThreadLocal 里面对应的值都是不同的。

下面是一个测试,在 UI 线程启动2个 thread ,这3个线程一齐修改同一个 ThreadLocal 里面的数据

ThreadLocal threadLocal = new ThreadLocal();

threadLocal.set("AA");

Thread t1 = new Thread() {

@Override

public void run() {

super.run();

threadLocal.set("BB");

Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());

}

};

Thread t2 = new Thread() {

@Override

public void run() {

super.run();

threadLocal.set("CC");

Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());

}

};

t1.start();

t2.start();

try {

Thread.sleep(300);

} catch (InterruptedException e) {

e.printStackTrace();

}

Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());

}

2501d293c444

测试结果

ThreadLocal 的使用场景并不多,基本都是用这个作用域范围,像 Looper,ActivityThread,AMS 这些。

趴趴源码实现

为啥 ThreadLocal 这么屌呢,其实也没啥,主要我们尝试看源码,就会发现很多东西其实也是很简单的,就是设计很灵活,很 Nice

looper 对象里有一个 static 的 ThreadLocal 对象,声明对象时就初始化了

static final ThreadLocal sThreadLocal = new ThreadLocal();

ThreadLocal 的 ge() 干了什么

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

}

ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

获取当前线程对象,然后从拿到线程对象里面的参数 ThreadLocalMap ,ThreadLocal 的 map 集合,然后以 ThreadLocal 对象自己为 key 获取到 value 值,也就是我们储存的数据

我们来看看这个map 里面的 Entry 的声明

static class Entry extends WeakReference> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal> k, Object v) {

super(k);

value = v;

}

}

很明显的可以看到 key 是 ThreadLocal 对象本身,value 是我们储存的值。每个 thread 对象内部都有一个 map 集合的话,通过同一个 key 我们当然会可以存不同的值进去了。

其他人的解释

ThreadLocal是一种保存变量的线程安全的类.

在多线程环境下能安全使用变量,最好的方式是在每个线程中定义一个local变量.

这样对线程中的local变量做修改就只会影响当前线程中的该变量,因此变量也就是线程安全的.

这种保证变量线程安全的思想,实际上就是ThreadLocal的实现.

Threadlocal在调用threadLocal.get方法时,会获取当前thread的threadLocalMap.

threadLocalMap是thread中的一个属性.

第一次调用ThreadLocal.get()方法时,会先判断当前线程对应的threadlocalMap是否被创建了,

如果没创建则会创建ThreadLocalMap,并把该对象赋值给thread.sThreadLocal对象.后续再获取当前thread的threadLocalMap时,就会取该赋值对象.

ThreadLocalMap就是用来保存线程中需要保存的变量的对象了.

因为threadLocalMap是赋值给当前thread的,属于thread的内部变量,

所以每个线程的threadlocalMap就都是不同的对象,也就是上面说的threadlocal是线程安全的原因了.

ThreadLocalMap内部实际上是一个Entry[],用来保存Entry对象的数组.

Entry对象是继承weakReference的,其中Entry的key为ThreadLocal对象,value为threadLocal需要保存的变量值.

调用ThreadLocal.set方法时,会向threadLocalMap中添加一个Entry对象.

调用get方法时,是通过将调用的threadLocal对象本身作为key,来遍历threadLocalMap数组.

当threadLocal等于Entry[]中的key时,则返回该Entry中的value

为什么主线程不会因为 Looper.loop() 的死循环卡死

这是个经典的问题了,我们先来看看主线程启动时干了什么

public static void main(String[] args) {

....

//创建Looper和MessageQueue对象,用于处理主线程的消息

Looper.prepareMainLooper();

//创建ActivityThread对象

ActivityThread thread = new ActivityThread();

//建立Binder通道 (创建新线程)

thread.attach(false);

Looper.loop(); //消息循环运行

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

}

核心就是给主线程添加了 looper 进去,然后启动 looper 这个队列,looper 实际是一个阻塞是死循环,简单介绍一下,有消息就处理,没有消息就会休眠,有消息就会唤醒继续处理消息,本质上是一套完整的线程通信机制

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

}

}

}

看到 loop 这个方法了没, for (;;) 就是个 while(true) 死循环,queue.next() 获取队列的消息,队列为空的时候会阻塞主线程,这里就是关键了,queue.next() 的阻塞是不会造成 ANR 的。

先说一点,为什么主线程是个死循环还要要阻塞,淡村的死循环空闲时也不阻塞,会消耗大量的 CPU 资源,这是非常不值得的操作,所以在空闲时要阻塞

java 的阻塞分两种,阻塞失去锁,阻塞不失去锁,不丢失锁的这种阻塞也叫休眠,随时可以唤醒,唤醒就能马上执行任务。wait() 是失去锁的阻塞,sleep() 就是不失去锁的阻塞也就是休眠了,只不过 loop 这里的休眠式阻塞是交给 linux 层去实现的

在添加任务到队列的时候会用 linux 的方式 nativeWake 唤醒主线程,nativeWake 是本地 C 的方法

boolean enqueueMessage(Message msg, long when) {

if (needWake) {

nativeWake(mPtr);

}

}

return true;

}

private native static void nativeWake(long ptr);

在获取任务时先用 linux 的方式 nativePollOnce 阻塞主线程,nativePollOnce 是本地 C 的方法

Message next() {

for (;;) {

if (nextPollTimeoutMillis != 0) {

Binder.flushPendingCommands();

}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {

.......

}

}

private native void nativePollOnce(long ptr, int timeoutMillis);

这里就涉及到Linux pipe/e****poll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,详情见Android消息机制1-Handler(Java层),此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程**,具体过程可查看 startService流程分析,这里不展开说,简单说Binder用于进程间通信,采用C/S架构。关于binder感兴趣的朋友,可查看

为什么Android要采用Binder作为IPC机制? - Gityuan的回答

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值