知识梳理系列之二——消息机制中的若干重要问题


这是一个老生常谈的知识,本文不是全面文章,主要记录一些非常有用的原理性知识点,方便深入理解。

轮询器Looper

子线程为何要手动准备和轮询?

一、MainThread的Looper创建和准备

Android 的启动过程是: 创建init进程 --> Zygote进程 --> SystemServer进程 --> 各个应用进程
在SystemServer进程启动后(由Zygote进程fork出)在调用run方法时,调用了Looper.prepareMainLooper();,在老版本的则是在ActivityThread.main()中调用的prepareMainLooper

// SystemServer.java
package com.android.server;

public final class SystemServer {
	...
	public static void main(String[] args) {
        new SystemServer().run();
    }

	private void run() {
		...
		Looper.prepareMainLooper();
		...
		startBootstrapService();
		startCoreService();
		startOtherService();
		...
		Looper.loop();
		...
	}
	...
}

于是,主线程在启动后,Activity#onCreate(Bundle) 调用时候就已经准备好Looper了
而工作线程Looper是没有调用prepare()/loop()的,因此需要自己手动调用或者使用HandlerThread/IntentService

如何保证线程Looper唯一?

遇到一个问Looper存储在哪里的问题

我们来看下Looper.java

// Looper.java
	...
	
	public static void prepare() {
		// 工作线程的prepare传入了true,表示允许消息队列退出
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
        	// 在调用prepare的时候如果ThreadLocal中就已经存在Looper了
        	// 则抛出异常,避免重复prepare和同一个线程出现多个Looper对象
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 向ThreadLocal 中存入创建的Looper对象
        sThreadLocal.set(new Looper(quitAllowed));
    }

	public static void prepareMainLooper() {
		// 主线程的prepare传入了false,表示主线程的消息队列是不允许对出的!!
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            // 此处为Looper对象的成员变量sMainLooper初始化
            // myLooper()方法实际还是调用ThreadLocal.get()
            sMainLooper = myLooper();
        }
    }

所以我们有答案了,Looper是保存在ThreadLocal这个线程本地存储里的,每一个线程只有一个线程本地存储,所以确保了同一个线程只有一个Looper!

附:ThreadLocal是怎样的数据结构:

// Looper.java
// Looper类中有静态常量ThreadLocal对象,通过ThreadLocal.get()获取Looper实例(线程唯一)
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

// ThreadLocal.java
// get方法获取了当前线程,通过线程取得线程的ThreadLocalMap引用,然后根据指定泛型对象获取实例
// 简单说就是从Map中获取了ThreadLocal<Looper>的键值对对象Entry,最后返回Entry.value即Looper对象
// 而这个Map是一个哈希数组,key是ThreadLocal<?>对象,就是说只有唯一的一个ThreadLocal<Looper>,
// 因为ThreadLocal<Looper>与别的泛型的ThreadLocal对象的hash值不同,因此确保了一个线程只有一个Looper
	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;
    }

// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

总结:
1. 主线程Looper在启动时就已经prepare/loop,子线程需要手动执行或使用HandlerThread/IntentService;
2. Looper被存储在相应Thread的ThreadLocal对象中,这个对象是一个哈希数组,保证了Looper线程唯一;

Looper.loop中是死循环为什么MainLooper不会阻塞主线程

在loop方法中,实际是一个死循环,即不断的从消息队列中获取消息,并分发给target(Handler)来处;
那么主线程是如何不会被这个死循环卡死的?

简化loop方法如下:

//Looper.java
	public static void loop() {
		// 获取不到Looper 抛出异常
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException(
            "No Looper; Looper.prepare() wasn't called on this thread.");
        }
        // 获取Looper 持有的消息队列
        final MessageQueue queue = me.mQueue;

        ...

        for (;;) {
        	// 进入死循环 不断从消息队列中获取下一个消息 
        	// 官方在这里注释了might block表示获取下一个消息有可能会阻塞
            Message msg = queue.next(); // might block
            if (msg == null) {// 拿到空消息退出循环 这时候loop也退出了
                // No message indicates that the message queue is quitting.
                return;
            }
            
			...
			
            try {
            	// 把非空消息分发给目标Handler
                msg.target.dispatchMessage(msg);
            } 
            ...
            // 对消息进行回收
            msg.recycleUnchecked();
        }
    }

通过loop方法可以看出端倪,死循环在 queue.next() 方法处阻塞
再看简化的MessageQueue.next方法

