Android消息处理机制——Looper、Handler、Message 源码分析

原文地址:http://blog.csdn.net/wzy_1988/article/details/38346637


前言

    虽然一直在做应用层开发,但是我们组是核心系统BSP,了解底层了解Android的运行机制还是很有必要的。就应用程序而言,Android系统中的Java应用程序和其他系统上相同,都是靠消息驱动来工作的,它们大致的工作原理如下:
  1. 有一个消息队列,可以往这个消息队列中投递消息。
  2. 有一个消息循环,不断从消息队列中取出消息,然后处理 。
    为了更深入的理解Android的消息处理机制,这几天空闲时间,我结合《深入理解Android系统》看了Handler、Looper、Message这几个类的源码,这里分享一下学习心得。

Looper类分析

    在分析之前,我先把Looper类的源码show出来,非常精简的代码,源码如下(frameworks/base/core/java/android/os/Looper.java):
  1. public final class Looper {  
  2.     private static final String TAG = "Looper";  
  3.   
  4.     // sThreadLocal.get() will return null unless you've called prepare().  
  5.     static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();  
  6.     private static Looper sMainLooper;  // guarded by Looper.class  
  7.   
  8.     final MessageQueue mQueue;  
  9.     final Thread mThread;  
  10.   
  11.     private Printer mLogging;  
  12.   
  13.      /** Initialize the current thread as a looper. 
  14.       * This gives you a chance to create handlers that then reference 
  15.       * this looper, before actually starting the loop. Be sure to call 
  16.       * {@link #loop()} after calling this method, and end it by calling 
  17.       * {@link #quit()}. 
  18.       */  
  19.     public static void prepare() {  
  20.         prepare(true);  
  21.     }  
  22.   
  23.     private static void prepare(boolean quitAllowed) {  
  24.         if (sThreadLocal.get() != null) {  
  25.             throw new RuntimeException("Only one Looper may be created per thread");  
  26.         }  
  27.         sThreadLocal.set(new Looper(quitAllowed));  
  28.     }  
  29.   
  30.     /** 
  31.      * Initialize the current thread as a looper, marking it as an 
  32.      * application's main looper. The main looper for your application 
  33.      * is created by the Android environment, so you should never need 
  34.      * to call this function yourself.  See also: {@link #prepare()} 
  35.      */  
  36.     public static void prepareMainLooper() {  
  37.         prepare(false);  
  38.         synchronized (Looper.class) {  
  39.             if (sMainLooper != null) {  
  40.                 throw new IllegalStateException("The main Looper has already been prepared.");  
  41.             }  
  42.             sMainLooper = myLooper();  
  43.         }  
  44.     }  
  45.   
  46.     /** Returns the application's main looper, which lives in the main thread of the application. 
  47.      */  
  48.     public static Looper getMainLooper() {  
  49.         synchronized (Looper.class) {  
  50.             return sMainLooper;  
  51.         }  
  52.     }  
  53.   
  54.     /** 
  55.      * Run the message queue in this thread. Be sure to call 
  56.      * {@link #quit()} to end the loop. 
  57.      */  
  58.     public static void loop() {  
  59.         final Looper me = myLooper();  
  60.         if (me == null) {  
  61.             throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  
  62.         }  
  63.         final MessageQueue queue = me.mQueue;  
  64.   
  65.         // Make sure the identity of this thread is that of the local process,  
  66.         // and keep track of what that identity token actually is.  
  67.         Binder.clearCallingIdentity();  
  68.         final long ident = Binder.clearCallingIdentity();  
  69.   
  70.         for (;;) {  
  71.             Message msg = queue.next(); // might block  
  72.             if (msg == null) {  
  73.                 // No message indicates that the message queue is quitting.  
  74.                 return;  
  75.             }  
  76.   
  77.             // This must be in a local variable, in case a UI event sets the logger  
  78.             Printer logging = me.mLogging;  
  79.             if (logging != null) {  
  80.                 logging.println(">>>>> Dispatching to " + msg.target + " " +  
  81.                         msg.callback + ": " + msg.what);  
  82.             }  
  83.   
  84.             msg.target.dispatchMessage(msg);  
  85.   
  86.             if (logging != null) {  
  87.                 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);  
  88.             }  
  89.   
  90.             // Make sure that during the course of dispatching the  
  91.             // identity of the thread wasn't corrupted.  
  92.             final long newIdent = Binder.clearCallingIdentity();  
  93.             if (ident != newIdent) {  
  94.                 Log.wtf(TAG, "Thread identity changed from 0x"  
  95.                         + Long.toHexString(ident) + " to 0x"  
  96.                         + Long.toHexString(newIdent) + " while dispatching to "  
  97.                         + msg.target.getClass().getName() + " "  
  98.                         + msg.callback + " what=" + msg.what);  
  99.             }  
  100.   
  101.             msg.recycle();  
  102.         }  
  103.     }  
  104.   
  105.     /** 
  106.      * Return the Looper object associated with the current thread.  Returns 
  107.      * null if the calling thread is not associated with a Looper. 
  108.      */  
  109.     public static Looper myLooper() {  
  110.         return sThreadLocal.get();  
  111.     }  
  112.   
  113.     public void setMessageLogging(Printer printer) {  
  114.         mLogging = printer;  
  115.     }  
  116.       
  117.     /** 
  118.      * Return the {@link MessageQueue} object associated with the current 
  119.      * thread.  This must be called from a thread running a Looper, or a 
  120.      * NullPointerException will be thrown. 
  121.      */  
  122.     public static MessageQueue myQueue() {  
  123.         return myLooper().mQueue;  
  124.     }  
  125.   
  126.     private Looper(boolean quitAllowed) {  
  127.         mQueue = new MessageQueue(quitAllowed);  
  128.         mThread = Thread.currentThread();  
  129.     }  
  130.   
  131.     /** 
  132.      * Returns true if the current thread is this looper's thread. 
  133.      * @hide 
  134.      */  
  135.     public boolean isCurrentThread() {  
  136.         return Thread.currentThread() == mThread;  
  137.     }  
  138.   
  139.     public void quit() {  
  140.         mQueue.quit(false);  
  141.     }  
  142.   
  143.     public void quitSafely() {  
  144.         mQueue.quit(true);  
  145.     }  
  146.   
  147.     public int postSyncBarrier() {  
  148.         return mQueue.enqueueSyncBarrier(SystemClock.uptimeMillis());  
  149.     }  
  150.   
  151.     public void removeSyncBarrier(int token) {  
  152.         mQueue.removeSyncBarrier(token);  
  153.     }  
  154.   
  155.     /** 
  156.      * Return the Thread associated with this Looper. 
  157.      */  
  158.     public Thread getThread() {  
  159.         return mThread;  
  160.     }  
  161.   
  162.     /** @hide */  
  163.     public MessageQueue getQueue() {  
  164.         return mQueue;  
  165.     }  
  166.   
  167.     /** 
  168.      * Return whether this looper's thread is currently idle, waiting for new work 
  169.      * to do.  This is intrinsically racy, since its state can change before you get 
  170.      * the result back. 
  171.      * @hide 
  172.      */  
  173.     public boolean isIdling() {  
  174.         return mQueue.isIdling();  
  175.     }  
  176.   
  177.     public void dump(Printer pw, String prefix) {  
  178.         pw.println(prefix + toString());  
  179.         mQueue.dump(pw, prefix + "  ");  
  180.     }  
  181.   
  182.     public String toString() {  
  183.         return "Looper (" + mThread.getName() + ", tid " + mThread.getId()  
  184.                 + ") {" + Integer.toHexString(System.identityHashCode(this)) + "}";  
  185.     }  
  186. }  
    Looper字面意思是“循环”,它被设计用来将一个普通的Thread线程变成Looper Thread线程。所谓Looper线程就是循环工作的线程,在程序开发(尤其是GUI开发)中,我们经常会使用到一个循环执行的线程,有新任务就立刻执行,没有新任务就循环等待。使用Looper创建Looper Thread很简单,示例代码如下:
  1. package com.example.testlibrary;  
  2.   
  3. import android.os.Handler;  
  4. import android.os.Looper;  
  5.   
  6. public class LooperTheread extends Thread{  
  7.     public Handler mhHandler;  
  8.   
  9.     @Override  
  10.     public void run() {  
  11.         // 1. 调用Looper  
  12.         Looper.prepare();  
  13.           
  14.         // ... 其他处理,例如实例化handler  
  15.           
  16.         // 2. 进入消息循环  
  17.         Looper.loop();  
  18.     }  
  19.       
  20. }  
    通过1、2两步核心代码,你的线程就升级为Looper线程了。下面,我们对两个关键调用1、2进行逐一分析。

