Android Handler消息机制(上)

大致流程:

调用 Handler 发送一个 MessageMessageQueue 中,Looper 调用 loop() 方法到 MessageQueue 中获取消息,并将消息交给 Messagetarget 对象 处理。流程完毕

具体流程:

首先看一下Android消息机制中所用到的几个类(Handler、Message、MessageQueue、Looper、ThreadLocal)

Message

定义包含描述和任意数据对象的消息

message 中主要的属性

//用来标识当前的消息,Handler接收到该消息的时候可以通过这个标识来知道需要做什么操作
public int what;

//如果你需要传递的数据是整型的话,可以采用这个两个参数,可以避免创建Bundle对象
public int arg1;
public int arg2;

//可以给接收者传递一个任意类型的对象,如果需要跨进程进行通信的话,那么可以使用它来进行传递数据
public Object obj;

//负责回复消息的 Messenger,有的场景下(比如接受者、发送者模型)需要使用它
public Messenger replyTo;

//当前消息的标志,只在被 Messenger 传递消息时使用,其他情况下都是 -1
public int sendingUid = -1;

//标识当前的消息是否在使用中
//当消息入队的时候这个状态就会被改变,在被回收的时候,状态重置
//当一个消息在使用中的时候,二次入队或者是回收的时候就会报错
/*package*/ static final int FLAG_IN_USE = 1 << 0;

/** If set message is asynchronous */
//标识当前的消息是否为异步
/*package*/ static final int FLAG_ASYNCHRONOUS = 1 << 1;

/** Flags to clear in the copyFrom method */

/*package*/ static final int FLAGS_TO_CLEAR_ON_COPY_FROM = FLAG_IN_USE;

/*package*/ int flags;

/*package*/ long when;

//数据的存储
/*package*/ Bundle data;

//发送和处理消息的handler
/*package*/ Handler target;

//消息回调
/*package*/ Runnable callback;

//在某些情况下,还会以链表的形式关联下一个消息
// sometimes we store linked lists of these things
/*package*/ Message next;

//消息池
private static final Object sPoolSync = new Object();
//回收消息链表
private static Message sPool;
private static int sPoolSize = 0;

private static final int MAX_POOL_SIZE = 50;

private static boolean gCheckRecycle = true;

如何去获取一个消息(Message),推荐做法:

  • Message.obtain()
  • Handler.obtainMessage()

原因是:采用这两种方法会从消息池中获取 Message 对象,在一定程度上减少了对象的创建和销毁,节省内存。

obtain 源码看一下:

public static Message obtain(Handler h, int what,
        int arg1, int arg2, Object obj) {
    Message m = obtain();
    m.target = h;
    m.what = what;
    m.arg1 = arg1;
    m.arg2 = arg2;
    m.obj = obj;

    return m;
}

就是在原有的 Obtain 的方法上加了一些属性

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;//当前被回收的空闲消息
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

sPool 是一个静态的属性,所以在内存中是共享的。

sPool 其实是一个最新的空闲 Message 对象,如果这个对象为空的话,那就 new Message(),如果 sPool 不为空的话:假设当前链表的结构如下图所示:

初始状态�

执行Message m = sPool;就变成下图
Message m = sPool

继续执行 sPool = m.next;
sPool = m.next

最后是 m.next = null;
m.next = null;

消息的回收再利用

void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;
    when = 0;
    target = null;
    callback = null;
    data = null;

    synchronized (sPoolSync) {
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }
}

可以看出消息回收的时候会将当前消息的标识重置为 FLAG_IN_USE,如果这个消息再被添加到队列中的时候就会报错了。另外它还清除了其他的数据,并判断了当前消息池中的数据量是否大于最大的消息量,如果没有超过的话就会将当前消息加到回收消息的链表中,并将 sPoolSize+1

假设当前的链表形式是这样的:

初始状态�

执行 next = sPool; 以后
next = sPool;

最后执行了 sPool = this; ,链表的结构就成了
sPool = this;

当是这个方法具体在什么地方被调用了,我们暂且先看一下其他几个类的源码,来找找这个方法的踪迹。

MessageQueue

