自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Android進階之Dialog對應的Context必須是Activity嗎?從源碼詳細分析

移動開發(fā) Android
創(chuàng)建Dialog的時候知道在Dialog的構造方法中需要一個上下文環(huán)境,而對這個“上下文”沒有具體的概念結果導致程序報錯,于是發(fā)現(xiàn)Dialog需要的上下文環(huán)境只能是activity。

[[419839]]

前言

創(chuàng)建Dialog的時候知道在Dialog的構造方法中需要一個上下文環(huán)境,而對這個“上下文”沒有具體的概念結果導致程序報錯,

于是發(fā)現(xiàn)Dialog需要的上下文環(huán)境只能是activity。

所以接下來這篇文章將會從源碼的角度來徹底的理順這個問題;

一、Dialog創(chuàng)建失敗

在Dialog的構造方法中傳入一個Application的上下文環(huán)境??纯闯绦蚴欠駡箦e:

  1. Dialog dialog = new Dialog(getApplication()); 
  2.      TextView textView = new TextView(this); 
  3.      textView.setText("使用Application創(chuàng)建Dialog"); 
  4.      dialog.setContentView(textView); 
  5.      dialog.show(); 

運行程序,程序不出意外的崩潰了,我們來看下報錯信息:

  1. Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application 
  2.     at android.view.ViewRootImpl.setView(ViewRootImpl.java:517) 
  3.     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:301) 
  4.     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:215) 
  5.     at android.view.WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:140) 

這段錯誤日志,有兩點我們需要注意一下

  • 程序報了一個BadTokenException異常;
  • 程序報錯是在ViewRootImpl的setView方法中;
  • 我們一定很疑惑BadTokenException到底是個啥,在說明這個之前我們首先需要了解Token,在了解了Token的概念之后,再結合ViewRootImpl的setView方法,就能理解BadTokenException這個到底是什么,怎么產(chǎn)生的;

二、Token分析

1、token詳解

Token直譯成中文是令牌的意思,android系統(tǒng)中將其作為一種安全機制,其本質是一個Binder對象,在跨進程的通行中充當驗證碼的作用。比如:在activity的啟動過程及界面繪制的過程中會涉及到ActivityManagerService,應用程序,WindowManagerService三個進程間的通信,此時Token在這3個進程中充當一個身份驗證的功能,ActivityManagerService與WindowManagerService通過應用程序的activity傳過來的Token來分辨到底是控制應用程序的哪個activity。具體來說就是:

  • 在啟動Activity的流程當中,首先,ActivityManagerService會創(chuàng)建ActivityRecord由其本身來管理,同時會為這個ActivityRecord創(chuàng)建一個IApplication(本質上就是一個Binder)。
  • ActivityManagerService將這個binder對象傳遞給WindowManagerService,讓WindowManagerService記錄下這個Binder。
  • 當ActivityManagerService這邊完成數(shù)據(jù)結構的添加之后,會返回給ActivityThread一個ActivityClientRecord數(shù)據(jù)結構,中間就包含了Token這個Binder對象。
  • ActivityThread這邊拿到這個Token的Binder對象之后,就需要讓WindowManagerService去在界面上添加一個對應窗口,在添加窗口傳給WindowManagerService的數(shù)據(jù)中WindowManager.LayoutParams這里面就包含了Token。
  • 最終WindowManagerService在添加窗口的時候,就需要將這個Token的Binder和之前ActivityManagerService保存在里面的Binder做比較,驗證通過說明是合法的,否則,就會拋出BadTokenException這個異常。
  • 到這里,我們就知道BadTokenException是怎么回事了,然后接下來分析為什么使用Application上下文會報BadTokenException異常,而Activity上下文則不會
圖片

2、為什么非要一個Token

因為在WMS那邊需要根據(jù)這個Token來確定Window的位置(不是說坐標),如果沒有Token的話,就不知道這個窗口應該放到哪個容器上了;

因為非Activity的Context它的WindowManger沒有ParentWindow,導致在WMS那邊找不到對應的容器,也就是不知道要把Dialog的Window放置在何處。

