preface

The network layer is also an indispensable part in an App, and engineers can play a large space in the network layer. In addition, The network request part has been well encapsulated by Apple, and AFNetworking is widely used in the industry. Other ASIHttpRequest, MKNetworkKit, etc are all good, but the former is dead and the latter is on the verge of being dead. In actual App development, Afnetworking has become a standard configuration in virtually every major App.

The network layer hosts tasks such as API calls, user action logging, and even instant messaging within an App. I’ve been working with some App code (open and non-open), and I was really excited to see the networking layer, especially when the various architects showed off their skills. But sometimes, often also feel disappointed for some of the defects.

There will be a lot of design schemes for the network layer, there will be a lot of trade-offs, and even a lot of controversy. But in any case, I will not make any escape from these problems, in this article I will give my views on them and solutions, not neutral views, will not fight tai chi.

This article will mainly talk about these aspects:

  1. Design the connection between network layer and business
  2. Implement the security mechanism at the network layer
  3. Optimization scheme of network layer

This article is reprinted from 2015: casatwy.com/iosying-yon… For the purpose of conveying more information, the copyright of this article belongs to the original author or source organization.

Design the connection between network layer and business

When the architecture of Anjuke App was updated, I deeply felt how important it was to design the connection between the network layer and business. Therefore, the biggest change I made was to change the connection between the network layer and business. The design of the connection between the network layer and the service layer will directly affect the mood of service engineers when they implement functions.

Before we get down to design, we need to discuss a few questions:

  1. What interaction pattern is used to interface with the business layer?
  2. Is it necessary to encapsulate the data returned by the API into objects that can then be delivered to the business layer?
  3. Do you use intensive or discrete calls to call the API?

After the discussion of these problems, I will give you a complete design scheme for your reference, the design scheme is fish, the discussion of these problems is fishing, I give everything, we take what we need.

What interaction pattern is used to interface with the business layer?

There are really two problems here:

  1. How is the data delivered to the business layer?
  2. What data is delivered to the business layer?

How is the data delivered to the business layer?

There are many ways to transfer data between objects in the field of iOS development. Most of the schemes I have seen adopted in the network layer of apps mainly focus on these three: Delegate, Notification and Block. KVO and Target-Action I haven’t seen in use yet.

At present, I know that the edge player mainly adopts block, and the great Wisdom mainly adopts Notification. In anjuke, block was the main method in the early stage, and then it was changed to Delegate. Ali did not find any place to transmit data through Notification (there may be). Delegate, Block, and target-Action are all available. According to the author of the Network layer of Alibaba iOS App, this is to make it easier for the business layer to choose its own method to use. Every time I see this part, I like to ask the authors why they are using this interaction scheme, but very few of them are able to come up with the rules.

On my side, however, my ideas are mostly Delegate, but not Notification. Here’s why:

  • Minimize the possibility of cross-layer data communication and limit coupling
  • Unified callback method for easy debugging and maintenance
  • The part that connects to the business layer uses only one docking device (in my case, just a delegate) to limit flexibility in exchange for application maintainability

Minimize the possibility of cross-layer data communication and limit coupling

What is cross-layer data communication? This is when a layer (or module) exchanges data with another layer (or module) with which it has no direct docking relationship. Why is this bad? Strictly speaking, most cases are not good, and sometimes cross-layer data communication is a requirement. The downside is that it can lead to code clutter and break module encapsulation. One of the goals of the layered architecture is that the lower layer abstracts the upper layer so that the upper layer can do its business without having to worry about the details of the lower layer.

Therefore, if the lower detail is exposed across layers, on the one hand, you can easily lose the protection of the adjacent layer to the exposed detail; On the other hand, you can’t afford not to work on this detail, so the code that works on this detail is scattered all over the place and ultimately difficult to maintain.

Concretely, let’s consider the case where A< -b < -c. When C has an event, it tells B in some way, and THEN B executes the corresponding logic. Once the notification is not properly handled, it is difficult to guarantee that A level business engineers will not deal with this detail in the future. Once A business engineer generates A processing operation at Layer A, either to supplement logic or to execute A business, A portion of the processing code for this detail is scattered at layer A. However, the former should not be scattered in the A layer, the latter may be the demand. In addition, since layer B is abstracted from layer A, the execution of supplementary logic may conflict with layer B’s handling logic for this event, which is undesirable.

So when does cross-layer data communication become a requirement? At the network layer, the signal changes from 2G to 3G to 4G to Wi-Fi, which is one of the requirements for cross-layer data communication. However, I can’t think of any other requirements for cross-layer data communication at the moment. Haha, this is probably the only one.


Strictly speaking, the use of Notification for data exchange between the network layer and the business layer does not necessarily mean cross-layer data exchange. However, the use of Notification opens an opening for cross-layer data exchange, because the impact surface of Notification cannot be controlled. As long as there are instances, there is the possibility of being affected. In addition, no one can guarantee that the relevant processing code is in one place, which can lead to maintenance disasters. As an architect, it is necessary here to give the business engineers the flexibility to limit their operations. In addition, Notification also supports one-to-many situations, which also allows for code scattering. At the same time, the response method corresponding to Notification is difficult to restrict at the compile level, and different business engineers can call it different names, which can also spell disaster for code maintainability.

