构建一个简易的线程调度工具(一)基础知识

1.Java中的多线程        

        Thread和Runnable是Java实现多线程的两个最主要的类,Runnable表示可以没有返回值地运行一段代码(如果需要返回值,可以使用Callable)。Thread表示一个线程,它有一个start方法来启动该线程。如果创建一个Runnable,直接调用它的run方法,它会在当前线程运行,如果把它作为Thread的构造入参并调用Thread的start方法,它会在新线程运行。

public static void main(String[] args) {

        //在子线程中启动Runnable
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread:Current Thread->"+Thread.currentThread());
            }
        }).start();

        //直接调用Runnabe的run方法
        new Runnable(){
            @Override
            public void run() {
                System.out.println("Main:Current Thread->"+Thread.currentThread());
            }
        }.run();
}

这段代码的运行结果是

显而易见的,第一个Runnable的线程环境是子线程,第二个Runnable的线程环境是主线程。

        Thread和Runnable一样有一个run方法,执行Thread的run方法,Thread内部的Runnable运行在哪个线程环境呢?

Runnable runnable = new Runnable(){	//初始化一个Runnable
            @Override
            public void run() {
                System.out.println("Main:Current Thread->"+Thread.currentThread());
            }
        };
new Thread(runnable).run();     //调用线程的run方法

这段代码的运行结果是

        由上图可见,同样是运行在主线程(线程创建的线程)。因为可以从源码中得知,Thread的run方法直接调用了内部Runnable的run方法


2.Android实现异步通信的方式分析

        安卓系统自带的异步消息处理机制主要由三个重要的组件实现:HandlerLooperMessageQueue

        它们之间的关系是:一个Handler持有一个Looper,一个Looper管理一个消息队列(MessageQueue),一个消息队列管理多个消息(Message),每个Message又持有发送消息的Handler。

1) Message

Message是安卓系统实现异步通信的信息载体,它使用一个简单的单向链表模型存储数据,Message最终是由Handler处理的,Message与Handler之间的关系如下:

  • Message是用来传递数据的对象,它包含了发送者Handler的引用;

  • Handler是用来发送和处理Message的对象,它可以通过obtainMessage方法创建一个Message,并把自己作为Message的发送者(target);

  • 这样做的目的是让Message知道由哪个Handler来处理它,从而实现线程间的通信。

 I.Message的类型

Message根据其包含的数据可分为以下几种类型。

(1)空消息

        指new出来的或者使用后被回收的消息。

(2)数据消息

        指包含what和obj但不包含callback的消息,供Handler自身或其callback的handleMessage方法处理。

(3)回调消息

        指包含callback的消息,供Handler的handleCallback方法处理。

(4)同步屏障

        同步屏障是一种特殊的数据消息,当消息队列检测到此种类型的消息后,只会处理被标记为“异步”的消息,而忽略同步消息。

 II.Message的回收与创建

        为了便于消息的复用,消息在被消费后会设置为一个空消息并被存储到消息池(sPool)中,sPool是一个静态变量,其也是一个单向的消息链表,在Message内部通过代码控制其最大容量为50(不同安卓版本可能存在差异)。  

        Message最基本的创建方法是obtain(),用于从池中回收或创建一个新的消息对象。

        obtain方法还有很多重载方法,每一个重载方法内部首先都会调用obtain()方法回收或者创建一个空的Message对象,然后将参数表中的数据设置到这个空的Message的对应属性上。比如:

        官方推荐的消息创建方式是通过obtain或者Handler的obtainMessage方法(而不是手动new),因为这样可以将Handler与Message绑定。


2) MessageQueue

        MessageQueue实现了一个基于单向链表的阻塞的消息队列,用于管理和调度消息,在消息队列中,消息是通过处理时间(when)排列的。

        MessageQueue通常由Looper创建,MessageQueue只有一个构造方法,此构造方法只有一个布尔类型入参quitAllowed,控制消息队列能否被手动的退出。

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }

        在构造时还做的一件事是在native层初始化此消息队列的结构并返回一个本地指针(mPtr)。

I. MessageQueue最重要的几个Native方法。

    Native方法是指被native修饰符修饰的方法,它们在java层有函数定义但是没有方法体,通常是由C/C++实现,运行在native线程(在native层的阻塞不会影响到java层)。MessageQueue中的native方法如下:

private native static long nativeInit();    //初始化描述符,供pipe/epoll使用
private native static void nativeDestroy(long ptr); //销毁消息队列
@UnsupportedAppUsage
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
private native static void nativeWake(long ptr);    //唤醒线程
private native static boolean nativeIsPolling(long ptr);    //判断当前MessageQueue是否处于轮询状态
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); //设置文件描述符事件

