Networking pattern based on NSURLSession

2019-03-27 23:58发布

问题:

I've been traditionally using a pattern where NSOperation subclasses create and manage their own NSURLConnection. The NSOperation subclass is instantiated by the view controller and will do its work without bothering the controller until it has finished. When it finishes retrieving data, it executes the completion block supplied by the view controller.

  1. ViewController instantiates NSOperation subclass (which encapsulates URL, parameters etc)
  2. NSOperation subclass instanciates NSURLConnection (which performs synchronous request and retrieves data)
  3. NSURLConnection dumps data to NSOperation-subclass
  4. NSOperation-subclass executes the completion block supplied by the view controller.

I'm trying to implement the same pattern with NSURLSession now. I want to be able to encapsulate the url and parameters required to make a network request inside a single object. Do I achieve this using NSURLSession subclasses or NSURLSessionTask subclasses?

I like to create separate classes for every network operation based on the actor design pattern.

回答1:

You can use the same pattern, replacing NSURLConnection with NSURLSessionTask subclasses (e.g. NSURLSessionDataTask).

As @CouchDeveloper suggests in the comments, an alternative is to wrap NSURLSessionTask in an non-NSOperation object with asynchronous semantics (cancel, resume, etc.). This wrapper object would do little more than encode and decode parameters, delegating most operations to the wrapped task.

In either case, to instantiate NSURLSessionTask, you will need a NSURLSession. (NSURLSession is the NSURLSessionTask factory.) If all of your operations use the same configuration (cookies, proxy, caching, etc.), you can simply use the shared session (+[NSURLSession sharedSession]). If they need different configurations, you'll have to give them a NSURLSession or enough information to create their own.



回答2:

The NSURLSessionTask class (and its subclasses) look a bit like operations, but they're not. Thus, you can remove operations from your code as you transition to NSURLSession, but if you do, you will lose certain NSOperation functionality (dependencies, controlling degree of concurrency, etc.). I'm not sure why you'd want to excise operations from your code as you transition to NSURLSession. Personally, anywhere I used to wrap a NSURLConnection in an operation, I now wrap a NSURLSessionTask with an operation.

As an aside, one of the significant annoyances with NSURLSession, though, is that the task delegates are set at the session object. We can make guesses why Apple did that, but it has all sorts of unfortunate implications. Clearly you can get around this by using the block based factory methods for creating your tasks, but then you lose the richness of the delegate API, if you happened to need that.

The implication of this is that if using block-based task factory methods, wrapping the task in a concurrent NSOperation subclass is fairly obvious. But, if using the delegate-based tasks, though, if you want custom handlers for the tasks, you have to go through some silliness with maintaining a mapping between task identifiers and the appropriate completion blocks (which I personally put in a session manager object that I used to wrap the NSURLSession). (FYI, I believe an implementation like this is expected in a forthcoming AFNetworking update, too. See the latter part of the discussion on Issue 1504 on the AFNetworking github site.)

Anyway, others have answered the question how you could replace your operation-based NSURLConnection code with non-operation-based NSURLSession code, but I'd personally suggest staying with operations.


By the way, I've uploaded a example implementation of an operation-based NSURLSession implementation on github: https://github.com/robertmryan/NetworkManager

This is not intended to be a complete solution, but illustrates the idea of how you might implement a delegate-based NSURLSession with NSOperation subclasses.



回答3:

I want to be able to encapsulate the url and parameters required to make a network request inside a single object. Do I achieve this using NSURLSession subclasses or NSURLSessionTask subclasses?

