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

用Swift編寫(xiě)網(wǎng)絡(luò)層:面向協(xié)議方式

移動(dòng)開(kāi)發(fā)
在這篇文章中我們會(huì)看到怎樣實(shí)現(xiàn)用純swift編寫(xiě)網(wǎng)絡(luò)層,而不依靠任何第三方庫(kù)。讓我們快去看看吧。相信看完之后我們的代碼能夠做到。

用Swift編寫(xiě)網(wǎng)絡(luò)層:面向協(xié)議方式

在這篇文章中我們會(huì)看到怎樣實(shí)現(xiàn)用純swift編寫(xiě)網(wǎng)絡(luò)層,而不依靠任何第三方庫(kù)。讓我們快去看看吧。相信看完之后我們的代碼能夠做到:

  • 面向協(xié)議
  • 易用
  • 容易實(shí)現(xiàn)
  • 類型安全
  • 用枚舉(enums)來(lái)配置終端(endPoints)

下面是一個(gè)最終我們網(wǎng)絡(luò)層的示例

 

2.png
這個(gè)項(xiàng)目的最終目標(biāo)

通過(guò)輸入router.request(. 借助枚舉的力量,我們可以看到所有有效的終端和我們請(qǐng)求的參數(shù))

首先,一些結(jié)構(gòu)

創(chuàng)建任何東西之前,有個(gè)結(jié)構(gòu)都是很重要的,這樣后面我們就容易找到需要的東西。我堅(jiān)定相信文件夾結(jié)構(gòu)對(duì)軟件架構(gòu)至關(guān)重要。為了讓我們的文件組織有序,讓我們提前建立好所有的組,我會(huì)標(biāo)記好每一個(gè)文件該放的位置。這是一個(gè)項(xiàng)目結(jié)構(gòu)總覽。(請(qǐng)注意這里的名字僅僅是建議,你可以按你喜好給你的類和組命名)

 

3.png
項(xiàng)目文件夾結(jié)構(gòu)

終端類型(EndPointType)協(xié)議

我們要做的***件事情就是定義我們的終端類型協(xié)議。這個(gè)協(xié)議要包含用于配置終端的所有信息。什么是終端?本質(zhì)上來(lái)講它是一個(gè)包含各種組件比如頭文件(headers),查詢參數(shù)(query parameters),體參數(shù)(body parameters)的URL請(qǐng)求(URLRequest)。終端類型協(xié)議是我們網(wǎng)絡(luò)層實(shí)現(xiàn)的基石。我們建一個(gè)文件,并命名EndPointType,把它放到服務(wù)組中(不是終端組,后面我們分清楚的)。

 

4.png
終端類型協(xié)議

HTTP協(xié)議

為了創(chuàng)建一個(gè)完整的終端,我的終端類型協(xié)議里有很多HTTP協(xié)議。讓我們看看這些協(xié)議需要什么。

HTTP方法

創(chuàng)建一個(gè)名為HTTPMethod的文件并把它放在服務(wù)組中。這個(gè)枚舉會(huì)用于設(shè)置我們請(qǐng)求用的HTTP方法。

 

5.png
HTTPMethod枚舉

HTTP任務(wù)

創(chuàng)建一個(gè)名為HTTPTask的文件并把它放在服務(wù)組中。HTTPTask用于為一個(gè)特定的終端配置參數(shù),你可以添加適當(dāng)數(shù)量的案例(cases)到你的網(wǎng)絡(luò)層請(qǐng)求中。我會(huì)按下圖建立我的請(qǐng)求,它只包含3個(gè)案例

 

6.png
HTTPTask枚舉

在下一章我們會(huì)討論參數(shù)和如何處理參數(shù)的編碼。

HTTP頭文件

HTTPHeaders是一個(gè)字典的別名(typealias)。你可以在你HTTPTask文件的開(kāi)頭創(chuàng)建它。

  1. public typealias HTTPHeaders = [String:String] 

參數(shù)與編碼