包含着一个 Message 的列表,消息不会直接添加进来,而是通过 Handler 进行添加,通过 Looper 进行读取

关于这个队列先说明一点,该队列的实现既非 Collection 的子类,亦非 Map 的子类,而是 Message 本身。因为 Message 本身就是链表节点。队列中的 Message mMessages; 成员即为队列,同时该字段直接指向队列中下一个需要处理的消息。

属性:

//队列是否可以退出
// True if the message queue can be quit.
private final boolean mQuitAllowed;

//底层调用的代码
@SuppressWarnings("unused")
private long mPtr; // used by native code

//消息链表的开头
Message mMessages;
private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;
private IdleHandler[] mPendingIdleHandlers;
private boolean mQuitting;

// Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
private boolean mBlocked;

// The next barrier token.
// Barriers are indicated by messages with a null target whose arg1 field carries the token.
private int mNextBarrierToken;

private native static long nativeInit();
private native static void nativeDestroy(long ptr);
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);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);

什么时候 MessageQueue 会被初始化呢?一般来说 MessageQueue 不是直接访问的,而是在 Looper.myQueue() 方法进行获取:

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

入队列的方法:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        //如果当前message所绑定的Headlder就会报错,也就是消息一定要跟Handler进行关联
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        //如果消息已经在使用中,还入队的话就报错
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    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;
        }
        //正常入队列以后,将消息的状态设置为正在使用
        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
                prev = p;
                //第一次循环的时候,将头部的消息的下一个消息重新赋值给p
                p = p.next;
                //如果p为空,也就是没有下一个消息,或者是当前的消息比下一个消息p要更早执行就break
                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;
}

插入队列的方法主要是先判断 Handler 是否绑定,再判断是否被使用中,如果都OK的话,就将当前的 Message 设置为正在使用中,并且将 Message 执行的时间戳存入到 Message 中,接下来的就是要判断当前的消息要插入到哪个位置。

if (p == null || when == 0 || when < p.when)

这个时候表示的是需要插入到队列的头部。

否则的话:(假设当前的链表结构如下)

初始状态

开始遍历的时候,定义一个变量 prev,将 p 的值赋值给 prev,然后将当前消息的下一个指向赋值给 p
prev和p后移

假设此时 p 所指消息的 when 比新消息晚,则新消息位置在 prevp 中间

如果当前的消息队列里面没有消息,或者是当前消息的执行状态时立马执行,或是当前消息的执行时间要比在队列中的所有消息都要早的话,那就将当前的消息插入队列中的头部。else的话,就进行判断,找出当前消息需要插入的位置。

出队列的方法

第一段
final long ptr = mPtr;
if (ptr == 0) {
    return null;
}

如果mPtr为0则返回 null。那么 mPtr 是什么?值为0又意味着什么?在 MessageQueue 构造方法中调用了 native 方法并返回了 mPtrmPtr = nativeInit(); ;在 dispose() 方法中将其值置 mPtr = 0; , 并且调用了 nativeDestroy() 。而 dispose() 方法又在 finalize() 中被调用。另外每次 mPtr 的使用都调用了 native 的方法,其本身又是 long 类型,因此推断它对应的是C/C++的指针。因此可以确定,mPtr 为一个内存地址,当其为0说明消息队列被释放了。这样就很容易理解为什么 mPtr==0 的时候返回 null 了。

第二段

这部分涉及到的代码基本上就是这个 next() 方法本身了,但可以肯定的是这里的返回语句是 return msg 。同时从 enqueueMessage() 方法可以看出来,在这个队列中取到的 message 对象不可能为空,因此这里的返回绝对不为空。

如果 next() 方法返回为空的时候,则说明这个消息队列正在退出或者是被释放回收了。

首先看一下 pendingIdleHandlerCount,这是一个局部变量,初始化为-1,然后被赋值为 mIdleHandlers.size()mIdleHandlers 是一个 ArrayList<IdleHandler>,在方法 addIdleHandler 中添加元素,在方法 removeIdleHandler 中移除元素。而我们所有的 Handler 并未实现 IdleHandler 接口,所以 mPendingIdleHandlers 的值应该是为0,因此可以看出与该变量相关的部分代码运行情况是确定的,好的,把不影响循环控制的代码减掉。