Looper.prepare()

    在调用prepare的线程中,new了一个Looper对象,并将这个Looper对象保存在这个调用线程的ThreadLocal中。而Looper对象内部封装了一个消息队列。

    我们来看一下Looper类的源码。第一个调用函数是Looper的prepare函数,它的源码如下:
  1. // 每个线程中的Looper对象其实是一个ThreadLocal,即线程本地存储(TLS)对象  
  2. static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();  
  3.   
  4. public static void prepare() {  
  5.     prepare(true);  
  6. }  
  7.   
  8. private static void prepare(boolean quitAllowed) {  
  9.     if (sThreadLocal.get() != null) {  
  10.         throw new RuntimeException("Only one Looper may be created per thread");  
  11.     }  
  12.     sThreadLocal.set(new Looper(quitAllowed));  
  13. }  
    根据上面的源码可知,prepare会在调用线程的局部变量中设置一个Looper对象,并且一个Thread只能有一个Looper对象。这个调用线程就是LooperThread的run线程。来看一下Looper对象的构造源码:
  1. private Looper(boolean quitAllowed) {  
  2.     mQueue = new MessageQueue(quitAllowed);  
  3.     mThread = Thread.currentThread();  
  4. }  
通过源码,我们可以轻松了解Looper的工作方式,其核心就是将Looper对象保存到当前线程的ThreadLocal中,并且保证该Looper对象只new一次。如果不理解ThreadLocal,可以参考我这篇文章: 正确理解ThreadLocal

