At present, the trend of the platformization of Native App is becoming more and more obvious, and the network layer architecture is becoming more and more complex. An App basically has multiple different network modules. From simple business data HTTP/HTTPS(based on NSURLConnection or NSURLSession), to WebCore network layer of WebView, to push module based on TCP long connection, to various third-party components such as statistics, log reporting their respective network layer, Or many apps use proprietary protocols based on TCP, etc. The network layer is becoming more and more complex, which is more and more like a black box module for Native developers. Native developers can only focus on business development and have no knowledge of network layer exceptions, performance, etc.

Getting to know the iOS network layer API

Let’s peel back the web-related SDKS and look at what each layer does, layer by layer.

AFNetworking

AFNetworking is the NSURLConnection/NSURLSession encapsulation. Added the following logic

  • Encapsulated in NSOperation, which provides resume/ Cancel and so on.
  • Added NSData file handling, upload/download
  • Easy to handle JSON/XML data
  • Easy to handle HTTPS
  • Have the Reachablity apis

NSURLSession/NSURLConnection

Is based on CFNetwork NSURLSession/NSURLConnection.

NSURLConnection – Encapsulation of CFURLConnection. Create,start, Cancel,send(synchronous or asynchronous), set callback, set runloop, etc.

NSURLSession/NSURLSessionTask – NSCFURLSession/NSCFURLSessionTask encapsulation and so on.

The NSURLXXX layer mainly handles:

  • Encapsulate the BLOCKhandler of CFNetwork as a delegate
  • Handles agents associated with NSURLProtocol
  • CFURLCache handles cache correlation for NSURLCache, which is a wrapper around CFURLCache
  • Encapsulate the sendAsync/ and sendSync methods.
  • Convert the statusCode of CFURLResponse to a String

CFNetwork

CFNetwork shows how to encapsulate byte streams into HTTP protocol requests.

[image upload failed…(image-4f1C4B-1535019216015)]

  • CFURLRequestIt is created by the user and contains the requested URL/header/body information. thenCFURLRequestWill be converted toCFHTTPMessageThe format.
  • CFHTTPMessageThe main HTTP protocol definition and conversion, each request request into the standard HTTP format text.
  • CFURLConnectionIt mainly deals with request tasks, including pThread thread, CFRunloop, request queue management and so on. So apis for start, Cancel, and so on are provided. There is also a operationCFReadStreamSuch as API
  • CFHost: responsible for DNS, there areCFHostStartInfoResolutionAnd so on, based ondns_async_startandgetaddrinfo_async_startMethods. Based in iOS8/9getaddrinfo. This is mainly the difference between synchronous and asynchronous invocation.
  • CFURLCache/CFURLCredential/CFHTTPCookie: Handles cache/certificate /cookie related logic, all with corresponding NS classes.

The main data exchange calls the CFStream-based API.

CFStream

With CFSocketStream, encapsulate the BSD Socket and SecurityTransport(SSL calls).

BSD sockets are called synchronously. So the CFStream layer is mostly Runloop logic, locks,dowhile waits, and so on. BSD socket is an API for data stream input/output.

CFStream is created with a bunch of callbacks, including open/close,read/write, and so on. For example, CFSocketStream, which encapsulates a BSD Socket, is passed as a callback to CFStream.

CFSocketStream also includes logic for DNS, SSL connections, Connect handshakes, and more.

BSD Socket

How much set of apis, including the connect/shutdown, the send/recv, the read/write, recvfrom/sendto, the recvmsg/sendmsg. Accept /bind is generally not used as a client.

The difference between send/recv and read/write is that there is a flags argument. When flag is 0, send equals write.

For sending messages. Send can only be used for connection-based sockets, and sendto and SendMsg can be used for both connectionless and connection-based sockets. Except that the socket is set to non-blocking mode, the call will block until the data is sent.

DNS method: getaddrinfo is the replacement of gethostbyname/gethostbyaddr, support for ipv6, struct returned an address list. It’s used in ios 8/9. Getaddrinfo_async_start is used in iOS10 and supports asynchrony.

