The mobile network environment has always been complex, such as WIFI, 2G, 3G, 4G, 5G, etc. Users may switch between these types when using App, which is also a difference between mobile network and traditional network, known as “Connection Migration”. In addition, DNS resolution is slow, high failure rate, carrier hijacking and other problems. Users have poor experience when using the App for some reasons. In order to improve the network situation, clear monitoring means are necessary.

1. App network request process

When an App sends a network request, it generally goes through the following key steps:

  • The DNS

    Domain Name System is essentially a distributed database that maps Domain names and IP addresses to each other, making it easier for people to access the Internet. First, the local DNS cache will be queried, and if the search fails, the DNS server will be queried, which may go through a lot of nodes, involving recursive query and iterative query. Operators may not do anything about it. One is carrier hijacking, where you see irrelevant ads when you visit a web page in the App. Another possible situation is to send your request to a distant base station for DNS resolution, resulting in a long time for DNS resolution of our App and low efficiency of App network. Generally do HTTPDNS solution to solve DNS problems.

  • TCP three-way handshake

    Check out this article on why the TCP handshake is three times rather than two or four times.

  • The TLS handshake

    For HTTPS requests, TLS handshake is also required, which is the process of key negotiation.

  • Send the request

    After the connection is established, you can send the request. At this point, you can record the request start time

  • Wait for the response

    Wait for the server to return a response. This time depends on the size of the resource and is the most time-consuming part of the network request process.

  • Returns a response

    The server sends a response to the client and determines whether the request is successful, cached, and needs to be redirected based on the status code in the HTTP header.

2. Monitoring principle

The name of the instructions
NSURLConnection It has been abandoned. Use simple
NSURLSession IOS7.0 comes with more power
CFNetwork The underlying NSURL, pure C implementation

The hierarchy of the iOS network framework is as follows:

The current status of iOS network is composed of 4 layers: BSD Sockets and SecureTransport at the bottom; The secondary layer is CFNetwork, NSURLSession, NSURLConnection, WebView is implemented in Objective-C, and CFNetwork is called; AFNetworking is based on NSURLSession and NSURLConnection.

At present, there are two main types of network monitoring in the industry: one is through NSURLProtocol monitoring, the other is through Hook monitoring. There are several ways to monitor network requests, each with advantages and disadvantages.

2.1 Scheme 1: NSURLProtocol monitors App network requests

As the upper layer interface, NSURLProtocol is relatively simple to use, but NSURLProtocol belongs to the URL Loading System System. Application protocols, such as FTP, HTTP, and HTTPS, are supported but cannot be monitored for other protocols. This restriction does not apply if you monitor the underlying network library CFNetwork.

The specific approach of NSURLProtocol has been described in this article. It inherits abstract classes and implements corresponding methods, and customizes to initiate network requests to achieve the purpose of monitoring.

Since iOS 10, a new delegate method has been added to NSURLSessionTaskDelegate:

/* * Sent when complete statistics information has been collected for the task. */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE (macosx (10.12), the ios (10.0), watchos (3.0), tvos (10.0));Copy the code

NSURLSessionTaskMetrics provides indicators of network performance. The parameters are as follows

@interface NSURLSessionTaskMetrics : NSObject /* * transactionMetrics array contains the metrics collected for every request/response transaction created during the task execution. */ @property (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transactionMetrics; /* * Interval from the task creation time to the task completion time. * Task creation time is the time when the task was instantiated. * Task completion time is the time when the task is about to change its internal state to completed. */ @property (copy, readonly) NSDateInterval *taskInterval; /* * redirectCount is the number of redirects that were recorded. */ @property (assign, readonly) NSUInteger redirectCount; - (instanceType)init API_DEPRECATED("Not supported", MacOS (10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), Tvos (10.0, 13.0)); + (instanceType)new API_DEPRECATED("Not supported", MacOS (10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), Tvos (10.0, 13.0)); @endCopy the code

TaskInterval indicates the total time between task creation and completion. The task creation time is the time when the task is instantiated, and the task completion time is the time when the task’s internal status is about to change to completion. RedirectCount indicates the number of redirects; The transactionMetrics array contains metrics collected from each request/response transaction during task execution, with the following parameters:

/*
 * This class defines the performance metrics collected for a request/response transaction during the task execution.
 */
API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0))
@interface NSURLSessionTaskTransactionMetrics : NSObject

/*
 * Represents the transaction request. 请求事务
 */
@property (copy, readonly) NSURLRequest *request;

/*
 * Represents the transaction response. Can be nil if error occurred and no response was generated. 响应事务
 */
@property (nullable, copy, readonly) NSURLResponse *response;

/*
 * For all NSDate metrics below, if that aspect of the task could not be completed, then the corresponding “EndDate” metric will be nil.
 * For example, if a name lookup was started but the name lookup timed out, failed, or the client canceled the task before the name could be resolved -- then while domainLookupStartDate may be set, domainLookupEndDate will be nil along with all later metrics.
 */

/*
 * 客户端开始请求的时间,无论是从服务器还是从本地缓存中获取
 * fetchStartDate returns the time when the user agent started fetching the resource, whether or not the resource was retrieved from the server or local resources.
 *
 * The following metrics will be set to nil, if a persistent connection was used or the resource was retrieved from local resources:
 *
 *   domainLookupStartDate
 *   domainLookupEndDate
 *   connectStartDate
 *   connectEndDate
 *   secureConnectionStartDate
 *   secureConnectionEndDate
 */
@property (nullable, copy, readonly) NSDate *fetchStartDate;