接下来要减的是:

if (nextPollTimeoutMillis != 0) {
    Binder.flushPendingCommands();
}

看源码说明:

Flush any Binder commands pending in the current thread to the kernel driver. This can be useful to call before performing an operation that may block for a long time, to ensure that any pending object references have been released in order to prevent the process from holding on to objects longer than it needs to.

Binder.flushPendingCommands() 方法被调用说明后面的代码可能会引起线程阻塞。然后把这段减掉。

最后再把代码精简一下的样子就成为了:

Message next() {
    int nextPollTimeoutMillis = 0;
    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    //当前时间要比消息队头的Message执行时间要早,
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                //让队列一直阻塞下去,直到下次被唤醒
                nextPollTimeoutMillis = -1;
            }
            if (mQuitting) {
                dispose();
                return null;
            }
        }
        nextPollTimeoutMillis = 0;
    }
}

虽然还是很长,但也不能再减了。大致思路如下:如果有异步的消息,先执行异步消息。如果没有的话先获取第一个同步的 message。如果它的 when 不晚与当前时间,就返回这个 message;否则计算当前时间到它的 when 还有多久并保存到 nextPollTimeMills 中,然后调用 nativePollOnce() 来延时唤醒,唤醒之后再照上面那样取 message

移除消息

当我们需要移除某个 Message 的时候,一般都会调用 HandlerremoveMessages 方法,其实 Handler 都是调用的 MessageQueue 中的方法:

public final void removeMessages(int what) {
    mQueue.removeMessages(this, what, null);
}

public final void removeMessages(int what, Object object) {
    mQueue.removeMessages(this, what, object);
}

public final void removeCallbacksAndMessages(Object token) {
    mQueue.removeCallbacksAndMessages(this, token);
}

再来看一下 MessageQueue 中的 void removeMessages(Handler h, int what, Object object) 方法:

void removeMessages(Handler h, int what, Object object) {
    if (h == null) {
        return;
    }
    synchronized (this) {
        Message p = mMessages;
        while (p != null && p.target == h && p.what == what
               && (object == null || p.obj == object)) {
            Message n = p.next;
            mMessages = n;
            p.recycleUnchecked();
            p = n;
        }
        while (p != null) {
            Message n = p.next;
            if (n != null) {
                if (n.target == h && n.what == what
                    && (object == null || n.obj == object)) {
                    Message nn = n.next;
                    n.recycleUnchecked();
                    p.next = nn;
                    continue;
                }
            }
            p = n;
        }
    }
}

首先判断 Handler 是否为空,这个是必不可少的。因为需要删除的是用当前 Handler 所发送的消息,如果没有 Handler 这个参数就无法得知要删除哪些消息了。接下来是一个同步代码块,代码块中有两个while循环,为什么需要两个呢?让我们拭目以待:

第一个While

假设当前链表的状态是这样的:

赋值给p

mMessages = n;
message后移

p.recycleUnchecked();
这个时候 message 就被回收了。然后执行 p = n;
p=n

就这样轻松的移除了一个数据,然后 while 循环继续执行,继续删除 message 。原本是很美好的遍历着链表中的数据,忽然碰到一个过不了条件的 message ,就会跳出循环,可是万一我们后面的部分数据是符合条件的,这可怎么办呢?都还没有删除完呢,接下来就是我们第二个循环出场了:

第二个循环

经过第一个循环以后,链表的结构除了长度上可能会有变化,其他的都是和原来一样的,我们在贴一次当前结构的图吧:

初始状态

Message n = p.next;
赋值给p

如果不为空,但是不满足移除条件,则将复制给P,然后继续将后移。假设如果我们当前的n是满足移除条件的,执行代码 Message nn = n.next;
nn初始化

n.recycleUnchecked();
消息n就会被回收了,然后执行 p.next = nn;
结束一轮循环

一轮循环结束,开启下一轮循环!!!

欲知后事,请听下回分解。

感谢

Android 进阶14:源码解读 Android 消息机制( Message MessageQueue Handler Looper)

【Android自助餐】Handler消息机制完全解析(二)MessageQueue的队列管理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容