Alamofire(五)-- Task代理
2019-08-22  本文已影响0人  Daniel_Harvey

前言

这篇文章,咱们来说说Task代理,通过之前的文章,我们可以知道一个普通的网络请求过程是:

  • 根据一个URL和若干的参数来生成Request
  • 根据Request生成一个会话Session
  • 再根据这个Session生成Task
  • 我们开启Task就完成了这个请求。

在这个请求过程中,还有重定向、数据的上传、证书的验证、配置等信息。

其实,我们做iOS开发的,对代理这个问题,不管是在网络请求中,还是用于代理回调等地方,我觉得代理就好比是一个拥有较高权限的管理员。这种方式在我们做业务开发中,是很好的处理方式。

URLSessionTask

Task分类

在苹果原生网络框架中,URLSessionTask是最基础的task任务封装,主要有以下几种task

  • URLSessionDataTask
  • URLSessionUploadTask
  • URLSessionDownloadTask
  • URLSessionStreamTask
    注意: URLSessionStreamTask这个我们先暂时不介绍,在后面的文章中单独介绍说明。

继承关系

我们先把URLSessionTask的相关继承关系图给到大家看一下,具体代理方法我们后面慢慢讲。

URLSessionTask

URLSessionTask子类继承关系:

URLSessionTask子类.png

URLSessionTaskDelegate

URLSessionTaskDelegate继承自URLSessionDelegateURLSessionTaskDelegate的主要协议方法有:

optional func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void)

optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

optional func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)

optional func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)

optional func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
// 上传比较特殊一点,只有这一个跟上传相关的代理方法

URLSessionDataDelegate

URLSessionDataDelegate则是继承自URLSessionTaskDelegate协议,它的主要协议方法有:

optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)

optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask)

optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)

optional func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void)

URLSessionDownloadDelegate

URLSessionDownloadDelegate同样是继承自URLSessionTaskDelegate协议,主要方法有:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)

optional func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)

optional func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64)

TaskDelegate

Alamofire中,咱们可以看到,TaskDelegate类继承自NSObject,处于继承链的最底层,它为我们提供了一些基础的属性,而且,这些属性是和其他的Delegate共享使用:

// MARK: Properties

    /// The serial operation queue used to execute all operations after the task completes.
    public let queue: OperationQueue

    /// The data returned by the server.
    public var data: Data? { return nil }

    /// The error generated throughout the lifecyle of the task.
    public var error: Error?

    var task: URLSessionTask? {
        set {
            taskLock.lock(); defer { taskLock.unlock() }
            _task = newValue
        }
        get {
            taskLock.lock(); defer { taskLock.unlock() }
            return _task
        }
    }

    var initialResponseTime: CFAbsoluteTime?
    var credential: URLCredential?
    var metrics: AnyObject? // URLSessionTaskMetrics

    private var _task: URLSessionTask? {
        didSet { reset() }
    }

    private let taskLock = NSLock()

我们接下来分析一下这些属性。

属性简介

queue

queue: OperationQueue,显而易见,这就是一个队列,在队列中,我们可以添加很多的operation,而且,当我们把isSuspended的值设置为true,就可以让队列中的所有operation暂停,如果想要继续执行operation,我们需要把isSuspended的值设置为false
Alamofire框架中,有以下几种情况,会加入该队列的operation

  • 队列在任务完成后,把isSuspended的值设置为false
  • 在任务完成后调用RequestendTime,就可以为Request设置请求结束时间。
  • 在处理response中,Alamofire中的响应回调是链式的,原理就是把这些回调函数通过operation添加到队列中,因此也保证了回调函数的访问顺序是正确的。
  • 还有上传数据成功后,删除一些临时文件等操作。

data

data: Data?,服务器返回的Data,?表示这个可能为空。

error

error: Error?,这个应该很好理解了,在网络请求过程中,很有可能出现错误,那么我们添加这个属性,就是为了抓取过程中出现的错误。

task

task: URLSessionTask?,就是表示一个task,很重要的属性。

initialResponseTime

initialResponseTime: CFAbsoluteTime?,这是一个task的响应时间,如果是URLSessionDataTask,则表示接收到数据的时间;如果是URLSessionDownloadTask,则表示开始写数据的时间;如果是URLSessionUploadTask,则表示上传数据的时间。

credential

credential: URLCredential?,这个表示证书,在做证书验证的时候用到。

metrics

metrics: AnyObject?,这是苹果提供的一个统计task信息的类URLSessionTaskMetrics,它可以统计相关任务的事务,任务开始时间、结束时间,以及重定向的次数等信息。