Xiaowu from the mobile Taobao architecture group once shared a problem with me, and HERE I share it with you: There was once an engineer who did not write the code to release the listening after monitoring the Notification. Of course, it took a long time to find out the reason. Now we have found the reason. We don’t know who the unreleased wiretap is. Later, after a long time of difficulties, an experienced engineer proposed to hook (Method Swizzling), and finally found the unreleased listening object and fixed the bug.

I share this question not to emphasize how bad Notification is, but to emphasize that Notification itself is a design pattern and a very good solution to its problem domain. But I want to emphasize that for the network layer problem domain, the architect must first limit the scope of the code and use small solutions when they are available, otherwise it will be very difficult to maintain if there are strange requirements or small problems in the future. Therefore, Notification is not a preferred option, but an alternative.

Notification is not completely unusable. We can use Notification when the requirement is cross-layer, such as the network condition switching mentioned earlier, and this requirement also needs to be one-to-many.

So, in order to meet these requirements, using a Delegate is a good way to avoid cross-layer access and limit the form of the response code, which is much more maintainable than Notification.


And then we’ll talk in passing about why you should try not to use blocks.

  • Blocks are hard to track and hard to maintain

When we’re debugging, we often step to a certain place and find out there’s a block, and if we want to know what’s going on in that block, it’s kind of a pain in the ass.

- (void)someFunctionWithBlock:(SomeBlock *)block { ... . -> block(); // It's hard to know what's going on inside the block when you step in here. . . }Copy the code
  • Blocks extend the life of the object in question

Block increments all internal object reference counts by one, which leads to a potential retain cycle that can be addressed by Weak Self. Another important aspect is that it extends the life of the object.

The use of blocks in network callbacks is one of the situations where blocks cause the life of an object to be extended. When the ViewController is removed from the window, if there are requests flying around with blocks, Then the block references the ViewController (which is very common), and the ViewController cannot be reclaimed in time. Even if you cancel the request, it still has to wait until the request lands.

The delegate is a weak reference, and even if the request is still flying around, the ViewController can be recycled in time, and the pointer will be set to nil. It doesn’t matter.

  • Block does not comply with the usage specifications in discrete scenarios

Block and delegate look similar at first glance, but there is a strict rule about how they should be chosen: In cases where the task to be done after the callback is consistent on every callback, select delegate, and in cases where the task to be done after the callback is not guaranteed to be consistent on every callback, select Block. In the case of discrete calls, where each callback is consistent with the task, delegate applies. This is why Apple’s native network calls also use a delegate, because Apple designs network calls based on a discrete model, and the network layer architecture that this article will introduce is designed based on discrete calls.

In the case of intensive calls, it makes sense to use blocks because each request has a different type, so the natural callback has a different task to do, so you have to use blocks. AFNetworking is an intensive call, so it uses blocks for callbacks.

As far as I know, most companies currently have intensive App network layer calls, so block callbacks are widely adopted. However, the idea of using intensive calls directly for business is problematic in the network layer architecture design of the App, so it is important to pay attention to this when migrating to discrete calls, and remember to migrate back to the delegate callback. About the introduction of discrete and intensive calls and how to select, I in the following intensive API call mode and discrete API call mode choice? This is explained in detail in the section.

So try not to abuse blocks, especially at the network layer.


Unified callback method for easy debugging and maintenance

I’ve talked about cross-layer issues, separating delegates from Notifications, and talking about blocks. And then we’re talking about another case where the Block scheme is not a good idea. First of all, blocks themselves are not good or wrong, only appropriate or inappropriate. In the case of this section, blocks do not have uniform callback methods, and are difficult to display on the call stack during debugging and maintenance, making it a pain to find.

Using blocks is fine for network requests and where the network layer accepts requests. However, it is best to notify the business side through a Delegate when data is obtained and handed to the business side. Because the Block contains the callback code in the same place as the calling logic, that part of the code becomes very long because it contains the before and after logic. On the other hand, this somewhat violates the single Function, single Task principle, where you need to call an API, you just write code for the API call, and where you need to call a callback, you write code for the callback.

And then I see that in most apps, when the business engineers write the code and write it over here, they realize this problem. So they’ll write a one-sentence method inside the block that takes arguments, forwards them, and then they can put that method somewhere else, bypassing the fact that the block’s callback landing sites are not uniform. Like this:

[API callApiWithParam:param successed:^(Response *response){ [self successedWithResponse:response];  } failed:^(Request *request, NSError *error){ [self failedWithRequest:request error:error]; }];Copy the code

This is essentially the same as using the Delegate method, but it’s a little bit of a roundabout, but it still doesn’t solve the problem of uniform callback methods, because the names of methods written in blocks can vary from ViewController object to ViewController object, and business engineers have a lot of different ideas. So instead of being too convenient for the architect, use the delegate approach so that the business engineer doesn’t have to be too convoluted. Block is the method adopted by most third-party network libraries at present. Because it is relatively simple to use blocks for sending requests, there is no problem in the request layer. However, after exchanging data, it is better to switch to a delegate, such as AFNetworking:

[AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){ if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) { [self.delegate successedWithResponse:response];  } } failed:^(Request *request, NSError *error){ if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) { [self failedWithRequest:request error:error]; } }];Copy the code

In this way, the callback function on the business side can be relatively unified and easy to maintain.

In summary, what are the ways in which data is delivered to the business layer? Here’s the answer to that question:

Whenever possible, the data is delivered via a Delegate callback to avoid unnecessary cross-tier access. When there is a need for cross-layer access (such as signal type switching), the data is delivered via Notification. Blocks should normally be avoided.

What data is delivered to the business layer?

I’ve seen a lot of apps where the web layer takes the JSON data and turns it into a corresponding object prototype. Notice I’m not talking about NSDictionary here, I’m talking about objects like Item. This will improve the readability of the code for subsequent operations. In a more intuitive way, this part of the transformation process is needed, but the cost of this part of the transformation process is very high, the main cost is:

  1. The cost of converting the contents of an array is high: each Item in the array has to be converted to an Item object, which is a headache if there are similar arrays in the Item object.
  2. In most cases, the transformed data cannot be displayed directly. In order to be displayed, a second transformation is required.
  3. These object prototypes (items) are highly reusable only if the data returned by the API is highly standardized, otherwise they are prone to type explosions and increase maintenance costs.
  4. Viewing the data content through object prototypes is not as intuitive as directly through NSDictionary/NSArray during debugging.
  5. When data from the same API is presented by different views, it is difficult to control the code that converts the data, and it can be scattered wherever it is needed.

Ideally, we want the API data to be presented to the View as soon as it’s delivered. The first thing to say is that it’s very rare. In addition, this approach makes the View and THE API closely related, which is not desirable.

When designing the network layer data delivery part of Anjuke, I added the reformer (name, whatever) object to encapsulate the logic of the data transformation. This object is a separate object that actually exists as an Adaptor pattern. Think of it this way: think of the shower head we use when we take a shower, and the water that comes out of the hose is raw data delivered by the API. The reformer is the different water baffle on the showerhead, which is used for whatever mode is needed.

In practice, the code looks like this:

@protocol ReformerProtocol <NSObject> - (NSDictionary)reformDataWithManager:(APIManager *)manager; @property (nonatomic, strong) id<ReformerProtocol> XXXReformer; @property (nonatomic, strong) id<ReformerProtocol> YYYReformer; #pragma mark - APIManagerDelegate - (void)apiManagerDidSuccess:(APIManager *)manager { NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer]; [self.XXXView configWithData:reformedXXXData]; NSDictionary *reformedYYYData = [manager fetchDataWithReformer:self.YYYReformer]; [self.YYYView configWithData:reformedYYYData]; } In APIManager, fetchDataWithReformer looks like this:  - (NSDictionary)fetchDataWithReformer:(id<ReformerProtocol>)reformer { if (reformer == nil) { return self.rawData; } else { return [reformer reformDataWithManager:self]; }}Copy the code
  • Point 1: The Reformer is a ReformerProtocol compliant object that provides a common method for the Manager to use.

  • Point 2: the API’s rawData (JSON objects) is held by the Manager instance. The reformer method takes the rawData of the Manager (manager.rawdata), converts it, and delivers it. The water pipe part of the shower head is the Manager, who is responsible for providing the original water flow (data flow). The reformer is a different mode, and any reformer can produce any water flow.

  • Point 3: The scenario in the example is one in which an API data is used by multiple Views, and represents a feature of reformer: it is possible to change the presentation of the same data source as needed. For example, the API data shows the “neighborhood”, then the data can be shared by a list (XXXView) and a map (YYYView). Different views use different data conversion methods, which can be used by different reformer.

  • Point 4: Reformer is a great tool in cases where a view is used to present different API data at the same time. For example, the data sources of anjuke list View may be three: second-hand house list API, rental house list API, and new house list API. The data returned by these apis may have the same value, but not the same key. The same reformer can then be used to standardize the output of the data, making it possible to reuse the view code. This shows another feature of reformer: the data produced by the same Reformer is highly standardized. Image point is: as long as the shower head is not changed, even if the water pipe into seawater or sewage, it can still output in line with the requirements of the fresh water bath. Here’s an example:

- (void)apiManagerDidSuccess:(APIManager *)manager {// But at the Controller level we don't need to make any extra distinction, as long as the data is from the same reformer, we can guarantee that it will be used by self.xxxView. Such assurance is provided by the implementer of the Reformer. NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer]; [self.XXXView configWithData:reformedXXXData]; }Copy the code
  • Point 5: Did you find the Controller code much cleaner after using Reformer? Moreover, the data prototype is unnecessary in this case, and the costs associated with it are bypassed.

A reformer is essentially an object that conforms to a protocol. When the controller needs to get data from the API Manager, it can pass the reformer into the object, so that it can get the data rewashed by the Reformer, and then it can use it directly.

More abstractly, reformer is an encapsulation of data transformation logic. After the controller takes the data from the manager and delivers it to the View, there is more or less a data conversion. Sometimes, different views have different conversion logic, but the data displayed is the same. And often this part of the code is very complex and business related, directly to the code, will be difficult to maintain in the future. Therefore, we can consider using different reformer to encapsulate different transformation logic, and then ask the controller to select an appropriate reformer to install according to the needs. Just like the shower head for bathing, it changes the head according to the needs of water flow (data expression form), but the water (data) is the same. This greatly improves the maintainability of your code and reduces the ViewController’s size.

To sum up, Reformer actually encapsulates transformed code and then separates it from the main business, which not only reduces the complexity of the original business, but more importantly, improves the flexibility of data delivery. In addition, since the Controller is responsible for scheduling the Manager and View, it knows the relationship between the Manager and View. Once the Controller knows this relationship, it has sufficient and necessary conditions to select different Reformer for different views. Use this Reformer to transform Mananger’s data, and then the ViewController can get the data processed by the Reformer and deliver it directly to the View for use. As a result, the Controller is slimmer, and the code responsible for business data transformation does not need to be written in the Controller, which improves maintainability.


So the Reformer mechanic provides the following benefits:

  • Benefit 1: It avoids the conversion of API data prototypes and the associated costs.
  • Benefit 2: When dealing with single-view-to-multiple apis, and in the case of single-APi-to-multiple Views, Reformer provides a very elegant way to respond to this need, isolating the transformation logic from the principal business logic and avoiding maintenance disasters.
  • Benefit 3: Centralized transformation logic, and convert the number of times to only once. The conversion logic of data archetypes is used at least twice, first to map JSON to the corresponding archetype, and second to convert the archetype to data that can be processed by the View. Reformer in one step. In addition, the conversion logic is in the reformer. If the API data changes in the future, just find the corresponding Reformer and change it.
  • Benefit 4: Controller can therefore save a lot of code, reduce the code complexity, and improve the flexibility of switching reformer at any time without switching business logic to deal with the data needs of different views.
  • Benefit 5: Business data and business are properly isolated. This way, if the business logic changes in the future, a reformer can be used. If other businesses have the same data conversion logic, other businesses can use the reformer without rewriting it. In addition, if the Controller is modified (for example, the UI interaction mode is changed), it can be relieved to change the controller without worrying about the processing of business data.

How do you keep your data readable without using specific objects to represent it?

When you’re not using objects to represent data, that’s actually when you’re using NSDictionary. In fact, the question is, how do you keep your data readable when it’s represented by NSDictionary?

Apple has a very good practice of using fixed strings for keys, like when you receive a Notification from KeyBoardWillShow with a userInfo, His key is like UIKeyboardAnimationCurveUserInfoKey, so we adopt such a scheme to maintain readability. Here’s an example:

PropertyListReformerKeys.h

extern NSString * const kPropertyListDataKeyID;
extern NSString * const kPropertyListDataKeyName;
extern NSString * const kPropertyListDataKeyTitle;
extern NSString * const kPropertyListDataKeyImage;
Copy the code
PropertyListReformer.h #import "PropertyListReformerKeys.h" ... .Copy the code
PropertyListReformer.m NSString * const kPropertyListDataKeyID = @"kPropertyListDataKeyID"; NSString * const kPropertyListDataKeyName = @"kPropertyListDataKeyName"; NSString * const kPropertyListDataKeyTitle = @"kPropertyListDataKeyTitle"; NSString * const kPropertyListDataKeyImage = @"kPropertyListDataKeyImage"; - (NSDictionary *)reformData:(NSDictionary *)originData fromManager:(APIManager *)manager { ... . . . NSDictionary *resultData = nil; if ([manager isKindOfClass:[ZuFangListAPIManager class]]) { resultData = @{ kPropertyListDataKeyID:originData[@"id"], kPropertyListDataKeyName:originData[@"name"], kPropertyListDataKeyTitle:originData[@"title"], kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"imageUrl"]] }; } if ([manager isKindOfClass:[XinFangListAPIManager class]]) { resultData = @{ kPropertyListDataKeyID:originData[@"xinfang_id"], kPropertyListDataKeyName:originData[@"xinfang_name"], kPropertyListDataKeyTitle:originData[@"xinfang_title"], kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"xinfang_imageUrl"]] }; } if ([manager isKindOfClass:[ErShouFangListAPIManager class]]) { resultData = @{ kPropertyListDataKeyID:originData[@"esf_id"], kPropertyListDataKeyName:originData[@"esf_name"], kPropertyListDataKeyTitle:originData[@"esf_title"], kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"esf_imageUrl"]] }; } return resultData; }Copy the code
PropertListCell.m

#import "PropertyListReformerKeys.h"

- (void)configWithData:(NSDictionary *)data
{
    self.imageView.image = data[kPropertyListDataKeyImage];
    self.idLabel.text = data[kPropertyListDataKeyID];
    self.nameLabel.text = data[kPropertyListDataKeyName];
    self.titleLabel.text = data[kPropertyListDataKeyTitle];
}
Copy the code

If I didn’t mention the main points, it would have been useless:

Let’s look at the structure first:

---------------------------------- ----------------------------------------- | | | | | PropertyListReformer.m | | PropertyListReformer.h | | | | | | #import PropertyListReformer.h | <------- | #import "PropertyListReformerKeys.h" | | NSString * const key = @"key" | | | | | | | ---------------------------------- -----------------------------------------  . /|\ | | | | --------------------------------- | | | PropertyListReformerKeys.h | | | | extern NSString * const key; | | | ---------------------------------Copy the code

A Const string is used to represent the Key. The definition of the string follows the reformer’s implementation file, and the extern declaration of the string is in a separate header file.

Such data generated reformer key use Const string, then every time somewhere else you need to use the relevant data, the PropertyListReformerKeys. H in this header file import.

Another point to note is that if an OriginData can be processed by multiple Reformer, the Key’s naming convention needs to be able to represent the name of the corresponding Reformer. If reformer is PropertyListReformer, then the name of the Key is PropertyListKeyXXXX.

The advantage of this is that it is quite convenient for future migration, as long as the header file can be thrown, only the header file will not cause the turnip to pull out the mud situation. It also avoids the extra volume of code that comes with custom objects.