What you are describing is NSURLRequest. Both NSURLSession and NSURLConnection take NSURLRequests to perform network connections (the NSURLRequest is what it's performing). What you seem to want is a set of factory methods for generating different, specialized NSURLRequests. For example, one that describes the "get my mail" request. You can do this easily by creating a category on NSURLRequest itself. Example:

@implementation NSURLRequest (Mail)

+ (instancetype) mailRequestForUser:(NSString *)user {
    NSURLRequest *result = nil;
    result = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"someServer""]];
    // set user in a header, whatever

    // return immutable copy
    return [result copy];
}

@end

Create something similar to the above that meets your needs. The resulting NSURLRequests can then be used to create connections. In Cocoa this is a very typical pattern for customizing objects, instead of subclassing. Subclassing is pretty rare - instead Cocoa has other mechanisms for customizing class behavior, from factory methods ("convenience methods" like the above) to delegation (where an object is given responsibility for the behavior of another object).



回答4:

What I did - is just allow Operation and URLSessionTask remain separately. For doing this I made general purpose asynchronous block operation which is used to instantiate, resume, cancel URLSessionTask.

So, in ViewController or Data layer I'm still using chained Operations with URLSessionTasks inside. Sample code below can be extended by subclassing AsynchronousBlockOperation.

File 1. Generic asynchronous operation.

open class AsynchronousOperation: Operation {

   private var lockOfProperties = NonRecursiveLock.makeDefaultLock()

   private var mFinished = false
   private var mExecuting = false

   public override init() {
      super.init()
   }

   /// Subclasses must launch job here.
   ///
   /// **Note** called between willChangeValueForKey and didChangeValueForKey calls, but after property mExecuting is set.
   open func onStart() {
   }

   /// Subclasses must cancel job here.
   ///
   /// **Note** called immediately after calling super.cancel().
   open func onCancel() {
   }

   /// Subclasses must release job here.
   ///
   /// **Note** called between willChangeValueForKey and didChangeValueForKey calls,
   /// but after properties mExecuting and mFinished are set.
   open func onFinish() {
   }
}

extension AsynchronousOperation {

   public final override var isAsynchronous: Bool {
      return true
   }

   public final override var isExecuting: Bool {
      return lockOfProperties.synchronized { mExecuting }
   }

   public final override var isFinished: Bool {
      return lockOfProperties.synchronized { mFinished }
   }
}

extension AsynchronousOperation {

   public final override func start() {
      if isCancelled || isFinished || isExecuting {
         return
      }
      willChangeValue(forKey: #keyPath(Operation.isExecuting))
      lockOfProperties.synchronized { mExecuting = true }
      onStart()
      didChangeValue(forKey: #keyPath(Operation.isExecuting))
   }

   public final override func cancel() {
      super.cancel()
      if isExecuting {
         onCancel()
         finish()
      } else {
         onCancel()
         lockOfProperties.synchronized {
            mExecuting = false
            mFinished = true
         }
      }
   }

   public final func finish() {
      willChangeValue(forKey: #keyPath(Operation.isExecuting))
      willChangeValue(forKey: #keyPath(Operation.isFinished))
      lockOfProperties.synchronized {
         mExecuting = false
         mFinished = true
      }
      onFinish()
      didChangeValue(forKey: #keyPath(Operation.isExecuting))
      didChangeValue(forKey: #keyPath(Operation.isFinished))
   }
}

File 2. Asynchronous block based operation:

open class AsynchronousBlockOperation: AsynchronousOperation {

   public typealias WorkItemType = OperationCancelationType & OperationResumingType
   public typealias FinaliseBlock = () -> Void
   public typealias WorkItemBlock = (@escaping FinaliseBlock) -> WorkItemType?

   private var executionBlock: WorkItemBlock
   private var blockExecutionQueue: DispatchQueue?
   private var workItemToken: WorkItemType?

   public init(blockExecutionQueue: DispatchQueue? = nil, executionBlock: @escaping WorkItemBlock) {
      self.blockExecutionQueue = blockExecutionQueue
      self.executionBlock = executionBlock
      super.init()
   }

   open override func onStart() {
      if let queue = blockExecutionQueue {
         queue.async {
            self.execute()
         }
      } else {
         execute()
      }
   }

   open override func onCancel() {
      workItemToken?.cancelOperation()
   }

   private func execute() {
      workItemToken = executionBlock { [weak self] in
         self?.finish()
      }
      if var token = workItemToken {
         token.resumeOperation()
      } else {
         finish()
      }
   }
}

File 3. Protocols

public protocol OperationCancelationType {
   mutating func cancelOperation()
}

public protocol OperationResumingType {
   mutating func resumeOperation()
}

extension URLSessionTask: OperationCancelationType {
   public func cancelOperation() {
      cancel()
   }
}

extension URLSessionTask: OperationResumingType {
   public func resumeOperation() {
      resume()
   }
}

Usage:

let operation = AsynchronousBlockOperation { [weak self] finalise in
   return session.dataTask(with: url) {
      ...
      finalise() // This will finish operation
   }
}