Looper循环

调用了Loop方法后,Looper线程就开始真正的工作了,它不断从自己的MessageQueue中取出对头的信息(也叫任务)执行,如图所示:

    其实现源码如下所示(这里我做了一些修整,去掉不影响主线的代码):
  1. /** 
  2.  * Run the message queue in this thread. Be sure to call 
  3.  * {@link #quit()} to end the loop. 
  4.  */  
  5. public static void loop() {  
  6.     final Looper me = myLooper();  
  7.     if (me == null) {  
  8.         throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  
  9.     }  
  10.     // 取出这个Looper的消息队列  
  11.     final MessageQueue queue = me.mQueue;  
  12.   
  13.     for (;;) {  
  14.         Message msg = queue.next(); // might block  
  15.         if (msg == null) {  
  16.             // No message indicates that the message queue is quitting.  
  17.             return;  
  18.         }  
  19.   
  20.         // 处理消息,Message对象中有一个target,它是Handler类型  
  21.         msg.target.dispatchMessage(msg);  
  22.         msg.recycle();  
  23.     }  
  24. }  
  25.   
  26. /** 
  27.  * Return the Looper object associated with the current thread.  Returns 
  28.  * null if the calling thread is not associated with a Looper. 
  29.  */  
  30. public static Looper myLooper() {  
  31.     return sThreadLocal.get();  
  32. }  
    通过上面的分析会发现,Looper的作用是:
  1. 封装了一个消息队列。
  2. Looper的prepare函数把这个Looper和调用prepare的线程(也就是最终处理的线程)绑定在一起,通过ThreadLocal机制实现的。
  3. 处理线程调用loop函数,处理来自该消息队列的消息。
    如何往MessageQueue里添加消息,是由Handler实现的,下面来分析一下Handler。

Handler分析

    什么是handler?handler扮演了往MessageQueue里添加消息和处理消息的角色(只处理由自己发出的消息),即通过MessageQueue它要执行一个任务(sendMessage),并在loop到自己的时候执行该任务(handleMessage),整个过程是异步的。