/*
 * domainLookupStartDate returns the time immediately before the user agent started the name lookup for the resource. DNS 开始解析的时间
 */
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;

/*
 * domainLookupEndDate returns the time after the name lookup was completed. DNS 解析完成的时间
 */
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;

/*
 * connectStartDate is the time immediately before the user agent started establishing the connection to the server.
 *
 * For example, this would correspond to the time immediately before the user agent started trying to establish the TCP connection. 客户端与服务端开始建立 TCP 连接的时间
 */
@property (nullable, copy, readonly) NSDate *connectStartDate;

/*
 * If an encrypted connection was used, secureConnectionStartDate is the time immediately before the user agent started the security handshake to secure the current connection. HTTPS 的 TLS 握手开始的时间
 *
 * For example, this would correspond to the time immediately before the user agent started the TLS handshake. 
 *
 * If an encrypted connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;

/*
 * If an encrypted connection was used, secureConnectionEndDate is the time immediately after the security handshake completed. HTTPS 的 TLS 握手结束的时间
 *
 * If an encrypted connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSDate *secureConnectionEndDate;

/*
 * connectEndDate is the time immediately after the user agent finished establishing the connection to the server, including completion of security-related and other handshakes. 客户端与服务器建立 TCP 连接完成的时间,包括 TLS 握手时间
 */
@property (nullable, copy, readonly) NSDate *connectEndDate;

/*
 * requestStartDate is the time immediately before the user agent started requesting the source, regardless of whether the resource was retrieved from the server or local resources.
 客户端请求开始的时间,可以理解为开始传输 HTTP 请求的 header 的第一个字节时间
 *
 * For example, this would correspond to the time immediately before the user agent sent an HTTP GET request.
 */
@property (nullable, copy, readonly) NSDate *requestStartDate;

/*
 * requestEndDate is the time immediately after the user agent finished requesting the source, regardless of whether the resource was retrieved from the server or local resources.
 客户端请求结束的时间,可以理解为 HTTP 请求的最后一个字节传输完成的时间
 *
 * For example, this would correspond to the time immediately after the user agent finished sending the last byte of the request.
 */
@property (nullable, copy, readonly) NSDate *requestEndDate;

/*
 * responseStartDate is the time immediately after the user agent received the first byte of the response from the server or from local resources.
 客户端从服务端接收响应的第一个字节的时间
 *
 * For example, this would correspond to the time immediately after the user agent received the first byte of an HTTP response.
 */
@property (nullable, copy, readonly) NSDate *responseStartDate;

/*
 * responseEndDate is the time immediately after the user agent received the last byte of the resource. 客户端从服务端接收到最后一个请求的时间
 */
@property (nullable, copy, readonly) NSDate *responseEndDate;

/*
 * The network protocol used to fetch the resource, as identified by the ALPN Protocol ID Identification Sequence [RFC7301].
 * E.g., h2, http/1.1, spdy/3.1.
 网络协议名,比如 http/1.1, spdy/3.1
 *
 * When a proxy is configured AND a tunnel connection is established, then this attribute returns the value for the tunneled protocol.
 *
 * For example:
 * If no proxy were used, and HTTP/2 was negotiated, then h2 would be returned.
 * If HTTP/1.1 were used to the proxy, and the tunneled connection was HTTP/2, then h2 would be returned.
 * If HTTP/1.1 were used to the proxy, and there were no tunnel, then http/1.1 would be returned.
 *
 */
@property (nullable, copy, readonly) NSString *networkProtocolName;

/*
 * This property is set to YES if a proxy connection was used to fetch the resource.
	该连接是否使用了代理
 */
@property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;

/*
 * This property is set to YES if a persistent connection was used to fetch the resource.
 是否复用了现有连接
 */
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;

/*
 * Indicates whether the resource was loaded, pushed or retrieved from the local cache.
 获取资源来源
 */
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;

/*
 * countOfRequestHeaderBytesSent is the number of bytes transferred for request header.
 请求头的字节数
 */
@property (readonly) int64_t countOfRequestHeaderBytesSent API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * countOfRequestBodyBytesSent is the number of bytes transferred for request body.
 请求体的字节数
 * It includes protocol-specific framing, transfer encoding, and content encoding.
 */
@property (readonly) int64_t countOfRequestBodyBytesSent API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * countOfRequestBodyBytesBeforeEncoding is the size of upload body data, file, or stream.
 上传体数据、文件、流的大小
 */
@property (readonly) int64_t countOfRequestBodyBytesBeforeEncoding API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * countOfResponseHeaderBytesReceived is the number of bytes transferred for response header.
 响应头的字节数
 */
@property (readonly) int64_t countOfResponseHeaderBytesReceived API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * countOfResponseBodyBytesReceived is the number of bytes transferred for response body.
 响应体的字节数
 * It includes protocol-specific framing, transfer encoding, and content encoding.
 */
@property (readonly) int64_t countOfResponseBodyBytesReceived API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * countOfResponseBodyBytesAfterDecoding is the size of data delivered to your delegate or completion handler.
给代理方法或者完成后处理的回调的数据大小
 
 */