What to monitor

The ios-monitor-Platform article suggests some monitoring metrics (which his approach does not).

  • TCP connection establishment time
  • DNS time
  • SSL time
  • The first package time
  • The response time
  • HTTP error rate
  • Network error rate
  • traffic

Some monitoring indicators provided by APM factory Listening cloud:

  • TOP5 hosts with slowest response times
  • TOP5 hosts with the highest throughput rate
  • TOP5 regions with the slowest DNS time
  • TCP Connection to the slowest host
  • Host that has the most connections

HTTP packet capture tool Charles provides the following monitoring indicators:

  • Request Start Time
  • Request End Time
  • Response Start Time
  • Response End Time
  • Duration
  • DNS
  • Connect
  • SSL Handshake
  • Latency

Of course, if possible, we want to monitor every detail. However, many data have implementation costs, so this paper tries to collect as many indicators as possible with the lowest cost.

The specific implementation

HTTP monitoring

The best practice for HTTP monitoring is, of course, to use NSURLSessionTaskMetrics for NSURLSession.

[image upload failed…(image-7b6C8-1535019216015)]

Want to explore NSURLSessionTaskMetrics implementation, if the decompiled CFNetwork source code, you can see – [NSURLSessionTaskMetrics _initWithPerformanceTiming] this method, The specification comes from a class called TimingPerformance. The TimingPerformance initialization method code is shown below, and you can see that the key required by all NSURLSessionTaskMetrics time nodes is defined, which is almost identical. The CFAbsoluteTimeGetCurrent function is then used to record the initialization time.

int__ZN17PerformanceTimingC2Ev() { rbx = rdi; CFObject::CFObject(); . *(rbx +0x20) = @"_kCFNTimingDataRedirectStart";
    *(rbx + 0x30) = @"_kCFNTimingDataRedirectEnd";
    *(rbx + 0x40) = @"_kCFNTimingDataFetchStart";
    *(rbx + 0x50) = @"_kCFNTimingDataDomainLookupStart";
    *(rbx + 0x60) = @"_kCFNTimingDataDomainLookupEnd";
    *(rbx + 0x70) = @"_kCFNTimingDataConnectStart";
    *(rbx + 0x80) = @"_kCFNTimingDataConnectEnd";
    *(rbx + 0x90) = @"_kCFNTimingDataSecureConnectionStart";
    *(rbx + 0xa8) = @"_kCFNTimingDataRequestStart";
    *(rbx + 0xb8) = @"_kCFNTimingDataRequestEnd";
    *(rbx + 0xc8) = @"_kCFNTimingDataResponseStart";
    *(rbx + 0xd8) = @"_kCFNTimingDataResponseEnd";
    *(rbx + 0xe8) = @"_kCFNTimingDataRedirectCountW3C";
    *(rbx + 0xf8) = @"_kCFNTimingDataRedirectCount";
    *(rbx + 0x108) = @"_kCFNTimingDataTaskResumed";
    *(rbx + 0x118) = @"_kCFNTimingDataConnectCreate";
    *(rbx + 0x128) = @"_kCFNTimingDataTCPConnected";
    *(rbx + 0x138) = @"_kCFNTimingDataFirstWrite";
    *(rbx + 0x148) = @"_kCFNTimingDataFirstRead";
    *(rbx + 0x158) = @"_kCFNTimingDataConnectionInit";
    *(rbx + 0x168) = @"_kCFNTimingDataConnected"; . *(rbx +0x1f0) = @"_kCFNTimingDataTimingDataInit"; . CFAbsoluteTimeGetCurrent(); .return rax;
}
Copy the code

NSCFURLSessionTask resume [NSCFURLSessionTask resume]