初识Handler

    Handler中的所包括的成员变量:
  1. final MessageQueue mQueue;  // Handler中也有一个消息队列  
  2. final Looper mLooper;   // 也有一个Looper  
  3. final Callback mCallback;   // 有一个回调类  
    这几个成员变量的使用,需要分析Handler的构造函数。Handler有N多构造函数,但是我们只分析最简单的情况,在当前线程中直接new一个Handler(Handler handler = new Handler())。我们看一下构造函数是如何完成初始化操作的(frameworks/base/core/java/android/os/Handler.java):
  1. public Handler() {  
  2.     this(nullfalse);  
  3. }  
  4.   
  5. /** 
  6.  * Use the {@link Looper} for the current thread with the specified callback interface 
  7.  * and set whether the handler should be asynchronous. 
  8.  * 
  9.  * Handlers are synchronous by default unless this constructor is used to make 
  10.  * one that is strictly asynchronous. 
  11.  * 
  12.  * Asynchronous messages represent interrupts or events that do not require global ordering 
  13.  * with represent to synchronous messages.  Asynchronous messages are not subject to 
  14.  * the synchronization barriers introduced by {@link MessageQueue#enqueueSyncBarrier(long)}. 
  15.  * 
  16.  * @param callback The callback interface in which to handle messages, or null. 
  17.  * @param async If true, the handler calls {@link Message#setAsynchronous(boolean)} for 
  18.  * each {@link Message} that is sent to it or {@link Runnable} that is posted to it. 
  19.  * 
  20.  * @hide 
  21.  */  
  22. public Handler(Callback callback, boolean async) {  
  23.     if (FIND_POTENTIAL_LEAKS) {  
  24.         final Class<? extends Handler> klass = getClass();  
  25.         if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&  
  26.                 (klass.getModifiers() & Modifier.STATIC) == 0) {  
  27.             Log.w(TAG, "The following Handler class should be static or leaks might occur: " +  
  28.                 klass.getCanonicalName());  
  29.         }  
  30.     }  
  31.   
  32.     mLooper = Looper.myLooper();  
  33.     if (mLooper == null) {  
  34.         throw new RuntimeException(  
  35.             "Can't create handler inside thread that has not called Looper.prepare()");  
  36.     }  
  37.     mQueue = mLooper.mQueue;  
  38.     mCallback = callback;  
  39.     mAsynchronous = async;  
  40. }  
    通过上面的构造函数,我们可以发现,当前Handler中的mLooper是从Looper.myLooper()函数获取来的,而这个函数的定义我再复制一下,如下所示:
  1. /** 
  2.  * Return the Looper object associated with the current thread.  Returns 
  3.  * null if the calling thread is not associated with a Looper. 
  4.  */  
  5. public static Looper myLooper() {  
  6.     return sThreadLocal.get();  
  7. }  
    google源码的注释也是很清楚的。可以看到,Handler中的Looper对象是Handler对象所属线程的Looper对象。如果Handler是在UI线程中实例化的,那Looper对象就是UI线程的对象。如果Handler是在子线程中实例化的,那Looper对象就是子线程的Looper对象(基于ThreadLocal机制实现)。