这几个方法各有这些作用:

  (1) nativeInit() 初始化本地结构并返回一个本地指针。  

  (2) nativePollOnce(long ptr,int timeoutMillis) 从消息队列获取一个消息,如果没有消息就阻塞一定时间知道有新消息的到来。

  (3) nativeWake(long ptr) 唤醒被nativePollOnce方法阻塞的线程,在插入新消息或退出消息循环时调用。

  (4) nativeDestroy(long ptr) 销毁本地结构并释放相关资源。

        这几个方法的共同媒介都是ptr,ptr是什么?ptr是native代码(通过C/C++实现)中MessageQueue的指针,因为MessageQueue的核心方法都是由C语言实现的,所以效率相对更高。


Tips:(简单了解)

        因为Android的内核是linux,linux的epoll机制提供了高效的IO多路复用,不会占用过多的cpu资源,可使一个线程同时监听多个文件描述符(FileDescriptor 简称fd),epoll的响应机制不是对所有fd进行遍历(获取fd的就绪状态),而是类似于观察者模式,基于事件驱动通过内核回调实现,因此具有很高的性能和可扩展性。

        管道(Pipe)是一种特殊的文件,本质是一个环形缓冲区,有一个读端和写端,数据从写端进入,从读端出来。管道可用于实现进程中通信,管道分为无名管道和有名管道,无名管道只能在有亲缘关系的进程间使用,而有名管道可以在任意进程间使用。

        在Native层的MessageQueue是一个文件操作符,MessageQueue通过Java层的变量mPtr与Native层交互,在MessageQueue初始化时,它会创建一个epoll文件描述符,并添加到自己的链表中,同时创建一个无名管道(pipe),将管道的读端文件描述符添加到epoll中。这样在有新的Message加入到MessageQueue时,就会向管道的写端写入一个字节,从而触发epoll的回调函数,让Looper知道有新的Message可以处理。


II.MessageQueue最重要的方法next的逻辑

在阅读此方法时,可暂时忽略IdleHandler的逻辑,重要的是nextPollTimeoutMillis的变化。

  i. 获取当前的队列指针ptr,初始化待定闲置处理器数量(pendingIdleHandlerCount)和下次取消息超时时间(nextPollTimeoutMillis)。

 ii. 开启一个无限循环,调用nativePollOnce从队列中取出一个消息。

 iii. 初始化当前时间(final long now = SystemClock.uptimeMillis();),如果消息不为空,并且消息的target为空,取一条消息(同步屏障)。

 iv. 如果取出的消息不为空:

        如果当前时间没有到消息的处理时间(when),将下次取消息超时时间设置处理时间和当前时间的间隔 (msg.when - now)。否则:不阻塞,将消息设置为正在使用,将此消息返回。

     如果取出的消息为空:

       将下次取消息超时时间设置为-1 陷入无限的等待中(直到新的消息到来)

 v. 如果正在退出(mQuitting == true),则执行dispose方法,返回null。

 vi. 如果等待中的闲置处理器数量为空,阻塞,循环继续。

 vii. (闲置处理器的其他逻辑)

 viii. 重置闲置处理器数量和下次取消息超时时间。

伪代码如下

Message next(){
	//1
	long ptr = mPtr
	
	int pendingIdleHandlerCount = -1
	int nextPollTimeoutMillis = 0
	
	//2
	while true do
		nativePollOnce(ptr,nextPollTimeoutMillis)
		//synchronized (this) { 加锁
		//3
		long now = currentTimeMillis	//获取当前时间
		Message prevMsg = null
		Message msg = mMessages
		
		//4
		if msg != null && msg.target == null
			while msg != null && !msg.isAsynchronous	//取出的消息是同步的
				prevMsg = msg
				msg = msg.next	//取出下一条消息
		end if		
		if msg != null
        	if now < msg.when
        		nextPollTimeoutMillis = msg.when - now	//设置下一次取消息时间
        	else 
            	mBlocked = false	//不阻塞插入队列操作
            	if prevMsg != null
            		prevMsg.next = msg.next
            	else 
                	mMesages = msg.next
                end if	
                msg.next = null	//标记消息为队尾
                msg.markInUse()	//标记此消息正在被使用
                return msg	//返回此消息
             end if   
		else 
			nextPollTimeoutMillis = -1	//no more messages
		end if	
		//5
		if mQuitting
			dispose()	//退出,调用nativeDestroy销毁内部结构,mPtr置0
			return null
		end if	
        //6 获取闲置处理器的数量
        if pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when))
        	pendingIdleHandlerCount = mIdleHandlers.size()
        end if	
        if pendingIdleHandlerCount <= 0	//如果没有闲置处理器
        	mBlocked = true	//阻塞插入队列操作
        	continue
        end if	
        //7	省略IdleHandler的逻辑
        ....
        //8
        pendingIdleHandlerCount = 0
        nextPollTimeoutMillis = 0
        //} end synchronized (this) 
		end while
}
 III.quit方法

 quit方法用于停止next()循环并回收未处理的消息。


