Handler是我们在开发中经常会接触到的类,因为在Android中,子线程一般是不能更新UI的.

所以我们会使用Handler切换到主线程来更新UI,那Handler是如何做到实现不同线程之间的切换呢?

先看一个例子

1.ThreadLocal的简单使用

public class HandlerActivity extends AppCompatActivity {
private final static String TAG = "HandlerActivity"; @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); //1.创建一个ThreadLocal对象,
final ThreadLocal<IThreadCallback> threadLocal = new ThreadLocal<>();
//对ThreadLocal赋值
threadLocal.set(new IThreadCallback() {
@Override
public void onExecute() {
Log.d(TAG,"当前线程:" + Thread.currentThread().getName());
}
}); //新开一个线程,并对threadLocal赋值
Thread thread = new Thread(){
@Override
public void run() {
threadLocal.set(new IThreadCallback() {
@Override
public void onExecute() {
Log.d(TAG,"当前线程:" + Thread.currentThread().getName());
}
});
threadLocal.get().onExecute();
}
}; //获取IThreadCallback接口实例并执行onExecute()方法
threadLocal.get().onExecute(); thread.setName("otherThread");
//开启线程,并在线程内部获取IThreadCallback接口实例并执行onExecute()方法
thread.start();
} //定义了一个回调接口
public interface IThreadCallback{
void onExecute();
}
}

可以看到,我们总共调用了两次set()方法,而执行的线程也是在不同线程下,所以,Handler是通过ThreadLocal来

进行线程切换的.那么ThreadLocal是怎么做到的呢?

2.ThreadLocal的原理

首先我们先看看ThreadLocal.set()方法做了什么,我们一行一行代码来看

public void set(T value) {
//获取当前方法(set方法)所执行的线程
Thread t = Thread.currentThread(); //getMap() ====> t.threadLocals,它是Thread类中定义的一个成员变量,也就是说
//每一个线程都一个独一无二的threadLocals,它是一个ThreadLocal.ThreadLocalMap类型,
//它是ThreadLocal的内部类
ThreadLocalMap map = getMap(t); if (map != null)
//直接将value和当前ThreadLocal绑定
map.set(this, value);
else
//为空则会直接new ThreadLocalMap(this, value);
createMap(t, value);
}

getMap() ====> t.threadLocals,它是Thread类中定义的一个ThreadLocal.ThreadLocalMap对象,

ThreadLocalMap是ThreadLocal的一个内部静态类,如果map不为空,它就会调用set方法把我们传进来的

对象封装成了一个Entry对象,并保存到Entry[]数组中

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); ...
...
//new了一个Entry()对象,也是ThreadLocal的内部静态类
//内部有一个Object类型的成员变量,用来保存我们传进来的值
tab[i] = new Entry(key, value);
...
...
}

get()方法

