本篇是Alamofire中的请求抽象层的讲解

前言

在Alamofire中,围绕着Request,设计了很多额外的特性,这也恰恰表明,Request是所有请求的基础部分和发起点。这无疑给我们一个Request很复杂的想法。但看了Alamofire中Request.swift中的代码,Request被设计的又是如此的简单,这就是为什么这些顶级框架如此让人喜爱的原因。

在后续的文章中,我会单独写一篇Swift中协议的使用技巧,在Alamofire源码解读系列(一)之概述和使用这篇的Alamofire高级用法中,我根据Alamofire官方文档做了一些补充,其中涉及到了URLConvertible和URLRequestConvertible的高级用法,在本篇中同样出现了3个协议:

  • RequestAdapter 请求适配器,目的是自定义修改请求,一个典型的例子是为每一个请求调价Token请求头
  • RequestRetrier 请求重试器, 目的是控制请求的重试机制,一个典型的例子是当某个特殊的请求失败后,是否重试。
  • TaskConvertible task转换器,目的是把task装换成特定的类型,在Alamofire中有4中task:Data/Download/Upload/Stream

有一点需要特别说明的是,在使用Alamofire的高级用法时,需要操作SessionManager这个类。

请求过程

明白Alamofire中一个请求的过程,是非常有必要的。先看下边的代码:

  1. Alamofire.request("https://httpbin.org/get")

上边的代码是最简单的一个请求,我们看看Alamofire.request中究竟干了什么?

  1. @discardableResult
  2. public func request(
  3. _ url: URLConvertible,
  4. method: HTTPMethod = .get,
  5. parameters: Parameters? = nil,
  6. encoding: ParameterEncoding = URLEncoding.default,
  7. headers: HTTPHeaders? = nil)
  8. -> DataRequest
  9. {
  10. return SessionManager.default.request(
  11. url,
  12. method: method,
  13. parameters: parameters,
  14. encoding: encoding,
  15. headers: headers
  16. )
  17. }

该函数内部调用了SessionManager的request方法,这说明请求的第一个发起点来自SessionManager,Alamofire.swift该文件是最上层的封装,紧邻其下的就是SessionManager.swift。接下来我们再看看SessionManager.default.request做了什么?

  1. @discardableResult
  2. open func request(
  3. _ url: URLConvertible,
  4. method: HTTPMethod = .get,
  5. parameters: Parameters? = nil,
  6. encoding: ParameterEncoding = URLEncoding.default,
  7. headers: HTTPHeaders? = nil)
  8. -> DataRequest
  9. {
  10. var originalRequest: URLRequest?
  11. /// 在这里计算出可能出现的额错误的类型
  12. /// 1.url 如果不能被转成URL被抛出一个error
  13. /// 2.originalRequest不能转换为URLRequest会抛出error
  14. do {
  15. originalRequest = try URLRequest(url: url, method: method, headers: headers)
  16. let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
  17. return request(encodedURLRequest)
  18. } catch {
  19. return request(originalRequest, failedWith: error)
  20. }
  21. }

上边的函数内部创建了一个Request对象,然后把参数编码进这个Request中,之后又调用了内部的一个request函数,函数的参数就是上边的Request对象。我们就绪看看这个request函数做了什么?

  1. open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
  2. var originalRequest: URLRequest?
  3. do {
  4. originalRequest = try urlRequest.asURLRequest()
  5. /// 这里需要注意的是Requestable并不是DataRequest的一个属性,前边是没有加let/var的,所以可以通过DataRequest.Requestable来操作
  6. let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)
  7. let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
  8. let request = DataRequest(session: session, requestTask: .data(originalTask, task))
  9. delegate[task] = request
  10. if startRequestsImmediately { request.resume() }
  11. return request
  12. } catch {
  13. return request(originalRequest, failedWith: error)
  14. }
  15. }

注意,上边的函数是一个open函数,因此可以使用SessionManager.request来发起请求,不过参数是_ urlRequest: URLRequestConvertible