生命周期Lifecycle

首先,还是来看一看代码:

// MARK: Lifecycle

    init(task: URLSessionTask?) {
        _task = task

        self.queue = {
            let operationQueue = OperationQueue()

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

            return operationQueue
        }()
    }

    func reset() {
        error = nil
        initialResponseTime = nil
    }

init函数

在初始化函数中,主要就是设置operationQueueoperationQueue.isSuspended = true,就可以保证队列中的operation都是暂停的,通常情况下,operation在被加入到队列中后,会立即执行。

reset函数

reset函数把errorinitialResponseTime都置为nil,这个就很简单了,不说了。

URLSessionTaskDelegate

接下来,我们看看URLSessionTaskDelegate的相关函数:

// MARK: URLSessionTaskDelegate

    var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
    var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
    var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
    var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?

taskWillPerformHTTPRedirection

首先来看一下第一个代理方法:

@objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        var redirectRequest: URLRequest? = request

        if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
            redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
        }

        completionHandler(redirectRequest)
    }

根据函数名的意思,我们应该知道,这个函数就是用来处理重定向问题的,这个函数要求返回一个redirectRequest,顾名思义,就是重定向Request的,处理方式就是:如果给代理的重定向函数赋值了,就会返回代理函数的返回值,否则返回服务器的Request

taskDidReceiveChallenge

这个方法就是用来处理请求验证相关的,看一下:

@objc(URLSession:task:didReceiveChallenge:completionHandler:)
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
    {
        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
        var credential: URLCredential?

        if let taskDidReceiveChallenge = taskDidReceiveChallenge {
            (disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
        } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let host = challenge.protectionSpace.host

            if
                let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
                let serverTrust = challenge.protectionSpace.serverTrust
            {
                if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
                    disposition = .useCredential
                    credential = URLCredential(trust: serverTrust)
                } else {
                    disposition = .cancelAuthenticationChallenge
                }
            }
        } else {
            if challenge.previousFailureCount > 0 {
                disposition = .rejectProtectionSpace
            } else {
                credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)

                if credential != nil {
                    disposition = .useCredential
                }
            }
        }

        completionHandler(disposition, credential)
    }

首先,我们来看一下disposition,它的类型是URLSession.AuthChallengeDisposition,这个类型就是一个枚举类型:

@available(iOS 7.0, *)
    public enum AuthChallengeDisposition : Int {

        
        case useCredential /* Use the specified credential, which may be nil */

        case performDefaultHandling /* Default handling for the challenge - as if this delegate were not implemented; the credential parameter is ignored. */

        case cancelAuthenticationChallenge /* The entire request will be canceled; the credential parameter is ignored. */

        case rejectProtectionSpace /* This challenge is rejected and the next authentication protection space should be tried; the credential parameter is ignored. */
    }
  • useCredential:使用的证书
  • performDefaultHandling:采用默认的方式,和服务器返回的authenticationMethod有很大关系
  • cancelAuthenticationChallenge:取消认证
  • rejectProtectionSpace:拒绝认证

我们在进行验证的时候,有三种验证方式:

  • 客户端验证
  • 服务器验证
  • 双向验证

从上面的函数方法,可以知道,如果服务器需要验证客户端,只需要给TaskDelegatetaskDidReceiveChallenge赋值就可以了。

Alamofire的双向验证中,客户端和服务端如果需要建立SSL,只需要2步可以完成:

  • 服务端返回WWW-Authenticate响应头,并返回自己信任证书
  • 客户端验证证书,然后用证书中的公钥把数据加密后发送给服务端

taskNeedNewBodyStream

同样的,我们先来看一下代码:

@objc(URLSession:task:needNewBodyStream:)
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
    {
        var bodyStream: InputStream?

        if let taskNeedNewBodyStream = taskNeedNewBodyStream {
            bodyStream = taskNeedNewBodyStream(session, task)
        }

        completionHandler(bodyStream)
    }

当给taskRequest提供一个body stream时才会调用,我们不需要关心这个方法,即使我们通过fileURL或者NSData上传数据时,该函数也不会被调用。

taskDidCompleteWithError

一样的,先看代码:

@objc(URLSession:task:didCompleteWithError:)
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let taskDidCompleteWithError = taskDidCompleteWithError {
            taskDidCompleteWithError(session, task, error)
        } else {
            if let error = error {
                if self.error == nil { self.error = error }

                if
                    let downloadDelegate = self as? DownloadTaskDelegate,
                    let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
                {
                    downloadDelegate.resumeData = resumeData
                }
            }

            queue.isSuspended = false
        }
    }

