在開(kāi)發(fā)iOS應(yīng)用過(guò)程中,如何高效的與服務(wù)端API進(jìn)行數(shù)據(jù)交換,是一個(gè)常見(jiàn)問(wèn)題。一般開(kāi)發(fā)者都會(huì)選擇一個(gè)第三方的網(wǎng)絡(luò)組件作為服務(wù),以提高開(kāi)發(fā)效率和穩(wěn)定性。這些組件把復(fù)雜的網(wǎng)絡(luò)底層操作封裝成友好的類(lèi)和方法,并且加入異常處理等。
那么,大家最常用的組件是什么?這些組件是如何提升開(kāi)發(fā)效率和穩(wěn)定性的?哪一款組件適合自己,是 AFNetworking(AFN)還是 ASIHTTPRequest(ASI)?幾乎每一個(gè)iOS互聯(lián)網(wǎng)應(yīng)用開(kāi)發(fā)者都會(huì)面對(duì)這樣的選擇題,要從這兩個(gè)最常用的組件里選出一個(gè)好的還真不是那么容易。
單單從兩個(gè)控件版本提交的時(shí)間節(jié)點(diǎn)來(lái)看,AFN的第一個(gè)提交是2011年的1月1日,那個(gè)時(shí)候ASI早已是1.8+的版本了;而當(dāng)AFN發(fā)布1.0版,2012年10月份的時(shí)候,ASI早早的已經(jīng)停止更新了。這樣看起來(lái),AFN是ASI的繼任者,似乎不存在之前提到的選擇困難的問(wèn)題,而事實(shí)并非如此。本文將從用法、功能、性能和原理幾個(gè)方面對(duì)二者進(jìn)行簡(jiǎn)單對(duì)比,看看二者之間到底存在著怎樣的區(qū)別,到底應(yīng)該如何選擇。
首先,從推薦用法上就可以看出二者設(shè)計(jì)理念上大有不同:
AFN官方推薦的使用方法是,為一系列相關(guān)的請(qǐng)求定義一個(gè)HTTPClient,共用一個(gè)BaseURL。每次請(qǐng)求把URL中除BaseURL的Path部分做為參數(shù)傳給HTTPClient的靜態(tài)方法,并注冊(cè)一個(gè)Block用于回調(diào)。
ASI推薦使用方法就非常傳統(tǒng),每一個(gè)請(qǐng)求都由構(gòu)造方法初始化一個(gè)(共享)實(shí)例,通過(guò)這個(gè)實(shí)例配置參數(shù)并發(fā)起請(qǐng)求。ASI最初使用delegate模式回調(diào),在iOS SDK支持Block之后也提供了注冊(cè)Block的實(shí)例方法。
以上引用的兩段代碼都出自各自項(xiàng)目的示例工程。對(duì)比兩段代碼可以很清楚的看出,同樣是發(fā)起一個(gè)最普通的異步請(qǐng)求,使用AFN只需要調(diào)用一個(gè)靜態(tài)方法,但代碼可讀性較差;而ASI的示例看起來(lái)更清晰,但需要調(diào)用多個(gè)實(shí)例方法才能完成一次請(qǐng)求。AFN的設(shè)計(jì)更加工程化,或者說(shuō)對(duì)使用者更友好,而ASI的設(shè)計(jì)更經(jīng)典,典型的OOP。
除了初級(jí)用法上的區(qū)別,二者的高級(jí)功能和對(duì)擴(kuò)展的支持也頗有不同。
2、高級(jí)功能
AFN只封裝了一些常用功能,滿足基本需求,而直接忽略了很多擴(kuò)展功能。例如:AFN默認(rèn)沒(méi)有封裝同步請(qǐng)求,如果開(kāi)發(fā)者需要使用同步請(qǐng)求,則需要重寫(xiě)getPath:parameters:success:failure方法,對(duì)AFHTTPRequestOperation進(jìn)行同步處理;而ASI則是直接通過(guò)調(diào)用一個(gè)startSynchronous方法。
此外AFN針對(duì)JSON、xml、PList和Image四種數(shù)據(jù)結(jié)構(gòu)封裝了各自處理器,開(kāi)發(fā)者可以把處理器注冊(cè)到操作隊(duì)列中,直接在回調(diào)方法中獲得格式化以后的數(shù)據(jù)。在示例工程中就使用了JSON處理器:把AFJSONRequestOperation注冊(cè)到操作隊(duì)列里。
而ASI在這方面顯得更原始,沒(méi)有針對(duì)任何數(shù)據(jù)類(lèi)型做特別封裝,只是預(yù)留了各種接口和工具供開(kāi)發(fā)者自行擴(kuò)展。ASI比AFN提供更多擴(kuò)展功能還有一個(gè)原因,它把許多內(nèi)部用到的功能也抽象成類(lèi)和方法。例如:
ASIHTTPRequestDataCompressor和ASIHTTPRequestDataDecompressor兩個(gè)類(lèi),只用于壓縮本地文件,構(gòu)造POST Body和解壓縮返回?cái)?shù)據(jù),但這兩個(gè)類(lèi)仍然被設(shè)計(jì)為獨(dú)立功能,提供了對(duì)多種數(shù)據(jù)結(jié)構(gòu)進(jìn)行壓縮和解壓縮的方法。
對(duì)比二者的高級(jí)功能和對(duì)擴(kuò)展的支持后,可以看出AFN把初級(jí)功能(或者叫常用功能)做到了90分。調(diào)用方式夠簡(jiǎn)單,處理器夠豐富,使用者用起來(lái)可以算是輕松加愉快。但它放棄了對(duì)高級(jí)功能的支持,要滿足較復(fù)雜的需求,就要大費(fèi)周折了,在這方面最多只有40分。而ASI顯然不滿足于做好初級(jí)功能,但為了提供更豐富的可擴(kuò)展接口,導(dǎo)致初級(jí)功能用起來(lái)也要花上一些力氣。雖然ASI單獨(dú)提供了支持Amazon S3和Rackspace Cloud Files的控件,但對(duì)于生在紅旗下的我朝開(kāi)發(fā)者來(lái)說(shuō)基本沒(méi)用,所以在初級(jí)功能的支持上ASI能得個(gè)70分,犧牲了初級(jí)功能的易用性,換來(lái)的是良好的擴(kuò)展性,在高級(jí)功能的使用上遠(yuǎn)遠(yuǎn)好于AFN,也能得個(gè)70分。
從使用角度對(duì)比過(guò)后,基本上對(duì)這兩個(gè)項(xiàng)目有一個(gè)整體上的認(rèn)識(shí),再深入下去看看二者的性能如何。
3、性能對(duì)比
我分別用AFN和ASI進(jìn)行了測(cè)試,測(cè)試環(huán)境如下:iphone5,聯(lián)通3G信號(hào)全滿,室內(nèi)靜止?fàn)顟B(tài),請(qǐng)求國(guó)內(nèi)雙線機(jī)房獨(dú)立服務(wù)器的靜態(tài)文件,1~20K共20個(gè)文件,每個(gè)文件請(qǐng)求20次,記錄從創(chuàng)建請(qǐng)求到完全下載文件的耗時(shí),結(jié)果如下:
圖4,AFN連續(xù)訪問(wèn)1 ~ 20K文件耗時(shí)
圖5,ASI連續(xù)訪問(wèn)1 ~ 20K文件耗時(shí)
圖4是AFN的記錄圖,綠色為20次請(qǐng)求中耗時(shí)最久的一次,藍(lán)色為耗時(shí)最短的一次,黃色為去除最大值和最小值的18次平均值。從這個(gè)圖可以看出,AFN最開(kāi)始創(chuàng)建對(duì)象耗時(shí)近2.5秒,隨后穩(wěn)定下來(lái),在3K、7K、15K和20K時(shí)出現(xiàn)了抖動(dòng)。圖5是ASI做相同測(cè)試的結(jié)果,首次創(chuàng)建對(duì)象近2.25秒,略優(yōu)于AFN,同樣在5K、11K、13K、14K和16K發(fā)生了一些抖動(dòng),但抖動(dòng)幅度似乎小于AFN,可見(jiàn)穩(wěn)定性更好一些。
下邊是把二者的測(cè)試結(jié)果放在一起的對(duì)比圖,可以更直觀的比較二者的區(qū)別。
圖6,ASI和AFN耗時(shí)最大值對(duì)比
圖6的最大值對(duì)比可以更明顯的看出二者的抖動(dòng)對(duì)比,ASI略好一些。
圖7,ASI和AFN耗時(shí)最小值對(duì)比
圖7的最小值對(duì)比可以看出,在每一個(gè)大小的測(cè)試中ASI的最佳性能似乎都要優(yōu)于AFN。
圖8,ASI和AFN耗時(shí)平均值對(duì)比
圖8是耗時(shí)平均值的對(duì)比,更能夠說(shuō)明問(wèn)題。文件小于12K的測(cè)試中ASI的性能優(yōu)勢(shì)并沒(méi)有非常明顯,超過(guò)12K以后,ASI優(yōu)勢(shì)開(kāi)始明顯起來(lái),每一次請(qǐng)求都要比AFN節(jié)約20% ~ 30%,近0.1秒。同時(shí)從這張圖上還可以看出,隨著下載文件變大,請(qǐng)求耗時(shí)并不是線形增長(zhǎng)的,這是由于一次請(qǐng)求大部分時(shí)間都消耗在建立連接上,而真正接收數(shù)據(jù)只占用了極少時(shí)間,這個(gè)問(wèn)題不在本篇文章的討論范圍,所以不多說(shuō),有興趣的讀者可以移步http://segmentfault.com/t/ios進(jìn)一步討論。
4、原理分析
ASI的性能似乎全面優(yōu)于AFN,那下邊從二者的實(shí)現(xiàn)原理上看一下到底是什么原因造成這種差距。ASI基于CFNetwork框架開(kāi)發(fā),而AFN基于NSURL,底層的區(qū)別是導(dǎo)致二者性能差距的重要原因之一。