void -[__NSCFURLSessionTask resume](void * self, void* _cmd) { rbx = self; . __setRecordForKeyInternalPerformanceTiming(@"streamTask-resume");
            r15 = rbx->_performanceTiming;
            if(r15 ! =0x0) {
                    PerformanceTiming::Class();
                    xmm0 = intrinsic_movsd(xmm0, *(r15 + 0x110));
                    xmm0 = intrinsic_ucomisd(xmm0, 0x0);
                    if ((xmm0 == 0x0) && (! CPU_FLAGS & P)) { CFAbsoluteTimeGetCurrent(); *(r15 +0x110) = intrinsic_movsd(*(r15 + 0x110), xmm0);
                    }
            }
            __setRecordForKeyInternalPerformanceTiming(@"start-task-resume-to-loader-start-load"); .return;
}
Copy the code

As you can see, the RBX register stores the NSCFURLSessionTask object, which has a member variable called _performanceTiming in register R15. The code above you can see (0 x108) corresponding _kCFNTimingDataTaskResumed is the key, and here xmm0 register is a floating point number stored register, storage (r15 + 0 x110), the corresponding should be, and then judge whether xmm0 is empty, if is empty, Just call CFAbsoluteTimeGetCurrent function gets the current CPU time, and then assign (r15 + 0 x110), the corresponding should be _kCFNTimingDataTaskResumed this key corresponding to the value.

As for __setRecordForKeyInternalPerformanceTiming this function, you can see its key does not exist in PerformanceTiming object initialization, it should be InternalPerformanceTiming, This is a different class, probably a subclass of PerformanceTiming. His key is different, and the judgment is used internally by the library and not passed as NSURLSessionTaskMetrics.

Found that __ZN17PerformanceTiming30fill __ZN17PerformanceTiming32fillW3NavigationTimingAWDMetricsEP27PerformanceTimingAWDMetrics StreamTaskTimingAWDMetricsEP26StreamTaskTimingAWDMetrics these two functions that PerformanceTiming and W3NavigationTiming, StreamTaskTiming and AWDMetrics are interchangeable. W3NavigationTiming is an EASY API for WebView Timing.

The nice thing about NSURLSessionTaskMetrics is that apple implemented it for us, but the serious drawback is that it only applies to NSURLSession after iOS10. NSURLConnection is not available. You can’t use it below iOS10. (See key findings below)

Analysis of other schemes

For NSURLSession and NSURLConnection up to iOS10, it’s hard to count time points, The main difficulty is that different SDKS have different API calls. For example, iOS8 and ios9 DNS can hook getaddrInfo,iOS10 sometimes can hook getaddrInfo_async_start, but for iOS11, I tried various DNS related functions. No hook at all. Decompiling CFNetwok function associated with DNS, also didn’t be NSURLSession/NSURLConnection calls. SSL is also very similar, only know iOS8/9 will pass SecurityTransport SSLHandshake/SSLRead/SSLWrite function, but the above iOS10 was completely didn’t force. These attempts have failed and come to an end.

Some articles believe that iOS10 after the system shield some BSD Socket functions hook, such as connect/read/write and so on. Not so far as I can see,BSD sockets are still able to hook, but most of the time these apis are not called, and in a few cases they are still used. If it is really shielded, should be completely hook less than.

Some articles have written about monitoring HTTP by using NSURLProtocol to intercept requests (such as listening to the cloud). I am skeptical, because monitoring performance is meaningless without DNS/ SSL-related monitoring, and there is no need to hook request/ Response monitoring (it can be implemented in the network layer part of its own encapsulation). Hooks for NSURL related apis do not count DNS/SSL because they are not implemented in this layer.

There are also some articles that hook CFStream (such as netease APM). CFStream is an API that encapsulates BSD sockets. Open/Close/read/Write CFStream is an API that encapsulates BSD sockets. There is not much difference between a hook CFStream and a BSD socket. Still no hook to DNS/SSL point.

Monitoring of the WebView

WebView monitoring is relatively simple, mainly Timing API.

[Image upload failed…(image-56e48E-1535019216015)]

The nice thing is that it’s compatible. Currently UIWebView and WKWebView are supported, iOS9 and above are supported. Because it’s a browser API.

PerformanceTiming API: PerformanceTiming API: PerformanceTiming API

class PerformanceTiming : public RefCounted<PerformanceTiming>, public DOMWindowProperty {
public:
    static Ref<PerformanceTiming> create(Frame* frame) { return adoptRef(*new PerformanceTiming(frame)); }