@property (readonly) int64_t countOfResponseBodyBytesAfterDecoding API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * localAddress is the IP address string of the local interface for the connection.
  当前连接下的本地接口 IP 地址
 *
 * For multipath protocols, this is the local address of the initial flow.
 *
 * If a connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSString *localAddress API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * localPort is the port number of the local interface for the connection.
 当前连接下的本地端口号
 
 *
 * For multipath protocols, this is the local port of the initial flow.
 *
 * If a connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSNumber *localPort API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * remoteAddress is the IP address string of the remote interface for the connection.
 当前连接下的远端 IP 地址
 *
 * For multipath protocols, this is the remote address of the initial flow.
 *
 * If a connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSString *remoteAddress API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * remotePort is the port number of the remote interface for the connection.
  当前连接下的远端端口号
 *
 * For multipath protocols, this is the remote port of the initial flow.
 *
 * If a connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSNumber *remotePort API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * negotiatedTLSProtocolVersion is the TLS protocol version negotiated for the connection.
  连接协商用的 TLS 协议版本号
 * It is a 2-byte sequence in host byte order.
 *
 * Please refer to tls_protocol_version_t enum in Security/SecProtocolTypes.h
 *
 * If an encrypted connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * negotiatedTLSCipherSuite is the TLS cipher suite negotiated for the connection.
 连接协商用的 TLS 密码套件
 * It is a 2-byte sequence in host byte order.
 *
 * Please refer to tls_ciphersuite_t enum in Security/SecProtocolTypes.h
 *
 * If an encrypted connection was not used, this attribute is set to nil.
 */
@property (nullable, copy, readonly) NSNumber *negotiatedTLSCipherSuite API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * Whether the connection is established over a cellular interface.
 是否是通过蜂窝网络建立的连接
 */
@property (readonly, getter=isCellular) BOOL cellular API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * Whether the connection is established over an expensive interface.
 是否通过昂贵的接口建立的连接
 */
@property (readonly, getter=isExpensive) BOOL expensive API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * Whether the connection is established over a constrained interface.
 是否通过受限接口建立的连接
 */
@property (readonly, getter=isConstrained) BOOL constrained API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

/*
 * Whether a multipath protocol is successfully negotiated for the connection.
 是否为了连接成功协商了多路径协议
 */
@property (readonly, getter=isMultipath) BOOL multipath API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));


- (instancetype)init API_DEPRECATED("Not supported", macos(10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), tvos(10.0,13.0));
+ (instancetype)new API_DEPRECATED("Not supported", macos(10.12,10.15), ios(10.0,13.0), watchos(3.0,6.0), tvos(10.0,13.0));

@end
Copy the code

Network monitoring simple code

// 监控基础信息
@interface  NetworkMonitorBaseDataModel : NSObject
// 请求的 URL 地址
@property (nonatomic, strong) NSString *requestUrl;
//请求头
@property (nonatomic, strong) NSArray *requestHeaders;
//响应头
@property (nonatomic, strong) NSArray *responseHeaders;
//GET方法 的请求参数
@property (nonatomic, strong) NSString *getRequestParams;
//HTTP 方法, 比如 POST
@property (nonatomic, strong) NSString *httpMethod;
//协议名,如http1.0 / http1.1 / http2.0
@property (nonatomic, strong) NSString *httpProtocol;
//是否使用代理
@property (nonatomic, assign) BOOL useProxy;
//DNS解析后的 IP 地址
@property (nonatomic, strong) NSString *ip;
@end

// 监控信息模型
@interface  NetworkMonitorDataModel : NetworkMonitorBaseDataModel
//客户端发起请求的时间
@property (nonatomic, assign) UInt64 requestDate;
//客户端开始请求到开始dns解析的等待时间,单位ms 
@property (nonatomic, assign) int waitDNSTime;
//DNS 解析耗时
@property (nonatomic, assign) int dnsLookupTime;
//tcp 三次握手耗时,单位ms
@property (nonatomic, assign) int tcpTime;
//ssl 握手耗时
@property (nonatomic, assign) int sslTime;
//一个完整请求的耗时,单位ms
@property (nonatomic, assign) int requestTime;
//http 响应码
@property (nonatomic, assign) NSUInteger httpCode;
//发送的字节数
@property (nonatomic, assign) UInt64 sendBytes;
//接收的字节数
@property (nonatomic, assign) UInt64 receiveBytes;


// 错误信息模型
@interface  NetworkMonitorErrorModel : NetworkMonitorBaseDataModel
//错误码
@property (nonatomic, assign) NSInteger errorCode;
//错误次数
@property (nonatomic, assign) NSUInteger errCount;
//异常名
@property (nonatomic, strong) NSString *exceptionName;
//异常详情
@property (nonatomic, strong) NSString *exceptionDetail;
//异常堆栈
@property (nonatomic, strong) NSString *stackTrace;
@end

  
// 继承自 NSURLProtocol 抽象类,实现响应方法,代理网络请求
@interface CustomURLProtocol () <NSURLSessionTaskDelegate>

@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSOperationQueue *sessionDelegateQueue;
@property (nonatomic, strong) NetworkMonitorDataModel *dataModel;
@property (nonatomic, strong) NetworkMonitorErrorModel *errModel;

@end

//使用NSURLSessionDataTask请求网络
- (void)startLoading {
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
  	NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
                                                          delegate:self
                                                     delegateQueue:nil];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
  	self.sessionDelegateQueue = [[NSOperationQueue alloc] init];
    self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
    self.sessionDelegateQueue.name = @"com.networkMonitor.session.queue";
    self.dataTask = [session dataTaskWithRequest:self.request];
    [self.dataTask resume];
}

#pragma mark - NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        [self.client URLProtocol:self didFailWithError:error];
    } else {
        [self.client URLProtocolDidFinishLoading:self];
    }
    if (error) {
        NSURLRequest *request = task.currentRequest;
        if (request) {
            self.errModel.requestUrl  = request.URL.absoluteString;        
            self.errModel.httpMethod = request.HTTPMethod;
            self.errModel.requestParams = request.URL.query;
        }
        self.errModel.errorCode = error.code;
        self.errModel.exceptionName = error.domain;
        self.errModel.exceptionDetail = error.description;
      // 上传 Network 数据到数据上报组件,数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲
    }
    self.dataTask = nil;
}


- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
       if (@available(iOS 10.0, *) && [metrics.transactionMetrics count] > 0) {
        [metrics.transactionMetrics enumerateObjectsUsingBlock:^(NSURLSessionTaskTransactionMetrics *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
            if (obj.resourceFetchType == NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad) {
                if (obj.fetchStartDate) {
                    self.dataModel.requestDate = [obj.fetchStartDate timeIntervalSince1970] * 1000;
                }
                if (obj.domainLookupStartDate && obj.domainLookupEndDate) {
                    self.dataModel. waitDNSTime = ceil([obj.domainLookupStartDate timeIntervalSinceDate:obj.fetchStartDate] * 1000);
                    self.dataModel. dnsLookupTime = ceil([obj.domainLookupEndDate timeIntervalSinceDate:obj.domainLookupStartDate] * 1000);
                }
                if (obj.connectStartDate) {
                    if (obj.secureConnectionStartDate) {
                        self.dataModel. waitDNSTime = ceil([obj.secureConnectionStartDate timeIntervalSinceDate:obj.connectStartDate] * 1000);
                    } else if (obj.connectEndDate) {
                        self.dataModel.tcpTime = ceil([obj.connectEndDate timeIntervalSinceDate:obj.connectStartDate] * 1000);
                    }
                }
                if (obj.secureConnectionEndDate && obj.secureConnectionStartDate) {
                    self.dataModel.sslTime = ceil([obj.secureConnectionEndDate timeIntervalSinceDate:obj.secureConnectionStartDate] * 1000);
                }

                if (obj.fetchStartDate && obj.responseEndDate) {
                    self.dataModel.requestTime = ceil([obj.responseEndDate timeIntervalSinceDate:obj.fetchStartDate] * 1000);
                }

                self.dataModel.httpProtocol = obj.networkProtocolName;

                NSHTTPURLResponse *response = (NSHTTPURLResponse *)obj.response;
                if ([response isKindOfClass:NSHTTPURLResponse.class]) {
                    self.dataModel.receiveBytes = response.expectedContentLength;
                }

                if ([obj respondsToSelector:@selector(_remoteAddressAndPort)]) {
                    self.dataModel.ip = [obj valueForKey:@"_remoteAddressAndPort"];
                }

                if ([obj respondsToSelector:@selector(_requestHeaderBytesSent)]) {
                    self.dataModel.sendBytes = [[obj valueForKey:@"_requestHeaderBytesSent"] unsignedIntegerValue];
                }
                if ([obj respondsToSelector:@selector(_responseHeaderBytesReceived)]) {
                    self.dataModel.receiveBytes = [[obj valueForKey:@"_responseHeaderBytesReceived"] unsignedIntegerValue];
                }

               self.dataModel.requestUrl = [obj.request.URL absoluteString];
                self.dataModel.httpMethod = obj.request.HTTPMethod;
                self.dataModel.useProxy = obj.isProxyConnection;
            }
        }];
				// 上传 Network 数据到数据上报组件,数据上报会在 [打造功能强大、灵活可配置的数据上报组件](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.80.md) 讲
    }
}
Copy the code

2.2 Scheme 2: NSURLProtocol monitoring App network requests in the Dark Arts

In 2.1, NSURLSessionTaskMetrics was analyzed, which seemed not perfect for network monitoring due to compatibility problems. However, I saw an article later when SEARCHING for data. Article in the analysis of WebView network monitoring when the analysis of Webkit source code found the following code

#if ! HAVE(TIMINGDATAOPTIONS) void setCollectsTimingData() { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [NSURLConnection _setCollectsTimingData:YES]; . }); } #endifCopy the code

NSURLConnection itself has a TimingData collection API, but it is not exposed to developers, apple is using it. _setCollectsTimingData: and _timingData are apis of NSURLConnection found in the Runtime header (available after iOS8).

NSURLSession uses _setCollectsTimingData before iOS9: TimingData is available.

Note:

  • Because it is a private API, there is confusion when using it. Such as[[@"_setC" stringByAppendingString:@"ollectsT"] stringByAppendingString:@"imingData:"].
  • Do not recommend private API, generally do APM belong to the public team, you think that although you do THE SDK to achieve the purpose of network monitoring, but in case to the business line of App shelves caused problems, it is not worth the loss. Generally this kind of opportunistic, not 100% sure thing can be used in the toy phase.
@interface _NSURLConnectionProxy : DelegateProxy

@end

@implementation _NSURLConnectionProxy

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"connectionDidFinishLoading:"]) {
        return YES;
    }
    return [self.target respondsToSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [super forwardInvocation:invocation];
    if ([NSStringFromSelector(invocation.selector) isEqualToString:@"connectionDidFinishLoading:"]) {
        __unsafe_unretained NSURLConnection *conn;
        [invocation getArgument:&conn atIndex:2];
        SEL selector = NSSelectorFromString([@"_timin" stringByAppendingString:@"gData"]);
        NSDictionary *timingData = [conn performSelector:selector];
        [[NTDataKeeper shareInstance] trackTimingData:timingData request:conn.currentRequest];
    }
}

@end