In addition, as for the delivered NSDictionary, it actually depends on the requirements of the view. The original intention of the design of Reformer is that it can be directly transformed into a view or an object (including NSDictionary) that can be directly used by the view. For example, map punctuation list API data can be converted by reformer into MKAnnotation, and then MKMapView can be used directly. This is just a case of how to ensure readability when your requirement is to deliver NSDictionary, and again, reformer delivers objects that can be used directly by the view, either NSDictionary or UIView, With the DataSource can even be delivered after UITableViewCell/UICollectionViewCell. Don’t get caught up in NSDictionary or the idea of converting to model and delivering.


In summary, what kind of data do I deliver to the business layer? Here’s the answer to the question:

For the business layer, according to the relationship between the View and APIManager, the Controller chooses the appropriate Reformer to transform the data that can be directly used by the View (or even the reformer can be used to directly generate the View) and then delivers it to the View. For the network layer, it only needs to keep the original data, and does not need to actively transform it into a data prototype. Then the data is represented by NSDictionary plus Const string key, which avoids the migration difficulty caused by using object representation and does not lose readability.


A choice between intensive and discrete API calls?

An intensive API call is a single class for all API calls, and that class takes the API name, API parameters, and the callback landing point (which can be target-action, block, delegate, or any other mode landing point) as parameters. It then executes a method like startRequest, and it takes off to call the API based on these parameters, and then gets the API data and lands at the specified landing point. Like this:

Intensive API calls:  [APIRequest startRequestWithApiName:@"itemList.v1" params:params success:@selector(success:) fail:@selector(fail:) target:self];Copy the code

The discrete API calls look like this: an API corresponds to an APIManager, and then the APIManager just needs to provide parameters to take off. The API name and landing method are integrated into the APIManager. Like this:

@ Property (Nonatomic, strong) ItemListAPIManager * ItemListAPIManager; // getter - (ItemListAPIManager *)itemListAPIManager { if (_itemListAPIManager == nil) { _itemListAPIManager = [[ItemListAPIManager alloc] init]; _itemListAPIManager.delegate = self; } return _itemListAPIManager; } / / use so write: [self. ItemListAPIManager loadDataWithParams: params];Copy the code

The two implementations of intensive and discrete API calls are not mutually exclusive. If you look at the lower layers, everyone is intensive. After making an API request, except for the business-relevant parts (such as parameters and API names, etc.), the rest is handled in a unified way: encryption, URL concatenation, and the takeoff and landing of API requests. If these processes are not implemented in an intensive way, the author is either crazy or crazy. However, for the entire network layer, especially those used by the business side, I prefer to provide discrete API calls and do not recommend intensive API calls directly for code at the business layer. Here’s why:

  • Cause 1: When the current request is flying outside, there are two different take-off strategies according to different service requirements. One is to cancel the newly initiated request and wait for the request to land. The other is to cancel the outside requests and let the new ones take off. Intensive API calls that meet this need require more judgment and cancellation code each time they are called, which is not very clean.

Examples of the former business scenarios are page refresh requests, refresh details, refresh lists, etc. An example of the latter business scenario would be a list multidimensional filter, where you first filter the item type and then the price range. Of course, the latter case does not necessarily call the API for every filter, but let’s assume that the filter must call the API every time to get the data.

If it is a discrete API call, different takeoff policies can be set for different apis when writing different Apimanagers. In actual use, you do not need to worry about takeoff policies, because the APIMananger has been written.

  • Reason 2: It’s easy to AOP on an API request. In the case of intensive API calls, how would the code be written if you were to AOP the takeoff and landing process of an API request… Oh, my god, this is so beautiful that I can’t even think about it.

  • Reason 3: When the API request’s landing point disappears, the discrete API call approach can handle the situation more transparently.

When a page request is flying through the air, the user gets impatient, clicks back, and the ViewController is popped and retrieved. The requested landing site is gone. This is a very dangerous situation, if the landing site is lost, it could easily crash. This is usually handled by canceling all requests for the current page at dealloc. If it’s an intensive API call, it’s going to be written to the Dealloc of the ViewController, but if it’s a discrete API call, it’s going to be written to the APIManager, and then as the ViewController collects, The APIManager is also recycled, and that part of the code gets called. In this way, the business side can pay more attention to the business without worrying about the disappearance of the landing site.

  • Reason 4: Discrete API calls, such as the Reformer mechanism based on discrete API calls, provide maximum flexibility to the business side. In addition, if you are targeting an API that provides a page-turning mechanism, APIManager can simply provide itloadNextPageMethod to load the next page, page number management is not managed by the business side. Also, if you want to validate business request parameters, such as the user filling in registration information, it’s very easy to implement in a discrete APIManager.

To sum up, I prefer this for intensive API calls versus discrete API calls: External to provide a BaseAPIManager to do derive business side, in the BaseManager inside the use of intensive means to assemble the request, release the request, but the business side call API, is to call the discrete API call way. If your App only provides an intensive approach and no discrete approach to the channel, THEN I suggest you wrap another layer so that the business side can release requests using discrete API calls.


How to do APIManager inheritance?

If you want to make discrete API calls, you can’t escape using inheritance. BaseAPIManager is responsible for the intensive part, and the external derived XXXAPIManager is responsible for the discrete part. For BaseAPIManager, some discrete parts are necessary, such as API names, and the purpose of our derivation is also to provide these data.

