Learning from Ele.me: How to Optimize the Network Layer Architecture of iOS Apps Using HTTP/2?

HTTP/2 is the first update to the HTTP protocol since its release, approved on February 17, 2015. It employs a series of optimization techniques to enhance the overall transmission performance of the HTTP protocol, such as asynchronous connection multiplexing and header compression, making it one of the essential solutions for optimizing network layer architecture in current internet application development.

Apple has also been very proactive regarding HTTP/2. Shortly after its official release in May, it announced at the WWDC 2015 conference in June that iOS 9 would support HTTP/2. Despite Apple’s early announcement of support for HTTP/2, most discussions in the tech community regarding iOS network layer architecture design still largely revolve around the HTTP 1.1 era, lacking a modern design strategy that incorporates HTTP/2 optimizations.

Regarding architecture design, I have previously stated that discussing architecture without considering business is purely nonsense. Therefore, architecture design must be aligned with current business needs and planned with a certain level of scalability to accommodate future changes.

This article will discuss the following aspects in conjunction with Ele.me’s current business:

  1. How to use HTTP/2 on iOS?

  2. How to design a network layer architecture for iOS?

  3. Our solutions in line with modern standards?

1Keeping Up with the Times: iOS Network Library under HTTP/2

Learning from Ele.me: How to Optimize the Network Layer Architecture of iOS Apps Using HTTP/2?

For mobile apps, the network layer is an almost indispensable role. It is precisely in the network layer that the differing business models and structures of various companies lead to a diverse array of architectural designs. On the other hand, Apple has also provided well-encapsulated APIs for the network layer; even if you are not very familiar with Apple’s network APIs, using popular industry libraries like AFNetworking or ASIHttpRequest can simplify many operations. However, the latter has not been maintained for many years, making AFNetworking essentially the standard for iOS apps.

Apple provides two main categories of network APIs based on the CFNetworking library at the CocoaTouch layer: NSURLConnection and NSURLSession. The latter appeared starting from iOS 7 and claims to be a replacement for NSURLConnection. Over time, the WWDC 2015 officially announced the deprecation of NSURLConnection in iOS 9, fulfilling its historical mission and honoring previous commitments.

However, in actual development, we find that despite being marked as deprecated, it does not mean that the NSURLConnection library cannot continue to be used; moreover, due to habitual issues, many engineers still cling to the familiarity that NSURLConnection provides, especially those using the AFNetworking library, as AFNetworking’s encapsulation of NSURLConnection is very elegant and allows for custom dependency additions based on business needs, making it feel incredibly “comfortable” in practical applications.

However, one cannot have both fish and bear’s paw. WWDC 2015 Session 711 tells us that the HTTP/2 protocol, which only started being supported from iOS 9, can only be used in NSURLSession. This means that to evolve to HTTP/2, one must abandon the long-familiar NSURLConnection and adjust the mindset of designing the network layer architecture to focus on NSURLSession.

Fortunately, AFNetworking has provided an implementation version based on NSURLSession since version 2.0, and the related APIs have not changed significantly, allowing for a smooth migration in general; furthermore, starting from AFNetworking 3.0, it officially abandoned NSURLConnection and fully embraced NSURLSession.

If you have used AFNetworking in your network layer design to reduce complexity, as mentioned earlier, due to the minimal differences in APIs, a smooth transition can be achieved during the general network layer migration. If you have directly used the native APIs in your network layer design, there is no need to worry, as the NSURLSession API is designed to be more elegant and user-friendly. However, despite the many commendable aspects of NSURLSession, it is still a new design philosophy and concept. Therefore, in practical use, we will find many areas that conflict with previous design thinking, especially in handling network dependencies, where even AFNetworking does not perform particularly well, a point we will revisit in later sections.

In summary, technology always moves forward, and based solely on HTTP/2, we believe that Apple will certainly shift its focus towards NSURLSession and continue to optimize it, while we must keep pace with the times, it is time to let the aging NSURLConnection rest.

2Advancing: Network Layer Architecture Design for iOS

Learning from Ele.me: How to Optimize the Network Layer Architecture of iOS Apps Using HTTP/2?

Architecture design is always combined with and adapted to business development. In the development of multiple apps for Ele.me, the differences in various businesses lead to different requirements for interfaces, protocols, etc., making it a challenge to design a network layer with clean APIs and high internal coupling for multiple apps, which will directly affect the development efficiency of our app business engineers. This section mainly discusses theory and raises some questions. The next section will provide a network layer solution designed in conjunction with the business of multiple Ele.me apps.

In this section, we will mainly discuss two points:

  1. Network layer design combined with business

  2. Network layer design related to security