URLRequestConvertible协议的目的是对URLRequest进行自定义的转换,因此,在获得转换后的URLRequest后,需要用URLRequest生成task,这样才能发起网络请求,在Alamofire中,但凡是request开头的函数,默认的都是DataRequest类型,现在有了URLRequest还不够,还需要检测她能否生成与之相对应的task。

在上边的函数中,用到了DataRequest.Requestable,Requestable其实一个结构体,他实现了TaskConvertible协议,因此,它能够用URLRequest生成与之相对应的task。接下来就初始化DataRequest,然后真正的开始发起请求。

我们总结一下这个过程:

明白了上边的过程,再回过头来看Request.swift也就是本篇的内容就简单多了,就下边几个目的:

  • 创建DataRequest/DownloadRequest/UploadRequest/StreamRequest
  • 发起请求

Request

有很多二次封装的网络框架中,一般都有这么一个Request类,用于发送网络请求,接受response,关联服务器返回的数据并且管理task。Alamofire中的Request同样主要实现上边的任务。

Request作为DataRequest、DownloadRequest、UploadRequest、StreamRequest的基类,我们一起来看看它有哪些属性:

  1. /// The delegate for the underlying task.
  2. /// 由于某个属性是通过另一个属性来setter和getter的,因此建议加一个锁
  3. open internal(set) var delegate: TaskDelegate {
  4. get {
  5. taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
  6. return taskDelegate
  7. }
  8. set {
  9. taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
  10. taskDelegate = newValue
  11. }
  12. }
  13. /// The underlying task.
  14. open var task: URLSessionTask? { return delegate.task }
  15. /// The session belonging to the underlying task.
  16. open let session: URLSession
  17. /// The request sent or to be sent to the server.
  18. open var request: URLRequest? { return task?.originalRequest }
  19. /// The response received from the server, if any.
  20. open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse }
  21. /// The number of times the request has been retried.
  22. open internal(set) var retryCount: UInt = 0
  23. let originalTask: TaskConvertible?
  24. var startTime: CFAbsoluteTime?
  25. var endTime: CFAbsoluteTime?
  26. var validations: [() -> Void] = []
  27. private var taskDelegate: TaskDelegate
  28. private var taskDelegateLock = NSLock()

这些属性没什么好说的,我们就略过这些内容,Request的初始化方法,有点意思,我们先看看代码:

  1. init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
  2. self.session = session
  3. switch requestTask {
  4. case .data(let originalTask, let task):
  5. taskDelegate = DataTaskDelegate(task: task)
  6. self.originalTask = originalTask
  7. case .download(let originalTask, let task):
  8. taskDelegate = DownloadTaskDelegate(task: task)
  9. self.originalTask = originalTask
  10. case .upload(let originalTask, let task):
  11. taskDelegate = UploadTaskDelegate(task: task)
  12. self.originalTask = originalTask
  13. case .stream(let originalTask, let task):
  14. taskDelegate = TaskDelegate(task: task)
  15. self.originalTask = originalTask
  16. }
  17. delegate.error = error
  18. delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
  19. }

要想发起一个请求,有一个task就足够了,在上边的方法中传递过来的session主要用于CustomStringConvertibleCustomDebugStringConvertible这两个协议的实现方法中获取特定的数据。

这里有一点小提示,在创建自定义的类的时候,实现上边这两个协议,通过打印,能够进行快速的调试。

上边方法中第二个参数是requestTask,它是一个枚举类型,我们看一下:

  1. enum RequestTask {
  2. case data(TaskConvertible?, URLSessionTask?)
  3. case download(TaskConvertible?, URLSessionTask?)
  4. case upload(TaskConvertible?, URLSessionTask?)
  5. case stream(TaskConvertible?, URLSessionTask?)
  6. }

