<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                本節將按如下順序分析query函數中的關鍵點: - 首先介紹服務端的CursorToBulkCursorAdaptor及其count函數。 - 跨進程共享數據的關鍵類CursorWindow。 - 客戶端的BulkCursorToCursorAdaptor及其initialize函數,以及返回給客戶端使用的CursorWrapperInner類 1. CursorToBulkCursorAdaptor函數分析 (1) 構造函數分析 CursorToBulkCursorAdaptor構造函數的代碼如下: **CursorToBulkCursorAdaptor.java::構造函數** ~~~ public CursorToBulkCursorAdaptor(Cursor cursor,IContentObserver observer, String providerName) { //傳入的cursor變量其真實類型是SQLiteCursor,它是CrossProcessCursor if(cursor instanceof CrossProcessCursor) { mCursor = (CrossProcessCursor)cursor; } else { mCursor = new CrossProcessCursorWrapper(cursor); } mProviderName = providerName; synchronized (mLock) {//和ContentObserver有關,我們以后再作分析 createAndRegisterObserverProxyLocked(observer); } } ~~~ CursorToBulkCursorAdaptor的構造函數很簡單,此處不詳述。來看下一個重要函數,即CursorToBulkCursorAdaptor的count。該函數返回本次查詢結果集所包含的行數。 (2) count函數分析 count函數的代碼如下: **CursorToBulkCursorAdaptor.java::count** ~~~ public int count() { synchronized (mLock) { throwIfCursorIsClosed();//如果mCursor已經關閉,則拋異常 //CursorToBulkCursorAdaptor的mCursor變量的真實類型是SQLiteCursor returnmCursor.getCount(); } } ~~~ count最終將調用SQLiteCursor的getCount函數,其代碼如下: **SQLiteCursor.java::getCount** ~~~ public int getCount() { if (mCount== NO_COUNT) {//NO_COUNT為-1,首次調用時滿足if條件 fillWindow(0);//關鍵函數 } returnmCount; } ~~~ getCount函數將調用一個非常重要的函數,即fillWindow。顧名思義,讀者可以猜測到它的功能:將結果數據保存到CursorWindow的那塊共享內存中。 下面單起一節來分析和CursorWindow相關的知識點。 2. CursorWindow分析 CursorWindow的創建源于前邊代碼中對fillWindow的調用。fillWindow的代碼如下: **SQLiteCurosr.java::fillWindow** ~~~ private void fillWindow(int startPos) { //①如果CursorWinodow已經存在,則清空(clear)它,否則新創建一個 //CursorWinodow對象 clearOrCreateLocalWindow(getDatabase().getPath()); mWindow.setStartPosition(startPos); //②getQuery返回一個SQLiteQuery對象,此處將調用它的fillWindow函數 int count= getQuery().fillWindow(mWindow); if (startPos == 0) { mCount= count; } ...... } ~~~ 先來看clearOrCreateLocalWindow函數。 (1) clearOrCreateLocalWindow函數分析 **SQLiteCursor.java::clearOrCreateLocalWindow** ~~~ protected void clearOrCreateLocalWindow(Stringname) { if(mWindow == null) { mWindow = new CursorWindow(name, true);//創建一個CursorWindow對象 }else mWindow.clear();//清空CursorWindow中的信息 } ~~~ CursorWindow的構造函數的代碼如下: **CursorWindow.java::CursorWindow** ~~~ public CursorWindow(String name, booleanlocalWindow) { mStartPos= 0;//本次查詢的起始行位置,例如查詢數據庫表中第10到第100行的結果, //其起始行就是10 /* 調用nativeCreate函數,注意傳遞的參數,其中sCursorWindowSize為2MB,localWindow 為true。sCursorWindowSize是一個靜態變量,其值取自frameworks/base/core/res/res /values/config.xml中定義的config_cursorWindowSize變量,該值是2048KB,而 sCursorWindow在此基礎上擴大了1024倍,最終的結果就是2MB */ mWindowPtr= nativeCreate(name, sCursorWindowSize, localWindow); mCloseGuard.open("close"); recordNewWindow(Binder.getCallingPid(), mWindowPtr); } ~~~ nativeCreate是一個native函數,其真正實現在android_database_CursorWindow.cpp中,其代碼如下: **android_database_CursorWindow.cpp::nativeCreate** ~~~ static jint nativeCreate(JNIEnv* env, jclassclazz, jstring nameObj, jint cursorWindowSize, jboolean localOnly) { String8name; if(nameObj) { const char* nameStr = env->GetStringUTFChars(nameObj, NULL); name.setTo(nameStr); env->ReleaseStringUTFChars(nameObj, nameStr); } ...... CursorWindow* window; //創建一個Native層的CursorWindow對象 status_tstatus = CursorWindow::create(name, cursorWindowSize, localOnly,&window); ...... returnreinterpret_cast<jint>(window);//將指針轉換成jint類型 } ~~~ 不妨再看看Native CursorWindow的create函數,其代碼如下: **CursorWindow.cpp::create** ~~~ status_t CursorWindow::create(const String8&name, size_t size, bool localOnly, CursorWindow** outCursorWindow) { String8ashmemName("CursorWindow: "); ashmemName.append(name); ashmemName.append(localOnly ? " (local)" : "(remote)"); status_tresult; //創建共享內存,調用Android平臺提供的ashmem_create_region函數 intashmemFd = ashmem_create_region(ashmemName.string(), size); if(ashmemFd < 0) { result = -errno; } else { result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE); if(result >= 0) { //映射共享內存以得到一塊地址,data變量指向該地址的起始位置 void* data = ::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,ashmemFd, 0); ...... result= ashmem_set_prot_region(ashmemFd, PROT_READ); if (result >= 0) { //創建一個CursorWindow對象 CursorWindow* window = new CursorWindow(name, ashmemFd, data, size, false); result = window->clear(); if (!result) { *outCursorWindow = window; return OK;//創建成功 } }......//出錯處理 } returnresult; } ~~~ 由以上代碼可知,CursorWindow的create函數將構造一個Native的CursorWindow對象。最終,Java層的CursorWindow對象會和此Native的CursorWindow對象綁定。 * * * * * **提示**:CursorWindow創建中涉及共享內存方面的知識,讀者可上網查詢或閱讀卷I的 7.2.2節。 * * * * * 至此,用于承載數據的共享內存已創建完畢,但我們還沒有執行SQL的SELECT語句。這個工作由SQLiteQuery的fillWindow函數完成。 (2) SQLiteQuery fillWindow分析 前面曾說過,SQLiteQuery保存了一個Native層的sqlite3_stmt實例,那么它的fillWindow函數是否就是執行SQL語句后將結果信息填充到CursorWindow中了呢?可以通過以下代碼來驗證。 **SQLiteQuery.java::fillWindow** ~~~ int fillWindow(CursorWindow window) { mDatabase.lock(mSql); longtimeStart = SystemClock.uptimeMillis(); try { acquireReference();//增加一次引用計數 try { window.acquireReference(); /* 調用nativeFillWindow函數完成功能。其中,nHandle指向Native層的 sqlite3實例,nStatement指向Native層的sqlite3_stmt實例, window.mWindowPtr指向Native層的CursorWindow實例, 該函數最終返回這次SQL語句執行后得到的結果集中的記錄項個數。 mOffsetIndex參數的解釋見下文 */ int numRows = nativeFillWindow(nHandle, nStatement,window.mWindowPtr, window.getStartPosition(), mOffsetIndex); mDatabase.logTimeStat(mSql, timeStart); return numRows; }...... finally { window.releaseReference(); } }finally { releaseReference(); mDatabase.unlock(); } } ~~~ mOffsetIndex和SQL語句的OFFSET參數有關,可通過一條SQL語句來認識它。 ~~~ SELECT * FROM IMAGES LIMIT 10 OFFSET 1 //上面這條SQL語句的意思是從IMAGES表中查詢10條記錄,10條記錄的起始位置從第1條開始。 //也就是查詢第1到第11條記錄 ~~~ 來看nativeFillWindow的實現函數,其代碼是: **android_database_SQLiteQuery.cpp::nativeFillWindow** ~~~ static jint nativeFillWindow(JNIEnv* env, jclassclazz, jint databasePtr, jintstatementPtr, jint windowPtr, jint startPos, jint offsetParam) { //取出Native層的實例 sqlite3*database = reinterpret_cast<sqlite3*>(databasePtr); sqlite3_stmt* statement =reinterpret_cast<sqlite3_stmt*>(statementPtr); CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr); if(offsetParam > 0) { //如果設置了查詢的OFFSET,則需要為其綁定起始行。,根據下面的設置,讀者能 //推測出未綁定具體值的SQL語句嗎?答案是: //SELECT* FROM TABLE OFFSET , 其中,offsetParam指明是第幾個通配符, //startPos用于綁定到這個通配符 interr = sqlite3_bind_int(statement, offsetParam, startPos); } ...... //計算本次query返回的結果集的列數 int numColumns =sqlite3_column_count(statement); //將SQL執行的結果保存到CursorWindow對象中 status_tstatus = window->setNumColumns(numColumns); ...... intretryCount = 0; inttotalRows = 0; intaddedRows = 0; boolwindowFull = false; boolgotException = false; //是否遍歷所有結果 constbool countAllRows = (startPos == 0); //注意下面這個循環,它將遍歷SQL的結果集,并將數據取出來保存到CursorWindow對象中 while(!gotException && (!windowFull || countAllRows)) { interr = sqlite3_step(statement); if(err == SQLITE_ROW) { retryCount = 0; totalRows += 1; //windowFull變量用于標示CursorWindow是否還有足夠內存。從前面的介紹可知, //一個CursorWindow只分配了2MB的共享內存空間 if (startPos >= totalRows || windowFull) { continue; } //在共享內存中分配一行空間用于存儲這一行的數據 status = window->allocRow(); if (status) { windowFull = true;// CursorWindow已經沒有空間了 continue; } for (int i = 0; i < numColumns; i++) { //獲取這一行記錄項中各列的值 int type = sqlite3_column_type(statement, i); if (type == SQLITE_TEXT) { //如果這列中存儲的是字符串,則將其取出來并通過CursorWindow的 //putString函數保存到共享內存中 const char* text =reinterpret_cast<const char*>( sqlite3_column_text(statement, i)); size_t sizeIncludingNull =sqlite3_column_bytes(statement, i) + 1; status = window->putString(addedRows, i, text, sizeIncludingNull); if (status) { windowFull = true; break;//CursorWindow沒有足夠的空間 } } ......處理其他數據類型 } if (windowFull || gotException) { window->freeLastRow(); } else { addedRows += 1; } }else if (err == SQLITE_DONE) { ......//結果集中所有行都遍歷完 break; }else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) { //如果數據庫正因為其他操作而被鎖住,此處將嘗試等待一段時間 if (retryCount > 50) {//最多等50次,每次1秒鐘 throw_sqlite3_exception(env,database, "retrycount exceeded"); gotException = true; } else { usleep(1000); retryCount++; } }...... } //重置sqlite3_stmt實例,以供下次使用 sqlite3_reset(statement); ......//返回結果集中的行數 returncountAllRows ? totalRows : 0; } ~~~ 通過以上代碼可確認,fillWindow函數實現的就是將SQL語句的執行結果填充到了CursorWindow的共享內存中。讀者如感興趣,不妨研究一下CursorWindow是如何保存結果信息的。 建議筆者在做網絡開發時常做的一件事情就是將自定義的一些類實例對象序列化到一塊內存中,然后將這塊內存的內容通過socket發送給一個遠端進程,而遠端進程再將收到的數據反序列化以得到一個實例對象。通過這種方式,遠端進程就得到了一個來自發送端的實例對象。讀者不妨自學序列化/反序列化相關的知識。 (3) CursorWindow分析總結 本節向讀者介紹了CursorWindow相關的知識點。其實,CursorWindow就是對一塊共享內存的封裝。另外我們也看到了如何將執行SELECT語句后得到的結果集填充到這塊共享內存中。但是這塊內存現在還僅屬于服務端進程,只有客戶端進程得到這塊內存后,客戶端才能真正獲取執行SELECT后的結果。那么,客戶端是何時得打這塊內存的呢?讓我們回到客戶端進程。 3. BulkCursorToCursorAdaptor和CursorWrapperInner分析 客戶端的工作是先創建BulkCursorToCursorAdaptor,然后根據遠端query的結果調用BulkCursorToCursorAdaptor的intialize函數。 **BulkCursorToCursorAdaptor.java** ~~~ public final class BulkCursorToCursorAdaptorextends AbstractWindowedCursor { privatestatic final String TAG = "BulkCursor"; //mObserverBridge和ContentOberver有關,我們留到7.5節再分析 privateSelfContentObserver mObserverBridge = new SelfContentObserver(this); privateIBulkCursor mBulkCursor; privateint mCount; privateString[] mColumns; privateboolean mWantsAllOnMoveCalls; //initialize函數 publicvoid initialize(IBulkCursor bulkCursor, int count, int idIndex, boolean wantsAllOnMoveCalls) { mBulkCursor = bulkCursor; mColumns = null; mCount = count; mRowIdColumnIndex = idIndex; mWantsAllOnMoveCalls = wantsAllOnMoveCalls;//該值為false } ...... } ~~~ 由以上代碼可知,BulkCursorToCursorAdaptor僅簡單保存了來自遠端的信息,并沒有什么特殊操作。看來客戶端進程沒有在上面代碼的執行過程中共享內存。該工作會不會由CursorWrapperInner來完成呢?看ContentResolver query最終返回給客戶端的對象的類CursorWrapperInner,其代碼也較簡單。 **ContentResolver.java::CursorWrapperInner** ~~~ private final class CursorWrapperInner extendsCursorWrapper { private final IContentProvider mContentProvider; public static final String TAG="CursorWrapperInner"; /* CloseGuard類是Android dalvik虛擬機提供的一個輔助類,用于幫助開發者判斷 使用它的類的實例對象是否被顯示關閉(close)。例如,假設有一個CursorWrapperInner 對象,當沒有地方再引用它時,其finalize函數將被調用。如果之前沒有調用過 CursorWrapperInner的close函數,那么finalize函數CloseGuard的warnIsOpen 將打印警告信息:"A resource was acquired atattached stack trace but never released.See java.io.Closeable for informationon avoiding resource leaks."。 感興趣的讀者可自行研究CloseGuard類 */ private final CloseGuard mCloseGuard = CloseGuard.get(); private boolean mProviderReleased; CursorWrapperInner(Cursor cursor, IContentProvider icp) { super(cursor);//調用基類的構造函數,其內部會將cursor變量保存到mCursor中 mContentProvider = icp; mCloseGuard.open("close"); } ...... } ~~~ CursorWrapperInner的構造函數也沒有去獲取共享內存。別急,先看看執行query后的結果。 客戶端通過Image.Media query函數,將得到一個CursorWrapperInner類型的游標對象。當然,客戶端并不知道這么重要的細節,它只知道自己用的是接口類Cursor。根據前面的分析,此時客戶端通過這個游標對象可與服務端的CursorToBulkCursorAdaptor交互,即進程間Binder通信的通道已經打通。但是此時客戶端還未拿到那塊至關重要的共享內存,即進程間的數據通道還沒打通。那么,數據通道是何時打通的呢? 數據通道打通的時間又和lazy creation聯系上了,即只在使用它時才打通。 4. moveToFirst函數分析 據前文的分析,客戶端從Image.Media query函數得到的游標對象,其真實類型是CursorWrapperInner。游標對象的使用有一個特點,即必須先調用它的move家族的函數。這個家族包括moveToFirst、moveToLast等函數。為什么一定要調用它們呢?來分析最常見的moveToFirst函數,該函數實際上由CursorWrapperInner的基類CursorWrapper來實現,代碼如下: **CursorWrapper.java::moveToFirst** ~~~ publicboolean moveToFirst() { //mCursor指向BulkCursorToCursorAdaptor returnmCursor.moveToFirst(); } ~~~ mCursor成員變量的真實類型是BulkCursorToCursorAdaptor,但其moveToFirst函數卻是該類的老祖宗AbstractCursor實現,代碼如下: **AbstractCursor.java::moveToFirst** ~~~ publicfinal boolean moveToFirst() { returnmoveToPosition(0);//調用moveToPosition,直接來看該函數 } //moveToPosition分析,其參數position表示將移動游標到哪一行 public final boolean moveToPosition(int position){ //getCount返回結果集中的行數,這個值在搭建Binder通信通道時,已經由服務端計算并返回 //給客戶端了 final int count = getCount(); //mPos變量記錄了當前游標的位置,該變量初值為-1 if(position >= count) { mPos = count; return false; } if(position < 0) { mPos = -1; returnfalse; } if(position == mPos) return true; //onMove函數為抽象函數,由子類實現 booleanresult = onMove(mPos, position); if(result == false) mPos = -1; else { mPos = position; if (mRowIdColumnIndex != -1) { mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex)); } } returnresult; } ~~~ 在上邊代碼中,moveToPosition將調用子類實現的onMove函數。在本例中,子類就是BulkCursorToCursorAdaptor,接下來看它的onMove函數。 (1) BulkCursorToCursorAdaptor的onMove函數分析 **BulkCursorToCursorAdaptor.java::onMove** ~~~ public boolean onMove(int oldPosition, intnewPosition) { throwIfCursorIsClosed(); try { //mWindow的類型就是CursorWindow。第一次調用該函數,mWindow為null if(mWindow == null ||newPosition < mWindow.getStartPosition() || newPosition >= mWindow.getStartPosition()+ mWindow.getNumRows()){ /* mBulkCurosr用于和位于服務端的IBulkCursor Bn端通信,其getWindow函數 將返回一個CursorWindow類型的對象。也就是說,調用完getWindow函數后, 客戶端進程就得到了一個CursorWindow,從此,客戶端和服務端之間的數據通道就 打通了 */ setWindow(mBulkCursor.getWindow(newPosition)); }else if (mWantsAllOnMoveCalls) { mBulkCursor.onMove(newPosition); } } ...... if (mWindow== null) return false; returntrue; } ~~~ 建立數據通道的關鍵函數是IBulkCurosr的getWindow。對于客戶端而言,IBulkCursor Bp端對象的類型是BulkCursorProxy,下面介紹它的getWindow函數。 (2) BulkCursorProxy的 getWindow函數分析 **BulkCursorNative.java::BulkCursorProxy:getWindow** ~~~ public CursorWindow getWindow(int startPos) throwsRemoteException { Parceldata = Parcel.obtain(); Parcelreply = Parcel.obtain(); try { data.writeInterfaceToken(IBulkCursor.descriptor); data.writeInt(startPos); mRemote.transact(GET_CURSOR_WINDOW_TRANSACTION, data, reply, 0); DatabaseUtils.readExceptionFromParcel(reply); CursorWindow window = null; if(reply.readInt() == 1) { /* 根據服務端reply包構造一個本地的CursorWindow對象,讀者可自行研究 newFromParcel函數,其內部會調用nativeCreateFromParcel函數以創建 一個Native的CursorWindow對象。整個過程就是筆者在前面提到的反序列化過程 */ window = CursorWindow.newFromParcel(reply); } return window; } ...... } ~~~ 再來看IBulkCursor Bn端的getWindow函數,此Bn端對象的真實類型是CursorToBulkCursorAdaptor。 (3) CursorToBulkCursorAdaptor的 getWindow函數分析 **CursorToBulkCursorAdaptor.java::getWindow** ~~~ public CursorWindow getWindow(int startPos) { synchronized (mLock) { throwIfCursorIsClosed(); CursorWindow window; //mCursor是MediaProvider query返回的值,其真實類型是SQLiteCursor,滿足 //下面的if條件 if(mCursor instanceof AbstractWindowedCursor) { AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor)mCursor; //對于本例而言,SQLiteCursor已經和一個CursorWindow綁定了,所以window的值 //不為空 window = windowedCursor.getWindow(); if (window == null) { window = new CursorWindow(mProviderName, false); windowedCursor.setWindow(window); } //調用SQLiteCursor的moveToPosition函數,該函數前面已經分析過了,在其 //內部將觸發onMove函數的調用,此處將是SQLiteCursor的onMove函數 mCursor.moveToPosition(startPos); } else { ...... } if (window != null) { window.acquireReference(); } return window; } } ~~~ 服務端返回的CursorWindow對象正是之前在count函數中創建的那個CursorWindow對象,其內部已經包含了執行本次query的查詢結果。 另外,在將服務端的CursorWindow傳遞到客戶端之前,系統會調用CursorWindow的writeToParcel函數進行序列化工作。讀者可自行閱讀CursorWindow的writeToParcel及其native實現nativeWriteToParcel函數。 (4) SQLiteCursor的 moveToPostion函數分析 該函數由SQLiteCursor的基類AbstractCursor實現。我們前面已經看過它的代碼了,其內部的主要工作就是調用AbstractCursor子類(此處就是SQLiteCursor自己)實現onMove函數,因此可直接看SQLiteCursor的onMove函數。 **SQLiteCursor.java::onMove** ~~~ public boolean onMove(int oldPosition, intnewPosition) { if(mWindow == null || newPosition < mWindow.getStartPosition() || newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { fillWindow(newPosition); } returntrue; } ~~~ 以上代碼中的if判斷很重要,具體解釋如下: - 當mWindow為空,即服務端未創建CursorWindow時(當然,就本例而言,CursorWindow早已在query時就創建好了),需調用fillWindow。該函數內部將調用clearOrCreateLocalWindow。如果CursorWindow不存在,則創建一個CursorWindow對象。如果已經存在,則清空CursorWindow對象的信息。 - 當newPosition小于上一次查詢得到的CursorWindow的起始位置,或者newPosition大于上一次查詢得到的CursorWindow的最大行位置,也需調用fillWindow。由于此時CursorWindow已經存在,則clearOrCreateLocalWindow會調用它的clear函數以清空之前保存的信息。 - 調用fillWindow后將執行SQL語句,以獲得正確的結果集。例如,假設上次執行query時設置了查詢從第10行開始的90條記錄(即10~100行的記錄),那么,當新的query若指定了從0行開始或從101行開始時,就需重新fillWindow,即將新的結果填充到CursorWindow中。如果新query查詢的行數位于10~100之間,則無需再次調用fillWindow了。 這是服務端針對query做的一些優化處理,即當CursorWindow已經包含了所要求的數據時,就沒有必要再次查詢了。按理說,客戶端也應該做類似的判斷,以避免發起不必要的Binder請求。我們回過頭來看客戶端BulkCursorToCursorAdaptor的onMove函數。 **BulkCursorToCursorAdaptor.java::onMove** ~~~ public boolean onMove(int oldPosition, intnewPosition) { throwIfCursorIsClosed(); try { //同樣,客戶端也做了對應的優化處理,如果不滿足if條件,客戶端根本無需調用 //mBulkCurosr的getWindow函數,這樣服務端也就不會收到對應的Binder請求了 if(mWindow == null ||newPosition < mWindow.getStartPosition() || newPosition >=mWindow.getStartPosition() + mWindow.getNumRows()){ setWindow(mBulkCursor.getWindow(newPosition)); ) ...... } ~~~ (5) moveToFirst函數分析總結 moveToFirst及相關的兄弟函數(如moveToLast和move等)的目的是移動游標位置到指定行。通過上面的代碼分析,我們發現它的工作其實遠不止移動游標位置這么簡單。對于還未擁有CursorWindow的客戶端來說,moveToFirst將導致客戶端反序列化來自服務端的CursorWindow信息,從而使客戶端和服務端之間的數據通道真正建立起來。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看