该函数在请求完成后被调用,值得注意的是error不为nil的情况,除了给自身的error属性赋值外,针对下载任务做了特殊处理,就是把当前已经下载的数据保存在downloadDelegate.resumeData中,有点像断点下载。

DataTaskDelegate

属性

首先,还是先来看一下它的属性:

// MARK: Properties

    var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }

    override var data: Data? {
        if dataStream != nil {
            return nil
        } else {
            return mutableData
        }
    }

    var progress: Progress
    var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?

    var dataStream: ((_ data: Data) -> Void)?

    private var totalBytesReceived: Int64 = 0
    private var mutableData: Data

    private var expectedContentLength: Int64?
  • dataTask: URLSessionDataTaskDataTaskDelegate管理URLSessionDataTask
  • data: Data?:同样是返回Data,但这里有一点不同,如果定义了dataStream方法的话,这个data返回为nil
  • progress: Progress:进度,这个就不解释了
  • progressHandler:这不是函数,是一个元组,我们等下具体说说
  • dataStream:自定义的数据处理函数
  • totalBytesReceived:已经接收的数据
  • mutableData:保存数据的容器
  • expectedContentLength:接收的数据的总大小

生命周期

先看代码:

// MARK: Lifecycle

    override init(task: URLSessionTask?) {
        mutableData = Data()
        progress = Progress(totalUnitCount: 0)

        super.init(task: task)
    }

    override func reset() {
        super.reset()

        progress = Progress(totalUnitCount: 0)
        totalBytesReceived = 0
        mutableData = Data()
        expectedContentLength = nil
    }

这些很简单的,没有什么可说的。

方法调用

主要是看看它的函数方法:

// MARK: URLSessionDataDelegate

    var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
    var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
    var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
    var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

dataTaskDidReceiveResponse

func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didReceive response: URLResponse,
        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
    {
        var disposition: URLSession.ResponseDisposition = .allow

        expectedContentLength = response.expectedContentLength

        if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
            disposition = dataTaskDidReceiveResponse(session, dataTask, response)
        }

        completionHandler(disposition)
    }

当收到服务端的响应后,该方法被触发。在这个函数中,我们能够获取到和数据相关的一些参数。

dataTaskDidBecomeDownloadTask

func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didBecome downloadTask: URLSessionDownloadTask)
    {
        dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
    }

dataTaskDidReceiveResponse方法中,disposition的类型是URLSession.ResponseDisposition,这是一个枚举类型:

@available(iOS 7.0, *)
    public enum ResponseDisposition : Int {

        
        case cancel /* Cancel the load, this is the same as -[task cancel] */

        case allow /* Allow the load to continue */

        case becomeDownload /* Turn this request into a download */

        @available(iOS 9.0, *)
        case becomeStream /* Turn this task into a stream task */
    }

因此,当我们设置成becomeDownload时,dataTaskDidBecomeDownloadTask方法就会被调用,创建了一个新的downloadTask

dataTaskDidReceiveData

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

        if let dataTaskDidReceiveData = dataTaskDidReceiveData {
            dataTaskDidReceiveData(session, dataTask, data)
        } else {
            if let dataStream = dataStream {
                dataStream(data)
            } else {
                mutableData.append(data)
            }

            let bytesReceived = Int64(data.count)
            totalBytesReceived += bytesReceived
            let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown

            progress.totalUnitCount = totalBytesExpected
            progress.completedUnitCount = totalBytesReceived

            if let progressHandler = progressHandler {
                progressHandler.queue.async { progressHandler.closure(self.progress) }
            }
        }
    }

这个方法会把数据放入对象中,对自定义函数和进度信息进行处理。

dataTaskWillCacheResponse

func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        willCacheResponse proposedResponse: CachedURLResponse,
        completionHandler: @escaping (CachedURLResponse?) -> Void)
    {
        var cachedResponse: CachedURLResponse? = proposedResponse

        if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
            cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
        }

        completionHandler(cachedResponse)
    }

该函数用于处理是否需要缓存响应,Alamofire默认是缓存这些response的,但是每次发请求,它不会再缓存中读取。

DownloadTaskDelegate

属性

// MARK: Properties

    var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }

    var progress: Progress
    var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?

    var resumeData: Data?
    override var data: Data? { return resumeData }

    var destination: DownloadRequest.DownloadFileDestination?

    var temporaryURL: URL?
    var destinationURL: URL?

    var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }

有和上面重复的属性,我就不说了,看下没有重复的部分:

downloadTask:和URLSessionDownloadDelegate相对应的URLSessionDownloadTask

  • resumeData:在上边我们提到过,当请求完成后,如果error不为nil,如果是DownloadTaskDelegate,就会给这个属性赋值
  • data:返回resumeData
  • destination:通过这个函数可以自定义文件保存目录和保存方式,这个保存方式分两种,为URl创建文件夹,删除已经下载且存在的文件,这个会在后续的文章中提到
    temporaryURL:临时的URL
    destinationURL:数据存储URL
    fileURLfileURL返回文件的路径,如果destination不为nil,就返回destinationURL,否则返回temporaryURL

生命周期

// MARK: Lifecycle

    override init(task: URLSessionTask?) {
        progress = Progress(totalUnitCount: 0)
        super.init(task: task)
    }

    override func reset() {
        super.reset()

        progress = Progress(totalUnitCount: 0)
        resumeData = nil
    }

方法调用

代理方法有三个:

// MARK: URLSessionDownloadDelegate

    var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
    var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
    var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?

downloadTaskDidFinishDownloadingToURL

func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL)
    {
        temporaryURL = location

        guard
            let destination = destination,
            let response = downloadTask.response as? HTTPURLResponse
        else { return }

        let result = destination(location, response)
        let destinationURL = result.destinationURL
        let options = result.options

        self.destinationURL = destinationURL

        do {
            if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
                try FileManager.default.removeItem(at: destinationURL)
            }

            if options.contains(.createIntermediateDirectories) {
                let directory = destinationURL.deletingLastPathComponent()
                try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
            }

            try FileManager.default.moveItem(at: location, to: destinationURL)
        } catch {
            self.error = error
        }
    }

当数据下载完成后,该函数被触发。系统会把数据下载到一个临时的locationURL的地方,我们就是通过这个URL拿到数据的。上边函数内的代码主要是把数据复制到目标路径中。

downloadTaskDidWriteData

func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64)
    {
        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

        if let downloadTaskDidWriteData = downloadTaskDidWriteData {
            downloadTaskDidWriteData(
                session,
                downloadTask,
                bytesWritten,
                totalBytesWritten,
                totalBytesExpectedToWrite
            )
        } else {
            progress.totalUnitCount = totalBytesExpectedToWrite
            progress.completedUnitCount = totalBytesWritten

            if let progressHandler = progressHandler {
                progressHandler.queue.async { progressHandler.closure(self.progress) }
            }
        }
    }

该代理方法在数据下载过程中被触发,主要的作用就是提供下载进度。

downloadTaskDidResumeAtOffset

func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didResumeAtOffset fileOffset: Int64,
        expectedTotalBytes: Int64)
    {
        if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
            downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
        } else {
            progress.totalUnitCount = expectedTotalBytes
            progress.completedUnitCount = fileOffset
        }
    }

如果一个下载的task是可以恢复的,那么当下载被取消或者失败后,系统会返回一个resumeData对象,这个对象包含了一些跟这个下载task相关的一些信息,有了它就能重新创建下载task,创建方法有两个:downloadTask(withResumeData:)downloadTask(withResumeData:completionHandler:),当task开始后,上边的代理方法就会被触发。

UploadTaskDelegate

属性

// MARK: Properties

    var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }

    var uploadProgress: Progress
    var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?

这些属性又得已经重复了,就不多说了。

生命周期

// MARK: Lifecycle

    override init(task: URLSessionTask?) {
        uploadProgress = Progress(totalUnitCount: 0)
        super.init(task: task)
    }

    override func reset() {
        super.reset()
        uploadProgress = Progress(totalUnitCount: 0)
    }

这也不用多说啥,主要的还是看看方法调用。

方法调用

这里只有一个方法调用:

// MARK: URLSessionTaskDelegate

    var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?

    func URLSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64)
    {
        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

        if let taskDidSendBodyData = taskDidSendBodyData {
            taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
        } else {
            uploadProgress.totalUnitCount = totalBytesExpectedToSend
            uploadProgress.completedUnitCount = totalBytesSent

            if let uploadProgressHandler = uploadProgressHandler {
                uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
            }
        }
    }

该函数主要目的是提供上传的进度,在Alamofire中,上传数据用的是stream,这个会在后续文章中给出详细的解释。

总结

学习框架的时候,好多东西当时记住了,可能一会儿就忘了,写下这篇文章的目的,就是为了加深理解印象,方便以后查阅笔记,如果文章有错误,还望指出,还得感谢一下这位朋友马在路上

上一篇下一篇

猜你喜欢

热点阅读