    unsigned long long navigationStart(a) const;
    unsigned long long unloadEventStart(a) const;
    unsigned long long unloadEventEnd(a) const;
    unsigned long long redirectStart(a) const;
    unsigned long long redirectEnd(a) const;
    unsigned long long fetchStart(a) const;
    / /... Omitting partial function
    unsigned long long domContentLoadedEventStart(a) const;
    unsigned long long domContentLoadedEventEnd(a) const;
    unsigned long long domComplete(a) const;
    unsigned long long loadEventStart(a) const;
    unsigned long long loadEventEnd(a) const;

private:
    explicit PerformanceTiming(Frame*);
    const DocumentTiming* documentTiming(a) const;
    DocumentLoader* documentLoader(a) const;
    LoadTiming* loadTiming(a) const;
};

} // namespace WebCore
Copy the code

The header file contains a bunch of getter definitions, and only one initialization method. The input parameter is a single Frame object, indicating that a Frame object can provide all these parameters.

unsigned long long PerformanceTiming::requestStart() const
{
    DocumentLoader* loader = documentLoader();
    if(! loader)return connectEnd();

    const NetworkLoadMetrics& timing = loader->response().deprecatedNetworkLoadMetrics();
    ASSERT(timing.requestStart >= 0_ms);
    return resourceLoadTimeRelativeToFetchStart(timing.requestStart);
}
unsigned long long PerformanceTiming::domInteractive() const
{
   const DocumentTiming* timing = documentTiming();
   if(! timing)return 0;
   return monotonicTimeToIntegerMilliseconds(timing->domInteractive);
}
unsigned long long PerformanceTiming::loadEventStart() const
{
    LoadTiming* timing = loadTiming();
    if(! timing)return 0;
    return monotonicTimeToIntegerMilliseconds(timing->loadEventStart());
}
Copy the code

The CPP file shows that PerformanceTiming is a wrapper around the parameters already counted in the Frame class, with no logic inside. The data comes from NetworkLoadMetrics, DocumentTiming, and LoadTiming. It is also easy to understand the performance statistics related to network requests, DOM loading, and WebView loading.

One detail is that NetworkLoadMetrics has a 0ms judgment, which ensures that NetworkLoadMetrics returns a value greater than 0. The data returned by DocumentTiming and LoadTiming is 0 if null. There are actually some cases where the parameters are 0 when using this set of data, and it is related to the interface that calls PerformanceTiming.

The class architecture of WebCore is shown below. [image-449013-1535019216015] [image-449013-1535019216015] [image-449013-1535019216015] [image-449013-1535019216015] [image-449013-1535019216015] Tracing the loadRequest: method of WKWebView, the call stack should look like this:

- (WKNavigation *)loadRequest:(NSURLRequest *)request
void WebPage::loadRequest(const LoadParameters& loadParameters)
void UserInputBridge::loadRequest(FrameLoadRequest&& request, InputSource)
void FrameLoader::load(FrameLoadRequest&& request)
void FrameLoader::load(DocumentLoader* newDocumentLoader)
void FrameLoader::loadWithDocumentLoader(DocumentLoader* loader, FrameLoadType type, FormState* formState, AllowNavigationToInvalidURL allowNavigationToInvalidURL)
void FrameLoader::continueLoadAfterNavigationPolicy(const ResourceRequest& request, FormState* formState, bool shouldContinue, AllowNavigationToInvalidURL allowNavigationToInvalidURL)
void DocumentLoader::startLoadingMainResource()
void ResourceLoader::start()
void ResourceHandle::createNSURLConnection(id delegate, bool shouldUseCredentialStorage, bool shouldContentSniff, SchedulingBehavior, NSDictionary *connectionProperties);
Copy the code

ResourceLoader is the resource load, and the class that actually operates on the network request is the ResourceHandle. Looking at the code, WebCore’s network layer on iOS is also based on NSURLConnection. You can hook into a location using AOP and then use the NSURLConnection API to do so.