在swift中枚举不仅仅用来区分不同的选项,更强大的是为每个选项绑定的数据。大家仔细想一下,在初始化Request的时候,只需要传递requestTask这个枚举值,我们就得到了两个重要的数据:Request的类型和相对应的task。这一变成手法的运用,大大提高了代码的质量。

RequestTask枚举中和选项绑定的数据有两个,TaskConvertible表示原始的对象,该对象实现了TaskConvertible协议,能够转换成task。URLSessionTask是原始对象转换后的task。因此衍生出一种高级使用方法的可能性,可以自定义一个类,实现TaskConvertible协议,就能够操纵task的转换过程,很灵活。

  1. delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }

上边的这一行代码。给代理的queue添加了一个操作,队列是先进先出原则,但是可以通过isSuspended暂停队列内部的操作,下边是一个例子演示:

  1. let queue = { () -> OperationQueue in
  2. let operationQueue = OperationQueue()
  3. operationQueue.maxConcurrentOperationCount = 1
  4. operationQueue.isSuspended = true
  5. operationQueue.qualityOfService = .utility
  6. return operationQueue
  7. }()
  8. queue.addOperation {
  9. print("1")
  10. }
  11. queue.addOperation {
  12. print("2")
  13. }
  14. queue.addOperation {
  15. print("3")
  16. }
  17. queue.isSuspended = false

打印结果:

  1. 1
  2. 2
  3. 3

队列提供了强大的功能,了解队列的知识点非常有必要,有很大的一种可能性,也许某个问题卡住了,用队列能够很轻松的解决。有兴趣可以看看我模仿SDWebImage写的下载器MCDownloader(iOS下载器)说明书

处理网络请求,就必须要面对安全的问题,为了解决数据传输安全问题,到目前为止,已经出现了很多种解决方式。想了解这方面的知识,可以去看<<HTTP权威指南>>

Alamofire源码解读系列(一)之概述和使用中的Alamofire高级使用技巧部分。

  1. /// Associates an HTTP Basic credential with the request.
  2. ///
  3. /// - parameter user: The user.
  4. /// - parameter password: The password.
  5. /// - parameter persistence: The URL credential persistence. `.ForSession` by default.
  6. ///
  7. /// - returns: The request.
  8. /// 这里需要注意一点,persistence表示持久性,可以点击去查看详细说明
  9. @discardableResult
  10. open func authenticate(
  11. user: String,
  12. password: String,
  13. persistence: URLCredential.Persistence = .forSession)
  14. -> Self
  15. {
  16. let credential = URLCredential(user: user, password: password, persistence: persistence)
  17. return authenticate(usingCredential: credential)
  18. }
  19. /// Associates a specified credential with the request.
  20. ///
  21. /// - parameter credential: The credential.
  22. ///
  23. /// - returns: The request.
  24. @discardableResult
  25. open func authenticate(usingCredential credential: URLCredential) -> Self {
  26. delegate.credential = credential
  27. return self
  28. }

上边的这两个函数能够处理请求中的验证问题,可以用来应对用户密码和证书验证。

  1. /// Returns a base64 encoded basic authentication credential as an authorization header tuple.
  2. ///
  3. /// - parameter user: The user.
  4. /// - parameter password: The password.
  5. ///
  6. /// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
  7. open static func authorizationHeader(user: String, password: String) -> (key: String, value: String)? {
  8. guard let data = "\(user):\(password)".data(using: .utf8) else { return nil }
  9. let credential = data.base64EncodedString(options: [])
  10. return (key: "Authorization", value: "Basic \(credential)")
  11. }

这个方法是一个辅助函数,某些服务器可能需要把用户名和密码拼接到请求头中,那么可以使用这个函数来实现。