In this article, I have listed the disadvantages of inheritance and urged you not to use it. But now we have to use inheritance, so I have to warn you not to use inheritance too badly.

In the case of APIManager, the most intuitive idea would be for BaseAPIManager to provide empty methods to override subclasses, such as apiMethodName, but my advice is not to do so. We can limit overloading of derived classes by IOP.

It looks something like this:

The BaseAPIManager init method says: // Note that it is weak. @property (nonatomic, weak) id<APIManager> child; - (instancetype)init { self = [super init]; if ([self conformsToProtocol:@protocol(APIManager)]) { self.child = (id<APIManager>)self; } else {// If the protocol does not comply with the crash, let it crash. NSAssert(NO, "Subclasses must implement APIManager protocol." ); } return self; } protocol () {override protocol () {override protocol ();}  @protocol APIManager <NSObject> @required - (NSString *)apiMethodName; . @end and then in the parent class if you want to use it, say :[self requestWithAPIName:[self.child apiMethodName]......] ;Copy the code

Check whether the subclass complies with the protocol specified by the subclass. This requires that all subclasses comply with the protocol specified by the subclass. Overloading and overwriting methods other than protocol are not allowed. In the parent code, you don’t have to follow the protocol, keeping future maintenance flexible.

This has the advantage of avoiding null parent methods, but it also puts a constraint on subclasses: if you want to be my child, you have to follow these rules and not mess around. When a business implements a subclass, it can implement it according to the methods in Protocol, and then the convention is easier: it does not allow overloading of the parent class methods, and only allows the implementation or non-implementation of the methods in Protocol.

A detailed discussion of this is available in this article, if you are interested.

Summary of the connection between network layer and business layer

This section focuses on the following points:

  1. Use a delegate for data docking, and use Notification for cross-layer access only when necessary
  2. Deliver the NSDictionary to the business layer, using Const strings as keys to maintain readability
  3. Providing a reformer mechanism to work with data feedback from the network layer is an important and beneficial mechanism
  4. The upper part of the network layer uses discrete design and the lower part uses intensive design
  5. Design a proper inheritance mechanism so that the derived APIManager is limited and chaos is avoided
  6. It should be more than these 5 points…

Security mechanism at the network layer

Determine if the API call request is from an authorized APP

There are two main purposes for using this mechanism:

  1. Make sure API callers are from your own APP to prevent competitors from crawling your API
  2. If you provide a registered API platform, you need to have a mechanism to identify if registered users are calling your API

Solution: Design signatures

To achieve the first goal, the server needs to give you a key, and every time you call an API, you use the key, the NAME of the API, and the parameters of the API request to compute a hash, and then carry the hash with the request. After receiving the request, the server computes a hash based on the same key and algorithm, and compares it with the hash brought by the request. If the hash is consistent, it means that the caller of this API is indeed your APP. In order to prevent others from accessing the key, it is best to write the key to the code instead of storing it locally. In addition, increase the complexity of the Hash algorithm appropriately, that is, various Hash algorithms (such as MD5) with salt, and then run a Hash. This addresses the first goal: make sure your API comes from your own App.

In general, most companies do not have the need to meet the second case unless they develop their own API platform for use by third parties. This requirement is a bit different from the previous one: there is more than one API requester eligible for authorization. So in this case, the required security mechanism is a bit more complicated.

Here’s an easy solution: when the client calls the API, it encrypts its key through a reversible encryption algorithm and sends it along with the request and the encrypted Hash. Of course, the reversible encryption algorithm must be compiled in the SDK that calls the API. Then the server gets the encrypted key and the encrypted Hash, decodes the original key, calculates the Hash with it, and compares it.

Ensure the security of transmitted data

There are two main purposes for using this mechanism:

  1. Prevent man-in-the-middle attacks, such as carriers who like to stuff Http requests with ads…
  2. SPDY relies on HTTPS and is the foundation for future HTTP/2, which can improve the overall performance of your APP at the network layer.

Solution: HTTPS

The main purpose of using HTTPS right now is to prevent carriers from adding ads to your Response Data (man-in-the-middle attacks), which is a much broader threat. Since 2011, the foreign industry has advocated that all requests (not only API, but also website) should go HTTPS. In China, it was almost two years later (around 2013) that this was advocated. Tmall only started to do the whole APP migration of HTTPS in these two months.

In terms of speed, HTTPS is definitely slower than HTTP because of the extra handshake, but with SPDY hanging up, there is a big improvement in performance with link reuse. The performance improvement here does not mean that a request that used to take 500ms to complete now takes 300ms, which is wrong. Overall performance is discussed in terms of a large number of requests: the same number of requests (say 100) occurs in a short period of time, and it takes less time to complete these tasks after SPDY is up than without SPDY. SPDY also has Header compression, but because an API request itself is small, the performance improvement is not significant, so the performance improvement is relatively small on a per-request basis. But that’s something to talk about in the next video, just by the way.

Summary of safety mechanism

This section describes two types of security mechanisms. Generally speaking, the first is standard and the second is optional. However, with the improvement of China’s Internet infrastructure, the improvement of mobile device performance and the improvement of optimization technology, the disadvantage of the second configuration (slow speed) is becoming increasingly insignificant, so HTTPS will become the standard configuration of the network layer security mechanism of App in the near future. Architects, if you don’t already have HTTPS on your App, it’s time to get started.