還有一個原因是沒有SYSTEM_ALERT_WINDOW權限(當然要加權限啦,DisplayArea.Tokens的子容器,級別比普通應用的Window高,也就是會顯示在普通應用Window的前面,如果不加權限控制的話,被濫用還得了)。

在獲得SYSTEM_ALERT_WINDOW權限并將Dialog的Window.type指定為SYSTEM_WINDOW之后能正常顯示,是因為WMS會為SYSTEM_WINDOW類型的窗口專門創(chuàng)建一個WindowToken(這下就有容器了),并放置在DisplayArea.Tokens里面(這下知道放在哪里了);

三、創(chuàng)建dialog流程分析

1、activity的界面最后是通過ViewRootImpl的setView方法連接WindowManagerService,從而讓WindowManagerService將界面繪制到手機屏幕上。而從上面的異常日志中其實也可以看出,Dialog的界面也是通過ViewRootImpl的setView連接WindowManagerService,從而完成界面的繪制的。

我們首先來看Dialog的構造方法。不管一個參數(shù)的構造方法。兩個參數(shù)的構造方法,最終都會調用到3個參數(shù)的構造方法:

  1. Dialog(@NonNull Context context, @StyleRes int themeResId, boolean  
  2. createContextThemeWrapper) { 
  3.         ...... 
  4.         //1.創(chuàng)建一個WindowManagerImpl對象 
  5.         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 
  6.         //2.創(chuàng)建一個PhoneWindow對象 
  7.         final Window w = new PhoneWindow(mContext); 
  8.         mWindow = w; 
  9.         //3.使dialog能夠響應用戶的事件 
  10.         w.setCallback(this); 
  11.         w.setOnWindowDismissedCallback(this); 
  12.         //4.為window對象設置WindowManager 
  13.         w.setWindowManager(mWindowManager, nullnull); 
  14.         w.setGravity(Gravity.CENTER); 
  15.         mListenersHandler = new ListenersHandler(this); 
  16.     } 

這段代碼可以看出dialog的創(chuàng)建實質上和activity界面的創(chuàng)建沒什么兩樣,都需要完成一個應用窗口Window的創(chuàng)建,和一個應用窗口視圖對象管理者WindowManagerImpl的創(chuàng)建。