我们对一个请求的操作有下边3中可能:

  • resume 唤醒该请求,这个非常简单,函数中做了3件事:记录开始时间,唤醒task,发通知。

    1. /// Resumes the request.
    2. open func resume() {
    3. guard let task = task else { delegate.queue.isSuspended = false ; return }
    4. if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
    5. task.resume()
    6. NotificationCenter.default.post(
    7. name: Notification.Name.Task.DidResume,
    8. object: self,
    9. userInfo: [Notification.Key.Task: task]
    10. )
    11. }
  • suspend 暂停

    1. /// Suspends the request.
    2. open func suspend() {
    3. guard let task = task else { return }
    4. task.suspend()
    5. NotificationCenter.default.post(
    6. name: Notification.Name.Task.DidSuspend,
    7. object: self,
    8. userInfo: [Notification.Key.Task: task]
    9. )
    10. }
  • cancel 取消

    1. /// Cancels the request.
    2. open func cancel() {
    3. guard let task = task else { return }
    4. task.cancel()
    5. NotificationCenter.default.post(
    6. name: Notification.Name.Task.DidCancel,
    7. object: self,
    8. userInfo: [Notification.Key.Task: task]
    9. )
    10. }

Request中对CustomDebugStringConvertible和CustomStringConvertible的实现,我们就不做太多介绍了,有两点需要注意:

  1. 类似像urlCredentialStorage, httpCookieStorage这种带有Storage字段的对象,需要仔细研究一下这种代码设计的规律。

  2. 下边这一小段代码正好提现了swift的优雅之处,需要记住:

    1. for (field, value) in headerFields where field != "Cookie" {
    2. headers[field] = value
    3. }

TaskConvertible

TaskConvertible协议给了给了我们转换task的能力,任何实现了该协议的对象,都表示能够转换成一个task。我们都知道DataRequest,DownloadRequest,UploadRequest,StreamRequest都继承自Request,最终应该是通过TaskConvertible协议来把一个URLRequest转换成对应的task。

而Alamofire的Request的设计中,采用struct或者enum来实现这个协议,我们来看看这些实现;

DataRequest:

  1. struct Requestable: TaskConvertible {
  2. let urlRequest: URLRequest
  3. func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
  4. do {
  5. let urlRequest = try self.urlRequest.adapt(using: adapter)
  6. return queue.sync { session.dataTask(with: urlRequest) }
  7. } catch {
  8. throw AdaptError(error: error)
  9. }
  10. }
  11. }

DownloadRequest:

  1. enum Downloadable: TaskConvertible {
  2. case request(URLRequest)
  3. case resumeData(Data)
  4. func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
  5. do {
  6. let task: URLSessionTask
  7. switch self {
  8. case let .request(urlRequest):
  9. let urlRequest = try urlRequest.adapt(using: adapter)
  10. task = queue.sync { session.downloadTask(with: urlRequest) }
  11. case let .resumeData(resumeData):
  12. task = queue.sync { session.downloadTask(withResumeData: resumeData) }
  13. }
  14. return task
  15. } catch {
  16. throw AdaptError(error: error)
  17. }
  18. }
  19. }

如果task的类型是下载,会出现两种情况,一种是直接通过URLRequest生成downloadTask,另一种是根据已有的数据恢复成downloadTask。我们之前已经讲过了,下载失败后会有resumeData。里边保存了下载信息,这里就不提了。总之,上边这个enum给我们提供了两种不同的方式来生成downloadTask。

这种代码的设计值得学习。

UploadRequest:

  1. enum Uploadable: TaskConvertible {
  2. case data(Data, URLRequest)
  3. case file(URL, URLRequest)
  4. case stream(InputStream, URLRequest)
  5. func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
  6. do {
  7. let task: URLSessionTask
  8. switch self {
  9. case let .data(data, urlRequest):
  10. let urlRequest = try urlRequest.adapt(using: adapter)
  11. task = queue.sync { session.uploadTask(with: urlRequest, from: data) }
  12. case let .file(url, urlRequest):
  13. let urlRequest = try urlRequest.adapt(using: adapter)
  14. task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
  15. case let .stream(_, urlRequest):
  16. let urlRequest = try urlRequest.adapt(using: adapter)
  17. task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
  18. }
  19. return task
  20. } catch {
  21. throw AdaptError(error: error)
  22. }
  23. }
  24. }

