简介
iOS常见网络库
-
短链接
-
NSURLSession
- Alamofire
- AFNetworking
- SDWebImage
- Kingfisher
-
CFNetwork
-
Libcurl
-
Cronet
-
-
长链接
- Socket
- WebSocket
URL Loading System
NSURLSession
当前iOS中网络请求最常用的是基于NSURLSession实现和封装的网络库,NSURLSession属于URL Loading System的一部分。
NSURLProtocol
URL Loading System中提供的NSURLProtocol,能够拦截所有基于URL Loading System发出的网络请求,网络请求拦截之后可以方便采集数据和做相关处理。
适用范围
由于Background Session, WKWebView由另外的进程处理,所以URLProtocol无法对其做拦截处理。由于CFNetwork, libcurl不属于URL Loading System,所以URLProtocol也无法对其处理。
一、网络流程
iOS中HTTP请求流程
二、网络监控
监控代码
注册
URLProtocol的注册有2种方式
registerClass
open class func registerClass(_ protocolClass: AnyClass) -> Bool
URLProtocol.registerClass(NetworkProtocol.classForCoder())
//用于sharedSession
protocolClasses
open var protocolClasses: [AnyClass]?
URLSessionConfiguration.default.protocolClasses = [NetworkProtocol.classForCoder()]
//用于defaultSessionConfiguration/ephemeralSessionConfiguration
对应的2种注册方式,protocol 的调用顺序都是按照 protocolClasses 数组中的顺序,从前往后依次调用各个 protocol 的 canInitWith 方法,找到第一个可以 handle 的 protocol,后续的 protocol 就不会再调用了。
- Hook URLSessionConfiguration.protocolClasses.get()
对于URLSessionConfiguration.default,每次返回的都是copy出来的对象,所以当我们无法得到对应URLSessionConfiguration创建出来的URLSession,我们只能hook URLSessionConfiguration中protocolClasses的get方法,来实现拦截。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SSRURLSessionConfiguration : NSObject
+ (SSRURLSessionConfiguration *)defaultConfiguration;
@property (nonatomic, assign) BOOL isSwizzle;
@property (nonatomic, strong) NSMutableArray *allProtocolClasses;
- (void)addURLProtocol:(Class)protocol;
- (void)swizzleURLSessionConfiguration;
@end
NS_ASSUME_NONNULL_END
#import "SSRURLSessionConfiguration.h"
#import <objc/runtime.h>
@implementation SSRURLSessionConfiguration
+ (SSRURLSessionConfiguration *)defaultConfiguration {
static SSRURLSessionConfiguration *staticConfiguration;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticConfiguration = [[SSRURLSessionConfiguration alloc] init];
});
return staticConfiguration;
}
- (void)swizzleURLSessionConfiguration {
self.isSwizzle = YES;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector: @selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub {
Method originalMethod = class_getInstanceMethod(original, selector);
Method stubMethod = class_getInstanceMethod(stub, selector);
if (!originalMethod || !stubMethod) {
[NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
}
method_exchangeImplementations(originalMethod, stubMethod);
}
- (void)addURLProtocol:(NSURLProtocol *)protocol {
if (self.allProtocolClasses == nil) {
self.allProtocolClasses = [NSMutableArray array];
}
if (![self.allProtocolClasses containsObject:protocol]) {
[self.allProtocolClasses addObject:protocol];
}
}
- (NSArray *)protocolClasses {
return [SSRURLSessionConfiguration defaultConfiguration].allProtocolClasses;
}
@end
let sessionConfiguration = SSRURLSessionConfiguration.default()
sessionConfiguration.addURLProtocol(YourNetworkProtocol.classForCoder())
if !sessionConfiguration.isSwizzle {
sessionConfiguration.swizzleURLSessionConfiguration()
}
转发
拦截到网络请求后,URLProtocol会依次执行下列方法:
// 该请求是否需要当前的协议对象拦截并处理
// 通常情况下判断scheme不为空,之后判断需要拦截的scheme,判断已经拦截过的scheme
override class func canInit(with request: URLRequest) -> Bool {
if let scheme = request.url?.scheme, (scheme == "http" || scheme == "https"),
URLProtocol.property(forKey: "Your Key Here", in: request) == nil {
return true
} else {
return false
}
}
// 返回规范化的请求(通常只要返回原来的请求就可以)
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
// 判断两个请求是否为同一个请求,如果为同一个请求就使用缓存数据
override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {
return super.requestIsCacheEquivalent(a, to: b)
}
在startLoading方法中,我们需要标记已拦截的请求,发起新的请求。
新的请求可以通过多种方式发起,URLSession, CFNetwork, Libcurl, Cronet等都可以,这边网络监控我们继续基于URLSession实现,iOS10以后URLSession提供了Metircs网络性能数据,iOS提供了更加丰富的网络性能数据,更加方便。
这边创建新的请求的时候,需要使用sharedSession,如果每次都创建新的Session,TCP连接将无法复用,每次都创建新的连接。
override func startLoading() {
if let mutableReqeust = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest {
// 标记已拦截的请求
URLProtocol.setProperty(true, forKey: "Your Key Here", in: mutableReqeust)
// 创建新的请求
let task = SSRNetworkSessionManager.shared.session.dataTask(with: mutableReqeust as URLRequest)
// 设置task回调
SSRNetworkSessionManager.shared.setDelegate(self, task: task)
sessionDataTask = task
sessionDataTask?.resume()
}
}
override func stopLoading() {
sessionDataTask?.cancel()
}
由于使用了sharedSession来发起新的请求,所以在回调的时候,需要根据task.identifier来找到对应的delegate,所以创建了task与delegate对应的dict
class SSRNetworkSessionManager: NSObject {
@objc public static dynamic let shared = SSRNetworkSessionManager()
weak var delegate: (SessionDelegate)?
var taskIdDelegateDict = [String: SessionDelegate]()
@objc dynamic let lock = NSLock()
lazy var session: URLSession = {
let configuration: URLSessionConfiguration = URLSessionConfiguration.ephemeral
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
return session
}()
func setDelegate(_ delegate: SessionDelegate, task: URLSessionTask) {
lock.lock()
defer {
lock.unlock()
}
taskIdDelegateDict[String(task.taskIdentifier)] = delegate
}
func delegate(_ task: URLSessionTask) -> SessionDelegate? {
lock.lock()
defer {
lock.unlock()
}
return taskIdDelegateDict[String(task.taskIdentifier)]
}
func removeDelegate(task: URLSessionTask) {
lock.lock()
defer {
lock.unlock()
}
taskIdDelegateDict.removeValue(forKey: String(task.taskIdentifier))
}
}
extension SSRNetworkSessionManager: URLSessionDataDelegate {
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if let delegate = SSRNetworkSessionManager.shared.delegate(dataTask) {
delegate.urlSession?(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if let delegate = SSRNetworkSessionManager.shared.delegate(dataTask) {
delegate.urlSession?(session, dataTask: dataTask, didReceive: data)
}
}
}
extension SSRNetworkSessionManager: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
if let delegate = SSRNetworkSessionManager.shared.delegate(task) {
delegate.urlSession?(session, task: task, didFinishCollecting: metrics)
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let delegate = SSRNetworkSessionManager.shared.delegate(task) {
delegate.urlSession?(session, task: task, didCompleteWithError: error)
}
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
if let delegate = SSRNetworkSessionManager.shared.delegate(task) {
delegate.urlSession?(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler)
}
}
}
回调
- URLSessionDataDelegate
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: URLCache.StoragePolicy.allowed)
completionHandler(URLSession.ResponseDisposition.allow)
}
// 该方法会多次回调,Body数据需要进行叠加
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if self.responseBodyData == nil {
self.responseBodyData = data
} else {
self.responseBodyData?.append(data)
}
self.client?.urlProtocol(self, didLoad: data)
}
- URLSessionTaskDelegate
URLSessionTaskMetrics中提供了丰富的性能数据,在下方提供了对应的结构体信息
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {}
didCompleteWithError是整个Task回调最后调用的方法,可以在这个方法里对数据进行计算和处理
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {}
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
self.client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response)
}
URLSessionTaskMetrics数据结构
@available(iOS 10.0, *)
open class URLSessionTaskTransactionMetrics : NSObject, @unchecked Sendable {
open var request: URLRequest { get }
@NSCopying open var response: URLResponse? { get }
open var fetchStartDate: Date? { get }
open var domainLookupStartDate: Date? { get }
open var domainLookupEndDate: Date? { get }
open var connectStartDate: Date? { get }
open var secureConnectionStartDate: Date? { get }
open var secureConnectionEndDate: Date? { get }
open var connectEndDate: Date? { get }
open var requestStartDate: Date? { get }
open var requestEndDate: Date? { get }
open var responseStartDate: Date? { get }
open var responseEndDate: Date? { get }
open var networkProtocolName: String? { get }
open var isProxyConnection: Bool { get }
open var isReusedConnection: Bool { get }
open var resourceFetchType: URLSessionTaskMetrics.ResourceFetchType { get }
@available(iOS 13.0, *)
open var countOfRequestHeaderBytesSent: Int64 { get }
@available(iOS 13.0, *)
open var countOfRequestBodyBytesSent: Int64 { get }
@available(iOS 13.0, *)
open var countOfRequestBodyBytesBeforeEncoding: Int64 { get }
@available(iOS 13.0, *)
open var countOfResponseHeaderBytesReceived: Int64 { get }
@available(iOS 13.0, *)
open var countOfResponseBodyBytesReceived: Int64 { get }
@available(iOS 13.0, *)
open var countOfResponseBodyBytesAfterDecoding: Int64 { get }
@available(iOS 13.0, *)
open var localAddress: String? { get }
@available(iOS 13.0, *)
open var remoteAddress: String? { get }
@available(iOS 13.0, *)
open var isCellular: Bool { get }
@available(iOS 13.0, *)
open var isExpensive: Bool { get }
@available(iOS 13.0, *)
open var isConstrained: Bool { get }
@available(iOS 13.0, *)
open var isMultipath: Bool { get }
@available(iOS 14.0, *)
open var domainResolutionProtocol: URLSessionTaskMetrics.DomainResolutionProtocol { get }
@available(iOS, introduced: 10.0, deprecated: 13.0, message: "Not supported")
public init()
@available(iOS, introduced: 10.0, deprecated: 13.0, message: "Not supported")
open class func new() -> Self
}
数据指标
基础信息和源信息
- port
- remoteAddress
- localAddress
- tlsVersionCode
- networkProtocolName
- resourceFetchType
- isProxyConnection
- requestHeadSize
- requestBodySize
- responseHeadSize
- responseBodySize
成功率
Status/HTTP Code
200..<300 的占比可以作为成功的网络请求
Business Code
部分失败的网络请求中,又可以按照业务状态码聚合和分类
耗时
整体耗时
阶段耗时
连接复用率
在连接复用的情况下,TCP+TLS建连这个阶段是没有的,可以节约很多的耗时,所以连接复用率是一个很重要的指标
三、网络优化
ROI
耗时类的ROI基本可以按照阶段耗时/总耗时来计算,之后降序排列,再依次按照难易度来解决和优化即可。
网络请求类型
- 接口请求
- 图片请求
- 日志请求
接口请求按照下述做优化,图片请求侧重源和硬件侧的优化,日志请求侧重压缩和聚合上传
阶段优化
TCP+TLS
HTTP2.0,QUIC等提升连接复用率,优化连接耗时
OCSP提升证书校验和TLS的耗时
TLS升级至1.3版本,提升TLS耗时
DNS
HTTPDNS,IP直连
HTTP服务端响应耗时
推进服务端处理
Task
缓存池复用
源
网络监控中我们得到Reqeust Size,Response Size,可以通过排序逐步排查优化Type和Size
硬件
CDN,全站加速等云方案