// MessageQueue.java
	Message next() {
        // MessageQueue持有Native消息队列的指针,如果已经退出了就会返回空消息
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
		int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
        	// 再进入一个死循环,调用本地方法去检测描述符有没有新的消息可以poll出来
        	// 参数nextPollTimeOutMillis表示的是没有检测到新消息时的超时阻塞时间,-1表示一直阻塞
        	nativePollOnce(ptr, nextPollTimeoutMillis);
        	
        	// 休眠被唤醒后执行同步代码块
        	synchronized (this) {
                // 获取系统时间 和 消息队列头部Message
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                // 省略同步屏障有关处理
                ...
                if (msg != null) {
                    if (now < msg.when) {
                        // 如果消息的时间没有到(比如延时消息),重新计算nextPollTimeoutMillis
                        // 下一次循环,nativePollOnce阻塞的时间就是确定的
                        nextPollTimeoutMillis = (int) Math.min(
                        msg.when - now, Integer.MAX_VALUE);
                    } else {
                        mBlocked = false;
                        if (prevMsg != null) {
                        	// 有同步屏障才会走这里
                            prevMsg.next = msg.next;
                        } else {
                        	// 更新消息队列对头
                            mMessages = msg.next;
                        }
                        // 修改use标记和从消息队列中剥离出msg,最后返回msg
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // 取到空消息,重置nextPollTimeoutMillis 继续循环
                    nextPollTimeoutMillis = -1;
                }
                ...
            }
           	...
        }

至此,已经找到问题的关键部分了。
Looper.loop()方法的死循环是在MessageQueue.next()获取队列中下一个消息时,阻塞的,并且如果主线程消息队列一直没有新消息时,就会一直阻塞,是被一个nativePollOnce()的本地方法阻塞的。

回到问题,阻塞了为什么没有导致主线程卡死。由于 Linux pipe/epoll机制,主线程会在此时(没有新的消息和事件进入),释放CPU资源,进入休眠状态,等待新的消息或者事件唤醒。

Linux pipe/epoll 机制是一种IO多路复用的机制,基于事件驱动,监测了多个文件描述符。即使没有消息,如果有其他的操作(比如事件)唤醒了主线程,主线程就退出休眠的状态,继续工作,而消息Looper的继续阻塞不会导致主线程卡死。

而在消息机制中,来了新的消息,也会调用一个本地方法唤醒休眠中的主线程

// MessageQueue.java
private native static void nativeWake(long ptr);

消息与队列 Message/MessageQueue

引:

一个线程不仅只有一个Looper,并且也只有一个MessageQueue,这是如何保证的;

如果向队列中发送大量消息,消息又一直在不断被处理,那么为什么不会频繁GC Message导致内存抖动?

带着这两个问题来看源码

线程MessageQueue唯一

// Looper.java
	private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 这里创建并向ThreadLocal中存入了Looper实例
        sThreadLocal.set(new Looper(quitAllowed));
    }
 
 	// 构造方法中创建了MessageQueue实例
 	private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

第一个问题已经有答案了:
因为Looper由线程私有存储ThreadLocal的哈希数组结构保证线程唯一,MessageQueue又是由Looper实例化的成员变量,那么也是线程唯一的。

IdleHandler的作用

在MessageQueue中有个接口IdleHandler,顾名思义是一个空闲时处理任务的处理器。

public static interface IdleHandler {
	boolean queueIdle();
}

这个接口提供了一个返回boolean值的方法。
在MessageQueue中有一个ArrayList<IdleHandler>的集合
此外,还提供了addIdle、removeIdle等方法用于向集合添加和移除IdleHandler。
那么这个IdleHandler有什么作用呢?在何时调用queueIdle方法?
在MessageQueue#next()中有了答案:

Message next() {
		// 省略了nativePollOnce等逻辑
		...
		// 在阻塞结束(消息队列中的消息执行完了,而队列中下一个要执行的消息还没有达到可执行的时间)时
		if (pendingIdleHandlerCount < 0
        	&& (mMessages == null || now < mMessages.when)) {
        	pendingIdleHandlerCount = mIdleHandlers.size();
        } // 获取列表中IdleHandler个数
        if (pendingIdleHandlerCount <= 0) {
        	// 没有IdleHandler直接进入下一次循环
            mBlocked = true;
            continue;
        }
        if (mPendingIdleHandlers == null) {
        	mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    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 {
        	// 在这里调用了queueIdle方法!!!
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

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

也就是当消息队列空闲时,可以执行IdleHandler的一些任务,这样可以提升性能。
比如在onResume方法中,使用主线程的消息队列的IdleHandler做一些准备工作,或者单纯的想获取UI绘制完成的回调,在绘制完成以后立即可以执行一些操作。

Message如何避免内存抖动——消息复用

在使用Message时,通常建议使用Message.obtain()系列的方法,这是有原因的。
当然也可以通过new Message() 来创建消息,只是更推荐上面的方式,并且new的方式最终也会被做一些响应处理。

几个重要变量的含义:

// Message.java
public final class Message implements Parcelable {
	...
	
	Message next;// 消息单向链表的下一个消息对象
    /** @hide */
    public static final Object sPoolSync = new Object();// 同步锁
    private static Message sPool;// 私有静态成员,消息池
    private static int sPoolSize = 0;// 消息池的大小,即消息对象个数
    private static final int MAX_POOL_SIZE = 50;// 消息池最多可容纳50个消息对象
    
	...
}

由此可以看出消息池最多可以服用的消息对象是50个,这个池是一个单向链表的数据结构;

从obtain()看消息复用

// Message.java
	public static Message obtain() {
        synchronized (sPoolSync) {// 同步锁
            if (sPool != null) {
            	// 当池可以复用时,取出单向链表队头的消息对象使用,
            	// 然后把队头重置为next,并维护池中可用的消息对象的个数
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        // 消息池被清空了智能创建新的消息对象
        return new Message();
    }

答案就是,消息对象维护了一个消息池,由于消息本身是一种单向链表的结构,维护对头来复用消息对象,并且在Looper.loop()方法中通过recycleUnchecked()方法来回收消息对象。


消息处理者Handler

Looper获取了消息队列中的消息后,通过Handler#dispathMessage()方法来分发给Handler,就会重发handleMessage方法,于是被重写的handleMessage中的业务逻辑就被执行了。

延时消息是怎样被入队和分发处理的?

在日常使用中,经常使用sendMessageDelay/postDelay/sendMesageAtTime等方法,那么延时消息是怎么入队的呢?

// Handler.java
	public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

首先,这些方法最终都是调用sendMessageAtTime实现的!

// Handler.java
	public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
       	...
        return enqueueMessage(queue, msg, uptimeMillis);// 
    }

其次,直接调用了enqueueMessage进行入队!这个方法最终调用了MessageQueue#enqueueMessage()方法

那么是怎么实现延时呢?
下面是简化后的MessageQueue#enqueueMessage()方法

// MessageQueue.java
	boolean enqueueMessage(Message msg, long when) {
        // 一些可用非空判断
		...
		
        synchronized (this) {
        	// 在退出队列的回收异常退出
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

			// 标记入队的消息inUse 获取时间 获取消息队列对头
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // 当消息队列中没有消息或者新入队的消息时间小或为0,需要先执行入队的新消息
                // 变更了对头
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // 同步屏障有关省略
                ...
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    // 找到第一个时间比要入队新消息的消息然后放入,或者没有就放队尾
                    // 实质上是消息队列用when这个时间来排序了,时间小的在队头,从小到大的顺序排列
                    // 新消息找到正确的入队位置就退出此循环
                    if (p == null || when < p.when) {
                        break;
                    }
                    // 同步屏障有关省略
                	...
                }
                // 然后这一步就是真正的入队
                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;
    }

答案有了:延时消息也是在触发时就入队了,只是消息队列里面会依据when这个时间标志从小到大屏排序;
那怎样保证延时处理呢?答案就在Looper.loop()方法中,阻塞的过程中有一个nextPollTimeoutMillis参数,如果消息队列中队头就是一个延时消息,那么loop在取消息的时候,会计算一个新的nextPollTimeoutMillis阻塞超时时间,来在delay的时间向Handler交付Message,于是就实现了延时消息!!!

题外话:Handler内存泄露的注意项

Handler在Activity中使用时常常要注意避免内存泄露,这里泄露的对象就是Activity,常常在使用延时消息时,需要注意如果延时消息还没有处理,Activity就销毁了,那么就会出问题。

Handler内存泄露的引用链

MainThread --> ThreadLocal<Looper> --> Looper --> MessageQueue --> Message --> Handler --> Activity

匿名内部类Handler持有Activity的引用,Message.target.dispatchMessage()持有了target(Handler)的引用,就是延时消息持有了Handler的引用,延时消息又enqueue在MessageQueue中,MessageQueue由是由Looper创建的,最终存放在主线程ThreadLocal中了。

解决方法

只要打破引用链的环节就解决了

  1. 让Handler作为静态内部类或者外部类,不持有Activity引用,即可避免Activity泄露,但是此时没有Activity的引用了,可以引入弱引用、软引用;
  2. 让Handler作为普通内部类使用,但是在Activity生命周期结束时,移除消息队列中的所有回调和消息;(即onDestroy中 调用Handler#removeCallbacksAndMessages一类的remove方法)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值