2019-09-20

本文鏈接:https://blog.csdn.net/Hello_Hwc/article/details/72853786

image.png

前言:

Alamofire是一個由Swift編寫的優(yōu)雅的網(wǎng)絡(luò)開發(fā)框架。

大部分用Swift編寫的iOS App的網(wǎng)絡(luò)模塊都是基于Alamofire的。作為Swift社區(qū)最活躍的幾個項目之一,有許多開發(fā)者在不斷的對其進行完善,所以學(xué)習這種優(yōu)秀的開源代碼對深入理解Swift的特性很有幫助。

本文很長,大到整個框架的設(shè)計,小到某些基礎(chǔ)功能的使用都會涉及。

URL Loading System
iOS的網(wǎng)絡(luò)開發(fā)(URL Loading System)的類層次如下:

image.png

從圖中可以看出,整個框架包括URL Loading相關(guān)的核心類和五種輔助類。其中,五種輔助類劃分如下

Configuration 配置信息,比如Cookie的存儲策略,TLS版本等等。
Authentication and Credentials 授權(quán)和證書
Protocol support 用做proxy來攔截或特殊處理某些URL
Cookie Storage 管理Cookie
Cache Management 管理緩存
Alamofire就是建立在NSURLSession上的封裝。

NSURLSession是在2013年推出的新API,并且Apple在2015年廢棄了NSURLConnection。如果你的App還在用以NSURLConnection建立的網(wǎng)絡(luò)層(比如AFNetworking 2.x),那么你真的應(yīng)該考慮升級到NSURLSession(比如AFNetworking 3.x),廢棄的API也許還能正常工作,但是Apple已對其不再維護,當然也就不支持HTTP 2.0等新特性。

關(guān)于NSURLSesson的基礎(chǔ)使用,我之前有過幾篇博客,可以在這個鏈接找到:

博客iOS網(wǎng)絡(luò)開發(fā)分類
那么,用NSURLSession來進行HTTP/HTTPS請求的時候,實際的過程如何呢?

image.png

建立NSURLSessionTask,并且resume.
檢查cache策略,如果有需要從本地cache中直接返回數(shù)據(jù)
通過DNS進行域名查找
建立TCP連接
如果是HTTPS,進行TLS握手(如有資源需要認證訪問,可能需要客戶端提供證書,用戶名密碼等信息)
請求開始,收到HTTP的Response
接收HTTP的Data
Tips: 理解HTTP/HTTPS的請求過程很重要,因為往往你需要統(tǒng)計API請求在哪個階段出了問題,然后對癥下藥,提高用戶體驗。

整體架構(gòu)

Alamofie的整體功能圖如下:

image.png

其中

左側(cè)是暴露給外部的接口,右側(cè)是內(nèi)部實現(xiàn)相關(guān)
這三個模塊比較獨立:AlamofireImage 和 AlamofireNetworkActivityIndicator 是基于Alamofire開發(fā)的獨立的庫,分別用來做圖片和網(wǎng)絡(luò)狀態(tài)小菊花,NetworkReachabilityManager也是先對獨立的用來檢測蜂窩移動,WIFI等網(wǎng)絡(luò)變化的。
我們先從一個API調(diào)用切入,來分析各個模塊的作用:

Alamofire.request(/**/).validate(/**/).responseJSON {/**/}

初始化SessionManager的單例default
//整理后代碼

self.delegate = SessionDelegate()
self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)

在初始化SessionManager代碼里,提供了一個默認的SessionDelegate,并且初始化了一個URLSession,這個URLSession的delegate是SessionDelegate。

通過這個初始化,我們知道URLSession的幾個代理事件都是傳遞給SessionManager的SessionDelegate了。

執(zhí)行全局方法Alamofire.request
方法體中調(diào)用SessionManager.default單例的實例方法來創(chuàng)建DataRequest。這一步做了如下動作:

根據(jù)傳入的url,parameters等參數(shù)創(chuàng)建URLRequest
根據(jù)URLRequest和SessionManager的屬性session(URLSession),adapter(請求適配器),queue(GCD queue)創(chuàng)建URLSessionDataTask
根據(jù)基類Request提供的方法,創(chuàng)建子類DataRequest實例,并且為子類DataRequest初始化一個DataTaskDelegate。
每一個DataRequest對應(yīng)一個DataTaskDelegate,每一個TaskDelegate有一個OperationQueue,這個queue在初始化的時候是掛起狀態(tài)的,并且是一個串行隊列(maxConcurrentOperationCount = 1)。

open class Request{
    init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
    self.session = session
    switch requestTask {
    case .data(let originalTask, let task):
        taskDelegate = DataTaskDelegate(task: task)
        self.originalTask = originalTask
        //省略
    }
    delegate.error = error
    delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() } //加入統(tǒng)計請求結(jié)束的Operation
    }
}

按需執(zhí)行DataTask的resume方法
執(zhí)行DataTask.validate
內(nèi)容很簡單,就是把傳入的閉包保存起來,等待后續(xù)執(zhí)行,并且返回Self

執(zhí)行DataTask.responseJSON
在這個方法里,創(chuàng)建一個NSOperation加入到DataTaskDelegate的queue中,這個queue在創(chuàng)建之初是刮掛起狀態(tài)的,所以提交的任務(wù)不會執(zhí)行。

URLSession收到數(shù)據(jù)
首先SessionDelegate代理方法被調(diào)用:

open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    if let dataTaskDidReceiveData = dataTaskDidReceiveData {//有自定義實現(xiàn)
            dataTaskDidReceiveData(session, dataTask, data)
        } else if let delegate = self[dataTask]?.delegate as? DataTaskDelegate {//走默認實現(xiàn)
            delegate.urlSession(session, dataTask: dataTask, didReceive: data)
        }
}

在這個代理方法里,根據(jù)存儲的字典 URLSessionTask -> TaskDelegate 找到這個task的DataTaskDelegate,調(diào)用其方法

 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    //整理后代碼
    mutableData.append(data) //存儲數(shù)據(jù)到內(nèi)存
   progressHandler.queue.async { progressHandler.closure(self.progress) } //回調(diào)progressHandler
}

URLSession完成Task
首先調(diào)用SessionDelegate中的URLSession的代理方法

open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
//整理后代碼

//執(zhí)行response的validation
request.validations.forEach { $0() } 
//喚起queue,來執(zhí)行提交的任務(wù)
strongSelf[task]?.delegate.queue.isSuspended = false
strongSelf[task] = nil
}

由于queue被喚起,所以之前提交的完成callback會被執(zhí)行。

執(zhí)行網(wǎng)絡(luò)請求完成的callback
//序列化請求結(jié)果,這里的responseSerializer為DataResponseSerializerProtocol協(xié)議類型

let result = responseSerializer.serializeResponse(
                self.request,
                self.response,
                self.delegate.data,
                self.delegate.error
            )
//建立Response對象
var dataResponse = DataResponse<T.SerializedObject>(
    request: self.request,
    response: self.response,
    data: self.delegate.data,
    result: result,
    timeline: self.timeline
)
//增加統(tǒng)計相關(guān)信息
dataResponse.add(self.delegate.metrics)

//執(zhí)行傳入的必報,也就是responseJSON函數(shù)傳入的閉包
(queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }

API設(shè)計
衡量一個框架好壞最重要的因素就是是否容易使用。

那么,如何定義容易使用呢?

根據(jù)二八原則,對于一個框架的使用百分之八十的時候都是很基礎(chǔ)的功能使用,當這些基礎(chǔ)的功能使用是容易的,我們認為這個框架是容易使用的。

我們來對比一下,同樣GET一個URL,然后把數(shù)據(jù)解析成JSON。使用NSURLSession層次的API如下