然后Dialog同樣有一個setContentView方法:

  1. public void setContentView(@LayoutRes int layoutResID) { 
  2.         mWindow.setContentView(layoutResID); 
  3.     } 
  4. 依然是調用PhoneWindow的setContentView方法。再接著我們來看下dialog的show方法: 
  5. public void show() { 
  6.         ...... 
  7.         //1.得到通過setView方法封裝好的DecorView  
  8.         mDecor = mWindow.getDecorView(); 
  9.         ...... 
  10.        //2.得到創(chuàng)建PhoneWindow時已經(jīng)初始化的成員變量WindowManager.LayoutParams 
  11.         WindowManager.LayoutParams l = mWindow.getAttributes(); 
  12.         if ((l.softInputMode 
  13.                 & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) { 
  14.             WindowManager.LayoutParams nl = new WindowManager.LayoutParams(); 
  15.             nl.copyFrom(l); 
  16.             nl.softInputMode |= 
  17.                     WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; 
  18.             l = nl; 
  19.         } 
  20.         try { 
  21.             //3.通過WindowManagerImpl添加DecorView到屏幕 
  22.             mWindowManager.addView(mDecor, l); 
  23.             mShowing = true
  24.             sendShowMessage(); 
  25.         } finally { 
  26.         } 
  27.     } 

這段代碼和activity的makeVisable方法類似,這里也不多說了,注釋已經(jīng)大概的寫清楚了。然后調用WindowManagerImpl的addView方法:

  1. @Override 
  2.     public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 
  3.         applyDefaultToken(params); 
  4.         mGlobal.addView(view, params, mDisplay, mParentWindow); 
  5.     } 
  6. 接著調用了WindowManagerGlobal的addView方法: 
  7. public void addView(View view, ViewGroup.LayoutParams params, 
  8.             Display display, Window parentWindow) { 
  9.         ...... 
  10.         //1.將傳進來的ViewGroup.LayoutParams類型的params轉成  
  11. WindowManager.LayoutParams類型的wparams  
  12.         final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)  
  13. params; 
  14.        //2.如果WindowManagerImpl是在activity的方法中被創(chuàng)建則不為空 
  15.         if (parentWindow != null) { 
  16.             parentWindow.adjustLayoutParamsForSubWindow(wparams); 
  17.         } else { 
  18.         ...... 
  19.         } 
  20.         ViewRootImpl root; 
  21.         View panelParentView = null
  22.         synchronized (mLock) { 
  23.         ...... 
  24.             root = new ViewRootImpl(view.getContext(), display); 
  25.             view.setLayoutParams(wparams); 
  26.             //3.將視圖對象view,ViewRootImpl以及wparams分別存入相應集合的對應位置 
  27.             mViews.add(view); 
  28.             mRoots.add(root); 
  29.             mParams.add(wparams); 
  30.         } 
  31.         // do this last because it fires off messages to start doing things 
  32.         try { 
  33.             //4.通過ViewRootImpl聯(lián)系WindowManagerService將view繪制到屏幕上 
  34.             root.setView(view, wparams, panelParentView); 
  35.         } catch (RuntimeException e) { 
  36.             // BadTokenException or InvalidDisplayException, clean up. 
  37.             synchronized (mLock) { 
  38.                 final int index = findViewLocked(viewfalse); 
  39.                 if (index >= 0) { 
  40.                     removeViewLocked(indextrue); 
  41.                 } 
  42.             } 
  43.             throw e; 
  44.         } 
  45.     } 
  1. //2.如果WindowManagerImpl是在activity的方法中被創(chuàng)建則不為空    
  2.   if (parentWindow != null) { 
  3.            parentWindow.adjustLayoutParamsForSubWindow(wparams); 
  4.        } else { 
  5.        ...... 
  6.        } 

