浅谈android应用之消息机制

1.前言

更多内容请查看android生态之应用篇

新人报道,这是我在CSDN发布的第一篇文章,欢迎大家技术交流。如需转载,请标明出处!

本菜鸟的github

2.引言

先提出一个问题:下载网络图片,并用imageview控件展示需要怎么做?
针对这个问题,需这样分析,由于涉及到访问网络,并有io读写操作,这个过程是比较耗时的,为了不造成程序卡顿(会有ANR风险),我们肯定不能直接在主线程中访问网络并下载图片,因此我们新开一个子线程做这个耗时操作。我们在子线程中下载好了网络图片,由于更新UI操作需要在主线程中进行(子线程更新UI不安全,如果两个子线程同时更新同一个UI,就会出问题),那就需要解决在子线程通知主线程更新UI的问题。我刚好认识线程通信的桥梁专家Handler,下面就将他介绍给大家。

2.1补充

子线程可以更新实现UI更新,但一般不建议在子线程直接执行更新UI操作。不仅使用需要适当的时机,还有可能会导致UI更新混乱问题。更新混乱可能都知道,这里提一下如果在子线程直接更新UI的时机。

用WindowManagerImpl可以在子线程中显示一个布局,或者在Activity中从onCreate直到onResume(包括onResume),都可以在子线程里面实现UI更新,并且不会有报异常,但如果在之后的生命周期中使用线程直接更新UI的话就会报CalledFromWrongThreadException的异常。

异常出现的原因跟一个叫ViewRootImpl的类有关,这个类有一个Thread类的属性mThread,该属性的值就是创建ViewRootImpl对象的线程,在执行某些方法的时候会检查当前线程和创建ViewRootImpl对象所在的线程是否为同一线程。

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

Activity中ViewRootImpl对象的创建都是在UI线程中,所以mThread指向的就是main线程对象,并且Activity的ViewRootImpl对象的创建是在执行完Activity的onResume方法之后,所以在onResume之前(包括onResume),都可以在子线程操作UI,因为此时ViewRootImpl对象还没有创建,在onResume方法之后,子线程操作UI就会报异常了。

2.2小结

Android的单线程模型有两条规则:
1、不要阻塞UI线程
2、不要在非UI线程中操作UI控件。

3.Handler简单使用

第一种方式,采用sendEmptyMessage方法发送消息,然后在 handleMessage处理消息,并更新UI:

    final Handler mhandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //得到这个消息标记开始处理消息
            if (msg.what == 1001)
                textView.setText("我更新了!");
        }
    };
    new Thread(new Runnable() {
        @Override
        public void run() {
        //发送消息
            mhandler.sendEmptyMessage(1001);
        }
    }).start();

第二种方式,采用post方法发送消息,并直接在Runnable中设置UI更新操作:

    mhandler.post(new Runnable() {
        @Override
        public void run() {
            textView.setText("我更新了!");
        }
    });

4.Handler源码分析

第一节
我们按照第一种Handler使用方式分析。看sendEmptyMessage函数实现,代码路径为:
./frameworks/base/core/java/android/os/Handler.java

    public final boolean sendEmptyMessage(int what)
    {
        return sendEmptyMessageDelayed(what, 0);
    }

继续往下:我们看一下sendEmptyMessageDelayed函数的实现,代码文件为:
./frameworks/base/core/java/android/os/Handler.java

    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }

再往下看sendMessageDelayed函数的实现,代码路径为:
./frameworks/base/core/java/android/os/Handler.java

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

快好了,再耐心看下去,我们看sendMessageAtTime函数的实现,代码路径为:
./frameworks/base/core/java/android/os/Handler.java

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
       MessageQueue queue = mQueue;
       if (queue == null) {
           RuntimeException e = new RuntimeException(
                   this + " sendMessageAtTime() called with no mQueue");
           Log.w("Looper", e.getMessage(), e);
           return false;
       }
       return enqueueMessage(queue, msg, uptimeMillis);
   }

从这里我们可以看到,我们将消息送入队列了,我们再看看enqueueMessage函数,代码路径为:
./frameworks/base/core/java/android/os/Handler.java

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

再往下看enqueueMessage的函数实现,代码路径为:
./frameworks/base/core/java/android/os/MessageQueue.java:


    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            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.
               /* *
                *如果p为空,表明消息队列中没有消息,那么msg将是第一个消息,needWake
                * 需要根据mBlocked的情况考虑是否触发
                */
                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 (;;) {
                 // 如果p不为空,表明消息队列中还有剩余消息,需要将新的msg加到消息尾
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                    // 因为消息队列之前还剩余有消息,所以这里不用调用nativeWakeup
                        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;
    }

到了这里我们从源码得知我们将消息送入消息队列中了,但是主线程中如何从消息队列中取出消息呢?请看第二节说明。

第二节
我们从一切的源头开始讲起走,需要知道,当应用起来的时候会走到在ActivityThread.java main函数中,即所谓的主线程中,代码路径为:
./frameworks/base/core/java/android/app/ActivityThread.java

    public static void main(String[] args) {
       ...
       Looper.loop();
       throw new RuntimeException("Main thread loop unexpectedly exited");
   }
   

再让我们走进loop函数去瞧瞧,代码路径为:
./frameworks/base/core/java/android/os/Looper.java:

    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;
        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

