zoukankan      html  css  js  c++  java
  • 第十章 Android的消息机制


    Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层MessageQueue和Looper的支撑。MessageQueue是采用单链表的数据结构来存储消息列表。 Looper会以无限循环的形式去查看是否有新消息,如果有就处理消息,否则就一直等待。ThreadLocal可以在不同线程中互不干扰的存储并提供数据,通过ThreadLocal可以轻松的获取每个线程的Looper。


    10.1 Android消息机制概述

    1. Handler的主要作用是将某个任务切换到某个指定的线程中去执行。为什么Android要提供这个功能呢?这是因为Android规定访问UI只能通过主线程,如果子线程访问UI,程序会抛出异常;ViewRootImpl在checkThread方法中做了判断。
    2. 由于Android不建议在主线程进行耗时操作,否则可能会导致ANR。那我们耗时操作在子线程执行完毕后,我们需要将一些更新UI的操作切换到主线程当中去。所以系统就提供了Handler。
    3. 系统为什么不允许在子线程中去访问UI呢?
      因为Android的UI控件不是线程安全的,多线程并发访问可能会导致UI控件处于不可预期的状态,为什么不加锁?因为加锁机制会让UI访问逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。所以Android采用了高效的单线程模型来处理UI操作。
    4. Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper就会报错。Handler可以通过post方法发送一个Runnable到消息队列中,也可以通过send方法发送一个消息到消息队列中,其实post方法最终也是通过send方法来完成。
    5. MessageQueue的enqueueMessage方法最终将这个消息放到消息队列中,当Looper发现有新消息到来时,处理这个消息,最终消息中的Runnable或者Handler的handleMessage方法就会被调用,注意Looper是运行Handler所在的线程中的,这样一来业务逻辑就切换到了Handler所在的线程中去执行了。


    10.2 Android的消息机制分析

    10.2.1 ThreadLocal的工作原理
    1. ThreadLocal是一个线程内部的数据存储类,通过它可以在指定线程存储数据,数据存储后,只能在指定的线程可以获取到存储的数据,对于其他线程则无法获取到数据。一般来说,当数据是以线程作为作用域并且不同线程有不同副本的时候,就可以考虑使用ThreadLocal。对于Handler来说,它需要获取当前线程的Looper,而Looper的作用于就是线程并且不同的线程具有不同的Looper,通过ThreadLocal可以轻松实现线程中的存取。
    2. ThreadLocal的另一个使用场景是复杂逻辑下的对象传递
    3. ThreadLocal原理:不同线程访问同一个ThreadLoacl的get方法,ThreadLocal的get方法会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找对应的Value值。


    (1) ThreadLocal set方法
    1. public void set(T value) {
    2. Thread currentThread = Thread.currentThread();
    3. Values values = values(currentThread);
    4. if (values == null) {
    5. values = initializeValues(currentThread);
    6. }
    7. values.put(this, value);
    8. }
    (2) ThreadLocal get方法
    1. public T get() {
    2. // Optimized for the fast path.
    3. Thread currentThread = Thread.currentThread();
    4. Values values = values(currentThread);
    5. if (values != null) {
    6. Object[] table = values.table;
    7. int index = hash & values.mask;
    8. if (this.reference == table[index]) {
    9. return (T) table[index + 1];
    10. }
    11. } else {
    12. values = initializeValues(currentThread);
    13. }
    14. return (T) values.getAfterMiss(this);
    15. }
    获取当前线程,并从当前线程中获取对应的ThreadLocalMap,通过key获取对应的value;如果未获取到对应的ThreadLocalMap,则创建并将该对象返回。
    (3)ThreadLocal的值在table数值中的位置总是ThreadLocal的索引+1。

    10.2.2 消息队列的工作原理
    1. MessageQueue主要有两个操作,插入和读取,读取操作伴随着删除操作;MessageQueue是通过单链表的数据结构来维护消息列表的。
    2. enqueueMessage方法的作用是往消息队列插入一条消息。next方法是一个无线循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里。当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。


    10.2.4 Handler的工作原理

    1. Handler的工作主要包含消息的发送和接受过程。发送过程通过post的一系列方法和send的一系列方法来实现。Handler发送过程仅仅是向消息队列中插入了一条消息。MessageQueue的next方法就会返回这条消息给Looper,Looper拿到这条消息就开始处理,最终消息会交给Handler来处理。
    Handler处理消息
    1. public void dispatchMessage(Message msg) {
    2. if (msg.callback != null) {
    3. //Message的callback是一个Runnable,
    4. //也就是Handler的 post方法所传递的Runnable参数
    5. handleCallback(msg);
    6. } else {
    7. //如果给Handler设置了Callback的实现,
    8. //则调用Callback的handleMessage(msg)
    9. if (mCallback != null) {
    10. if (mCallback.handleMessage(msg)) {
    11. return;
    12. }
    13. }
    14. //调用Handler的handleMessage方法来处理消息,
    15. //该Handler子类需重写handlerMessage(msg)方法
    16. handleMessage(msg);
    17. }
    18. }
    19. private static void handleCallback(Message message) {
    20. message.callback.run();
    21. }
    22. public interface Callback {
    23. public boolean handleMessage(Message msg);
    24. }
    25. //默认空实现
    26. public void handleMessage(Message msg) {
    27. }

    Handler还有一个特殊的构造方法,可以指定一个特殊的Looper来构造Handler。

    1. public Handler(Looper looper) {
    2. this(looper, null, false);
    3. }
    1. Handler创建需要Looper,否则会抛出异常,默认获取当前线程的Looper。主线程也就是ActivityThread会自动创建Looper,其他线程如果需要Looper均需要手动创建。


    10.3 主线程消息循环

    Android的主线程就是ActivityThread,主线程的入口方法为main,在main方法中系统会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环。
    1. public static void main(String[] args) {
    2. ...
    3. Process.setArgV0("<pre-initialized>");
    4. Looper.prepareMainLooper();
    5. ActivityThread thread = new ActivityThread();
    6. thread.attach(false);
    7. if (sMainThreadHandler == null) {
    8. sMainThreadHandler = thread.getHandler();
    9. }
    10. AsyncTask.init();
    11. if (false) {
    12. Looper.myLooper().setMessageLogging(new
    13. LogPrinter(Log.DEBUG, "ActivityThread"));
    14. }
    15. Looper.loop();
    16. throw new RuntimeException("Main thread loop unexpectedly exited");
    17. }
    Looper.loop(),这里是一个死循环,如果主线程的Looper终止,则应用程序会抛出异常。那么问题来了,既然主线程卡在这里了,(1)那Activity为什么还能启动;(2)点击一个按钮仍然可以响应?
    问题1:startActivity的时候,会向AMS(ActivityManagerService)发一个跨进程请求(AMS运行在系统进程中),之后AMS启动对应的Activity;AMS也需要调用App中Activity的生命周期方法(不同进程不可直接调用),AMS会发送跨进程请求,然后由App的ActivityThread中的ApplicationThread会来处理,ApplicationThread会通过主线程线程的Handler将执行逻辑切换到主线程。重点来了,主线程的Handler把消息添加到了MessageQueue,Looper.loop会拿到该消息,并在主线程中执行。这就解释了为什么主线程的Looper是个死循环,而Activity还能启动,因为四大组件的生命周期都是以消息的形式通过UI线程的Handler发送,由UI线程的Looper执行的。
    问题2:和问题1原理一样,最终都是由系统发消息来处理的,都经过了Looper.loop()。





















    http://blog.csdn.net/lmj623565791/article/details/38377229
    下面是博客的内容

    很多人面试肯定都被问到过,请问Android中的Looper , Handler , Message有什么关系?本篇博客目的首先为大家从源码角度介绍3者关系,然后给出一个容易记忆的结论。

    1、 概述

    Handler 、 Looper 、Message 这三者都与Android异步消息处理线程相关的概念。那么什么叫异步消息处理线程呢?
    异步消息处理线程启动后会进入一个无限的循环体之中,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环。若消息队列为空,线程则会阻塞等待。

    说了这一堆,那么和Handler 、 Looper 、Message有啥关系?其实Looper负责的就是创建一个MessageQueue,然后进入一个无限循环体不断从该MessageQueue中读取消息,而消息的创建者就是一个或多个Handler 。

    2、 源码解析

    1、Looper

    对于Looper主要是prepare()和loop()两个方法。
    首先看prepare()方法
    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public static final void prepare() {  
    2.         if (sThreadLocal.get() != null) {  
    3.             throw new RuntimeException("Only one Looper may be created per thread");  
    4.         }  
    5.         sThreadLocal.set(new Looper(true));  
    6. }  

    sThreadLocal是一个ThreadLocal对象,可以在一个线程中存储变量。可以看到,在第5行,将一个Looper的实例放入了ThreadLocal,并且2-4行判断了sThreadLocal是否为null,否则抛出异常。这也就说明了Looper.prepare()方法不能被调用两次,同时也保证了一个线程中只有一个Looper实例~相信有些哥们一定遇到这个错误。
    下面看Looper的构造方法:
    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. private Looper(boolean quitAllowed) {  
    2.         mQueue = new MessageQueue(quitAllowed);  
    3.         mRun = true;  
    4.         mThread = Thread.currentThread();  
    5. }  
    在构造方法中,创建了一个MessageQueue(消息队列)。
    然后我们看loop()方法:
    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public static void loop() {  
    2.         final Looper me = myLooper();  
    3.         if (me == null) {  
    4.             throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");  
    5.         }  
    6.         final MessageQueue queue = me.mQueue;  
    7.   
    8.         // Make sure the identity of this thread is that of the local process,  
    9.         // and keep track of what that identity token actually is.  
    10.         Binder.clearCallingIdentity();  
    11.         final long ident = Binder.clearCallingIdentity();  
    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.             // This must be in a local variable, in case a UI event sets the logger  
    21.             Printer logging = me.mLogging;  
    22.             if (logging != null) {  
    23.                 logging.println(">>>>> Dispatching to " + msg.target + " " +  
    24.                         msg.callback + ": " + msg.what);  
    25.             }  
    26.   
    27.             msg.target.dispatchMessage(msg);  
    28.   
    29.             if (logging != null) {  
    30.                 logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);  
    31.             }  
    32.   
    33.             // Make sure that during the course of dispatching the  
    34.             // identity of the thread wasn't corrupted.  
    35.             final long newIdent = Binder.clearCallingIdentity();  
    36.             if (ident != newIdent) {  
    37.                 Log.wtf(TAG, "Thread identity changed from 0x"  
    38.                         + Long.toHexString(ident) + " to 0x"  
    39.                         + Long.toHexString(newIdent) + " while dispatching to "  
    40.                         + msg.target.getClass().getName() + " "  
    41.                         + msg.callback + " what=" + msg.what);  
    42.             }  
    43.   
    44.             msg.recycle();  
    45.         }  
    46. }  

    第2行:
    public static Looper myLooper() {
    return sThreadLocal.get();
    }
    方法直接返回了sThreadLocal存储的Looper实例,如果me为null则抛出异常,也就是说looper方法必须在prepare方法之后运行。
    第6行:拿到该looper实例中的mQueue(消息队列)
    13到45行:就进入了我们所说的无限循环。
    14行:取出一条消息,如果没有消息则阻塞。
    27行:使用调用 msg.target.dispatchMessage(msg);把消息交给msg的target的dispatchMessage方法去处理。Msg的target是什么呢?其实就是handler对象,下面会进行分析。
    44行:释放消息占据的资源。

    Looper主要作用:
    1、 与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。
    2、 loop()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage去处理。
    好了,我们的异步消息处理线程已经有了消息队列(MessageQueue),也有了在无限循环体中取出消息的哥们,现在缺的就是发送消息的对象了,于是乎:Handler登场了。

    2、Handler

    使用Handler之前,我们都是初始化一个实例,比如用于更新UI线程,我们会在声明的时候直接初始化,或者在onCreate中初始化Handler实例。所以我们首先看Handler的构造方法,看其如何与MessageQueue联系上的,它在子线程中发送的消息(一般发送消息都在非UI线程)怎么发送到MessageQueue中的。
    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public Handler() {  
    2.         this(nullfalse);  
    3. }  
    4. public Handler(Callback callback, boolean async) {  
    5.         if (FIND_POTENTIAL_LEAKS) {  
    6.             final Class<? extends Handler> klass = getClass();  
    7.             if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&  
    8.                     (klass.getModifiers() & Modifier.STATIC) == 0) {  
    9.                 Log.w(TAG, "The following Handler class should be static or leaks might occur: " +  
    10.                     klass.getCanonicalName());  
    11.             }  
    12.         }  
    13.   
    14.         mLooper = Looper.myLooper();  
    15.         if (mLooper == null) {  
    16.             throw new RuntimeException(  
    17.                 "Can't create handler inside thread that has not called Looper.prepare()");  
    18.         }  
    19.         mQueue = mLooper.mQueue;  
    20.         mCallback = callback;  
    21.         mAsynchronous = async;  
    22.     }  

    14行:通过Looper.myLooper()获取了当前线程保存的Looper实例,然后在19行又获取了这个Looper实例中保存的MessageQueue(消息队列),这样就保证了handler的实例与我们Looper实例中MessageQueue关联上了。

    然后看我们最常用的sendMessage方法

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public final boolean sendMessage(Message msg)  
    2.  {  
    3.      return sendMessageDelayed(msg, 0);  
    4.  }  

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {  
    2.      Message msg = Message.obtain();  
    3.      msg.what = what;  
    4.      return sendMessageDelayed(msg, delayMillis);  
    5.  }  

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public final boolean sendMessageDelayed(Message msg, long delayMillis)  
    2.    {  
    3.        if (delayMillis < 0) {  
    4.            delayMillis = 0;  
    5.        }  
    6.        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);  
    7.    }  

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    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.    }  

    辗转反则最后调用了sendMessageAtTime,在此方法内部有直接获取MessageQueue然后调用了enqueueMessage方法,我们再来看看此方法:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {  
    2.        msg.target = this;  
    3.        if (mAsynchronous) {  
    4.            msg.setAsynchronous(true);  
    5.        }  
    6.        return queue.enqueueMessage(msg, uptimeMillis);  
    7.    }  

    enqueueMessage中首先为meg.target赋值为this,【如果大家还记得Looper的loop方法会取出每个msg然后交给msg,target.dispatchMessage(msg)去处理消息】,也就是把当前的handler作为msg的target属性。最终会调用queue的enqueueMessage的方法,也就是说handler发出的消息,最终会保存到消息队列中去。


    现在已经很清楚了Looper会调用prepare()和loop()方法,在当前执行的线程中保存一个Looper实例,这个实例会保存一个MessageQueue对象,然后当前线程进入一个无限循环中去,不断从MessageQueue中读取Handler发来的消息。然后再回调创建这个消息的handler中的dispathMessage方法,下面我们赶快去看一看这个方法:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public void dispatchMessage(Message msg) {  
    2.         if (msg.callback != null) {  
    3.             handleCallback(msg);  
    4.         } else {  
    5.             if (mCallback != null) {  
    6.                 if (mCallback.handleMessage(msg)) {  
    7.                     return;  
    8.                 }  
    9.             }  
    10.             handleMessage(msg);  
    11.         }  
    12.     }  

    可以看到,第10行,调用了handleMessage方法,下面我们去看这个方法:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. /** 
    2.    * Subclasses must implement this to receive messages. 
    3.    */  
    4.   public void handleMessage(Message msg) {  
    5.   }  
    6.     
    可以看到这是一个空方法,为什么呢,因为消息的最终回调是由我们控制的,我们在创建handler的时候都是复写handleMessage方法,然后根据msg.what进行消息处理。

    例如:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. private Handler mHandler = new Handler()  
    2.     {  
    3.         public void handleMessage(android.os.Message msg)  
    4.         {  
    5.             switch (msg.what)  
    6.             {  
    7.             case value:  
    8.                   
    9.                 break;  
    10.   
    11.             default:  
    12.                 break;  
    13.             }  
    14.         };  
    15.     };  

    到此,这个流程已经解释完毕,让我们首先总结一下

    1、首先Looper.prepare()在本线程中保存一个Looper实例,然后该实例中保存一个MessageQueue对象;因为Looper.prepare()在一个线程中只能调用一次,所以MessageQueue在一个线程中只会存在一个。

    2、Looper.loop()会让当前线程进入一个无限循环,不端从MessageQueue的实例中读取消息,然后回调msg.target.dispatchMessage(msg)方法。

    3、Handler的构造方法,会首先得到当前线程中保存的Looper实例,进而与Looper实例中的MessageQueue想关联。

    4、Handler的sendMessage方法,会给msg的target赋值为handler自身,然后加入MessageQueue中。

    5、在构造Handler实例时,我们会重写handleMessage方法,也就是msg.target.dispatchMessage(msg)最终调用的方法。

    好了,总结完成,大家可能还会问,那么在Activity中,我们并没有显示的调用Looper.prepare()和Looper.loop()方法,为啥Handler可以成功创建呢,这是因为在Activity的启动代码中,已经在当前UI线程调用了Looper.prepare()和Looper.loop()方法。

    3、Handler post

    今天有人问我,你说Handler的post方法创建的线程和UI线程有什么关系?

    其实这个问题也是出现这篇博客的原因之一;这里需要说明,有时候为了方便,我们会直接写如下代码:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. mHandler.post(new Runnable()  
    2.         {  
    3.             @Override  
    4.             public void run()  
    5.             {  
    6.                 Log.e("TAG", Thread.currentThread().getName());  
    7.                 mTxt.setText("yoxi");  
    8.             }  
    9.         });  

    然后run方法中可以写更新UI的代码,其实这个Runnable并没有创建什么线程,而是发送了一条消息,下面看源码:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public final boolean post(Runnable r)  
    2.    {  
    3.       return  sendMessageDelayed(getPostMessage(r), 0);  
    4.    }  
    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. private static Message getPostMessage(Runnable r) {  
    2.       Message m = Message.obtain();  
    3.       m.callback = r;  
    4.       return m;  
    5.   }  

    可以看到,在getPostMessage中,得到了一个Message对象,然后将我们创建的Runable对象作为callback属性,赋值给了此message.

    注:产生一个Message对象,可以new  ,也可以使用Message.obtain()方法;两者都可以,但是更建议使用obtain方法,因为Message内部维护了一个Message池用于Message的复用,避免使用new 重新分配内存。

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public final boolean sendMessageDelayed(Message msg, long delayMillis)  
    2.    {  
    3.        if (delayMillis < 0) {  
    4.            delayMillis = 0;  
    5.        }  
    6.        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);  
    7.    }  

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    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.    }  
    最终和handler.sendMessage一样,调用了sendMessageAtTime,然后调用了enqueueMessage方法,给msg.target赋值为handler,最终加入MessagQueue.

    可以看到,这里msg的callback和target都有值,那么会执行哪个呢?

    其实上面已经贴过代码,就是dispatchMessage方法:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. public void dispatchMessage(Message msg) {  
    2.        if (msg.callback != null) {  
    3.            handleCallback(msg);  
    4.        } else {  
    5.            if (mCallback != null) {  
    6.                if (mCallback.handleMessage(msg)) {  
    7.                    return;  
    8.                }  
    9.            }  
    10.            handleMessage(msg);  
    11.        }  
    12.    }  
    第2行,如果不为null,则执行callback回调,也就是我们的Runnable对象。

    好了,关于Looper , Handler , Message 这三者关系上面已经叙述的非常清楚了。

    最后来张图解:


    希望图片可以更好的帮助大家的记忆~~

    4、后话

    其实Handler不仅可以更新UI,你完全可以在一个子线程中去创建一个Handler,然后使用这个handler实例在任何其他线程中发送消息,最终处理消息的代码都会在你创建Handler实例的线程中运行。

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. new Thread()  
    2.         {  
    3.             private Handler handler;  
    4.             public void run()  
    5.             {  
    6.   
    7.                 Looper.prepare();  
    8.                   
    9.                 handler = new Handler()  
    10.                 {  
    11.                     public void handleMessage(android.os.Message msg)  
    12.                     {  
    13.                         Log.e("TAG",Thread.currentThread().getName());  
    14.                     };  
    15.                 };<pre code_snippet_id="445431" snippet_file_name="blog_20140808_19_1943618" name="code" class="java">                               Looper.loop();                                                                                                                              }             </pre>  

    Android不仅给我们提供了异步消息处理机制让我们更好的完成UI的更新,其实也为我们提供了异步消息处理机制代码的参考~~不仅能够知道原理,最好还可以将此设计用到其他的非Android项目中去~~




















  • 相关阅读:
    修复PLSQL Developer 与 Office 2010的集成导出Excel 功能
    Using svn in CLI with Batch
    mysql 备份数据库 mysqldump
    Red Hat 5.8 CentOS 6.5 共用 输入法
    HP 4411s Install Red Hat Enterprise Linux 5.8) Wireless Driver
    变更RHEL(Red Hat Enterprise Linux 5.8)更新源使之自动更新
    RedHat 5.6 问题简记
    Weblogic 9.2和10.3 改密码 一站完成
    ExtJS Tab里放Grid高度自适应问题,官方Perfect方案。
    文件和目录之utime函数
  • 原文地址:https://www.cnblogs.com/You0/p/5984481.html
Copyright © 2011-2022 走看看