虽然内容与上边的DownloadRequest不同,但是套路却相同。从代码中,也能看出,上传数据有3种介质,分别是:data,file,stream。

StreamRequest:

  1. enum Streamable: TaskConvertible {
  2. case stream(hostName: String, port: Int)
  3. case netService(NetService)
  4. func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
  5. let task: URLSessionTask
  6. switch self {
  7. case let .stream(hostName, port):
  8. task = queue.sync { session.streamTask(withHostName: hostName, port: port) }
  9. case let .netService(netService):
  10. task = queue.sync { session.streamTask(with: netService) }
  11. }
  12. return task
  13. }
  14. }

netService超出了本文的范围,就不做介绍了,平时用的也少。

我们对上边这些struct,enum做一个总结:由于struct,enum是值拷贝,因此他们比较适合作为数据的载体。一个方案的逻辑中,如果可能出现多个可能性,就考虑使用enum。还有最重要的一点,尽量把一个单一的功能的作用域限制的越小越好。功能越单一,结构越简单的函数越安全。

忽略的内容

在Request.swift的源码中,还有一个给任务添加进度的方法,在这里就不做介绍了,原理就是自定义一个函数,传递给task的代理。在DownloadRequest中对取消下载任务做了一些额外的处理。还有设置下载后的目录等等。

DownloadOptions

这个DownloadOptions其实挺有意思的,他实现了OptionSet协议,因此它就有了集合的一些特性。

在OC中,我们往往通过掩码来实现多个选项共存这一功能,但DownloadOptions用另一种方式实现了这一功能:

  1. /// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
  2. /// destination URL.
  3. public struct DownloadOptions: OptionSet {
  4. /// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol.
  5. public let rawValue: UInt
  6. /// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
  7. public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0)
  8. /// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
  9. public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1)
  10. /// Creates a `DownloadFileDestinationOptions` instance with the specified raw value.
  11. ///
  12. /// - parameter rawValue: The raw bitmask value for the option.
  13. ///
  14. /// - returns: A new log level instance.
  15. public init(rawValue: UInt) {
  16. self.rawValue = rawValue
  17. }
  18. }

上边的代码只扩展了两个默认选项:

  • createIntermediateDirectories
  • removePreviousFile

可以采用类似的手法,自己扩展更多的选项。看一下下边的例子就明白了:

  1. var op = DownloadRequest.DownloadOptions(rawValue: 1)
  2. op.insert(DownloadRequest.DownloadOptions(rawValue: 2))
  3. if op.contains(.createIntermediateDirectories) {
  4. print("createIntermediateDirectories")
  5. }
  6. if op.contains(.removePreviousFile) {
  7. print("removePreviousFile")
  8. }

上边代码中,if语句内的打印都会调用。

总结

这一篇文章与上一篇间隔了很长时间,原因是公司做了一个项目。这个中小型项目结束后,也有一些需要总结的地方,我会把这些感触写下来,和大家讨论一些项目开发的内容。

读的越多,越发觉得Alamofire中的函数的设计很厉害。不是一时半会能够全部串联的。

由于知识水平有限,如有错误,还望指出

链接

Alamofire源码解读系列(一)之概述和使用 简书-----博客园

Alamofire源码解读系列(二)之错误处理(AFError) 简书-----博客园

Alamofire源码解读系列(三)之通知处理(Notification) 简书-----博客园

Alamofire源码解读系列(四)之参数编码(ParameterEncoding) 简书-----博客园

Alamofire源码解读系列(五)之结果封装(Result) 简书-----博客园

Alamofire源码解读系列(六)之Task代理(TaskDelegate) 简书-----博客园

Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 简书-----博客园

Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy) 简书-----博客园

Alamofire源码解读系列(九)之响应封装(Response) 简书-----博客园