2、這里會首先判斷一個類型為Window的parentWindow 是否為空,如果不為空會通過Window的adjustLayoutParamsForSubWindow方法調整一個類型為WindowManager.LayoutParams的變量wparams的一些屬性值。應用程序請求WindowManagerService服務時會傳入一個Token,其實那個Token就會通過Window的adjustLayoutParamsForSubWindow方法存放在wparams的token變量中,也就是說如果沒有調用Window的adjustLayoutParamsForSubWindow方法就會導致wparams的token變量為空。然后我們接下來看一下wparams的token變量是如何賦值的:

  1. void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { 
  2.         CharSequence curTitle = wp.getTitle(); 
  3.         if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && 
  4.             wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { 
  5.         ...... 
  6.         } else { 
  7.             if (wp.token == null) { 
  8.                 wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; 
  9.             } 
  10.         ...... 
  11.         } 
  12.         if (wp.packageName == null) { 
  13.             wp.packageName = mContext.getPackageName(); 
  14.         } 
  15.         if (mHardwareAccelerated) { 
  16.             wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; 
  17.         } 

這里我們可以看到這段代碼首先會做一個判斷如果wp.type的值有沒有位于WindowManager.LayoutParams.FIRST_SUB_WINDOW與WindowManager.LayoutParams.LAST_SUB_WINDOW之間,如果沒有則會給wp.token賦值。wp.type代表窗口類型,有3種級別,分別為系統(tǒng)級,應用級以及子窗口級。而這里是判斷是否為子窗口級級別。而Dialog的WindowManager.LayoutParams.type默認是應用級的,因此會走else分支,給wp.token賦值mAppToken。至于mAppToken是什么,我們待會再來分析。

3、看WindowManagerGlobal的addView方法的,會調用ViewRootImpl的setView方法,我們來看一下,ViewRootImpl是如何連接WindowManagerService傳遞token的:

  1. public void setView(View view, WindowManager.LayoutParams attrs, View  
  2. panelParentView) { 
  3.         synchronized (this) { 
  4.             if (mView == null) { 
  5.                 mView = view
  6.                 try { 
  7.                     ...... 
  8.                     //1.通過binder對象mWindowSession調用WindowManagerService的接口請求 
  9.                     res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, 
  10.                             getHostVisibility(), mDisplay.getDisplayId(), 
  11.                             mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, 
  12.                             mAttachInfo.mOutsets, mInputChannel); 
  13.                 } catch (RemoteException e) { 
  14.                     ...... 
  15.                     throw new RuntimeException("Adding window failed", e); 
  16.                 } finally { 
  17.                     if (restore) { 
  18.                         attrs.restore(); 
  19.                     } 
  20.                 } 
  21.                     ...... 
  22.                 if (res < WindowManagerGlobal.ADD_OKAY) { 
  23.                     ...... 
  24.                     switch (res) { 
  25.                         case WindowManagerGlobal.ADD_BAD_APP_TOKEN: 
  26.                         case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN: 
  27.                             throw new WindowManager.BadTokenException( 
  28.                                     "Unable to add window -- token " + attrs.token 
  29.                                     + " is not valid; is your activity running?"); 
  30.                         //2.如果請求失?。╰oken驗證失?。﹦t拋出BadTokenException異常 
  31.                         case WindowManagerGlobal.ADD_NOT_APP_TOKEN: 
  32.                             throw new WindowManager.BadTokenException( 
  33.                                     "Unable to add window -- token " + attrs.token 
  34.                                     + " is not for an application"); 
  35.                         case WindowManagerGlobal.ADD_APP_EXITING: 
  36.                             throw new WindowManager.BadTokenException( 
  37.                                     "Unable to add window -- app for token " +  
  38. attrs.token 
  39.                                     + " is exiting"); 
  40.                         case WindowManagerGlobal.ADD_DUPLICATE_ADD: 
  41.                             throw new WindowManager.BadTokenException( 
  42.                                     "Unable to add window -- window " + mWindow 
  43.                                     + " has already been added"); 
  44.                         case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED: 
  45.                             // Silently ignore -- we would have just removed it 
  46.                             // right away, anyway. 
  47.                             return
  48.                         case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON: 
  49.                             throw new WindowManager.BadTokenException( 
  50.                                     "Unable to add window " + mWindow + 
  51.                                     " -- another window of this type already  
  52. exists"); 
  53.                         case WindowManagerGlobal.ADD_PERMISSION_DENIED: 
  54.                             throw new WindowManager.BadTokenException( 
  55.                                     "Unable to add window " + mWindow + 
  56.                                     " -- permission denied for this window type"); 
  57.                         case WindowManagerGlobal.ADD_INVALID_DISPLAY: 
  58.                             throw new WindowManager.InvalidDisplayException( 
  59.                                     "Unable to add window " + mWindow + 
  60.                                     " -- the specified display can not be found"); 
  61.                         case WindowManagerGlobal.ADD_INVALID_TYPE: 
  62.                             throw new WindowManager.InvalidDisplayException( 
  63.                                     "Unable to add window " + mWindow 
  64.                                     + " -- the specified window type is not valid"); 
  65.                     } 
  66.                     throw new RuntimeException( 
  67.                             "Unable to add window -- unknown error code " + res); 
  68.                 } 
  69.         ...... 
  70.             } 
  71.         } 
  72.     } 

這段代碼有兩處需要注意:

  • 會通過一個mWindowSession的binder對象請求WindowManagerService服務,傳遞一個類型為WindowManager.LayoutParams的變量mWindowAttributes到WindowManagerService,mWindowAttributes里面裝有代表當前activity的token對象。然后通過WindowManagerService服務創(chuàng)建屏幕視圖。
  • 會根據(jù)請求WindowManagerService服務的返回結果判斷是否請求成功,如果請求失敗會拋出異常,注釋的地方就是在文章開頭示例拋出的異常。此時attrs.token為空。如果創(chuàng)建dialog的上下文環(huán)境改為activity的為什么就不為空呢?

四、分析創(chuàng)建Dialog的上下文Activity為何與眾不同

1、上文的分析中可以看出attrs.token的賦值在Window的adjustLayoutParamsForSubWindow方法中。而Dialog默認的WindowManager.LayoutParams.type是應用級別的,因此,如果能進入這個方法內,attrs.token肯定能被賦值。現(xiàn)在只有一種情況,如果不是activity的上下文環(huán)境就沒有進入到這個方法內。這時我們再看WindowManagerGlobal的addView方法的:

  1. public void addView(View view, ViewGroup.LayoutParams params, 
  2.             Display display, Window parentWindow) { 
  3.         ...... 
  4.        //2.如果WindowManagerImpl是在activity的方法中被創(chuàng)建則不為空 
  5.         if (parentWindow != null) { 
  6.             parentWindow.adjustLayoutParamsForSubWindow(wparams); 
  7.         } else { 
  8.         ...... 
  9.         } 
  10.         ...... 
  11.     } 

從這里看出如果Window類型的parentWindow為空,就不會進入adjustLayoutParamsForSubWindow方法。從而可以得出結論如果不是activity的上下文環(huán)境WindowManagerGlobal的第四個參數(shù)parentWindow為空。緊接著我們再來分析為什么其他的上下文會導致parentWindow為空。

WindowManagerGlobal調用addView方法在WindowManagerImpl的addView方法中:

  1.  @Override 
  2.     public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 
  3.         applyDefaultToken(params); 
  4.         mGlobal.addView(view, params, mDisplay, mParentWindow); 
  5.     } 
  6. WindowManagerImpl的addView方法在Dialog的首位方法中調用: 
  7. public void show() { 
  8.         ...... 
  9.         try { 
  10.             mWindowManager.addView(mDecor, l); 
  11.             mShowing = true
  12.             sendShowMessage(); 
  13.         } finally { 
  14.         } 
  15.     } 