public T get() {
//获取当前方法(get方法)所在线程
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);
if (map != null) {
//从Entry[] table数组中去获取保存的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked") //将我们传进来的value强转为T所表示的类型,这个value就是对应线程的对象了
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

上面大抵描述了一个ThreadLocal的基本流程,就算你还是看不懂、理解不了也没关系,

你只要知道,你在哪个线程中调用set方法,那么那个对象就属于那个线程,因为每个线程是有一块自己的

内存区域,在线程中定义的变量,都会存在于当前线程自己的内存区域.

比如:

//这是在主线程初始化,new的实例,那它就存在于主线程的内存区域
threadLocal.set(new IThreadCallback() {
@Override
public void onExecute() {
Log.d(TAG,"当前线程:" + Thread.currentThread().getName());
}
});
Thread thread = new Thread(){
@Override
public void run() {
//不在主线程,所以new IThreadCallback()
//它是保存在当前线程(otherThread),还记得吗,我们给它
//设置了一个名称otherThread
threadLocal.set(new IThreadCallback() {
@Override
public void onExecute() {
Log.d(TAG,"当前线程:" + Thread.currentThread().getName());
}
});
threadLocal.get().onExecute();
}
};

3.Handler的UI线程切换原理

在知道了ThreadLocal的使用原理之后,其实就很简单了,跟我们上面的例子类似.

我们知道,在子线程(非UI线程)中创建Handler会报错.

解决方法也很简单,只要在创建Handler之前,调用Looper.prepare()方法就不会报错了,

我们看一下Looper.prepare()做了什么

public static void prepare() {
prepare(true);
} private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

关于消息循环的机制,读者自己去找相关资料了解,这不是本文的重点.这里只简单提一下.

可以看到,Looper的内部有一个ThreadLocal的实例sThreadLocal,调用prepare()方法会直接new Looper()对象,将Looper对象通过ThreadLocal绑定到相应的线程,这样Looper中所有执行的方法和变量都在对应的线程中,达到不同线程之间切换的效果.Looper类是Handler能完成消息发送的一个重要成员,在创建了Handler之后,调用Looper.loop()就能进行消息循环,内部有一个死循环不断会从消息队列中去取Message,按照先后顺序依次调用Message.target.dispatchMessage()方法,最后调用到handleMessage()方法,Message.target是一个Handler类型的对象,它在sendMessage()的时候被赋值.

我们可以在子线程中尝试使用Toast,如果不调用Looper.loop()方法,在以前版本的系统上,Toast是不会显示,必须要调用Looper.loop()进行消息循环才能显示.Android11不调用也能显示,应该是内部帮我们调用了.

handleMessage之所以能执行在主线程,是因为Loop对象本身就是在主线程创建的.有人会说,我又没有调用Looper.prepare()方法,它为什么不报错呢,因为主线程的Looper对象已经在ActivityThread.main方法中

已经调用过了,所以我们不需要调用Looper.prepare()就能使用.如果要获取主线程的Looper对象,可以调用Looper.getMainLooper()方法获取,可以利用这个Looper对象来创建一个Handler对象,让它的HandleMessage方法执行在主线程.

Android的Handler线程切换原理的更多相关文章

  1. EventBus 线程切换原理

    主要问题其实只有两个,其一:如何判断当前发送事件的线程是否是主线程:其二:如何在接收事件时指定线程并执行: 一个一个来看. 1.如何判断是否在主线程发送 EventBus在初始化的时候会初始化一个Ma ...

  2. [转]Android中handler机制的原理

    Andriod提供了Handler 和 Looper 来满足线程间的通信.Handler先进先出原则.Looper类用来管理特定线程内对象之间的消息交换(MessageExchange). 1)Loo ...

  3. 【转载】Android 的 Handler 机制实现原理分析

    handler在安卓开发中是必须掌握的技术,但是很多人都是停留在使用阶段.使用起来很简单,就两个步骤,在主线程重写handler的handleMessage( )方法,在工作线程发送消息.但是,有没有 ...

  4. android的子线程切换到主线程

    在子线程中,如果想更新UI,必须切换到主线程,方法如下: if (Looper.myLooper() != Looper.getMainLooper()) { // If we finish mark ...

  5. Android Handler 消息机制原理解析

    前言 做过 Android 开发的童鞋都知道,不能在非主线程修改 UI 控件,因为 Android 规定只能在主线程中访问 UI ,如果在子线程中访问 UI ,那么程序就会抛出异常 android.v ...

  6. Android更新主线程UI的两种方式handler与runOnUiThread()

    在android开发过程中,耗时操作我们会放在子线程中去执行,而更新UI是要主线程(也叫做:UI线程)来更新的,自然会遇到如何更新主线程UI的问题.如果在主线程之外的线程中直接更新页面显示常会报错.抛 ...

  7. android高级---->Handler的原理

    andriod提供了Handler来满足线程间的通信,上次在更新UI的时候也提到过Handler的使用,关于Handler的基本使用,参见博客(android基础---->子线程更新UI).今天 ...

  8. Android 为什么要有handler机制?handler机制的原理

    为什么要有handler机制? 在Android的UI开发中,我们经常会使用Handler来控制主UI程序的界面变化.有关Handler的作用,我们总结为:与其他线程协同工作,接收其他线程的消息并通过 ...

  9. EventBus 消息的线程切换模型与实现原理

    一. 序 EventBus 是一个基于观察者模式的事件订阅/发布框架,利用 EventBus 可以在不同模块之间,实现低耦合的消息通信. EventBus 因为其使用简单且稳定,被广泛应用在一些生产项 ...

随机推荐

  1. TypeScript 泛型(generic) 入门介绍

    TypeScript 泛型函数 下面来创建第一个使用泛型的例子:identity函数.这个函数会返回任何传入它的值.你可以把这个函数当成是echo命令.不用泛型的话,这个函数可能是下面这样: func ...

  2. 1.1 操作系统的第一个功能——虚拟化硬件资源 -《zobolの操作系统学习札记》

    1.1 操作系统的第一个功能--虚拟化硬件资源 目录 1.1 操作系统的第一个功能--虚拟化硬件资源 问1:操作系统一般处于计算机系统的哪一个位置? 问2:管理硬件资源为什么要单独交给操作系统? 问3 ...

  3. Vue之封装二次axios

    第一步,首先安装axios,这里推荐局部安装 npm i -D axios 第二步,在src目录下创建request文件夹,然后在里面创建两个文件http.js.api.js http.js impo ...

  4. 聊聊Adapter模式

    今天我们聊一个最简单的设计模式,适配器Adapter.跟以往一样,我们还是从一个例子出发. 一个例子 最开始的结构 假设我们有个数据分析软件,其中包含了数据收集器和数据分析器,数据收集器基于XML格式 ...

  5. Android Studio 的初次使用

    记录我第一次使用Android Studio时遇到的问题以及一些简单的笔记. 我所使用的是Android Studio 2.2版本 遇到的问题 创建一个Hello World!项目无疑是相当简单的,我 ...

  6. PostgreSQL 9.1 飞升之路

    PostgreSQL upgrade 以升级 PostgreSQL 9.1 至 PostgreSQL 11 (跨越 9.2.9.3.9.4.9.5.9.6.10 六个大版本) 为例,本文将分享一下过去 ...

  7. 《AlignedReID:Surpassing Human-Level Performance in Person Re-Identification》理解

  8. MyBatis 映射文件

    Mybatis映射文件简介 1) MyBatis 的真正强大在于它的映射语句.由于它的异常强大,映射器的 XML 文件就显得相对简单.如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉 ...

  9. MIT 6.824 Llab2B Raft之日志复制

    书接上文Raft Part A | MIT 6.824 Lab2A Leader Election. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

  10. Linux挂载webdav

    Docker挂载webdav(推荐): docker run -itd \ --name mydav \ --device /dev/fuse \ --cap-add SYS_ADMIN \ --se ...