創(chuàng)建一個(gè)名為ParameterEncoding的文件并把它放在編碼組中。我們首先要定義一個(gè)參數(shù)的別名,通過(guò)它我們可以讓代碼更干凈簡(jiǎn)潔。

  1. public typealias Parameters = [String:Any

之后用一個(gè)靜態(tài)函數(shù)編碼定義一個(gè)協(xié)議參數(shù)編碼器(ParameterEncoder)。這種編碼方式含有2個(gè)參數(shù),一個(gè)inout URLRequest和Parameters。(為了防止混淆,后面我會(huì)把函數(shù)參數(shù)稱為參量)。INOUT是一個(gè)swift關(guān)鍵詞,用于把一個(gè)參量定義為引用參量。通常變量作為值類型傳送給函數(shù)。通過(guò)在參量的開(kāi)頭加上inout,我們把它定義為引用類型。要學(xué)更多關(guān)于雙向參量,你可以點(diǎn)擊這里。參數(shù)編碼器協(xié)議會(huì)通過(guò)JSONParameterEncoder和URLPameterEncoder實(shí)現(xiàn)。

 

  1. public protocol ParameterEncoder { 
  2.  static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws 

參數(shù)編碼器執(zhí)行編碼參數(shù)的函數(shù),這個(gè)方法會(huì)失敗,返回一個(gè)錯(cuò)誤,因而我們需要處理它。

能夠返回一個(gè)自定的錯(cuò)誤提示比標(biāo)準(zhǔn)錯(cuò)誤提示會(huì)更有價(jià)值。我總是花很多時(shí)間去分析Xcode給的一些錯(cuò)誤提示。有了自定的錯(cuò)誤提示你就可以定義屬于自己的錯(cuò)誤信息,就能清楚知道錯(cuò)誤到底來(lái)自哪里。為了做到這些,我創(chuàng)建了一個(gè)繼承自Error的枚舉。

 

7.png
NetworkError枚舉

URL參數(shù)編碼器

創(chuàng)建一個(gè)名為URLParameterEncoder的文件并把它放在編碼組中。

 

8.png
URL參數(shù)編碼器代碼

上面的代碼含有一些參數(shù),它可以將他們變成URL參數(shù)來(lái)安全傳遞。你要知道一些字符在URL中一些字符是禁用的。參數(shù)也被‘&’標(biāo)記分開(kāi),我們需要考慮到所有這些。如果之前沒(méi)有設(shè)置,我們還要為請(qǐng)求添加合適的頭文件。

這個(gè)示例代碼是使用單元測(cè)試時(shí)應(yīng)該考慮到的。如果URL沒(méi)有正確建立,我們就會(huì)有很多不必要的錯(cuò)誤。如果你在使用一個(gè)開(kāi)放API,你一定不希望自己的請(qǐng)求配額被一堆錯(cuò)誤測(cè)試用完。如果你想學(xué)更多關(guān)于單元測(cè)試內(nèi)容,你可以看S.T.Huang的這篇文章。

JSON參數(shù)編碼器

創(chuàng)建一個(gè)名為JSONParameterEncoder的文件,也把它放在編碼組中。

 

9.png
JSON參數(shù)編碼器代碼

類似URL參數(shù)編碼器,不過(guò)這里是為JSON編碼參數(shù),同樣要添加合適的頭文件。

網(wǎng)絡(luò)路由器

創(chuàng)建一個(gè)名為NetworkRouter的文件并把它放在服務(wù)組中。我們從為一個(gè)完成部分(completion)定義別名開(kāi)始。

  1. public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() 

之后我們定義一個(gè)協(xié)議網(wǎng)絡(luò)路由器

 

10.png
NetworkRouter代碼

一個(gè)網(wǎng)絡(luò)路由器有一個(gè)用于產(chǎn)生請(qǐng)求的終端,一旦請(qǐng)求產(chǎn)生,它會(huì)傳遞對(duì)完成部分的應(yīng)答。我加入了一個(gè)取消函數(shù),有它當(dāng)然好,但不是一定要用到。這個(gè)函數(shù)可以在一個(gè)請(qǐng)求存在周期的任意時(shí)刻調(diào)用并取消它。如果你的應(yīng)用有上傳或下載任務(wù),這會(huì)很有用。為了讓我們的路由器能處理任何終端類型,我們這里使用了關(guān)聯(lián)類型。如果不用關(guān)聯(lián)類型,路由器就不得不有一個(gè)具體的終端類型。想對(duì)關(guān)聯(lián)類型了解更多,建議看NatashaTheRobot的這篇文章。

路由器

創(chuàng)建一個(gè)名為Router的文件并把它放在服務(wù)組中。我們聲明一個(gè)URLSessionTask類型的私有變量任務(wù)。這個(gè)任務(wù)本質(zhì)上是整個(gè)工作要做的。我們讓這個(gè)變量私有化,因?yàn)槲覀儾幌肴魏芜@個(gè)類之外的任何東西會(huì)調(diào)整我們的任務(wù)。

 

11.png
Router方法存根

請(qǐng)求

這里我們使用共享的會(huì)話管理(session)創(chuàng)建URLSession,這是創(chuàng)建URLSession最簡(jiǎn)單的辦法,但請(qǐng)記住這不是***的方法。要實(shí)現(xiàn)對(duì)URLSession更復(fù)雜的配置,則要用能夠改變會(huì)話管理表現(xiàn)的配置。想了解更多,我推薦讀一讀這篇文章。

這里我們通過(guò)調(diào)用buildRequest生成我們的請(qǐng)求,并給它一個(gè)終端作為路徑。這個(gè)buildRequest的調(diào)用被限制在一個(gè)do-try-catch區(qū)塊,因?yàn)槲覀兊木幋a器可能會(huì)報(bào)出錯(cuò)誤。我們僅僅把所有應(yīng)答,數(shù)據(jù)和錯(cuò)誤傳送給完成部分。

 