Optimization scheme of network layer

The optimization means of the network layer are mainly considered from the following three aspects:

  1. Optimization for link establishment
  2. Optimization for the amount of data transferred over links
  3. Optimization for link reuse

These three aspects are the content of all optimization methods, all kinds of optimization methods basically will not escape these three aspects, I will respectively talk about the corresponding optimization methods for these three aspects.

1. Optimization of link establishment

The API initiates a request to establish a link, which is roughly divided into these steps:

  1. The initiating
  2. DNS resolves the domain name to obtain an IP address
  3. A three-way handshake based on the IP (HTTPS four-way handshake) succeeds

In fact, the optimization method in step 3 is the same as the optimization method in step 2, and I’ll talk about it in step 2.

1.1 Optimization methods for initiating requests

The question is whether the network layer should make a request for this API call.

  • 1.1.1 Use caching to reduce the number of times a request is initiated

For most API call requests, some API requests bring data with a long time, such as product details, such as App skin, etc. We can then cache this data locally so that we don’t have to make a new request the next time we request it.

Generally, the API name and parameter are spelled into a string and MD5 is used as the key to store the returned data. So the next time you have the same request, you can read it directly. There is a cache policy here that needs to be discussed: when is the cache cleared? Either the cleanup is based on the timeout limit or the cache data size. The choice of this policy depends on the operation logs of the specific App.

For example, the log data records of Anjuke App show that the average user time is less than 3 minutes, but the number of times that users check the housing details is relatively large, and the amount of housing details data is relatively large. This is a good time to cache based on the duration of use. I set the juju cache timeout to 3 minutes to ensure that the cache works for most of the user’s use time. Well, it doesn’t matter what caching means are used in extreme cases, as long as 80% of the users are well served, and optimization measures adopted for extreme cases are not necessary for most ordinary users, which will affect them instead.

For example, the network image cache, the amount of data is basically very large, this is more suitable for the cache size to clear the cache strategy.

In addition, the premise of previous caches was based on memory. We can also store need to clear the cache on the hard drive (APP local store, I’ll use hard to say, although few mobile hard disk, haha), such as the aforementioned image cache, because the picture is likely after a long time, to be displayed, so that need to be clear pictures of the cache, we can consider to save to the hard disk. Next time there is a need to display network pictures, we can first look for memory, memory can not find that from the hard disk, this can not find, then initiate a request.

Of course, this method cannot be used for some API data with very short timeliness, such as the user’s money data, which needs to be called every time.

  • 1.1.2 Policies are used to reduce the number of times requests are initiated

I mentioned earlier that there is a request policy for the initiation and cancellation of repeated requests. Let’s start with the cancellation strategy.

In the case of interface refresh requests and repeated requests (pull-down refresh, in which the user repeatedly pulls down the request until it lands), API requests resulting from subsequent repeated operations do not need to be sent.

If it is a conditional filter, cancel the request already sent. While it is likely that the request has already been executed, the performance gain from cancellation is largely gone. However, if the request is still in the queue, the corresponding link can be omitted.

The other case is request policy: a request policy that resembles a user action log.

User operations trigger operation logs to be reported to the Server. Such requests are frequent but are conducted in secret and do not require users to be aware of them. Therefore, there is no need to make a request for every operation. The strategy here is to record the user’s operation records locally and make a request to upload the operation records to the server when 30 records are recorded. Then, every time the App starts, upload the operation records left over from the last time. In this way, the power consumption of user devices can be effectively reduced and the performance of the network layer can be improved.

A small summary

Optimization for the part of establishing a connection is based on this principle: make as few requests as you can, and merge as many requests as you can when you have to. However, any optimization method has a premise, and there are no guarantees that it will work for all requirements. Some API requests do not meet the premise of these optimization methods, so just do it. However, the percentage of such API requests is generally small, and most requests are more or less eligible for optimization, so it is worthwhile to optimize for sending requests.

1.2 & 1.3 Optimization for DNS domain name resolution, and optimization for establishing links

In fact, there is DNS cache on the whole DNS link, which can theoretically improve the speed. The DNS cache on this link is effective for PC users because the DNS link of PC users is relatively stable and the signal source does not change. On the mobile device side, however, the performance benefit of DNS caching on the link is less obvious. Because the actual application scenario of mobile devices is complex, the network signal source often changes, and the corresponding DNS resolution link changes once the signal source changes, so the DNS cache on the original link does not work. And the situation of signal source change is very, very frequent, so for mobile device users, we can basically default to no LINK DNS cache. Therefore, most of the time, it is the local DNS cache of the mobile phone system that plays a role. However, generally speaking, the demand for Internet access on mobile devices is also very frequent. The DNS cache specially made for our App may be crowded out and cleared by other DNS caches. Users will probably come back with their own App’s local DNS cache gone. In addition, there is a problem that can only be found in the Internet environment of socialism with Chinese characteristics: due to the existence of the GFW in the Internet environment in China, the DNS service speed is much slower than normal.

As a result, API requests can take a lot of time during DNS resolution.

The optimal solution for this is to simply go through the IP request, which bypasses the DNS service time.


In addition, the third step of establishing links mentioned above, the domestic network environment is divided into north Network, South Telecom (of course, the actual situation is more complicated, just talk about it here), the connection between different service providers, the delay is very large, we need to find ways to let users provide services on the MOST suitable IP. So there is an additional requirement for our means of circumventing DNS services: try not to let the user use an IP that is slow for him.