Handler真面目

    由上面分析可知,Handler中的消息队列实际上就是Handler所属线程的Looper对象的消息队列,我们可以为之前的LooperThread类增加Handler,代码如下:
  1. public class LooperThread extends Thread{  
  2.     public Handler mhHandler;  
  3.   
  4.     @Override  
  5.     public void run() {  
  6.         // 1. 调用Looper  
  7.         Looper.prepare();  
  8.           
  9.         // ... 其他处理,例如实例化handler  
  10.         Handler handler = new Handler();  
  11.           
  12.         // 2. 进入消息循环  
  13.         Looper.loop();  
  14.     }  
  15.       
  16. }  
    加入Handler的效果图如下所示:
    问一个问题,假设没有Handler,我们该如何往Looper的MessageQueue里插入消息呢?这里我说一个原始的思路:
  1. 调用Looper的myQueue,它将返回消息队列对象MessageQueue。
  2. 构造一个Message,填充它的成员,尤其是target对象。
  3. 调用MessageQueue的enqueueMessage,将消息插入到消息队列中。
    上面的方法虽然能工作,但是非常原始,有了Handler以后,它像一个辅助类,提供了一系列API调用,帮我们简化编程工作。常用API如下:
  1. post(Runnable)
  2. postAtTime(Runnable, long)
  3. postDelayed(Runnable, long)
  4. sendEmptyMessage(int)
  5. sendMessage(Message)
  6. sendMessageAtTime(Message, long)
  7. sendMessageDelayed(Message, long)
    光看以上的API,你会认为handler可能会发送两种信息,一种是Runnable对象,一种是Message对象,这是主观的理解,但是从源码中我们可以看到,post发出的Runnable对象最后都被封装成了Message对象,源码如下:
  1. public final boolean post(Runnable r)  
  2. {  
  3.    return  sendMessageDelayed(getPostMessage(r), 0);  
  4. }  
  5.   
  6. private static Message getPostMessage(Runnable r) {  
  7.     Message m = Message.obtain();   // 得到空的message  
  8.     m.callback = r; // 将runnable设置为message的callback  
  9.     return m;  
  10. }  
  11.   
  12. public final boolean sendMessageDelayed(Message msg, long delayMillis)  
  13. {  
  14.     if (delayMillis < 0) {  
  15.         delayMillis = 0;  
  16.     }  
  17.     return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);  
  18. }  
    最终发送消息都会调用sendMessageAtTime函数,我们看一下它的源码实现:
  1. public boolean sendMessageAtTime(Message msg, long uptimeMillis) {  
  2.     MessageQueue queue = mQueue;  
  3.     if (queue == null) {  
  4.         RuntimeException e = new RuntimeException(  
  5.                 this + " sendMessageAtTime() called with no mQueue");  
  6.         Log.w("Looper", e.getMessage(), e);  
  7.         return false;  
  8.     }  
  9.     return enqueueMessage(queue, msg, uptimeMillis);  
  10. }  
  11.   
  12. private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {  
  13.     msg.target = this;  // 将Message的target设置为当前的Handler,然后将消息自己加到消息队列中  
  14.     if (mAsynchronous) {  
  15.         msg.setAsynchronous(true);  
  16.     }  
  17.     return queue.enqueueMessage(msg, uptimeMillis);  
  18. }  

Handler处理消息

    讲完了消息发送,再看一下Handler是如何处理消息的。消息的处理是通过核心方法dispatchMessage(Message msg)与钩子方法handleMessage(Message msg)完成的,源码如下:
  1. /** 
  2.  * Handle system messages here. 
  3.  */  
  4. public void dispatchMessage(Message msg) {  
  5.     if (msg.callback != null) {  
  6.         handleCallback(msg);  
  7.     } else {  
  8.         if (mCallback != null) {  
  9.             if (mCallback.handleMessage(msg)) {  
  10.                 return;  
  11.             }  
  12.         }  
  13.         handleMessage(msg);  
  14.     }  
  15. }  
  16.   
  17. private static void handleCallback(Message message) {  
  18.     message.callback.run();  
  19. }  
  20.   
  21. /** 
  22.  * Subclasses must implement this to receive messages. 
  23.  */  
  24. public void handleMessage(Message msg) {  
  25. }  
    dispatchMessage定义了一套消息处理的优先级机制,它们分别是:
  1. 如果Message自带了callback处理,则交给callback处理。例如上文分析的,Handler里通过post(Runnable r)发生一个Runnable对象,则msg的callback对象就被赋值为Runnable对象。
  2. 如果Handler设置了全局的mCallback,则交给mCallback处理。
  3. 如果上述都没有,该消息会被交给Handler子类实现的handlerMessage(Message msg)来处理。当然,这需要从Handler派生并重写HandlerMessage函数。
    在通过情况下,我们一般都是采用第三种方法,即在子类中通过重载handlerMessage来完成处理工作。

Handler的用处

    看完了Handler的发送消息和处理消息,我们来学习一下Handler被称为异步处理大师的真正牛逼之处。Hanlder有两个重要的特点:
    1. handler可以在任意线程上发送消息,这些消息会被添加到Handler所属线程的Looper对象的消息队列里。

    2. handler是在实例化它的线程中处理消息的。

    这解决了Android经典的不能在非主线程中更新UI的问题。Android的主线程也是一个Looper线程,我们在其中创建的Handler将默认关联主线程Looper的消息队列。因此,我们可以在主线程创建Handler对象,在耗时的子线程获取UI信息后,通过主线程的Handler对象引用来发生消息给主线程,通知修改UI,当然消息了还可以包含具体的UI数据。

