Handler,MessageQueue,Runnable与Looper
来源:互联网 发布:python的idle mac 编辑:程序博客网 时间:2024/05/17 08:38
三者关系
Handler是处理消息或发送消息,MessageQueue存储消息,Runnable安排执行任务,Looper循环MessageQueue消息队列,并取出消息到给制定Handler处理。其三者具体关系如下:
- 每个Thread只对应一个Looper。
- 每个Looper只对应一个MessageQueue。
- 每个MessageQueue中有N个Message。
- 每个Message中最多指定一个Handler来处里消息(处理线程为Looper绑定的线程)。
采用这机制对消息进行处理,有下面两点好处: - 在多线程并发执行的同时,做出一些改变的时候,这方法可以避免了一些判断条件。
- 一般来说,Thread一旦完成执行任务,就不可以重复执行新的任务,而采用这种机制它可以让Thread可以在后台重复执行任务,而不用创建新的Thread实例,因为它的生命周期是伴随Looper的生命周期,只要Looper不调用
quit
方法,Thread的生命周期就不会结束。
Handler
handler这个角色起着两个重要作用,一是:处理Message;二是:将某个Message压入MessageQueue。
构造函数源码分析:
//默认设置Looper和MessageQueue public Handler(Callback callback, boolean async) { if (FIND_POTENTIAL_LEAKS) { final Class<? extends Handler> klass = getClass(); //检查Handler是否是时静态内部类,静态实例,静态匿名类,不是发出内存泄漏警告 if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && (klass.getModifiers() & Modifier.STATIC) == 0) { Log.w(TAG, "The following Handler class should be static or leaks might occur: " + klass.getCanonicalName()); } } //获得当前线程的Looper mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()"); } //获取消息绑定的队列 mQueue = mLooper.mQueue; mCallback = callback; //是否可以异步处理消息,默认是同步处理消息 mAsynchronous = async; } //自定义设置Looper和MessageQueue public Handler(Looper looper, Callback callback, boolean async) { mLooper = looper; mQueue = looper.mQueue; mCallback = callback; mAsynchronous = async; }
从源码分析可以看出,如果你创建Handler实例时,当地线程没有Looper实例且有没有传入Looper实例,就会抛出Can't create handler inside thread that has not called Looper.prepare()
异常。
发送消息源码分析:
//发送任务 public final boolean post(Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); } //发送延时执行任务 public final boolean postAtTime(Runnable r, long uptimeMillis) { return sendMessageAtTime(getPostMessage(r), uptimeMillis); } //发送带有令牌标延时执行的任务 public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { return sendMessageAtTime(getPostMessage(r, token), uptimeMillis); } //发送延时任务 public final boolean postDelayed(Runnable r, long delayMillis) { return sendMessageDelayed(getPostMessage(r), delayMillis); } //发送下一个将要执行的任务 public final boolean postAtFrontOfQueue(Runnable r) { return sendMessageAtFrontOfQueue(getPostMessage(r)); } //将Runnable封装成Message消息 private static Message getPostMessage(Runnable r) { //从消息池获取Message Message m = Message.obtain(); m.callback = r; return m; } //定时发送消息到MessageQueue 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); } private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { //绑定处理消息处理的Handler msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } //插入消息到MessageQueue return queue.enqueueMessage(msg, uptimeMillis); } //插入消息 boolean enqueueMessage(Message msg, long when) { //检查该消息是否有对应的Handler 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) { //检查Looper循环是否已停止 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; //检查是否是msg头部消息(即原本消息队列为空)或处于空闲等待状态,如果是唤醒休眠的事件队列 if (p == null || when == 0 || when < p.when) { //m该sg为头部 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; //寻找msg执行后的下一个执行消息 for (;;) { prev = p; p = p.next; //若到消息队列结束或下一个将要执行的消息大于msg的延时时间, if (p == null || when < p.when) { break; } //如果是异步,则不用唤醒 if (needWake && p.isAsynchronous()) { needWake = false; } } //下一个将要执行的message对象 msg.next = p; prev.next = msg; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { //调用本地方法唤醒主线程 nativeWake(mPtr); } } return true; }
从源码分析来看,发送消息事,传送的Runnable对象会被封装成Message对象,插入到消息队列。
处理消息源码分析:
//分发消息 public void dispatchMessage(Message msg) { if (msg.callback != null) { //执行Runnable任务 handleCallback(msg); } else { //new Callback任务 if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } //执行我们重写的handlerMessage方法 handleMessage(msg); } } //执行Runnable的run方法 private static void handleCallback(Message message) { message.callback.run(); } //回调接口 public interface Callback { public boolean handleMessage(Message msg); }
从源码分析来看,Handler处理消息过程如图:
MessageQueue
构造函数源码分析:
//调用本地方法初始化MessageQueue(boolean quitAllowed) { //是否运行线程退出Looper循环,主线程默认不可以退出Looper循环 mQuitAllowed = quitAllowed; mPtr = nativeInit(); }//本地方法实现static jint android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { //创建本地NativeMessageQueue对象 NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue(); if (!nativeMessageQueue) { jniThrowRuntimeException(env, "Unable to allocate native queue"); return 0; } //增加引用计数 nativeMessageQueue->incStrong(env); return reinterpret_cast<jint>(nativeMessageQueue);}
本地的NativeMessageQueue构造方法源码分析:
NativeMessageQueue::NativeMessageQueue() : mInCallback(false), mExceptionObj(NULL) { //获取本地方法的Looper对象 mLooper = Looper::getForThread(); if (mLooper == NULL) { //没有创建本地Looper对象,并epoll中监听回调不允许为空 mLooper = new Looper(false); Looper::setForThread(mLooper); }}
本地Looper的构造方法源码分析:
Looper::Looper(bool allowNonCallbacks) : mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) { //存储管道读写端的句柄 int wakeFds[2]; int result = pipe(wakeFds); LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe. errno=%d", errno); mWakeReadPipeFd = wakeFds[0]; mWakeWritePipeFd = wakeFds[1]; //创建管道读端的句柄 result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK); LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking. errno=%d", errno); //创建管道写端的句柄 result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK); LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking. errno=%d", errno); mIdling = false; // 创建epoll句柄,并传入能监听文件句柄的最大数量. mEpollFd = epoll_create(EPOLL_SIZE_HINT); LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance. errno=%d", errno); struct epoll_event eventItem; memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union eventItem.events = EPOLLIN; eventItem.data.fd = mWakeReadPipeFd; //注册管道监听,一旦管道有内容可读,就唤醒正在等待管道中的线程 result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem); LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance. errno=%d", errno);}
从源码分析可以知道,其进程通信采用管道机制和linux的epoll机制,对于管道机制来说,一般的使用方式就是,一个线程通过读文件描述符中来读管道的内容,当管道没有内容时,这个线程就会进入等待状态,而另外一个线程通过写文件描述符来向管道中写入内容,写入内容的时候,如果另一端正有线程正在等待管道中的内容,那么这个线程就会被唤醒。至于如何唤醒是借用通过linux的epoll机制。linux的epoll机制可以参考下面文章了解:
http://blog.csdn.net/zhaozhanyong/article/details/5410887
从上面源勉分析来看,java层的MessageQueue,Looper和native层的MessageQueue,Looper一一对应,其关系如图:
Looper
Looper的创建源码分析:
//普通线程调用其创建Looper对象,其Looper循环可以退出public static void prepare() { prepare(true); } //UI线程调用其创建Looper对象,UI线程默认调用,其Looper循环不可以退出 public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); } } //创建Looper对象,quitAllowed参数控制Looper循环是否可以退出 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构造方法private Looper(boolean quitAllowed) { //创建消息队列 mQueue = new MessageQueue(quitAllowed); //绑定本线程 mThread = Thread.currentThread(); }
从源码看出,Looper对象会通过内部构造方法绑定一个消息队列,而普通线程是通过Looper.prepare()
方法创建Looper和MessageQueue对象,且默认Looper循环可以退出,而主线程是通过Looper.prepareMainLooper()
创建对象Looper和MessageQueue对象,且默认Looper循环不可以退出。
循环消息源码分析:
//循环取出消息,直到调用quit()方法退出 public static void loop() { final Looper me = myLooper(); //检查是否有Looper对象 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 //当调用了quit方法 if (msg == null) { //退出循环 return; } // This must be in a local variable, in case a UI event sets the logger //Log日志对象 Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } //进行消息处理 msg.target.dispatchMessage(msg); 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(); } }
从源码分析可以看出,Looper实际通过MessageQueue.next()
方法取出消息。
MessageQueue.next()取出消息的方法源码分析:
Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. //验证epoll句柄是否创建成功 final long ptr = mPtr; if (ptr == 0) { return null; } //运行空闲Handler的数量 int pendingIdleHandlerCount = -1; // -1 only during first iteration //轮询超时时间设置 int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { //释放那些在处理过程的部分不再需要的引用对象 Binder.flushPendingCommands(); } //本地方法的轮询 nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; // 如果当前消息非空,但是当前消息的处理handle是空;则获取第一个异步消息 if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } //当当前消息非空,且有对应处理handle或当前消息的是异步消息 if (msg != null) { if (now < msg.when) { // 如果当前消息还不到执行,就设置一个和现在时间差的等待执行时间 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } //如果该消息到了执行时间 else { // Got a message. mBlocked = false; //当当前执行消息为取出的第一个异步消息,删除异步消息,连接异步消息下一个消息 if (prevMsg != null) { prevMsg.next = msg.next; } //当当前执行消息是有对应的handle消息时,连接一下个消息 else { mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, "Returning message: " + msg); //标记该消息对象被使用 msg.markInUse(); return msg; } } else { // 没有消息时 nextPollTimeoutMillis = -1; } // Process the quit message now that all pending messages have been handled. if (mQuitting) { dispose(); return null; } // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the queue (possibly a barrier) is due to be handled in the future. if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { //获取正在运行的空闲Handler数目 pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { //没有空闲的Handler正在运行,那么Looper会阻塞等待 mBlocked = true; continue; } //第一次调用next()方法时候会创建四个空闲的Handler对象实例 if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } //只在第一次调用该next()方法时,才会执行到下面代码去运行空闲的Handlers for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler //是否保持该空闲Handler运行的标记 boolean keep = false; try { keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } //停止该空闲Handler的运行 if (!keep) { synchronized (this) { mIdleHandlers.remove(idler); } } } // 设置空闲Handler数量为0,防止再次创建和运行空闲Handler pendingIdleHandlerCount = 0; //通过运行空闲的Handler去等待新的Message到来,而不是通过阻塞状态 nextPollTimeoutMillis = 0; } }
本地方法的pollOnce源码分析:
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jclass clazz, jint ptr, jint timeoutMillis) { NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr); //调用本地MessageQeueu的pollOnce方法 nativeMessageQueue->pollOnce(env, timeoutMillis);}//本地轮询方法的实现void NativeMessageQueue::pollOnce(JNIEnv* env, int timeoutMillis) { mInCallback = true; //调用本地Looper的pollOnce方法 mLooper->pollOnce(timeoutMillis); mInCallback = false; if (mExceptionObj) { env->Throw(mExceptionObj); env->DeleteLocalRef(mExceptionObj); mExceptionObj = NULL; }}//本地Looper的轮询方法实现,获取下一个执行的消息,以唤醒线程int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { int result = 0; //这里很多地方看不太懂,就直接看pollInner(timeoutMillis)方法 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); }}
本地Looper的pollInner()源码分析:
int Looper::pollInner(int timeoutMillis) {#if DEBUG_POLL_AND_WAKE ALOGD("%p ~ pollOnce - waiting: timeoutMillis=%d", this, timeoutMillis);#endif // Adjust the timeout based on when the next message is due. if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) { nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC); int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime); if (messageTimeoutMillis >= 0 && (timeoutMillis <0 || messageTimeoutMillis < timeoutMillis)) { timeoutMillis = messageTimeoutMillis; }#if DEBUG_POLL_AND_WAKE ALOGD("%p ~ pollOnce - next message in %lldns, adjusted timeout: timeoutMillis=%d", this, mNextMessageUptime - now, timeoutMillis);#endif } // Poll. int result = ALOOPER_POLL_WAKE; mResponses.clear(); mResponseIndex = 0; struct epoll_event eventItems[EPOLL_MAX_EVENTS]; //等待消息队列的事件发生,就是被监听的消息管道,一旦管道有可读内容,线程就会被唤醒 int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); // Acquire lock. mLock.lock(); //轮询监听出错,则直接跳到Done处. if (eventCount <0) { if (errno == EINTR) { goto Done; } ALOGW("Poll failed with an unexpected error, errno=%d", errno); result = ALOOPER_POLL_ERROR; goto Done; } // 超时,就是在一定超时设置时间范围内,还没有产生可读内容事件,则直接跳到Done处。 if (eventCount == 0) {#if DEBUG_POLL_AND_WAKE ALOGD("%p ~ pollOnce - timeout", this);#endif result = ALOOPER_POLL_TIMEOUT; goto Done; } // Handle all events.#if DEBUG_POLL_AND_WAKE ALOGD("%p ~ pollOnce - handling events from %d fds", this, eventCount);#endif // 如果有可读内容事件发生,则逐个取出消息,如果是是写事件(EPOLLIN),则调用awoken()唤醒线程 for (int i = 0; i <eventCount; i++) { int fd = eventItems[i].data.fd; uint32_t epollEvents = eventItems[i].events; if (fd == mWakeReadPipeFd) { if (epollEvents & EPOLLIN) { awoken(); } else { ALOGW("Ignoring unexpected epoll events 0x%x on wake read pipe.", epollEvents); } } else { ssize_t requestIndex = mRequests.indexOfKey(fd); if (requestIndex >= 0) { int events = 0; if (epollEvents & EPOLLIN) events |= ALOOPER_EVENT_INPUT; if (epollEvents & EPOLLOUT) events |= ALOOPER_EVENT_OUTPUT; if (epollEvents & EPOLLERR) events |= ALOOPER_EVENT_ERROR; if (epollEvents & EPOLLHUP) events |= ALOOPER_EVENT_HANGUP; pushResponse(events, mRequests.valueAt(requestIndex)); } else { ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is " "no longer registered.", epollEvents, fd); } } }Done: ; // Invoke pending message callbacks. mNextMessageUptime = LLONG_MAX; while (mMessageEnvelopes.size() != 0) { nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC); const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0); if (messageEnvelope.uptime <= now) { // Remove the envelope from the list. // We keep a strong reference to the handler until the call to handleMessage // finishes. Then we drop it so that the handler can be deleted *before* // we reacquire our lock. { // obtain handler sp<MessageHandler> handler = messageEnvelope.handler; Message message = messageEnvelope.message; mMessageEnvelopes.removeAt(0); mSendingMessage = true; mLock.unlock();#if DEBUG_POLL_AND_WAKE || DEBUG_CALLBACKS ALOGD("%p ~ pollOnce - sending message: handler=%p, what=%d", this, handler.get(), message.what);#endif handler->handleMessage(message); } // release handler mLock.lock(); mSendingMessage = false; result = ALOOPER_POLL_CALLBACK; } else { // The last message left at the head of the queue determines the next wakeup time. mNextMessageUptime = messageEnvelope.uptime; break; } } // Release lock. mLock.unlock(); // Invoke all response callbacks. for (size_t i = 0; i <mResponses.size(); i++) { Response& response = mResponses.editItemAt(i); if (response.request.ident == ALOOPER_POLL_CALLBACK) { int fd = response.request.fd; int events = response.events; void* data = response.request.data;#if DEBUG_POLL_AND_WAKE || DEBUG_CALLBACKS ALOGD("%p ~ pollOnce - invoking fd event callback %p: fd=%d, events=0x%x, data=%p", this, response.request.callback.get(), fd, events, data);#endif int callbackResult = response.request.callback->handleEvent(fd, events, data); if (callbackResult == 0) { removeFd(fd); } // Clear the callback reference in the response structure promptly because we // will not clear the response vector itself until the next poll. response.request.callback.clear(); result = ALOOPER_POLL_CALLBACK; } } return result;}//读取管道内容,来唤醒线程void Looper::awoken() {#if DEBUG_POLL_AND_WAKE ALOGD("%p ~ awoken", this);#endif char buffer[16]; ssize_t nRead; do { //唤醒线程 nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer)); } while ((nRead == -1 && errno == EINTR) || nRead == sizeof(buffer));}
从源码分析来看,pollInner()就是先通过epoll_wait()进入空闲等待状态,等待消息队列的管道上的消息(IO事件),如果有消息待处理(即管道上有IO写事件发生,写事件是EPOLLIN类型),则调用awoken()(通过往管道中读数据来使处于空闲等待状态的主线程继续运行)将消息读取出来.
总结
总过上面java层到native层的分析,可以很清楚知道Handler,MessageQueue,Looper在java层和native层之间的关系和协作,其整个过程如图:
创建实例过程:
取出消息过程:
参考资料:
http://blog.nikitaog.me/2014/10/11/android-looper-handler-handlerthread-i/
http://blog.nikitaog.me/2014/10/18/android-looper-handler-handlerthread-ii/
http://wangkuiwu.github.io/2014/08/26/MessageQueue/
http://www.programering.com/a/MjM2QDMwATc.html
- Handler, MessageQueue, Runnable与Looper
- Handler,MessageQueue,Runnable与Looper
- Handler,MessageQueue,Runnable,Looper
- Handler,MessageQueue,Runnable,Message与Looper
- Android多媒体系统分析-Handler, MessageQueue, Runnable与Looper概念
- Handler、MessageQueue、Runnable与Looper的源码浅析
- Message MessageQueue Runnable Looper 和Handler的关系理解
- AsyncTask的坑,Handler,Looper与MessageQueue
- AsyncTask的坑,Handler,Looper与MessageQueue
- AsyncTask的坑,Handler,Looper与MessageQueue
- AsyncTask的坑,Handler,Looper与MessageQueue
- AsyncTask的坑,Handler,Looper与MessageQueue
- AsyncTask的坑,Handler,Looper与MessageQueue
- AsyncTask的坑,Handler,Looper与MessageQueue
- Handler与looper、MessageQueue的关系
- Handler、Looper与MessageQueue源码解析
- Android Handler、Message、MessageQueue与Looper介绍
- Handler与Looper,MessageQueue的关系
- 组合问题
- 华为机试day1
- HttpClient和HttpURLConnection获取服务器返回的内容
- Android 开发百度地图之一基础地图
- python程序的优化经验
- Handler,MessageQueue,Runnable与Looper
- 山东省第五届蓝桥杯 ///标题:切面条//c/c++组
- 检测一下你对Swift的掌握(swift问答)
- 音频变调算法小结
- 基于启发式的分割算法
- 山东省第五届蓝桥杯 ///标题:啤酒和饮料//c/c++组
- Multiple webcams on ZoneMinder
- 为bootstrap设计的漂亮图标
- LaTeX的格式模板一-文章框架