@implementation NSURLConnection(tracker)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(initWithRequest:delegate:);
        SEL swizzledSelector = @selector(swizzledInitWithRequest:delegate:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
        
        NSString *selectorName = [[@"_setC" stringByAppendingString:@"ollectsT"] stringByAppendingString:@"imingData:"];
        SEL selector = NSSelectorFromString(selectorName);
        [NSURLConnection performSelector:selector withObject:@(YES)];
    });
}

- (instancetype)swizzledInitWithRequest:(NSURLRequest *)request delegate:(id<NSURLConnectionDelegate>)delegate
{
    if (delegate) {
        _NSURLConnectionProxy *proxy = [[_NSURLConnectionProxy alloc] initWithTarget:delegate];
        objc_setAssociatedObject(delegate ,@"_NSURLConnectionProxy" ,proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return [self swizzledInitWithRequest:request delegate:(id<NSURLConnectionDelegate>)proxy];
    }else{
        return [self swizzledInitWithRequest:request delegate:delegate];
    }
}

@end
Copy the code

2.3 Plan 3: Hook

There are two types of hook technology in iOS, one is NSProxy and the other is Method Swizzling (Isa Swizzling).

2.3.1 method a

Writing the SDK is definitely not possible to manually hack into the business code (you do not have that permission to submit to the online code 😂), so both APM and no trace burial point are Hook way.

Aspect-oriented Programming (AOP) is a Programming paradigm in computer science that further separates crosscutting concerns from business principals to improve the modularity of program code. Dynamically add functionality to a program without modifying the source code. The core idea is to separate business logic (core concerns, primary system functions) from common functions (crosscutting concerns, such as logging systems) to reduce complexity and keep the system modular, maintainable, and reusable. It is used in the log system, performance statistics, security control, transaction processing, and exception handling scenarios.

AOP implementation in iOS is based on the Runtime mechanism, currently by three ways: Method Swizzling, NSProxy, FishHook (mainly used for Hook C code).

In 2.1, NSURLProtocol monitors network requests of NSURLConnection and NSURLSession, and can initiate network requests and obtain such information as request start time, request end time and header information after its own proxy. However, you cannot obtain very detailed network performance data, such as the time when DNS resolution starts, how long DNS resolution takes, the time when rePONse starts to return, and how long it returns. – (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task DidFinishCollectingMetrics: (NSURLSessionTaskMetrics *) metrics API_AVAILABLE (macosx (10.12), the ios (10.0), watchos (3.0), Tvos (10.0)); , can obtain the precise network data. But it’s compatible. The information obtained from the Webkit source code is discussed in section 2.2 of this article. TimingData can be obtained through the private methods _setCollectsTimingData: and _timingData.

However, if all network requests need to be monitored, it cannot meet the requirements. After consulting the data, I found that Alibaicuan has APM solution, so I came up with plan 3, which needs to do the following for network monitoring

If you are unfamiliar with CFNetwork, take a look at the hierarchy and simple usage of CFNetwork

CFNetwork is based on CFSocket and CFStream.

CFSocket: The Socket is the basic foundation of network communication. It allows two Socket ports to send data to each other. The most common Socket abstraction in iOS is the BSD Socket. CFSocket is the OC wrapper of BSD socket, which implements almost all BSD functions and adds RunLoop.

CFStream: provides a device-independent method for reading and writing data. It allows streaming of data in memory, files, and network (using sockets) without writing all data to memory. CFStream provides apis that provide abstractions for two types of CFType objects: CFReadStream and CFWriteStream. It is also the basis of CFHTTP and CFFTP.

A simple Demo

- (void)testCFNetwork { CFURLRef urlRef = CFURLCreateWithString(kCFAllocatorDefault, CFSTR("https://httpbin.org/get"), NULL); CFHTTPMessageRef httpMessageRef = CFHTTPMessageCreateRequest(kCFAllocatorDefault, CFSTR("GET"), urlRef, kCFHTTPVersion1_1); CFRelease(urlRef); CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, httpMessageRef); CFRelease(httpMessageRef); CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFOptionFlags eventFlags = (kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered); CFStreamClientContext context = { 0, NULL, NULL, NULL, NULL } ; // Assigns a client to a stream, which receives callbacks when certain events occur. CFReadStreamSetClient(readStream, eventFlags, CFNetworkRequestCallback, &context); // Opens a stream for reading. CFReadStreamOpen(readStream); } // callback void CFNetworkRequestCallback (CFReadStreamRef _Null_unspecified stream, CFStreamEventType type, void * _Null_unspecified clientCallBackInfo) { CFMutableDataRef responseBytes = CFDataCreateMutable(kCFAllocatorDefault, 0); CFIndex numberOfBytesRead = 0; do { UInt8 buffer[2014]; numberOfBytesRead = CFReadStreamRead(stream, buffer, sizeof(buffer)); if (numberOfBytesRead > 0) { CFDataAppendBytes(responseBytes, buffer, numberOfBytesRead); } } while (numberOfBytesRead > 0); CFHTTPMessageRef response = (CFHTTPMessageRef)CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); if (responseBytes) { if (response) { CFHTTPMessageSetBody(response, responseBytes); } CFRelease(responseBytes); } // close and cleanup CFReadStreamClose(stream); CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes); CFRelease(stream); // print response if (response) { CFDataRef reponseBodyData = CFHTTPMessageCopyBody(response); CFRelease(response); printResponseData(reponseBodyData); CFRelease(reponseBodyData); } } void printResponseData (CFDataRef responseData) { CFIndex dataLength = CFDataGetLength(responseData); UInt8 *bytes = (UInt8 *)malloc(dataLength); CFDataGetBytes(responseData, CFRangeMake(0, CFDataGetLength(responseData)), bytes); CFStringRef responseString = CFStringCreateWithBytes(kCFAllocatorDefault, bytes, dataLength, kCFStringEncodingUTF8, TRUE); CFShow(responseString); CFRelease(responseString); free(bytes); } // console { "args": {}, "headers": { "Host": "httpbin.org", "User-Agent": "Test/1 CFNetwork/1125.2 Darwin/19.3.0", "x-amzn-trace-id ": "Root= 1-5e8980d0-581f3f44724C7140614c2564"}, "origin": "183.159.122.102", "url" : "https://httpbin.org/get"}Copy the code

We know that the use of NSURLSession, NSURLConnection, CFNetwork all require a bunch of methods to be set up and then you need to set up the proxy object to implement the proxy method. So the first thing that comes to mind when monitoring this situation is to use the Runtime hook to remove the method hierarchy. But the proxy method on the set proxy object cannot hook because it does not know which class the proxy object is. So try to hook up the proxy object step, replace the proxy object with a class we designed, and then let this class to implement NSURLConnection, NSURLSession, CFNetwork related proxy methods. Then call the method implementation of the original proxy object inside each of these methods. So our requirements are met, we can get monitoring data in the corresponding methods, such as request start time, end time, status code, content size, etc.

NSURLSession and NSURLConnection hooks are as follows.

There are APM schemes for CFNetwork in the industry, and the following are summarized and described:

CFNetwork is c language, to hook C code need to use Dynamic Loader hook library – Fishhook.

Dynamic Loader (DYLD) binds symbols by updating Pointers saved in the Mach-O file. It can be used to modify the pointer to a C function call at Runtime. Fishhook is implemented by: Iterate through the symbols in the __nl_symbol_ptr and __la_symbol_ptr sections of the __DATA segment. Through the coordination of Indirect Symbol Table, Symbol Table and String Table, I can find my own functions to be replaced to achieve the purpose of hook.

/* Returns the number of bytes read, or -1 if an error occurs preventing any

bytes from being read, or 0 if the stream’s end was encountered.

It is an error to try and read from a stream that hasn’t been opened first.

This call will block until at least one byte is available; it will NOT block

until the entire buffer can be filled. To avoid blocking, either poll using

CFReadStreamHasBytesAvailable() or use the run loop and listen for the

kCFStreamEventHasBytesAvailable event for notification of data available. */

CF_EXPORT

CFIndex CFReadStreamRead(CFReadStreamRef _Null_unspecified stream, UInt8 * _Null_unspecified buffer, CFIndex bufferLength);

CFNetwork uses CFReadStreamRef to pass the data and receives the response from the server in the form of callback functions. When the callback function receives

The detailed steps and key codes are as follows, taking NSURLConnection as an example

  • Because there’s a lot to Hook up with, I’ll write a method Swizzling tool class

    #import <Foundation/ foundation.h > NS_ASSUME_NONNULL_BEGIN @interface NSObject (hook) /** Hook object method @param OriginalSelector the original object method that needs a hook @param swizzledSelector the object method to replace */ + (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector; /** @param originalSelector @param swizzledSelector @param swizzledSelector (void)apm_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector; @end NS_ASSUME_NONNULL_END + (void)apm_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { class_swizzleInstanceMethod(self, originalSelector, swizzledSelector); } + (void)apm_swizzleClassMethod:(SEL)originalSelector :(SEL)swizzledSelector :(SEL)swizzledSelector { That is, the class method is equivalent to the instance method of the metaclass, so you just pass in the metaclass, and the other logic is the same as the interaction instance method. Class class2 = object_getClass(self); class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector); } void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL) { Method originMethod = class_getInstanceMethod(class, originalSEL); Method replaceMethod = class_getInstanceMethod(class, replacementSEL); if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) { class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); }else { method_exchangeImplementations(originMethod, replaceMethod); }}Copy the code
  • Create a class that inherits from NSProxy abstract class and implement the corresponding method.

    #import <Foundation/ foundation. h> NS_ASSUME_NONNULL_BEGIN // Set proxy forwarding for NSURLConnection, NSURLSession, CFNetwork proxies @interface NetworkDelegateProxy : NSProxy + (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate; @end NS_ASSUME_NONNULL_END // .m @interface NetworkDelegateProxy () { id _originalTarget; id _NewDelegate; } @end @implementation NetworkDelegateProxy #pragma mark - life cycle + (instancetype)sharedInstance { static NetworkDelegateProxy *_sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedInstance = [NetworkDelegateProxy alloc]; }); return _sharedInstance; } #pragma mark - public Method + (instancetype)setProxyForObject:(id)originalTarget withNewDelegate:(id)newDelegate { NetworkDelegateProxy *instance = [NetworkDelegateProxy sharedInstance]; instance->_originalTarget = originalTarget; instance->_NewDelegate = newDelegate; return instance; } - (void)forwardInvocation:(NSInvocation *)invocation { if ([_originalTarget respondsToSelector:invocation.selector]) {  [invocation invokeWithTarget:_originalTarget]; [((NSURLSessionAndConnectionImplementor *)_NewDelegate) invoke:invocation]; } } - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [_originalTarget methodSignatureForSelector:sel]; } @endCopy the code
  • Create an object that implements the NSURLConnection, NSURLSession, NSIuputStream proxy methods

    // NetworkImplementor.m #pragma mark-NSURLConnectionDelegate - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"%s", __func__); } - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response { NSLog(@"%s", __func__); return request; } #pragma mark-NSURLConnectionDataDelegate - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSLog(@"%s", __func__); } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"%s", __func__); } - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite { NSLog(@"%s", __func__); } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"%s", __func__); } #pragma mark-NSURLConnectionDownloadDelegate - (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes { NSLog(@"%s", __func__); } - (void)connectionDidResumeDownloading:(NSURLConnection *)connection totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes { NSLog(@"%s", __func__); } - (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL { NSLog(@"%s", __func__); } // Write the data items to be monitored as requiredCopy the code
  • Add a Category to NSURLConnection and set up hook proxy objects and hook NSURLConnection object methods

    // NSURLConnection+Monitor.m @implementation NSURLConnection (Monitor) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [[self class] apm_swizzleMethod:@selector(apm_initWithRequest:delegate:) swizzledSelector:@selector(initWithRequest: delegate:)]; }}); } - (_Nonnull instancetype)apm_initWithRequest:(NSURLRequest *)request delegate:(nullable id)delegate { /* 1. Replace the Delegate when setting the Delegate. 2. To monitor data in each proxy method, hook all proxy methods. */ NSString *traceId = @"traceId"; NSMutableURLRequest *rq = [request mutableCopy]; NSString *preTraceId = [request.allHTTPHeaderFields valueForKey:@"head_key_traceid"]; If (preTraceId) {return NSURLConnection [self apm_initWithRequest:rq delegate:delegate]; } else { [rq setValue:traceId forHTTPHeaderField:@"head_key_traceid"]; NSURLSessionAndConnectionImplementor *mockDelegate = [NSURLSessionAndConnectionImplementor new]; [self registerDelegateMethod:@"connection:didFailWithError:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; [self registerDelegateMethod:@"connection:didReceiveResponse:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; [self registerDelegateMethod:@"connection:didReceiveData:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; [self registerDelegateMethod:@"connection:didFailWithError:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@@"]; [self registerDelegateMethod:@"connectionDidFinishLoading:" originalDelegate:delegate newDelegate:mockDelegate flag:"v@:@"]; [self registerDelegateMethod:@"connection:willSendRequest:redirectResponse:" originalDelegate:delegate newDelegate:mockDelegate flag:"@@:@@"]; delegate = [NetworkDelegateProxy setProxyForObject:delegate withNewDelegate:mockDelegate]; NSURLConnection return [self apm_initWithRequest:rq delegate:delegate]; } } - (void)registerDelegateMethod:(NSString *)methodName originalDelegate:(id<NSURLConnectionDelegate>)originalDelegate  newDelegate:(NSURLSessionAndConnectionImplementor *)newDelegate flag:(const char *)flag { if ([originalDelegate respondsToSelector:NSSelectorFromString(methodName)]) { IMP originalMethodImp = class_getMethodImplementation([originalDelegate class], NSSelectorFromString(methodName)); IMP newMethodImp = class_getMethodImplementation([newDelegate class], NSSelectorFromString(methodName)); if (originalMethodImp ! = newMethodImp) { [newDelegate registerSelector: methodName]; NSLog(@""); } } else { class_addMethod([originalDelegate class], NSSelectorFromString(methodName), class_getMethodImplementation([newDelegate class], NSSelectorFromString(methodName)), flag); } } @endCopy the code

In this way, the network information can be monitored, and then the data can be submitted to the DATA report SDK, and the data can be reported according to the data report policy.

2.3.2 method 2

In fact, there is another way to meet the above requirements, that is Isa Swizzling.

By the way, after the above hook for NSURLConnection, NSURLSession, NSInputStream proxy object, there is another way to use NSProxy to implement proxy object method forwarding, that is isa Swizzling.

  • Method swizzling principle

    struct old_method {
        SEL method_name;
        char *method_types;
        IMP method_imp;
    };
    Copy the code

    An improved version of Method Swizzling is shown below

    Method originalMethod = class_getInstanceMethod(aClass, aSEL);
    IMP originalIMP = method_getImplementation(originalMethod);
    char *cd = method_getTypeEncoding(originalMethod);
    IMP newIMP = imp_implementationWithBlock(^(id self) {
      void (*tmp)(id self, SEL _cmd) = originalIMP;
      tmp(self, aSEL);
    });
    class_replaceMethod(aClass, aSEL, newIMP, cd);
    Copy the code
  • isa swizzling

    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    /// A pointer to an instance of a class.
    typedef struct objc_object *id;
    
    Copy the code

Let’s look at why modifying ISA works.

  1. The person writing the APM monitor has no way to determine the business code
  2. It is not possible to write classes that allow line of business developers not to use the system NSURLSession, NSURLConnection classes for the convenience of monitoring APM

How does KVO work? Combine this with the diagram above

  • Create a monitoring object subclass
  • Overrides getters and seeters for attributes in subclasses
  • Directs the ISA pointer to the monitored object to the newly created subclass
  • Intercepts value changes in getters and setters of subclasses to notify monitoring of value changes
  • After monitoring, restore the ISA of the monitored object

We can also subclass NSURLConnection, NSURLSession load methods, override methods in subclasses, For example – (**nullable** **instancetype**)initWithRequest:(NSURLRequest *)request delegate:(**nullable** **id**)delegate startImmediately:(**BOOL**)startImmediately; Then point the ISA of NSURLSession and NSURLConnection to dynamically created subclasses. Restore the isa pointer itself after these methods are finished processing.

However, ISA Swizzling is still aimed at Method Swizzling, and the proxy object is uncertain, so NSProxy is still needed for dynamic processing.

As for how to modify ISA, I write a simple Demo to simulate KVO

- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath Options: (NSKeyValueObservingOptions) options context: (nullable void *) context {/ / generate custom name nsstrings * className = NSStringFromClass(self.class); NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingString:className]; Myclass = objc_allocateClassPair(self. Class, [currentClassName UTF8String], 0); Objc_registerClassPair (myClass); //2. Override setter class_addMethod(myclass,@selector(say), (IMP)say, "v@:@"); // class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@"); Isa object_setClass(self, myclass); //4. Save the observer to the current object objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN); Objc_setAssociatedObject (self, "context", (__bridge ID _Nullable)(context), OBJC_ASSOCIATION_RETAIN); } void say(id self, SEL _cmd) {struct objc_super superclass = {self, [self superclass]}; ((void(*)(struct objc_super *,SEL))objc_msgSendSuper)(&superclass,@selector(say)); NSLog(@"%s", __func__); // Class = [self Class]; // object_setClass(self, class_getSuperclass(class)); // objc_msgSend(self, @selector(say)); } void setName (id self, SEL _cmd, NSString *name) { NSLog(@"come here"); // Switch to the parent of the current class, then send the message setName, then switch to the current subclass //1. Class = [self Class]; object_setClass(self, class_getSuperclass(class)); //2. Call the parent setName method objc_msgSend(self, @selector(setName:), name); //3. Call observer id observer = objc_getAssociatedObject(self, "observer"); id context = objc_getAssociatedObject(self, "context"); if (observer) { objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context); } //4. Subclass object_setClass(self, class); } @endCopy the code

2.4 Solution 4: Monitor common App network requests

Due to the cost, the network monitoring in this paper can be completed quickly because most of the network capabilities of current projects are completed through AFNetworking.

AFNetworking will be notified when it initiates a network. AFNetworkingTaskDidResumeNotification and AFNetworkingTaskDidCompleteNotification. Get network health information by listening for the parameters carried by the notification.

self.didResumeObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingTaskDidResumeNotification Object :nil queue:self.queue usingBlock:^(NSNotification * _Nonnull note) {// start __strong __typeof(weakSelf)strongSelf =  weakSelf; NSURLSessionTask *task = note.object; NSString *requestId = [[NSUUID UUID] UUIDString]; task.apm_requestId = requestId; [strongSelf.networkRecoder recordStartRequestWithRequestID:requestId task:task]; }]; self.didCompleteObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingTaskDidCompleteNotification object:nil queue:self.queue usingBlock:^(NSNotification * _Nonnull note) { __strong __typeof(weakSelf)strongSelf = weakSelf; NSError *error = Note. The userInfo [AFNetworkingTaskDidCompleteErrorKey]; NSURLSessionTask * task = note. The object; the if (! Error) {/ / success [strongSelf.net workRecoder recordFinishRequestWithRequestID: task. Apmn_requestId task: task];} else {/ / failure [strongSelf.networkRecoder recordResponseErrorWithRequestID:task.apmn_requestId task:task error:error]; } }];Copy the code

Assemble the data in networkRecoder’s method, hand it to the data reporting component, and wait for the appropriate timing strategy to report.

Because the network is an asynchronous process, you need to set a unique identifier for each network when the network request starts. After the network request is completed, you can determine how long the network takes and whether the network is successful according to the identifier of each request. Add a class to the NSURLSessionTask. Add a attribute (unique identifier) via Runtime.

In a word, be careful when naming categories, as well as internal properties and methods. What if you don’t pay attention? If you want to add the ability to hide the middle of the ID number for the NSString class, then driver A, who has been writing code for A long time, adds A method name for the NSString called getMaskedIdCardNumber, but he needs to hide it from the four-digit string [9, 12]. A few days later, colleague B also met a similar requirement. He was also an old driver. He added a method also called getMaskedIdCardNumber to NSString, but his requirement was to hide it from the 4-digit string [8, 11]. The single test written for this method failed. He thought he had written the wrong interception method. After checking several times, he found that the project introduced another NSString classification with the same name 😂 true pit.

The following example is an SDK, but so is daily development.

  • Category name: It is recommended to prefix the Category with the abbreviation of the current SDK name, underline it, and add the functionality of the current Category, i.eClass name +SDK Name Short _ Function name. For example, if the current SDK is called JuhuaSuanAPM, then the NSURLSessionTask Category name is calledNSURLSessionTask+JuHuaSuanAPM_NetworkMonitor.h
  • Category attribute name: It is recommended to prefix the current SDK name with an underscore and the attribute name, i.eSDK Name Abbreviation _ Attribute name. Such as JuhuaSuanAPM_requestId `
  • Category method name: It is recommended to prefix the current SDK name with the abbreviation, underline, and add the method name, i.eSDK Name Abbreviation _ Method name. Such as-(BOOL)JuhuaSuanAPM__isGzippedData

Examples are as follows:

#import <Foundation/Foundation.h>

@interface NSURLSessionTask (JuhuaSuanAPM_NetworkMonitor)

@property (nonatomic, copy) NSString* JuhuaSuanAPM_requestId;

@end

#import "NSURLSessionTask+JuHuaSuanAPM_NetworkMonitor.h"
#import <objc/runtime.h>

@implementation NSURLSessionTask (JuHuaSuanAPM_NetworkMonitor)

- (NSString*)JuhuaSuanAPM_requestId
{
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setJuhuaSuanAPM_requestId:(NSString*)requestId
{
    objc_setAssociatedObject(self, @selector(JuhuaSuanAPM_requestId), requestId, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
Copy the code