圖9,ASI和AFN以及底層框架的關(guān)系
我們知道所有網(wǎng)絡(luò)通信的基礎(chǔ)是Socket,一個(gè)Socket與另一個(gè)連接并傳送數(shù)據(jù)。BSD Socket是一類(lèi)最常見(jiàn)的Socket抽象接口。
Core Foundation框架中的Cfsocket就是基于BSD Socket開(kāi)發(fā)的。它幾乎涵蓋了BSD Socket的全部功能,更重要的是把Socket整合到事件的處理循環(huán)中。Core Founda-tion中較高層的CFStream是基于CFSocket開(kāi)發(fā)的讀寫(xiě)流支持。
CFNetwork是基于Core Foundation中CFStream的一個(gè)底層高性能網(wǎng)絡(luò)框架,它由提供基礎(chǔ)服務(wù)的CFSocketStream,支持HTTP協(xié)議的CFHTTP,基于CFHTTP用于身份認(rèn)證的CFHTTPAuthentication和支持FTP協(xié)議的CFFTP組成。
正如圖9所示,ASI是基于CFHTTP開(kāi)發(fā)的一個(gè)組件;而AFN的基礎(chǔ)——NSURL,也是基于CFNetwork開(kāi)發(fā)的。也就是說(shuō)ASI相比AFN更加底層,這就從一定程度上造成二者的性能差距。
另一個(gè)方面,雖然二者都使用NSOperation和NSOperationQueue實(shí)現(xiàn)但底層的區(qū)別也導(dǎo)致實(shí)現(xiàn)方式上有非常大的差別。
ASI的直接操作對(duì)象ASIHTTPRequest是NSOperation的子類(lèi),實(shí)現(xiàn)了NSCopying協(xié)議。在initialize和initWithURL:方法中初始化相關(guān)屬性并配置一系列請(qǐng)求相關(guān)參數(shù)默認(rèn)值。此外,ASIHTTPRequest還提供了一系列的實(shí)例方法用來(lái)配置請(qǐng)求對(duì)象。在異步請(qǐng)求的處理上,ASIHTTPRequest對(duì)象初始化結(jié)束后,在startAsynchronous方法中把對(duì)象加入共享操作隊(duì)列。此后,包括創(chuàng)建CFHTTPMessageRef,也就是處理網(wǎng)絡(luò)請(qǐng)求的主要對(duì)象(事實(shí)上是一個(gè)指向__CFHTTPMessage結(jié)構(gòu)的指針),在內(nèi)的所有操作都在ASIHTTPRequest對(duì)象所屬的子線程中完成。
AFN的直接操作對(duì)象AFHTTPClient不同于ASI,是一個(gè)實(shí)現(xiàn)了NSCoding和NSCopying協(xié)議的NSObject子類(lèi)。AFHTTPClient是一個(gè)封裝了一系列操作方法的“工具類(lèi)”,處理請(qǐng)求的操作類(lèi)是一系列單獨(dú)的,基于NSOperation封裝的,AFURLConnectionOperation的子類(lèi)。AFN的示例代碼中通過(guò)一個(gè)靜態(tài)方法,使用dispatch_once()的方式創(chuàng)建AFHTTPClient的共享實(shí)例,這也是官方建議的使用方法。在創(chuàng)建AFHTTPClient的初始化方法中,創(chuàng)建了OperationQueue并設(shè)置一系列參數(shù)默認(rèn)值。在getPath:parameters:success:failure方法中創(chuàng)建NSURLRequest,以NSURLRequest對(duì)象實(shí)例作為參數(shù),創(chuàng)建一個(gè)NSOperation,并加入在初始化發(fā)方中創(chuàng)建的NSOperationQueue。以上操作都是在主線程中完成的。在NSOperation的start方法中,以此前創(chuàng)建的NSURLRequest對(duì)象為參數(shù)創(chuàng)建NSURLConnection并開(kāi)啟連結(jié)。
在異步回調(diào)的處理上二者也有區(qū)別,ASI采取的是CFHTTP請(qǐng)求完成,直接回調(diào)ASIHTTPRequest的實(shí)例方法,通過(guò)儲(chǔ)存的實(shí)例對(duì)象記錄的信息完成Delegate模式或Block模式的回調(diào)。而AFN則直接使用了NSOperation的completionBlock屬性。
這些實(shí)現(xiàn)方式也可以看出,ASI顯得更加底層,并沒(méi)有過(guò)多使用Cocoa框架中已經(jīng)封裝的API,而AFN則更加實(shí)用主義,邏輯簡(jiǎn)單清晰,大量使用了框架API。這一點(diǎn)也是造成二者性能差別的原因之一。
對(duì)比 | ASI | AFN |
| 更新?tīng)顟B(tài) | 2012年10月份,已經(jīng)停止更新 | 持續(xù)更新中,目前已更新至2.0版 |
| 介紹 | ASI的直接操作對(duì)象ASIHTTPRequest,是一個(gè)實(shí)現(xiàn)了了NSCopying協(xié)議的NSOperation子類(lèi)。 在initialize和initWithURL:方法中初始化相關(guān)屬性并配置一系列請(qǐng)求相關(guān)參數(shù)默認(rèn)值。此外,ASIHTTPRequest還提供了一系列的實(shí)例方法用來(lái)配置請(qǐng)求對(duì)象。 | AFN的直接操作對(duì)象AFHTTPClient,是一個(gè)實(shí)現(xiàn)了NSCoding和NSCopying協(xié)議的NSObject子類(lèi)。AFHTTPClient是一個(gè)封裝了一系列操作方法的“工具類(lèi)”,處理請(qǐng)求的操作類(lèi)是一系列單獨(dú)的,基于NSOperation封裝的,AFURLConnectionOperation的子類(lèi)。 |
| 線程處理模式 | 每一個(gè)請(qǐng)求都由構(gòu)造方法初始化一個(gè)(共享)實(shí)例,通過(guò)這個(gè)實(shí)例配置參數(shù)并發(fā)起請(qǐng)求。ASI最初使用delegate模式回調(diào),在iOS SDK支持Block之后也提供了注冊(cè)Block的實(shí)例方法。 ASI采取的是CFHTTP請(qǐng)求完成,直接回調(diào)ASIHTTPRequest的實(shí)例方法,通過(guò)儲(chǔ)存的實(shí)例對(duì)象記錄的信息完成Delegate模式或Block模式的回調(diào)。 在異步請(qǐng)求的處理上,ASIHTTPRequest對(duì)象初始化結(jié)束后,在startAsynchronous方法中把對(duì)象加入共享操作隊(duì)列。此后,包括創(chuàng)建CFHTTPMessageRef,也就是處理網(wǎng)絡(luò)請(qǐng)求的主要對(duì)象(事實(shí)上是一個(gè)指向__CFHTTPMessage結(jié)構(gòu)的指針),在內(nèi)的所有操作都在ASIHTTPRequest對(duì)象所屬的子線程中完成。 | AFN的示例代碼中通過(guò)一個(gè)靜態(tài)方法,使用dispatch_once()的方式創(chuàng)建AFHTTPClient的共享實(shí)例,這也是官方建議的使用方法。在創(chuàng)建AFHTTPClient的初始化方法中,創(chuàng)建了OperationQueue并設(shè)置一系列參數(shù)默認(rèn)值。在getPath:parameters:success:failure方法中創(chuàng)建NSURLRequest,以NSURLRequest對(duì)象實(shí)例作為參數(shù),創(chuàng)建一個(gè)NSOperation,并加入在初始化發(fā)方中創(chuàng)建的NSOperationQueue。 以上操作都是在主線程中完成的。在NSOperation的start方法中,以此前創(chuàng)建的NSURLRequest對(duì)象為參數(shù)創(chuàng)建NSURLConnection并開(kāi)啟連結(jié)。 |
| 數(shù)據(jù)處理模式 | ASI在這方面顯得更原始,沒(méi)有針對(duì)任何數(shù)據(jù)類(lèi)型做特別封裝,只是預(yù)留了各種接口和工具供開(kāi)發(fā)者自行擴(kuò)展。 | AFN針對(duì)JSON、XML、PList和Image四種數(shù)據(jù)結(jié)構(gòu)封裝了各自處理器,開(kāi)發(fā)者可以把處理器注冊(cè)到操作隊(duì)列中,直接在回調(diào)方法中獲得格式化以后的數(shù)據(jù)。 |
| 同步請(qǐng)求 | ASI則是直接通過(guò)調(diào)用一個(gè)startSynchronous方法。 | AFN默認(rèn)沒(méi)有封裝同步請(qǐng)求,如果開(kāi)發(fā)者需要使用同步請(qǐng)求,則需要重寫(xiě)getPath:parameters:success:failure方法,對(duì)AFHTTPRequestOperation進(jìn)行同步處理 |
| 異步回調(diào)的處理 | 【使用AFNetworking進(jìn)行網(wǎng)絡(luò)異步請(qǐng)求時(shí),block:(void(^)代碼塊實(shí)際返回到UI主線程中。即使在子線程中使用AFNetWorking進(jìn)行網(wǎng)絡(luò)的異步請(qǐng)求,block:(void(^)代碼塊仍然返回到UI主線程中(AF框架,它里面已經(jīng)create了異步線程 )。因此無(wú)論當(dāng)前處在主線程還是子線程,異步返回均返回到UI主線程中。】 | 為一系列相關(guān)的請(qǐng)求定義一個(gè)HTTPClient,共用一個(gè)BaseURL。每次請(qǐng)求把URL中除BaseURL的Path部分做為參數(shù)傳給HTTPClient的靜態(tài)方法,并注冊(cè)一個(gè)Block用于回調(diào)。 AFN則直接使用了NSOperation的completionBlock屬性。 |
基于的底層開(kāi)發(fā)框架 | CFNetwork框架 使用CFnetwork而不是Cocoa框架NSURL有幾點(diǎn)好處。CFNetwork更加專(zhuān)注于網(wǎng)絡(luò)協(xié)議,而NSURL更加專(zhuān)注于數(shù)據(jù)訪問(wèn),比如通過(guò)HTTP或者FTP傳輸數(shù)據(jù)。盡管NSURL的確也提供了一些可配置功能,可是CFNetwork提供的要多的多。另外NSURL還需要你使用Objective_c。如果做不到這點(diǎn)的話,還是應(yīng)該使用CFNetwork | NSURL 【使用iOS5.0 SDK NSURLConnection: 1、進(jìn)行網(wǎng)絡(luò)同步請(qǐng)求(sendSynchronousRequest)時(shí),調(diào)用該請(qǐng)求接口的操作在哪個(gè)線程,同步返回的網(wǎng)絡(luò)結(jié)果就處于哪個(gè)線程,因此通常進(jìn)行網(wǎng)絡(luò)同步請(qǐng)求時(shí),為了避免阻塞UI主線程,需要在子線程中進(jìn)行網(wǎng)絡(luò)請(qǐng)求; 2、進(jìn)行網(wǎng)絡(luò)異步請(qǐng)求(sendAsynchronousRequest)時(shí),block:(void(^)代碼塊實(shí)際返回到子線程中。因此,此時(shí)如需要向UI線程發(fā)送通知,則需要跳轉(zhuǎn)到主線程中發(fā)送通知dispatch_async(dispatch_get_main_queue(), ^{});】 |
| 底層開(kāi)發(fā)礦建介紹 | CFNetwork是基于Core Foundation中CFStream的一個(gè)底層高性能網(wǎng)絡(luò)框架,它由提供基礎(chǔ)服務(wù)的CFSocketStream,支持HTTP協(xié)議的CFHTTP,基于CFHTTP用于身份認(rèn)證的CFHTTPAuthentication和支持FTP協(xié)議的CFFTP組成。 Core Foundation框架中的CFSocket就是基于BSD Socket開(kāi)發(fā)的。它幾乎涵蓋了BSD Socket的全部功能,更重要的是把Socket整合到事件的處理循環(huán)中。Core Founda-tion中較高層的CFStream是基于CFSocket開(kāi)發(fā)的讀寫(xiě)流支持。 | 如圖所示,ASI是基于CFHTTP開(kāi)發(fā)的一個(gè)組件;而AFN的基礎(chǔ)——NSURL,也是基于CFNetwork開(kāi)發(fā)的,也就是說(shuō)ASI相比AFN更加底層。 |
| 性能對(duì)比 | AFN請(qǐng)求優(yōu)于ASI | |
| 總結(jié) | ASI更適合已經(jīng)發(fā)展了一段時(shí)間的應(yīng)用,或者開(kāi)發(fā)資源相對(duì)豐富的團(tuán)隊(duì),因?yàn)橥@些團(tuán)隊(duì)(或他們的應(yīng)用)已經(jīng)積累了一定的經(jīng)驗(yàn),無(wú)論是產(chǎn)品上還是技術(shù)上的。需求復(fù)雜度就是在這種時(shí)候高起來(lái),而且底層訂制的需求也越來(lái)越多,此時(shí)AFN就很難滿足需求,需要犧牲一定的易用性,使用ASI作為網(wǎng)絡(luò)底層控件。 | AFN適合邏輯簡(jiǎn)單的應(yīng)用,或者更適合開(kāi)發(fā)資源尚不豐富的團(tuán)隊(duì),因?yàn)锳FN的易用性要比ASI好很多,而這樣的應(yīng)用(或團(tuán)隊(duì))對(duì)底層網(wǎng)絡(luò)控件的定制化要求也非常低。 |
總結(jié)
通過(guò)以上的對(duì)比,基本可以這樣評(píng)價(jià):AFN適合邏輯簡(jiǎn)單的應(yīng)用,或者更適合開(kāi)發(fā)資源尚不豐富的團(tuán)隊(duì),因?yàn)锳FN的易用性要比ASI好很多,而這樣的應(yīng)用(或團(tuán)隊(duì))對(duì)底層網(wǎng)絡(luò)控件的定制化要求也非常低。ASI更適合已經(jīng)發(fā)展了一段時(shí)間的應(yīng)用,或者開(kāi)發(fā)資源相對(duì)豐富的團(tuán)隊(duì),因?yàn)橥@些團(tuán)隊(duì)(或他們的應(yīng)用)已經(jīng)積累了一定的經(jīng)驗(yàn),無(wú)論是產(chǎn)品上還是技術(shù)上的。需求復(fù)雜度就是在這種時(shí)候高起來(lái),而且底層訂制的需求也越來(lái)越多,此時(shí)AFN就很難滿足需求,需要犧牲一定的易用性,使用ASI作為網(wǎng)絡(luò)底層控件。SegmentFault開(kāi)源客戶端現(xiàn)在被設(shè)計(jì)為一款簡(jiǎn)單的閱讀客戶端,幾乎沒(méi)有定制要求,因此,目前我選擇了AFN作為網(wǎng)絡(luò)控件。
以上對(duì)ASI和AFN兩款最常用的iOS底層網(wǎng)絡(luò)控件做了初步的介紹,要更深入的了解兩款控件,還需要大家繼續(xù)研究各自的源碼。大家遇到任何關(guān)于iOS的技術(shù)問(wèn)題都可以在
http://segmentfault.com/t/ios
進(jìn)行討論。另外大家也可以持續(xù)關(guān)注SegmentFault的開(kāi)源客戶端
https://github.com/gaosboy/iOSSF
,與更多的開(kāi)發(fā)者共同探討iOS開(kāi)發(fā)技術(shù)。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注