guard let url = URL(string: "https://raw.githubusercontent.com/LeoMobileDeveloper/React-Native-Files/master/person.json") else {
    return;
}
let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
    guard let data = data else{
        return;
    }
    do{
        let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
        print(json)
    }catch let error{
        print(error)
    }
};
dataTask.resume()

使用Alamofire

Alamofire.request("https://raw.githubusercontent.com/LeoMobileDeveloper/React-Native-Files/master/person.json").responseJSON { (response) in
    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

Tips: 這里的Alamofire.request指的是module(模塊) Alamofire的一個全局方法request調(diào)用。

可以看到,使用系統(tǒng)的API,我們不得不先創(chuàng)建URL,然后建立DataTask,并且Resume。接著在callback里去解析JSON。由于Swift是一種強類型的語言,我們不得不進行大量的邏輯判斷和try-catch。

而Alamofire把這些步驟簡化成了一個靜態(tài)的方法調(diào)用,并且用鏈式的方式來處理異步的Response解析。由于是鏈式的,你可以用鏈式的方式實現(xiàn)很多邏輯,比如驗證返回值:

Alamofire.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300) //返回值驗證
    .responseData { response in //解析返回的數(shù)據(jù)
        switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }
    }

用鏈式的方式進行異步處理是一個很好的實踐,延伸閱讀可以參考:PromiseKit,RxSwift。

鏈式的異步處理有很多優(yōu)點:

優(yōu)雅的處理大量的callback
代碼更容易理解,更容易維護
不需要在每一步都進行錯誤檢查
80%情況下的API調(diào)用
Alamofire是采用靜態(tài)方法的方式來提供80%情況下的API,這些全局方法可以在Alamofire.swift找到,以request為例:

@discardableResult //關(guān)鍵詞告訴編譯器,即使返回值不被持有,也別報警告
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    return SessionManager.default.request(
        url,
        method: method,
        parameters: parameters,
        encoding: encoding,
        headers: headers
    )
}

我們來分析下這個簡單卻又精煉的方法,方法的幾個參數(shù)

url 請求的URL,協(xié)議URLConvertible類型(Alamofire用extension的方式為URL,String,URLComponents實現(xiàn)了這個協(xié)議)
method 請求的HTTP方法,默認為GET
parameters 請求的參數(shù),默認為nil
encoding,,參數(shù)編碼類型,默認URLEncoding.default,也就是根據(jù)HTTP方法的類型決定參數(shù)是query或者body里
headers, HTTP Header
返回值是一個DataRequest實例,這個實例就是異步調(diào)用鏈的頭部。

Tips: 用默認參數(shù)來實現(xiàn)默認配置是一個很好的實踐。

如何實現(xiàn)鏈式調(diào)用

open class Request {
    var validations: [() -> Void] = []
    public func validate(_ validation: @escaping Validation) -> Self {
        let validationExecution: () -> Void = {/**/}
        validations.append(validationExecution)
        return self
    }
}

從代碼中,我們可以比較清楚的看出鏈式調(diào)用的原理:

函數(shù)的參數(shù)是閉包類型,方法體把這個閉包類型輸入存儲起來,并且返回Self。在合適的時候執(zhí)行閉包即可實現(xiàn)異步的鏈式調(diào)用。

模塊功能
軟件設(shè)計有一個非常重要的原則就是:單一功能原則。

Alaofire的文件劃分如下:

image.png

我們來分析Alamofire的各個模塊負責的功能:

SessionManager 整個Alamofire框架的核心樞紐,封裝了URLSession。負責提供外部調(diào)用的API,處理請求適配器,請求的重拾。
SessionDelegate SessionManager的代理,封裝了URLSessionDelegate。負責對Task的回調(diào)事件提供默認實現(xiàn)(轉(zhuǎn)發(fā)給TaskDelegate進行實際處理),并且以閉包的方式暴露給外部,讓外部可以自定義實現(xiàn)。
TaskDelegate 對URLSessionTask的回調(diào)進行實際的處理,并且執(zhí)行task的完成回調(diào)用
Request 是URLSessionTask的封裝,是暴露給上層的請求任務(wù)
ParameterEncoding 對參數(shù)進行Encoding(JSON,query等)
Response 代表返回數(shù)據(jù)序列化后的結(jié)果
ResponseSerializer 對返回的數(shù)據(jù)進行序列化(JSON,property list等)
ServerTrustPolicyManager/ServerTrustPolicy 對TLS等過程中發(fā)生的認證進行處理
Timeline 純粹的用來進行網(wǎng)絡(luò)請求過程的數(shù)據(jù)統(tǒng)計
線程
Alamofire的線程處理都是采用GCD和NSOperation,并沒有使用底層的Thread。

SessionManager
每一個SessionManager有一個常量屬性

let queue = DispatchQueue(label: "org.alamofire.session-manager." + UUID().uuidString)
1
這個queue用來做task的初始化工作,也做了比如文件創(chuàng)建等

//task初始化
return task = queue.sync { session.downloadTask(with: urlRequest) }
//創(chuàng)建目錄
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)

URLSession
Session是這樣被初始化的:

self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
1
delegateQueue是URLSession的各種回調(diào)函數(shù)被調(diào)用的串行隊列,這里傳入nil,表示由系統(tǒng)自動為我們創(chuàng)建回調(diào)隊列。

GlobalQueue
關(guān)于全局隊列,有如下使用

//重試
DispatchQueue.utility.after{}

//初始化上傳的MultipartFormData
DispatchQueue.global(qos: .utility).async

TaskDelegate
每一個Task有一個TaskDelegate,每一個TaskDelegate有一個常量屬性queue

self.queue = {
            let operationQueue = OperationQueue()

            operationQueue.maxConcurrentOperationCount = 1
            operationQueue.isSuspended = true
            operationQueue.qualityOfService = .utility

            return operationQueue
        }()

這個queue有一點黑科技,在創(chuàng)建的時候是掛起的,然后不斷的往里塞任務(wù):比如responseJSON等。然后等Task完成的時候,再喚起queue,執(zhí)行這些任務(wù)。

還是舉一個例子,我們來看看隊列之前的切換:

Alamofire.request(//).validate(//).responseJSON {/**/}
1
主隊列調(diào)用request方法
sync到SessionManager的queue上創(chuàng)建URLSessionDataTask
主隊列調(diào)用validate方法和responseJSON保存相關(guān)閉包
URLSession中由系統(tǒng)自動創(chuàng)建的queue收到delegate事件回調(diào)
收到URLSessionTask完成的回調(diào),TaskDelegate的queue被喚起
異步到主隊列執(zhí)行responseJSON中傳入的閉包
當然,上述的隊列使用不包括以參數(shù)方式傳遞進入的,比如responseJSON,就可以指定這個閉包執(zhí)行的隊列

  public func responseJSON(
        queue: DispatchQueue? = nil,
        options: JSONSerialization.ReadingOptions = .allowFragments,
        completionHandler: @escaping (DataResponse<Any>) -> Void)
        -> Self{}

錯誤處理

數(shù)據(jù)結(jié)構(gòu)
AlamoFire的錯誤處理是采用了帶關(guān)聯(lián)值枚舉,在Swift開發(fā)中,枚舉是最常見的用來處理錯誤的。

在關(guān)聯(lián)值枚舉中,Alamofire還定義了內(nèi)部類型,來對錯誤類型進行二次分類。代碼如下:

public enum AFError: Error {
    public enum ParameterEncodingFailureReason {/*省略*/}
    public enum MultipartEncodingFailureReason {/*省略*/}
    public enum ResponseValidationFailureReason {/*省略*/}
    public enum ResponseSerializationFailureReason {/*省略*/}

    //枚舉的可能值
    case invalidURL(url: URLConvertible)
    case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
    case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
    case responseValidationFailed(reason: ResponseValidationFailureReason)
    case responseSerializationFailed(reason: ResponseSerializationFailureReason)
}

我們來分析為什么要這樣定義這些錯誤類型,一個典型的網(wǎng)絡(luò)庫的請求數(shù)據(jù)流如下:

其中,在調(diào)用URLSession相關(guān)的API之前,我們要先創(chuàng)建URLRequest,然后交給URLSession去做實際的HTTP請求,然后拿到HTTP請求的二進制數(shù)據(jù),根據(jù)需要轉(zhuǎn)換成字符串/JSON等交給上層。

所以,Alomofire的錯誤處理思想是:

根據(jù)錯誤發(fā)生的位置進行一級分類,再用嵌套類型對錯誤進行二次分類。

除了錯誤定義之外,開發(fā)者抓到錯誤能有友善的描述信息也是很重要的,這就是。Swift提供LocalizedError

extension AFError: LocalizedError {
    public var errorDescription: String? {
        /*省略*/
    }
}
extension AFError.ParameterEncodingFailureReason {
    var localizedDescription: String {
      /*省略*/
    }
}

Swift錯誤處理延伸閱讀: 詳解Swift中的錯誤處理

繼承

NRULSessionTask是由繼承來實現(xiàn)的,繼承關(guān)系如下

URLSessionTask — Task的基類
URLSessionDataTask - 拉取URL的內(nèi)容NSData
URLSessionUploadTask — 上傳數(shù)據(jù)到URL,并且返回是NSData
URLSessionDownloadTask - 下載URL的內(nèi)容到文件
URLSessionStreamTask — 建立TCP/IP連接
仿照這種關(guān)系,Alamofire的Request也是類似的繼承關(guān)系:

Request — Task的基類
DataRequest - 拉取URL的內(nèi)容NSData
UploadRequest — 上傳數(shù)據(jù)到URL,并且返回是NSData
DownloadRequest - 下載URL的內(nèi)容到文件
StreamRequest — 建立TCP/IP連接
其實原因和很簡單:父類提供基礎(chǔ)的屬性和方法來給子類復(fù)用。

在Request中,除了繼承,還使用了聚類的方式:由父類提供接口,初始化子類

    init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
        self.session = session

        switch requestTask {
        case .data(let originalTask, let task):
            taskDelegate = DataTaskDelegate(task: task)
            self.originalTask = originalTask
            /**/
        }
    }

協(xié)議

Swift是面向協(xié)議編程的語言。

Alamofire的很多設(shè)計都是以協(xié)議為中心的,

image.png

以ParameterEncoding協(xié)議:

定義如下:

public protocol ParameterEncoding {
    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}

接口依賴于這個協(xié)議類型

public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    /*略*/
}

這樣在傳入的時候,只要是這個協(xié)議類型都可以,不管是struct,enum或者class。

Alamofire實現(xiàn)了三種Encoding方式:

public struct URLEncoding: ParameterEncoding {}
public struct JSONEncoding: ParameterEncoding {}
public struct PropertyListEncoding: ParameterEncoding {}

擴展性

由于提供的接口是協(xié)議類型的,于是你可以方便直接把一個實例當作url,并且自定義encodeing方法