Alamofire源码解读系列(十)之序列化(ResponseSerialization) 简书-----博客园

Alamofire源码解读系列(十一)之多表单(MultipartFormData) 简书-----博客园

Alamofire源码解读系列(十二)之时间轴(Timeline) 简书-----博客园

Alamofire源码解读系列(十二)之请求(Request)的更多相关文章

  1. Alamofire源码解读系列(十二)之时间轴(Timeline)

    本篇带来Alamofire中关于Timeline的一些思路 前言 Timeline翻译后的意思是时间轴,可以表示一个事件从开始到结束的时间节点.时间轴的概念能够应用在很多地方,比如说微博的主页就是一个 ...

  2. Alamofire源码解读系列(十)之序列化(ResponseSerialization)

    本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...

  3. Alamofire源码解读系列(十一)之多表单(MultipartFormData)

    本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...

  4. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

  5. Alamofire源码解读系列(四)之参数编码(ParameterEncoding)

    本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...

  6. Alamofire源码解读系列(三)之通知处理(Notification)

    本篇讲解swift中通知的用法 前言 通知作为传递事件和数据的载体,在使用中是不受限制的.由于忘记移除某个通知的监听,会造成很多潜在的问题,这些问题在测试中是很难被发现的.但这不是我们这篇文章探讨的主 ...

  7. Alamofire源码解读系列(五)之结果封装(Result)

    本篇讲解Result的封装 前言 有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解.在Alamofire中,使用Response来描述请求后的结果.我们都知道Alamof ...

  8. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  9. Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager)

    Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 本篇主要讲解iOS开发中的网络监控 前言 在开发中,有时候我们需要获取这些信息: 手机是否联网 ...

随机推荐

  1. Unity3D中的AI架构模型

    我们都知道现在AI(由人工制造出来的系统所表现出来的模拟人类的智能活动)非常的火,可以说是家喻户晓.当然,在游戏中,AI也是到处可以找到的,对于AI,我们应该关注的问题是如何让游戏角色能够向人或动物那 ...

  2. React+Node初尝试

    这是第一次写React和Node,选用的是前端Material-ui框架,后端使用的是Express框架,数据库采用的是Mongodb. 项目代码在:GitHub/lilu_movie 这是一个通过从 ...

  3. 一张图解析nvm,npm,nodejs之间的关系

  4. SearchBar简单展示

    import UIKit class SearchViewController: UIViewController,UISearchBarDelegate { let SCREEN_WIDTH = U ...

  5. MySQL元数据库——information_schema

    平时使用MySQL客户端操作数据库的同学,只要稍微留神都会发现,除了我们建的库之外,还经常看到三个数据库的影子: 1. information_schema 2. performance_schema ...

  6. Code First约定-Fluent API配置

    转自:http://blog.163.com/m13864039250_1/blog/static/2138652482015283397609/ 用Fluent API 配置/映射属性和类型 简介 ...

  7. Load 数据1

    Druid 的load 数据分为两类 :批量load(历史数据) 和实时load(新数据) ,本文介绍批量load 数据 indexing 服务 批量load 数据需要用到indexing 服务,它是 ...

  8. java面试题(一)

    1.面向对象的特征有哪些方面? 答:面向对象的特征主要有以下几个方面: - 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面.抽象只关注对象有哪些属性和行为,并不关注 ...

  9. SIP DB33标准笔记 监控图像获取

    实时监控图像的获取: a) 实时监控图像的获取过程应包括获取实时流.释放实时流.应使用 RFC 3261 中定义的方法INVITE 获取一个摄像机的实时监控视频流. 取消没有完成的连接应采用 CANC ...

  10. 动画制作 手机APP制作以及响应式的实现

    babber 宽度:浏览器的100% 原则上:高度-=图片高度 banner img 宽度:图片的实际宽度 高度:充满父容器 使用定位:让图片在父容器中绝对居中.     <!DOCTYPE h ...