Network Layer Design Combined with Business

First, let’s look at network layer design combined with business.

The closest connection to the business is undoubtedly the input and output, and the function of the network layer is to accept input data, select a corresponding channel to assemble data to send to the server, and then return the data returned by the server to the upper layer, i.e., output data. Next, we will examine what issues need to be considered in network layer design from the perspective of data. We will elaborate on the following three aspects:

  1. Data input

  2. Data callback

  3. Data transformation

  • Data Input

First is the input process. Business data calling the network layer interface can be referred to as input, and there are generally two forms of design.

The first, more common form is often referred to as centralized API processing, which encapsulates some frequently used network layer calling code into one or two functions for the upper layer to call. The upper layer inputs the relevant parameters to obtain the corresponding callback. For example, the following function:

+ (void)networkTransferWithURLString:(NSString *)urlString

andParameters:(NSDictionary *)parameters

isPOST:(BOOL)isPost

transferType:(NETWORK_TRANSFER_TYPE)transferType

andSuccessHandler:(void (^)(id responseObject))successHandler

andFailureHandler:(void (^)(NSError *error))failureHandler {

// Encapsulate AFN

}

The other form of design adopts an inheritance model where each API corresponds to a class, and this class sets all parameters for that API and provides a “start” interface and a “return” Block. This is often referred to as distributed API processing. A more general BaseAPI can have the following configurable items:

typedef NS_ENUM(NSUInteger, DRDRequestMethodType) {

DRDRequestMethodTypeGET = 0,

DRDRequestMethodTypePOST = 1,

DRDRequestMethodTypeHEAD = 2,

DRDRequestMethodTypePUT = 3,

DRDRequestMethodTypePATCH = 4,

DRDRequestMethodTypeDELETE = 5

};

@interface DRDBaseAPI : NSObject

@property (nonatomic, copy, nullable) NSString *baseUrl;

@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject, NSError * _Nullable error);

– (DRDRequestMethodType)apiRequestMethodType;

– (DRDRequestSerializerType)apiRequestSerializerType;

– (DRDResponseSerializerType)apiResponseSerializerType;

– (void)start;

– (void)cancel;

@end

Each specific API can inherit from this BaseAPI. When the upper layer business needs to make a network call, it instantiates the required API interface, encodes the returned Block, and starts the interface. For example, the following code:

DRDAPIPostCall *apiPost = [[DRDAPIPostCall alloc] init];

[apiPost setApiCompletionHandler:^(id responseObject, NSError * error) {

}];

[apiPost start];

Both forms of network layer interface design correspond to different business-generated thinking, thus inevitably having their pros and cons. For example, the advantage of the first form of interface is its simplicity, suitable for relatively simple and uniform business logic RESTFUL API network interfaces. However, the drawbacks are also very apparent; once faced with slightly more complex network interface situations, a large amount of logic needs to be written in the ViewController to achieve the goal. This also makes the already bloated ViewController even larger.

The second design is much more elegant. It places a lot of configuration logic in another class file, making the code in the ViewController lighter. Additionally, placing configurations in a class allows for the addition of many rarely used configurable items, increasing the overall scalability of the network layer; meanwhile, each API corresponding to a different class design allows different APIs to have different appearances, such as adhering to different JSON-RPC versions.

However, this form of design also has a glaring issue, which is class explosion. If it is a small app, the problem is not so obvious; however, for medium to large apps, with potentially hundreds of APIs, maintaining them later can become quite challenging.

  • Data Callback

Having discussed the input issues, let’s move on to the output design. In the output part of the design, various methods can be employed.

Network layer transmission is mostly based on asynchronous loading, meaning that after the server responds, the network layer is responsible for pushing the data to the upper layer business thread. In the iOS system, there are many ways to handle such scenarios, such as direct broadcasting via Notification, callback functions using delegates, and the most distinctive Block, all of which can accomplish this task. So which method should be used? Before answering this question, let’s first look at the pros and cons of each of these methods.

  • Notification

Notification, as the name suggests, is a broadcast, characterized by one-to-many notifications of relevant data. The advantages are very clear and easy to implement; however, the drawbacks are also apparent, as it disrupts the hierarchical structure of the entire app architecture, causing cross-layer calls and processing.

  • Delegate

Delegate is the most commonly used callback method. The advantage is that it is easy to maintain later and does not cause cross-layer calls; however, the drawback is that the callback code and input logic code are often not placed together, increasing the cost of reading later.

  • Block