3) Looper 

        Looper存储在线程的局部变量ThreadLocal里,从而保证各个线程之间的Looper互相隔离。ThreadLocal通过get和set方法获取和设置当前线程变量的值,通过remove移除变量以防止内存泄漏。

        在安卓系统中子线程的Looper需要通过调用prepare方法手动创建,并且只能被创建一次,从源码可知,在一个线程中多次调用prepare方法会抛出异常,因此一个线程也只能有一个Looper。

        在前面我们得知quitAllowed这个参数实际是用于控制是否允许消息队列退出循环并回收所有消息,子线程的消息队列是可以被退出的,主线程的消息队列是不允许被退出的。


Tips:

        在Android 10版本,系统为Looper类添加了Observer类观测Looper的消息调度,该功能在设计之初只是为了统计并观测系统服务的Looper的消息调度性能,因此Looper.Observer类机器相关API都被标记为@hidden。

        在先前的版本应用的Android应用启动后并不会自动创建MainLooper,在安卓11版本后,为了避免在多显示器的情况下出现混乱或错误的MainLooper,主线程会自动创建一个Looper,它的变量名是sMainLooper,不允许被手动退出(quitAllowed为false),可以通过getMainLooper静态方法获取到主线程的Looper。


        Looper的核心方法是loop,当Looper调用loop()方法时,它会不断地从MessageQueue中取出消息,如果没有消息,它会阻塞等待,直到有新的消息到来。loop方法的核心代码如下。(...省略了不太核心的代码)

public static void loop(){
	final Looper me = myLooper();	//获取当前线程的Looper
	...//Looper 判空
	if(me.mInLoop){//判断looper是否已经在循环中
		...//输出警告日志
	}
	me.mInLoop = true;
	final MessageQueue queue = me.mQueue;	//获取消息队列
	...//
	for(;;){	//无限循环
		Message msg = queue.next(); // 从队列中取出消息 might block (可能会阻塞)
		//消息判空、日志输出、构造一个观察者用于监听消息循环的开始和结束等等处理
		try{
			msg.target.dispatchMessage(msg);	//(重要)调用Handler的消息处理方法
		}catch(Exception exception){
			//观察者处理抛出的异常
			throw exception
		}
		...//
       msg.recycleUnchecked();	//回收消息
	}
}

4) Handler

Handler的构造有三个核心参数。

    1.Looper 用于从消息队列中读取消息;

    2.Callback 消息处理回调;

    3.async 控制处理消息是异步或同步。

        通常情况下,Handler使用哪一个线程的Looper作为入参创建,Handler就可以从其他线程发送消息到此线程处理。

Handler有两种方式处理消息:

  (1) 在构造时设置Callback,通过回调处理消息;

  (2) 重写handleMessage方法。

Handler的处理消息会调用dispatchMessage方法,需要注意的是如果消息设置了回调,则重写handleMessage方法不起作用。

        如果消息自身带一个Runnable,则直接执行Runnable,忽略Callback和对handleMessage的方法重写。

    private static void handleCallback(Message message) {
        message.callback.run(); //直接调用Message的Runnable的run方法
    }

        由此可见Handler消息处理的优先级是Message的Runnable的run方法 > Handle构造时的Callback > 对handleMessage的重写。

核心问题:

Handler是怎么保证Message里的Runnable在Looper所在的线程执行的?

因为dispatchMessage方法是运行在loop方法内的,loop方法是运行在Looper创建线程的,所以Runnable本质上是在loop方法内被执行。

对Message进行处理本质上是调用Message持有的Handler的dispatchMessage方法,如果Message没有设置target对象,怎么知道这个Message是哪个Handler发送的?

从源码可以得知,Handler的sendMessage方法最终会调用enqueueMessage将消息插入队列,在插入队列前,会将消息的target设置为自身,从而避免了不调用obtain方法target为null的问题。

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;  //将Message的target设置为自身
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis); //将消息插入队列
    }

3.结语

        本篇文章描述了安卓的异步通信机制的实现,在下一篇文章将继续讲解Handler在安卓异步通信框架中的应用。

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值