从上面的代码中我们可以发现里面有个for循环,这里不断从消息队列中获取消息,然后将消息传递给dispatchMessage函数,我们再来看一下这个dispatchMessage函数的代码链接,代码路径为:
/frameworks/base/core/java/android/os/Handler.java

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

当msg.callback不为空的情况下,我们走handleCallback(msg)分支,即现象post(Runnable)的run方法,为空的情况就是走handleMessage(msg)分支,即回调handleMessage方法,执行里面的函数,由于现在还是在主线程中,所以在回调的函数里,可以实现UI更新操作。分析msg.callback是否为空的话,可以看自行去看handler 的post(Runnable)方式,代码路径为:
./frameworks/base/core/java/android/os/Handler.java:

    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

可以发现post方法与sendEmptyMessage异曲同工之妙,不同的是前者是带getPostMessage®参数,后者是带常量what参数,我们再去看getPostMessage®做了什么,代码路径 ./frameworks/base/core/java/android/os/Handler.java:

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

看到了这里懂了吧,这里给message添加callback属性,所以使用post方式的话msg.callback不为空。

5.Hanlder常见问题

问题提出
在下面代码我们可以看到loop函数中是个死循环,试问,为啥不会造成卡顿?

    public static void main(String[] args) {
       ...
       Looper.loop();
       throw new RuntimeException("Main thread loop unexpectedly exited");
   }
   

问题解决
请看这行代码:

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

如果loop不一直循环,那么执行完loop就会抛异常,导致程序退出。所以loop函数中的for循环有存在的必要性!我们继续看loop函数的代码,这里只展示里面部分代码:

    public static void loop() {
        ....
        for (;;) {
        /**
         *queue.next()中会判断,如果msg为空,会阻塞在native,线程休眠,等待新的消息唤醒,
         *所以不会走在下面msg等于空退出的情况
         */
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
         ....
        }
    }

我们可以看到官方给 Message msg = queue.next();这行代码写的might block注释,这里就是为啥不会卡顿奥秘存在。我们追踪一下next函数的实现,为了方便阅读,只留下关键代码,代码路径为:
./frameworks/base/core/java/android/os/MessageQueue.java

    Message next() {
        ...
        for (;;) {
        ...
            nativePollOnce(ptr, nextPollTimeoutMillis);
        ...   
        }
    }

我们主要看 nativePollOnce(ptr, nextPollTimeoutMillis);这一行代码,需要关心nextPollTimeoutMillis的值,当没有消息的时候是为-1,无限阻塞。这是一个nativie代码,但别担心,我去n源码中把他的实现提出来,代码路径为:
./system/core/libutils/Looper.cpp:

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) {
                int fd = response.request.fd;
                int events = response.events;
                void* data = response.request.data;
#if DEBUG_POLL_AND_WAKE
                ALOGD("%p ~ pollOnce - returning signalled identifier %d: "
                        "fd=%d, events=0x%x, data=%p",
                        this, ident, fd, events, data);
#endif
                if (outFd != NULL) *outFd = fd;
                if (outEvents != NULL) *outEvents = events;
                if (outData != NULL) *outData = data;
                return ident;
            }
        }

        if (result != 0) {
#if DEBUG_POLL_AND_WAKE
            ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
            if (outFd != NULL) *outFd = 0;
            if (outEvents != NULL) *outEvents = 0;
            if (outData != NULL) *outData = NULL;
            return result;
        }

        result = pollInner(timeoutMillis);
    }
}

其他先不管,主要看上面最后一行代码的pollInner函数的实现,已提取关键内容:
./system/core/libutils/Looper.cpp:

int Looper::pollInner(int timeoutMillis) {
  ...
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
  ...
    }

这里通过epoll_wait调用,当mEpollFd监控的文件描述符没有IO事件发生时,线程会阻塞住且不会被CPU分配时间片了,如果有IO事件发生或事件超时了,该方法会返回了。

我们在看handler的sendmessage向消息队列发送消息,最终会调用到MessageQueue.java的enqueueMessage方法,代码路径为:
./frameworks/base/core/java/android/os/MessageQueue.java

boolean enqueueMessage(Message msg, long when) {
            ...
             if (needWake) {
                nativeWake(mPtr);
            }
            ...
    }

当线程needWake时,说明当前线程挂起,需要唤醒线程,会调用nativeWake方法,代码路径为:
./system/core/libutils/Looper.cpp

void Looper::wake() {
    ssize_t nWrite;
    do {
        nWrite = write(mWakeWritePipeFd, "W", 1);
    } while (nWrite == -1 && errno == EINTR);

    if (nWrite != 1) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

往管道写端写入W,唤醒主线程。
小结:主线程不断的从消息队列中取出消息干活,当没有消息的时候呢,主线程休息。当又有消息的时候,就通过管道告诉主线程起来干活,正所谓劳逸结合,干活不累,哈哈,因此并不会造成程序卡顿。

6.总结

子线程中创建消息,消息会自动进入消息队列,等待主线程从消息队列中去取出消息干活,当消息队列没有消息的时候,主线程就休息去了,当消息队列又来消息的时候,就通过管道叫主线程起来干活。

7.补充

源码:Android 6.0
工具:Android studio
用到技能:java,C,linux shell

下一篇文章:关于Android系统源码下载及编译原理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值