對比這兩個方法??梢钥闯鯳indowManagerImpl的addView方法調用WindowManagerGlobal的addView方法是多出來了兩個參數(shù)mDisplay, mParentWindow,我們只看后一個,多了一個Window類型的mParentWindow,可以一mParentWindow并不是在Dialog的show方法中賦值的。那么它在哪賦值呢?在WindowManagerImpl類中搜索mParentWindow發(fā)現(xiàn)它在WindowManagerImpl的兩個參數(shù)的構造方法中被賦值。從這里我們可以猜測,如果是使用的activity上下文,那么在創(chuàng)建WindowManagerImpl實例的時候用的是兩個參數(shù)的構造方法,而其他的上下文是用的一個參數(shù)的構造方法?,F(xiàn)在問題就集中到了WindowManagerImpl是如何被創(chuàng)建的了。

我們再回過頭來看Dialog的構造方法中WindowManagerImpl是如何創(chuàng)建的:

  1. Dialog(@NonNull Context context, @StyleRes int themeResId, boolean  
  2. createContextThemeWrapper) { 
  3.         ...... 
  4.         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 
  5.         ...... 
  6.     } 
  7. 然后分別查看activity的getSystemService方法,和Application的getSystemService方法: 
  8. activity的getSystemService方法 
  9. @Override 
  10.    public Object getSystemService(@ServiceName @NonNull String name) { 
  11.        ...... 
  12.        if (WINDOW_SERVICE.equals(name)) { 
  13.            return mWindowManager; 
  14.        } else if (SEARCH_SERVICE.equals(name)) { 
  15.            ensureSearchManager(); 
  16.            return mSearchManager; 
  17.        } 
  18.        return super.getSystemService(name); 
  19.    } 

在這個方法中直接返回了activity的mWindowManager對象,activity的mWindowManager對象在activity的attach方法中:

  1. final void attach(Context context, ActivityThread aThread, 
  2.             Instrumentation instr, IBinder token, int ident, 
  3.             Application application, Intent intent, ActivityInfo info, 
  4.             CharSequence title, Activity parent, String id, 
  5.             NonConfigurationInstances lastNonConfigurationInstances, 
  6.             Configuration config, String referrer, IVoiceInteractor voiceInteractor) { 
  7.       ...... 
  8.       mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(), 
  9.                 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); 
  10.        ......   
  11.     } 