Message

    在整个消息处理机制中,Message又叫做task,封装了任务携带的消息和处理该任务的handler。Message的源码比较简单,源码位置(frameworks/base/core/java/android/os/Message.java)这里简单说明几点注意事项:
    1. 尽管Message有public的默认构造方法,但是你应该通过Message.obtain()来从消息池中获得空消息对象,以节省资源,源码如下:
  1. /** 
  2.  * Return a new Message instance from the global pool. Allows us to 
  3.  * avoid allocating new objects in many cases. 
  4.  */  
  5. public static Message obtain() {  
  6.     synchronized (sPoolSync) {  
  7.         if (sPool != null) {  
  8.             Message m = sPool;  
  9.             sPool = m.next;  
  10.             m.next = null;  
  11.             sPoolSize--;  
  12.             return m;  
  13.         }  
  14.     }  
  15.     return new Message();  
  16. }  
  17. /** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}). 
  18. */  
  19. public Message() {  
  20. }  
    2. 如果你的Message只需要携带简单的int信息,应该优先使用Message.arg1和Message.arg2来传递信息,这比使用Bundler节省内存。
  1. /** 
  2.  * arg1 and arg2 are lower-cost alternatives to using 
  3.  * {@link #setData(Bundle) setData()} if you only need to store a 
  4.  * few integer values. 
  5.  */  
  6. public int arg1;   
  7.   
  8. /** 
  9.  * arg1 and arg2 are lower-cost alternatives to using 
  10.  * {@link #setData(Bundle) setData()} if you only need to store a 
  11.  * few integer values. 
  12.  */  
  13. public int arg2;  
  14.   
  15. /** 
  16.  * Sets a Bundle of arbitrary data values. Use arg1 and arg1 members  
  17.  * as a lower cost way to send a few simple integer values, if you can. 
  18.  * @see #getData()  
  19.  * @see #peekData() 
  20.  */  
  21. public void setData(Bundle data) {  
  22.     this.data = data;  
  23. }  
    3. 用Message.what来标识信息,以便用不同方式处理message。

示例代码

    写了一个子线程利用主线程Handler更新UI的示例代码,如下:
  1. public class MainActivity extends Activity {  
  2.     TextView mTextView;  
  3.     MyHandler mHandler = new MyHandler();  
  4.       
  5.   
  6.     @Override  
  7.     protected void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.activity_main);  
  10.           
  11.         mTextView = (TextView)findViewById(R.id.test1);  
  12.         new Thread(new UpdateTitleTask(mHandler)).start();  
  13.     }  
  14.   
  15.     private class MyHandler extends Handler {  
  16.   
  17.         @Override  
  18.         public void handleMessage(Message msg) {  
  19.             Bundle bundle = msg.getData();  
  20.             mTextView.setText(bundle.getString("title"""));  
  21.         }  
  22.           
  23.     }  
  24. }  
  1. public class UpdateTitleTask implements Runnable{  
  2.     private Handler handler;  
  3.       
  4.     public UpdateTitleTask(Handler handler) {  
  5.         this.handler = handler;  
  6.     }  
  7.       
  8.     private Message prepareMsg() {  
  9.         Message msg = Message.obtain();  
  10.         Bundle bundle = new Bundle();  
  11.         bundle.putString("title""From Update Task");;  
  12.         msg.setData(bundle);  
  13.         return msg;  
  14.     }  
  15.       
  16.     @Override  
  17.     public void run() {  
  18.         try {  
  19.             Thread.sleep(2000);  
  20.             Message msg = prepareMsg();  
  21.             handler.sendMessage(msg);  
  22.         } catch (InterruptedException e) {  
  23.               
  24.         }  
  25.     }  
  26.   
  27. }  

参考文献

1. 《深入理解Android 卷一》
2.  http://www.cnblogs.com/codingmyworld/archive/2011/09/12/2174255.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值