12.png
Request方法代碼

建立請(qǐng)求

在Router中創(chuàng)建一個(gè)名為buildRequest的私有函數(shù),這個(gè)函數(shù)負(fù)責(zé)我們網(wǎng)絡(luò)層中一切重要工作。本質(zhì)上就是把EndPointType轉(zhuǎn)化為URLRequest。一旦我們的終端生成請(qǐng)求,我們可以把它傳遞給會(huì)話管理。這里有很多工作要做,所以我們將會(huì)分別看看每個(gè)方法。讓我們分解buildRequest方法:

我們舉了一個(gè)URLRequest類型的變量請(qǐng)求的例子。把我們的基礎(chǔ)URL給它,并附上我們要用到的路徑。

我們?cè)O(shè)定這請(qǐng)求的httpMethod和我們終端的一致。

考慮到我們的編碼器會(huì)報(bào)告錯(cuò)誤,我們創(chuàng)建一個(gè)do-try-catch區(qū)塊。只要?jiǎng)?chuàng)建一個(gè)大的do-try-catch區(qū)塊,我們就不需要為每次嘗試分別建一個(gè)。

開(kāi)啟route.task

根據(jù)任務(wù),調(diào)用合適的編碼器。

 

13.png
buildRequest方法代碼.

配置參數(shù)

在Router中創(chuàng)建一個(gè)名為configureParameters的函數(shù)

 

14.png
configureParameters方法的實(shí)現(xiàn)

這個(gè)函數(shù)負(fù)責(zé)為我們的參數(shù)編碼。因?yàn)槲覀兊腁PI要求所有的bodyParameters都是JSON,并且URLParameters是URL編碼的,我們把合適的參數(shù)傳遞給設(shè)計(jì)好的編碼器。如果你正在用一個(gè)有多種編碼方式的API,我建議修改HTTPTask來(lái)使用編碼器枚舉。這個(gè)枚舉需要包含所有你需要的不同類型編碼器。之后在configureParameters添加一個(gè)關(guān)于你編碼枚舉的附加參量。開(kāi)啟這個(gè)枚舉,合適地為參數(shù)編碼。

添加附加頭文件

在Router中創(chuàng)建一個(gè)名為addAdditionalHeaders的函數(shù)

 

15.png
addAdditionalHeaders方法的實(shí)現(xiàn)

添加所有附加頭文件,讓它們成為請(qǐng)求頭文件的一部分。

取消

取消函數(shù)的實(shí)現(xiàn)是這樣的:

 

16.png
cancel方法的實(shí)現(xiàn)

實(shí)踐

現(xiàn)在讓我們用一個(gè)實(shí)際例子看看我們建立的網(wǎng)絡(luò)層。我們將從TheMovieDB獲取一些電影數(shù)據(jù)到我們的應(yīng)用。

電影終端(MovieEndPoint)