2、我們再看Window的setWindowManager方法:

  1. public void setWindowManager(WindowManager wm, IBinder appToken, String appName, 
  2.             boolean hardwareAccelerated) { 
  3.         //1.將ActivityManagerService傳過來的Token保存到mAppToken中 
  4.         mAppToken = appToken; 
  5.         //2.創(chuàng)建WindowManagerImpl 
  6.         mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); 
  7.     } 

這段代碼兩個地方需要注意,一是前ActivityManagerService傳過來的Token賦值給Winow的mAppToken,這個token最后會保存到attr.token,具體操作在Window的adjustLayoutParamsForSubWindow方法中。二是調用WindowManagerImpl的createLocalWindowManager方法創(chuàng)建WindowManagerImpl:

  1. public WindowManagerImpl createLocalWindowManager(Window parentWindow) { 
  2.         return new WindowManagerImpl(mDisplay, parentWindow); 
  3.     } 

到這里就可以看出如果創(chuàng)建Dialog的上下文是activity,則會調用WindowManagerImpl兩個參數(shù)的構造方法,從而導致parentWindow不為空。

3、Application的getSystemService方法:

由于Application是Context的子類,所以Application的getSystemService最終會調到ContextImpl的getSystemService方法

  1. @Override 
  2.     public Object getSystemService(String name) { 
  3.         return SystemServiceRegistry.getSystemService(this, name); 
  4.     } 
  5. 直接調用了SystemServiceRegistry的getSystemService方法,這個方法又會得到匿名內部類CachedServiceFetcher<WindowManager>的createService方法的返回值。 
  6.  @Override 
  7.             public WindowManager createService(ContextImpl ctx) { 
  8.                 return new WindowManagerImpl(ctx.getDisplay()); 
  9.             }}); 

從這個方法中可以看出上下文為Application時,調用的是WindowManagerImpl的一個參數(shù)的構造方法,從而parentWindow為空;

總結

  • 創(chuàng)建dialog時,如果傳入構造方法不是一個activity類型的上下文,則導致WindowManagerImpl類型為Window的變量mParentWindow,從而導致WindowManagerGlobal的addView不會調用Window的adjustLayoutParamsForSubWindow方法,從而不會給attr.token賦值,導致在WindowManagerService服務中的身份驗證失敗,拋出BadTokenException異常;
  • Show一個普通的Dialog需要的并不是Activity本身,而是一個容器的token,我們平時會傳Activity,只不過是Activity剛好對應WMS那邊的一個WindowState的容器而已;

本文轉載自微信公眾號「Android開發(fā)編程」

 

責任編輯:姜華 來源: Android開發(fā)編程
相關推薦

2021-08-28 07:48:04

AndroidActivityRecActivitySta

2021-08-19 06:58:49

Android場景FirstActivi

2009-03-24 08:30:54

AndroidGoogle移動os

2009-03-24 09:17:58

驅動GSMAndroid

2009-06-18 14:00:51

2009-09-25 14:23:39

2009-09-28 10:39:01

Hibernate基礎

2013-12-04 10:21:38

AndroidAndroidMani

2010-02-06 15:19:35

2010-04-26 18:17:19

Oracle存儲過程

2009-12-03 17:41:40

PHP應用發(fā)展

2021-10-25 19:52:52

IntentFilte

2009-09-09 09:48:43

Linq延遲加載

2009-10-10 13:52:57

VB Update方法

2009-09-14 16:21:34

LINQ To XML

2009-09-14 13:50:35

LINQ編程模型

2009-09-08 15:56:50

Linq使用Group

2009-11-20 13:11:44

Oracle XML數(shù)

2010-01-06 13:50:37

.NET Framew

2010-01-06 16:12:52

分析Json stri
點贊
收藏

51CTO技術棧公眾號