One interesting thing about WebCore is that there’s an NSURLSession implemented in WebCore, called WebCoreNSURLSession. This class seems to be used only in MediaPlayer. Again, they have similar apis like dataTaskWithRequest: and so on, but the internal implementation is different. WebCoreNSURLSession is also a subclass based on ResourceLoader. NSURLSession is based on CFNetwork.

There are also details to pay attention to when using the Timing series apis. The WebCore kernel is not the same on iOS as it is on Safari on Mac. WKWebView was implemented after iOS10. ToJSON (). If it’s a UIWebView, or a WKWebView under iOS10, you need to execute a JS script to convert the JS object to JSON.

NSString *funcStr = @"function flatten(obj) {"
        "var ret = {}; "
        "for (var i in obj) { "
        "ret[i] = obj[i];"
        "}"
        "return ret; }";
[webView stringByEvaluatingJavaScriptFromString:funcStr];
Copy the code

TCP surveillance

Generally, the long connection at the network layer of App will implement a custom protocol based on TCP or use Websocket. Some apps are packaged based on BSD sockets (such as wechat’s Mars). Some use open source frameworks such as CocoaAsyncSocket or SocketRocket before encapsulating them.

CocoaAsyncSocket

CocoaAsyncSocket is based on the BSD Socket, CFStream SecurityTransport encapsulation, encapsulated into a TCP/UDP protocol. What these apis have in common is the form of data stream reads and writes. BSD sockets block primarily synchronously, whereas CFStream blocks asynchronously.

CocoaAsyncSocket must include data stream processing and conversion. It mainly includes buffer, ReadBuffer/WriteBuffer, CRLF to judge the end of read, length to read and timeout mechanism to read, etc. CocoaAsyncSocket also encapsulates DNS, ipv4 and ipv6, SSL, and other logic.

SocketRocket

Nsstream-based encapsulation, different from CocoaAsyncSocket transport layer protocol, supports HTTP/WebSocket application layer protocol, defines header fields, etc. Since it is read and write based on data stream, it also includes readBuffer/WriteBuffer and other data processing logic. It also includes asynchronous processing logic like Runloop, thread, and blocking synchronization logic. Also implemented PingPong, with the server side of the survival logic.

So the TCP monitor can hook the BSD Socket API, including the connect/disconnect/read/write calls, if it is a synchronous call, so can the execution function before and after buried point computation time. Also needs to hook the DNS apis, such as gethostbyname/getaddrinfo synchronization and getaddrinfo_async_start asynchronous invocation API calls. Can also hook API SSL, such as SSLHandshake/SSLRead/SSLWrite, realize the monitoring of the SSL connection.

Plot twist, major discovery (this paragraph was added later)

HTTP performance monitoring seemed to be dead in the water due to NSURLSessionTaskMetrics compatibility issues, but I made a huge discovery while writing part 2 of the WebView. This discovery came from looking at the WebCore source code and found something amazing.

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

NSURLConnection has a collection API for TimingData, but it’s not exposed to developers, but WebCore uses it all the time. Apple why are you so mean? ! Then you can easily find the _setCollectsTimingData: API for NSURLConnection in the Runtime header, as well as the _timingData API. This goods iOS8 after all is supported, iOS8 may also support before.

What about NSURLSession? Is that something like that? Indeed as expected. Before iOS9, you just set _setCollectsTimingData:. After searching Google and Github, I was probably the first to discover this private API…

So magically, it’s easy to implement full support for NSURLConnection and NSURLSession….

conclusion

Most of the network performance data collection across HTTP/WebView/TCP frameworks can be done with almost no code. If you put compatibility into a table you can see that we support almost all scenarios.

iOS SDK NSURLConnetion NSURLSession UIWebView WKWebView TCP
8.4 YES YES via TCP via TCP YES
9.3 YES YES YES YES YES
10.3 YES YES YES YES YES
11.3 YES YES YES YES YES

NetworkTracker is part of the code THAT I encapsulate. And drew a simple graph of the monitoring results. It’s kind of intuitive.