電影終端與我們?cè)贕etting Started with Moya中提到的目標(biāo)類型很相似。與實(shí)現(xiàn)Moya中目標(biāo)類型不同的是這里我們實(shí)現(xiàn)我們自己的終端類型。把這個(gè)文件放在終端組中。

 

  1. import Foundation   
  2.   
  3. enum NetworkEnvironment { 
  4.     case qa 
  5.     case production 
  6.     case staging 
  7.   
  8. public enum MovieApi { 
  9.     case recommended(id:Int
  10.     case popular(page:Int
  11.     case newMovies(page:Int
  12.     case video(id:Int
  13.   
  14. extension MovieApi: EndPointType { 
  15.      
  16.     var environmentBaseURL : String { 
  17.         switch NetworkManager.environment { 
  18.         case .production: return "https://api.themoviedb.org/3/movie/" 
  19.         case .qa: return "https://qa.themoviedb.org/3/movie/" 
  20.         case .staging: return "https://staging.themoviedb.org/3/movie/" 
  21.         } 
  22.     } 
  23.       
  24.     var baseURL: URL { 
  25.         guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} 
  26.         return url 
  27.     } 
  28.      
  29.     var path: String { 
  30.         switch self { 
  31.         case .recommended(let id): 
  32.             return "\(id)/recommendations" 
  33.         case .popular: 
  34.             return "popular" 
  35.         case .newMovies: 
  36.             return "now_playing" 
  37.         case .video(let id): 
  38.             return "\(id)/videos" 
  39.         } 
  40.     } 
  41.       
  42.     var httpMethod: HTTPMethod { 
  43.         return .get 
  44.     } 
  45.       
  46.     var task: HTTPTask { 
  47.         switch self { 
  48.         case .newMovies(let page): 
  49.             return .requestParameters(bodyParameters: nil, 
  50.                                       urlParameters: ["page":page, 
  51.                                                       "api_key":NetworkManager.MovieAPIKey]) 
  52.         default
  53.             return .request 
  54.         } 
  55.     } 
  56.       
  57.     var headers: HTTPHeaders? { 
  58.         return nil 
  59.     } 

終端類型

電影模式(MovieModel)

因?yàn)閷?duì)TheMovieDB的回應(yīng)同樣是JSON,我們的電影模式也不會(huì)改變。我們用可解碼協(xié)議來(lái)把JSON轉(zhuǎn)化為我們的模式。把這個(gè)文件放在模式組中。

 

  1. import Foundation 
  2.   
  3. struct MovieApiResponse { 
  4.     let page: Int 
  5.     let numberOfResults: Int 
  6.     let numberOfPages: Int 
  7.     let movies: [Movie] 
  8.   
  9. extension MovieApiResponse: Decodable { 
  10.      
  11.     private enum MovieApiResponseCodingKeys: String, CodingKey { 
  12.         case page 
  13.         case numberOfResults = "total_results" 
  14.         case numberOfPages = "total_pages" 
  15.         case movies = "results" 
  16.     } 
  17.      
  18.     init(from decoder: Decoder) throws { 
  19.         let container = try decoder.container(keyedBy: MovieApiResponseCodingKeys.self) 
  20.           
  21.         page = try container.decode(Int.self, forKey: .page) 
  22.         numberOfResults = try container.decode(Int.self, forKey: .numberOfResults) 
  23.         numberOfPages = try container.decode(Int.self, forKey: .numberOfPages) 
  24.         movies = try container.decode([Movie].self, forKey: .movies) 
  25.           
  26.     } 
  27.   
  28.   
  29. struct Movie { 
  30.     let id: Int 
  31.     let posterPath: String 
  32.     let backdrop: String 
  33.     let title: String 
  34.     let releaseDate: String 
  35.     let rating: Double 
  36.     let overview: String 
  37.   
  38. extension Movie: Decodable { 
  39.      
  40.     enum MovieCodingKeys: String, CodingKey { 
  41.         case id 
  42.         case posterPath = "poster_path" 
  43.         case backdrop = "backdrop_path" 
  44.         case title 
  45.         case releaseDate = "release_date" 
  46.         case rating = "vote_average" 
  47.         case overview 
  48.     } 
  49.       
  50.      
  51.     init(from decoder: Decoder) throws { 
  52.         let movieContainer = try decoder.container(keyedBy: MovieCodingKeys.self) 
  53.          
  54.         id = try movieContainer.decode(Int.self, forKey: .id) 
  55.         posterPath = try movieContainer.decode(String.self, forKey: .posterPath) 
  56.         backdrop = try movieContainer.decode(String.self, forKey: .backdrop) 
  57.         title = try movieContainer.decode(String.self, forKey: .title) 
  58.         releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate) 
  59.         rating = try movieContainer.decode(Double.self, forKey: .rating) 
  60.         overview = try movieContainer.decode(String.self, forKey: .overview) 
  61.     } 

電影模式

網(wǎng)絡(luò)管理員

創(chuàng)建一個(gè)名為NetworkManager的文件并把它放在管理員組中。

現(xiàn)在開(kāi)始我們的網(wǎng)絡(luò)管理員將僅有2個(gè)靜態(tài)屬性:你的API密碼和網(wǎng)絡(luò)環(huán)境(引用MovieEndPoint)。網(wǎng)絡(luò)管理員也有一個(gè)類型為MovieApi的Router。

 

17.png
NetworkManager代碼

網(wǎng)絡(luò)響應(yīng)

在NetworkManager中創(chuàng)建一個(gè)名為NetworkResponse的枚舉。

 

18.png
NetworkResponse枚舉

我們將用這個(gè)枚舉處理來(lái)自API的響應(yīng),并顯示相應(yīng)的信息。

結(jié)果

在NetworkManager中創(chuàng)建一個(gè)枚舉Result。

 

19.png
Result枚舉

一個(gè)結(jié)果枚舉可以用在很多不同事情上,非常有用。我們根據(jù)結(jié)果確定我們對(duì)API的調(diào)用是成功還是失敗。如果失敗了,我們會(huì)返回一個(gè)錯(cuò)誤信息并說(shuō)明原因。想了解更多面向結(jié)果的編程,你可以看這篇對(duì)話。

處理網(wǎng)絡(luò)響應(yīng)

創(chuàng)建一個(gè)名為handleNetworkResponse的函數(shù),這個(gè)函數(shù)有一個(gè)參量,即HTTPResponse,并返回一個(gè)Result.

 

20.png

這里我們開(kāi)啟HTTPResponse的狀態(tài)碼,狀態(tài)碼是一個(gè)能告訴我們響應(yīng)狀態(tài)的HTTP協(xié)議?;旧?00-299之間都是成功。

產(chǎn)生調(diào)用

現(xiàn)在我們已經(jīng)為我們的網(wǎng)絡(luò)層打下雄厚的基礎(chǔ)。是時(shí)候開(kāi)始調(diào)用了。

我們將會(huì)從API獲取一個(gè)新電影列表。創(chuàng)建一個(gè)名為getNewMovies的函數(shù)。

 

21.png
getNewMovies方法的實(shí)現(xiàn)

讓我們分解這個(gè)方法的每一步

  1. 我們定義getNewMovies方法含有2個(gè)參量:一個(gè)頁(yè)碼和一個(gè)能返回電影數(shù)組或錯(cuò)誤信息的完成部分(completion)。
  2. 我們調(diào)用我們的路由器,輸入頁(yè)碼并在一個(gè)閉包(closure)內(nèi)處理這個(gè)完成部分。
  3. 如果沒(méi)有網(wǎng)絡(luò)或者出于一些原因無(wú)法調(diào)用API,URLSession會(huì)返回錯(cuò)誤。請(qǐng)注意這并不是API的失敗。這種失敗多是客服端的,很可能是因?yàn)榫W(wǎng)絡(luò)連接不好。
  4. 我們需要把我們的響應(yīng)轉(zhuǎn)變?yōu)橐粋€(gè)HTTPURLResponse,因?yàn)槲覀冃枰L問(wèn)狀態(tài)碼屬性。
  5. 我們聲明一個(gè)從handleNetworkResponse方法得到的結(jié)果,之后在switch-case區(qū)塊檢查這個(gè)結(jié)果。
  6. 成功意味著我們成功地和API聯(lián)系,并得到一個(gè)適當(dāng)?shù)捻憫?yīng)。之后我們檢查這個(gè)響應(yīng)是否攜帶數(shù)據(jù)。如果沒(méi)有數(shù)據(jù)我們就用返回語(yǔ)句退出這個(gè)方法。
  7. 如果攜帶有數(shù)據(jù),我們需要把數(shù)據(jù)編碼成我們的模式,之后我們把編碼好的電影傳遞給完成部分。
  8. 如果結(jié)果是失敗,我們就把錯(cuò)誤傳遞給完成部分。

這就完成了,這就是我們不依賴Cocoapods和第三方庫(kù)的純Swift網(wǎng)絡(luò)層。想要測(cè)試api請(qǐng)求能否獲取電影,就創(chuàng)建一個(gè)帶有Network Manager 的viewController之后在管理員調(diào)用getNewMovies。

 

  1. class MainViewController: UIViewController { 
  2.      
  3.     var networkManager: NetworkManager! 
  4.       
  5.     init(networkManager: NetworkManager) { 
  6.         super.init(nibName: nil, bundle: nil) 
  7.         self.networkManager = networkManager 
  8.     } 
  9.      
  10.     required init?(coder aDecoder: NSCoder) { 
  11.         fatalError("init(coder:) has not been implemented"
  12.     } 
  13.       
  14.     override func viewDidLoad() { 
  15.         super.viewDidLoad() 
  16.         view.backgroundColor = .green 
  17.         networkManager.getNewMovies(page: 1) { movies, error in 
  18.             if let error = error { 
  19.                 print(error) 
  20.             } 
  21.             if let movies = movies { 
  22.                 print(movies) 
  23.             } 
  24.         } 
  25.     } 

MainViewControoler的示例

迂回網(wǎng)絡(luò)(DETOUR- NETWORK)記錄器

我最喜歡的Moya特性之一就是網(wǎng)絡(luò)記錄器。它使得調(diào)試變得更容易,并且通過(guò)記錄所有網(wǎng)絡(luò)通信可以看到關(guān)于請(qǐng)求和響應(yīng)發(fā)生了什么。我決定實(shí)現(xiàn)這個(gè)網(wǎng)絡(luò)層時(shí)候就想要有這個(gè)特性了。創(chuàng)建一個(gè)名為NetworkLogger的文件并把它放在服務(wù)組中。我已經(jīng)實(shí)現(xiàn)了一個(gè)記錄對(duì)控制臺(tái)請(qǐng)求的代碼。我不會(huì)展示我們應(yīng)該把代碼放到代碼層中的哪里。這是對(duì)你的一個(gè)挑戰(zhàn),創(chuàng)建一個(gè)記錄控制臺(tái)響應(yīng)的函數(shù),并在我們的架構(gòu)中找到合適的位置放置它們。

提示:靜態(tài)函數(shù)記錄(響應(yīng):URLResponse)

小技巧

你在Xcode中遇到過(guò)不理解的占位符嗎?比如讓我們看看剛剛為了實(shí)現(xiàn)Router寫(xiě)的代碼

 

22.png

NetworkRouterCompletion是我們實(shí)現(xiàn)的。即使我們實(shí)現(xiàn)了它,有時(shí)候也很難記清它是哪種類型,我們?cè)撛趺从盟?。我們喜歡的Xcode有解決辦法。只要在占位符上雙擊,Xcode就會(huì)告訴你。

 

23.png

結(jié)論

我們有了一個(gè)簡(jiǎn)單好用,面向協(xié)議,還可以自己定制的網(wǎng)絡(luò)層。我們能完全控制它的功能,完全理解它的機(jī)制。通過(guò)進(jìn)行這個(gè)練習(xí),我可以說(shuō)我本人學(xué)到不少新事情。所以比起那些只需要裝一個(gè)庫(kù)就能完成的工作,我對(duì)這項(xiàng)工作更感到自豪。希望這篇文章能說(shuō)明,用Swift創(chuàng)建你自己的網(wǎng)絡(luò)層并沒(méi)那么難。只要不做這樣的事情就行了:

 

[[228878]]

你可以在我的GitHub上找到源代碼,感謝閱讀。

責(zé)任編輯:未麗燕 來(lái)源: Malcolm Kumwenda
相關(guān)推薦

2015-09-15 10:40:41

Swift2.0MVVM

2015-08-04 08:56:14

swift子類

2018-07-23 15:55:28

協(xié)議自定義viewSwift

2014-06-27 10:04:55

網(wǎng)絡(luò)協(xié)議ipv4IP

2010-07-13 13:50:44

HART協(xié)議

2018-09-19 15:53:11

SwiftiOS系統(tǒng)

2010-07-06 16:08:51

HART協(xié)議

2019-04-14 22:33:52

網(wǎng)絡(luò)層協(xié)議VLAN虛擬局域網(wǎng)

2019-01-30 10:18:46

七層協(xié)議網(wǎng)絡(luò)通信

2022-07-30 23:41:53

面向過(guò)程面向?qū)ο?/a>面向協(xié)議編程

2021-03-11 13:56:13

協(xié)議Python網(wǎng)絡(luò)

2011-11-10 09:43:14

ZigBee協(xié)議棧網(wǎng)絡(luò)層

2010-09-09 16:56:08

七層網(wǎng)絡(luò)協(xié)議

2010-09-09 16:48:50

七層網(wǎng)絡(luò)協(xié)議

2010-06-09 10:28:20

2016-12-12 15:22:41

編程

2023-04-06 07:57:29

RPC服務(wù)網(wǎng)絡(luò)協(xié)議

2022-06-27 09:00:55

SwiftGit Hooks

2019-05-21 09:11:50

七層協(xié)議OSITCP

2010-06-09 12:20:34

網(wǎng)絡(luò)通信協(xié)議層
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)