Block is a feature in OC language, and its advantage is precisely the disadvantage of Delegate, as it allows the callback code to remain in the same location as the calling code, facilitating static code tracking and logical continuity. The downside is that it can easily lead to retain cycles; moreover, for large apps, embedding such AOP behavior in Blocks can be challenging and can complicate debugging.

When using Blocks, it is essential to use weakSelf and strongSelf to break the retain cycle. Otherwise, the resulting memory leaks can make later troubleshooting difficult.

  • Summary

Readers may find themselves more confused after this; what is the best solution? Personally, I believe it should be designed based on business needs. From my perspective, I prefer a combination of Block and Notification, supplemented by Delegate when appropriate.

  • Data Transformation

The issue of data callbacks has been largely resolved, but a new question arises: What kind of data should the upper layer see?

Here we can see many application scenarios. For example, in most cases, the business layer hopes to return data structures (Model instances) related to itself, making it very convenient to perform related operations on local data; or, for instance, when querying an operation result, the data may only be a yes or no, in which case using a data structure to encompass it would seem complex and bloated; or if the network layer adopts a protocol like JSON-RPC, the returned information may contain a lot of redundant data, while the upper layer business only needs a small portion.

From these various scenarios, we can see that the data format required by the business is highly variable, so the best approach is to let the upper layer handle it themselves. A common method is to set a Delegate or Block to return data transformation, converting formats like JSON or XML into the required data format for the upper layer business to continue processing. However, I personally prefer to implement this transformation function described by the Delegate or Block directly in the API itself, as it makes the API’s structure clearer. The next section will discuss our handling methods.

Network Layer Design Related to Security

Next, let’s look at designs related to security. In general, using HTTPS is usually sufficient to ensure your network security, and I will not enumerate its benefits here. In fact, most large companies abroad and the domestic BAT have almost all switched to HTTPS, and the difficulty of applying for free SSL certificates is continuously decreasing, making it virtually barrier-free. Therefore, for the security of the site, let’s switch to HTTPS.

However, if HTTPS is not used properly, there can still be some minor flaws, one of which is the MITMA (Man-in-the-middle attack). Consider a scenario where we use a packet capture tool like Charles to capture HTTPS packets; Charles will prompt us to install its own root certificate. Once we choose to trust this root certificate, we will find that Charles can successfully display the entire HTTPS communication.

To address such man-in-the-middle attacks, the current general solution is to adopt SSL Pinning, which involves packaging the server’s public key certificate with the entire app and then comparing the server’s certificate sent during the network request with the local certificate to avoid the possibility of man-in-the-middle attacks. AFNetworking already has relevant implementations for this part, which I have detailed in “Correctly Using AFNetworking’s SSL to Ensure Network Security,” so I will not elaborate further here.

3Let’s Get Practical: Solutions

Learning from Ele.me: How to Optimize the Network Layer Architecture of iOS Apps Using HTTP/2?

Having discussed the theory, let’s now talk about our practical solutions.

To use HTTP/2, we must choose NSURLSession for our network library to achieve this, and we do not want to implement serialization and the complexities of RESTFUL ourselves, so AFNetworking 3.0 becomes a good choice. However, it seems that this alone is not enough. The following sections will discuss our solutions:

  • Business Protocol

  • Input and Configuration

  • Data Transformation and Output

  • Security

Business Protocol

From the perspective of business protocol, each app among Ele.me’s many apps has its own characteristics; for example, some adopt RESTFUL designs, while others use JSON-RPC designs to achieve business objectives. If a centralized API design is adopted, a large amount of RPC protocol encapsulation code will be generated for JSON-RPC. Moreover, for different versions and types of RPC protocols, different centralized functions or a large number of parameters need to be added to handle the differences.

If a distributed API design is adopted, this protocol code can be handled within the API’s own class. Here, I designed an RPCProtocol, allowing the business side to define the required business RPC standards. Each API retains an rpcDelegate field to customize its upper layer protocol, and if it is empty, it indicates that no RPC encapsulation will be performed, and the request will be sent directly, thus achieving coexistence of JSON-RPC and RESTFUL in one app; furthermore, since each API can specify different rpcDelegates, it can accommodate compatibility with different RPC versions on the server side. The RPCProtocol will have some definitions like this:

NS_ASSUME_NONNULL_BEGIN

@protocol DRDRPCProtocol <NSObject>

– (nullable NSString *)rpcRequestUrlWithAPI:(DRDBaseAPI *)api;

– (nullable id)rpcRequestParamsWithAPI:(DRDBaseAPI *)api;

– (nullable id)rpcResponseObjReformer:(id)responseObject withAPI:(DRDBaseAPI *)api;