So the solution is this: you have a local list of all the IP addresses that provide the API, and each time the application starts, you take the ping delay for all the IP addresses in the list, and then use the IP address with the smallest delay as the IP address for future requests.


The optimization for establishing connections is actually the same optimization for DNS domain name resolution. However, this requires your server to provide services to the network situation to be more, generally now the server is double network card, telecom and netcom. Due to the distribution of Internet ISPs with Chinese characteristics, there is a bottleneck between the north and south networks, and the link optimization method of our App mainly focuses on how to reduce the impact of this bottleneck on the App. Therefore, we need to maintain an IP list, so that we can connect to the nearest network and achieve optimization effect.

We usually get the ping values of all the IP addresses in the local list when the application is started, and then change the HOST in the URL to the fastest IP we find by means of NSURLProtocol. In addition, the list of local IP addresses will also need to be maintained through an API, which is usually read once a day at the first startup and then updated locally.

If you are not familiar with how NSURLProtocol should be played, you will be after reading the official documentation, this article and this Demo. It is very simple. In addition, the author of that article (Mattt) wrote this NSURLProtocol based tool, which is quite handy and can be directly integrated into a project.

Without the NSURLProtocol, this could have been done by other means, but those would have been silly.

2. Optimize the amount of data transmitted through links

This makes sense, less data is transferred, so the natural speed goes up. There’s nothing fancy here. It’s just compression. All kinds of compression.

3. Optimization for link reuse

Establishing a link consumes resources and power. SPDY comes with link reuse and data compression functions, so when the server supports SPDY, App can directly hang SPDY. If the server does not support SPDY, you can also use PipeLine, which is native to Apple.

It is generally accepted in the industry that SPDY is superior to PipeLine, but even so, the network layer efficiency gains that SPDY can bring are not as obvious as the graphs in the literature, but there are still performance gains. Another clumsy approach to link reuse is to maintain a queue and then compress the queue into a single request, which is stuck in the queue while the last one is still floating around. The end result is similar to link reuse on the surface, but it’s not really link reuse, just request merge.

Still, I suggest it is best to use SPDY, SPDY and pipeline although both belong to the category of link reuse, but pipeline is not really link reuse, SPDY link reuse is more thorough than pipeline. SPDY also has a client SDK available, including CocoaSPDY for Twitter and Voxer/iSPDY. Both libraries are active, so you can choose which one you want to use.

However, the industry trend is to use HTTP/2.0 instead of SPDY, but HTTP/2.0 has not been officially released yet, and most of the implementation is in the demo stage, so we will work on SPDY first. It is likely that SPDY will be abandoned in favor of HTTP/2.0 for network optimization. This is something to remind architects of. Well, I don’t know when HTTP/2.0 will come out.


The fish said, the fish came

Here is the network layer architecture code that I designed and realized in those days. Of course, I have been desensitized where I should be, so making up is normal, hahaha. But the code is quite complete, I also wrote a lot of important comments. In addition, I have included a powerpoint presentation from the presentation introducing the framework so that you can see the code clearly. (As a bonus note, many people in the comments section asked where PPT could not be found. PPT is also in the repO mentioned above. It is a file with the suffix key, which is opened in Keynote.)

Then again, there were a lot of issues that weren’t thought through as clearly as they are now, so there are still areas that weren’t done as well, like interceptors and inheritance. And at that time, the optimization means only local cache, anjuke did not have so many IP to ping me, SPDY was not popular, and THE API also does not support HTTPS, so the code at that time did not do optimization in these places, relatively primitive. However, the basic idea of the entire architecture remains the same: serve the business side first. In addition, the anjuke network layer has a service concept, which I did not cover in this article. The main reason is that there are many API providers for anjuke. The APIS of second-hand house, rental house, new house and X project are all provided by different API teams, which are distinguished by Service. If your app is in a similar situation, I also suggest that you design a service mechanism. Now I have deleted only one of these services from Google, because the other services are sensitive content.

In addition, I really hope you can take the time to look at the PPT provided here. There are some more detailed things in the PPT that I did not write in my blog, mainly because I am lazy, and this article took a long time. It is not interesting to take time to carry this, but the content is still worth reading by readers. If you have any questions about the PPT, you can also ask them in the comments section, and I will answer them.

conclusion

The first part is mainly about the network layer should be how to interact with the business layer data, data when interacting with how the data formats, and some problems on the code structure design, such as inheritance, the processing of the processing of the callback, the choice of the ways of interaction, the design of the reformer, keep the data readable, and so on and so on, mainly focus on the design (this is art of living, Hahaha).

The second part of the network security, the client to do two. Of course, from the perspective of network security, the server also need to do a lot of things, the client side to do some details of the things will also have a lot of, such as doing some code obfuscation, as far as possible to avoid the code in the clear display of keys. But the big head is mainly these two, and also need to cooperate with the server side of the students. The emphasis is mainly on introduction. (Mainly there is no good practice, just Google a tutorial to follow).

The third part deals with optimization, and all aspects of optimization have been laid out. If the industry has any other means, it will not escape the scope of this article. There are some optimization methods that require the cooperation of students on the server side, and some that do not, depending on your own situation to decide. The emphasis is mainly on practice.

Sharing:IOS development of all kinds of books to download