enum API:URLConvertible{
    case login
    public func asURL() throws -> URL {
        //Return login URL
    }
}
class CustomEncoding: ParameterEncoding{/*/*}

然后,你就可以這么調(diào)用了

Alamofire.request(API.login, method: .post, encoding: CusomEncoding())

可以看到,使用協(xié)議提供的接口是抽象的接口,與具體的class/enum/struct無關(guān),也就易于擴展

代理

代理是CocoaTouch一個很優(yōu)秀的設(shè)計模式,它提供了一種盲通信的方式把相關(guān)的任務(wù)劃分到不同的類中。

在Alamofire中,最主要的就是這兩對代理關(guān)系:

image.png

由于Delegate的存在,

SessionManager只需要關(guān)注URLSession的封裝即可,session層面的事件回調(diào)交給由SessionDelegate處理
Request只需要關(guān)注URLSessionTask的封裝,task層面的任務(wù)交給RequestDelegate處理。
這樣,保證了各個模塊之間的功能單一,不會互相耦合。

類型安全
Swift本身是一種類型安全的語言,這意味著如果編譯器發(fā)現(xiàn)類型不對,你的代碼將編譯不通過。

URLRequest有一個屬性是HTTPMethod

var httpMethod: String? { get set }

它的類型是String類型,這意味著你可以隨意的賦值,編譯器缺不會提示你你的輸入可能又問題。

request.httpMethod = "1234"

考慮到HTTPMethod無非也就是那幾種,很適合用enum來做,Alamofire對其進行了封裝

public enum HTTPMethod: String {
    case options = "OPTIONS"
    case get     = "GET"
    case head    = "HEAD"
    case post    = "POST"
    case put     = "PUT"
    case patch   = "PATCH"
    case delete  = "DELETE"
    case trace   = "TRACE"
    case connect = "CONNECT"
}

然后,上層的方法提供的接口是枚舉類型:

public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get, //這里
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    /*略*/
}

這樣,編譯器就能夠進行合理的檢查,也不容易出錯了。

版本與平臺適配
Alamofire適配的平臺有ios/osx/tvos/watchos,適配的最低iOS版本是iOS 8。 那么,就出現(xiàn)了一個問題

有些平臺沒有對應(yīng)的API
有些API是高版本的系統(tǒng)才有的
舉個例子:

func streamTask(with service: NetService) -> URLSessionStreamTask

Alamofire采用如下方式進行適配:

@avialable - 用來標記適配系統(tǒng)版本(for編譯器)
比如,這個函數(shù)被標記為iOS 9.0后可用,如果直接在target iOS 8的調(diào)用,則會報錯??梢栽趇f #available{}中調(diào)用

@discardableResult
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
public func stream(withHostName hostName: String, port: Int) -> StreamRequest {
    return SessionManager.default.stream(withHostName: hostName, port: port)
}

if ... #endif - 用作條件編譯(for編譯器)

例如:在watchOS上不編譯

#if !os(watchOS)
@discardableResult
@available(iOS 9.0, macOS 10.11, tvOS 9.0, *)
public func stream(withHostName hostName: String, port: Int) -> StreamRequest {
    return SessionManager.default.stream(withHostName: hostName, port: port)
}
#endif

available - 滿足平臺和系統(tǒng)要求才調(diào)用(for 編譯器,運行時)

extension Response {
    mutating func add(_ metrics: AnyObject?) {
        #if !os(watchOS)
            guard #available(iOS 10.0, macOS 10.12, tvOS 10.0, *) else { return }
            guard let metrics = metrics as? URLSessionTaskMetrics else { return }

            _metrics = metrics
        #endif
    }
}

總結(jié)

Alamofire是一個優(yōu)雅的Swift開源庫,它的代碼真的很優(yōu)雅,強烈建議對Swift感興趣并且想深入學(xué)習的同學(xué)用幾天的空余時間去研究下。看的時候多問自己幾個問題:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • pymysql是python中操作mysql數(shù)據(jù)庫的一個優(yōu)秀的模塊,使用起來也是非常的簡單和方便,只用記住幾個下面...
    小白學(xué)習手帳閱讀 566評論 0 0
  • 智能指針在37_智能指針分析中詳細學(xué)習過。此處實例中通過模板實現(xiàn)了可復(fù)用的智能指針模板類 需要一個特殊的指針——通...
    編程半島閱讀 398評論 0 0
  • 之前寫過很多和PPT相關(guān)的文章,都是偏干貨性。但是PPT除了這些以外,和你的創(chuàng)意、想法也很有關(guān)系,當然其他也是一樣...
    大夢Power閱讀 1,434評論 2 9

友情鏈接更多精彩內(nèi)容