– (nullable id)rpcResultWithFormattedResponse:(id)formattedResponseObj withAPI:(DRDBaseAPI *)api;

– (NSError *)rpcErrorWithFormattedResponse:(id)formattedResponseObj withAPI:(DRDBaseAPI *)api;

@end

NS_ASSUME_NONNULL_END

The Protocol will design RPC boxing for RequestURL and RequestParams, and for the response, it will have rpcResponseObjReformer for unboxing, processing usable values and error values with rpcResultWithFormattedResponse and rpcErrorWithFormattedResponse before returning them to the upper business layer.

By using RPCProtocol, we maintain a certain level of scalability for the upper layer protocol of the entire network layer.

Input and Configuration

After resolving the protocol issues, let’s look at the input and configuration issues.

At first glance, the relationship between input and configuration may not seem significant; indeed, in centralized API design, it is often about passing parameters. However, in distributed API design, since each API inherits from BaseAPI and then overrides each required configuration function in the subclass, it appears that each API is more like a configuration process.

After configuration, each API might have previously corresponded to an APIManager, and in relation to AFNetworking, it might mean that each API uses an AFHTTPRequestOperationManager to initiate requests.

However, this form would seem foolish in the context of HTTP/2. We all know that HTTP/2 reuses TCP pipeline connections, which is reflected in NSURLSession’s underlying reuse of multiple tasks for each session.

If we continue to use multiple AFHTTPSession requests, it will lead to multiple TCP connections, which are uncontrollable. Reusing sessions can fully utilize NSURLSession’s concurrency control and HTTP/2’s high reuse to improve performance. I have detailed this in another article, “Don’t Say You Know AFNetworking 3.0/NSURLSession,” so I will not elaborate further here.

Therefore, I will place each configured API into a shared APIManager, thus returning the distributed API to a centralized calling approach. The APIManager will be responsible for providing the SessionManager strategy. Additionally, through Global configuration, we can determine the maximum concurrency for each session. At the same time, AFNetworking is encapsulated within the entire APIManager, maintaining transparency to the outside.

This way, if we upgrade the AFNetworking version in the future or plan to switch to directly using NSURLSession for network connections, the upper layer business APIs will not require any changes, further enhancing future configurability.

In the previous section, we mentioned distributed APIs, which have a significant drawback of causing class explosion, resulting in potentially thousands of API files. To address this, I designed another BaseAPI, which I call GeneralAPI. How does this API differ from the previous one? Let’s look at a typical GET request API class file content:

– (NSString *)requestMethod {

return @”get”;

}

– (id)requestParameters {

return nil;

}

– (DRDRequestMethodType)apiRequestMethodType {

return DRDRequestMethodTypeGET;

}

– (DRDRequestSerializerType)apiRequestSerializerType {

return DRDRequestSerializerTypeHTTP;

}

– (DRDResponseSerializerType)apiResponseSerializerType {

return DRDResponseSerializerTypeHTTP;

}

In this API, overriding several functions can complete the corresponding content. In the ViewController, the call would look like this:

DRDAPIGetCall *apiGet = [[DRDAPIGetCall alloc] init];

[apiGet setApiCompletionHandler:^(id responseObject, NSError * error) {

NSLog(@”responseObject is %@”, responseObject);

if (error) {

NSLog(@”Error is %@”, error.localizedDescription);

}

}];

[apiGet start];

However, if using GeneralAPI, the code in the ViewController would look like this:

DRDGeneralAPI *apiGeGet = [[DRDGeneralAPI alloc] initWithRequestMethod:@”get”];

apiGeGet.apiRequestMethodType = DRDRequestMethodTypeGET;

apiGeGet.apiRequestSerializerType = DRDRequestSerializerTypeHTTP;

apiGeGet.apiResponseSerializerType = DRDResponseSerializerTypeHTTP;

[apiGeGet setApiCompletionHandler:^(id responseObject, NSError * error) {

NSLog(@”responseObject is %@”, responseObject);

if (error) {

NSLog(@”Error is %@”, error.localizedDescription);

}

}];

[apiGeGet start];

Indeed, using GeneralAPI allows for some simple, uncomplicated API configurations to be directly assigned using properties in the ViewController, thus reducing the explosion of class files for simple APIs. This enhances the usability and simplicity of the entire network layer.

Data Transformation and Output

Now let’s look at data transformation and output.

Here we will discuss data transformation and output together. The previous section mentioned what kind of data to deliver to the business layer and the operations for data transformation, but did not provide answers. Now, let’s examine how to operate data transformation in conjunction with business.

As mentioned earlier, each app product line of Ele.me has its own personality and characteristics, and data transformation will also present its own preferences. Some prefer to use Mantle, some have used MJExtension, others use YYModel, and some experts implement their own JSON<–>Model transformations. Therefore, the network layer should ideally not interfere with the data transformation methods and processes but rather provide an opportunity for the upper layer business to adopt its own methods for transformation. Thus, in the API design, I provided a function like this:

– (nullable id)apiResponseObjReformer:(id)responseObject andError:(NSError * _Nullable)error;

In GeneralAPI, this corresponds to:

@property (nonatomic, copy, nullable) id _Nullable (^apiResponseObjReformerBlock)(id responseObject, NSError * _Nullable error);

This function defaults to empty, with the responseObject parameter being the responseObject generated after unpacking by rpcDelegate. In this function, the upper layer business can perform the Model transformation on the responseObject, returning the Model as the return value to the apiCompletionHandler function for processing. This maintains the simplicity of the ViewController while ensuring the diversity of JSON<–>Model transformations for various upper layer businesses, while also ensuring future extensibility of transformation methods.

Security

Finally, let’s talk about security.

In fact, there is not much left to discuss regarding security; everything that needs to be said has been covered in the previous section. Since the APIManager simplifies the complexity of SSL Pinning using AFNetworking, only three steps are needed to complete SSL Pinning in the network layer:

  1. Instantiate a DRDSecurityPolicy, setting the DRDSSLPinningMode in the SecurityPolicy to either DRDSSLPinningModePublicKey or DRDSSLPinningModeCertificate.

  2. Set the API’s apiSecurityPolicy to the above instance.

  3. Place the server’s public key certificate in the APP Bundle.

Done!

  • Bonus: One More Thing!

**Concurrent execution of multiple network requests. **

Imagine a scenario where we want several network requests to notify the upper layer application of completion only after all these requests have finished.

In the early days, when using AFNetworking’s AFHTTPRequestOperationManager solution, AFN controlled network request dependencies using NSOperation and NSOperationQueue for the entire system. However, with HTTP/2’s NSURLSession, this design is no longer applicable, making concurrent dependencies difficult to maintain.

Fortunately, AFNetworking still retains the dispatch_group_t interface in the implementation of SessionManager. Therefore, we created the DRDAPIBatchAPIRequests class using dispatch_group to achieve concurrent network requests.

@interface DRDAPIBatchAPIRequests : NSObject

@property (nonatomic, strong, readonly, nullable) NSMutableSet *apiRequestsSet;

@property (nonatomic, weak, nullable) id<DRDAPIBatchAPIRequestsProtocol> delegate;

– (void)addAPIRequest:(nonnull DRDBaseAPI *)api;

– (void)addBatchAPIRequests:(nonnull NSSet *)apis;

– (void)start;

@end

As always, we maintain simplicity at the API level. Here we use Delegate to handle callback operations after multiple network requests are completed:

@protocol DRDAPIBatchAPIRequestsProtocol <NSObject>

– (void)batchAPIRequestsDidFinished:(nonnull DRDAPIBatchAPIRequests *)batchApis;

@end

In this design, each API can have its own callback upon completion. After all concurrent network requests are completed, there can still be a common callback, allowing for a well-handled situation where both discrete and centralized designs can be effectively managed, ensuring the upper layer business’s scalability.

In summary,

Learning from Ele.me: How to Optimize the Network Layer Architecture of iOS Apps Using HTTP/2?

Only by combining with business can we discuss architecture; continuous optimization is necessary to keep pace with the times.

Finally, we have also open-sourced our DRDNetworking library, and we welcome everyone to provide valuable feedback.

Author Introduction

Wang Chaocheng, Head of Mobile Technology Architecture/Framework Group at Ele.me, Senior iOS Engineer, responsible for the long-term planning of Ele.me’s mobile technology, technology architecture selection, external technology solution evaluation, etc. His current areas of focus include mobile architecture, mobile security, and automated testing. Wang previously worked at ZTE’s European division and has also been an entrepreneur serving as the main technical leader. In his spare time, he enjoys photography and is a signed photographer for Huagai Creative.

Announcement One:

InfoQ’s “Give me a reason, and I’ll send you a good book” event’s fourth day’s results have been announced! Want to know if you are on the book giveaway list? Click Read the original text now!

Learning from Ele.me: How to Optimize the Network Layer Architecture of iOS Apps Using HTTP/2?

Announcement Two:

Tomorrow is International Women’s Day! Do you have any high-profile female programmer colleagues around you? Feel free to leave a message with “photo” in the WeChat backend, and we will compile and release it on the holiday. Successful selections will receive a free book!

Leave a Comment