Sharing of iOS network layer architecture design

Sharing of iOS network layer architecture design

[[166105]]

Preface

A few days ago, I helped the company to reconstruct the network layer. At that time, I wanted to share it with everyone after it was completed. Later, I continued to work on the requirements of the new version. Now I have time to organize it.

The previous network layer was imported into AF by directly dragging and dropping the project, and then a lot of source code was modified. After 2 years, AF has been updated many times, which made the whole reconstruction and migration very troublesome. However, looking at the code written by the senior, he must be an expert. Many of his ideas are the same as mine, but the implementation methods are different, which gives me a good reference.

When designing the network layer architecture, we also referred to the architectural ideas of Casa, but there are still some differences.

This article does not have too much theory or too many professional terms. Firstly, it is convenient for everyone to read. Secondly, my foundation is not that good. There are not too many fancy words. For the architecture, the main thing is the ideas. With the ideas, there will be no problem with the specific implementation.

This article mainly introduces the following points:

1. Network interface specifications

2. Multi-server and multi-environment settings

3. Network layer data transmission (request and return)

4. Business layer connection method

5. How to automatically cancel network requests

6. Network layer error handling

No Demo, no article. Demo download: https://github.com/SummertimSadness/NetWorkingDemo

Network interface specifications

The request examples in the demo are found on the Internet and do not conform to the specifications I mentioned. They are only used as examples.

Specifications are very important. With reasonable specifications, a lot of code logic can be simplified, especially interface compatibility, which is the most basic design at the bottom level. Put the interface specifications first.

When doing this reconstruction, I put forward some standard points for your reference

1. Two-layer three-part data structure

The first data returned by the interface is a dictionary, which is divided into two layers and three parts: code, msg, data

  1. "code" : 0 ,
  2. "msg" : "" ,
  3. "data" : {
  4. "upload_log" : true ,
  5. "has_update" : false ,
  6. "admin_id" : "529ecfd64"  
  7. }
  • code: error code, which can be recorded to quickly locate the cause of the interface error. You can define a set of error codes, such as 200 normal, 1 re-login...

  • msg: interface text prompts, including error prompts, used to display directly to users, so this set of error prompts cannot be a string of English errors.

  • data: the data to be returned, which can be a dictionary or an array

The interface defines code and msg for us, so does that mean we don’t need to do error handling? Of course not. The error logic on the server is simple after all, and there may still be errors in the data processing in the data, so error handling is essential. The following will introduce error handling separately.

2. Unified network request parameter upload method

This can generally be done, and there are also additional ones. For example, one of our server interfaces was developed relatively early, and the POST interface was not used in a standardized manner at the time. Ordinary application information channelID and device_id were concatenated at the end of the string, while the actual request parameters needed to be converted into json and passed in a field, that is, the interface GET and POST coexisted, causing the network layer to need special processing

Therefore, standard GET and POST request methods are necessary.

3. About Null Type

As we all know, the Null type is very special in iOS. My suggestion is to do it on the client side for many reasons:

1) Not every company can define the standard definition of the interface from the beginning. If you want to remove the Null field from the old interface, the change is very large.

2) The client can also solve the problem by using an interface filter, once and for all, without worrying about crashes due to interface problems one day, and some third-party Model libraries can also solve this problem very well. I have to say that Swift's type detection is really convenient. A previous project was written in Swift, and the code was more standardized, and there would be no crashes due to parameter type problems

Multi-server multi-environment setup

This part is basically copied from the design of the great casa. Here I extended a multi-environment design. Small projects usually have one server, but for projects like Taobao, it is obviously impossible to have one server. The design of multiple servers is still very common. According to an enumeration variable, the corresponding server configuration is generated through the ServerFactory singleton

1. Server environment

A standard APP has four environments: development, testing, pre-release, and official. Especially for the server code, it cannot be said that all code changes are in the official environment. The code should be updated from development->test->pre-release->official. Development is the change when new requirements and optimizations are made. Testing is the change after submitting to the tester. At this time, the change is on a new branch. After completion, it must be merged into the test branch and the development branch. The change in pre-release is relatively small at this time. Generally, it will be released to the whole company for testing after the tester is completed. Only when there is a problem will it be changed. After the change, it will also be merged into the development branch. The official version is an emergency BUG fix for the online release version. After the modification, it will also be merged into the development branch. So the development branch is always the latest. On this basis, there may be other environments, such as hotfix environment, customized h5/backend local debugging environment.

These environments also exist on the client side, and an entry point for switching must be provided.

In my demo, two sets of settings are provided, one is the initialization environment (macro definition) for the first installation of the application, and the other is the settings for manually switching the environment (enumeration EnvironmentType). There is a rather convoluted logic here. The formal environment settings defined by the macro are higher than the manually switched environment settings, and the manually switched environment settings are higher than the macro defined other environments.

  1. //Macro definition environment settings  
  2. # if !defined YA_BUILD_FOR_DEVELOP && !defined YA_BUILD_FOR_TEST && !defined YA_BUILD_FOR_RELEASE && !defined YA_BUILD_FOR_PRERELEASE
  3. #define YA_BUILD_FOR_DEVELOP
  4. //#define YA_BUILD_FOR_TEST  
  5. //#define YA_BUILD_FOR_PRERELEASE  
  6. //#define YA_BUILD_FOR_HOTFIX  
  7. //#define YA_BUILD_FOR_RELEASE //This environment has the highest priority  
  8. #endif
  9. //Manual environment switching settings  
  10. #ifdef YA_BUILD_FOR_RELEASE
  11. //Priority macro definition formal environment  
  12. self.environmentType = EnvironmentTypeRelease;
  13. # else  
  14. // After manually switching the environment, the settings will be saved  
  15. NSNumber *type = [[NSUserDefaults standardUserDefaults] objectForKey:@ "environmentType" ];
  16. if (type) {
  17. //Prioritize reading of manual switching settings  
  18. self.environmentType = (EnvironmentType)[type integerValue];
  19. } else {
  20. #ifdef YA_BUILD_FOR_DEVELOP
  21. self.environmentType = EnvironmentTypeDevelop;
  22. #elif defined YA_BUILD_FOR_TEST
  23. self.environmentType = EnvironmentTypeTest;
  24. #elif defined YA_BUILD_FOR_PRERELEASE
  25. self.environmentType = EnvironmentTypePreRelease;
  26. #elif defined YA_BUILD_FOR_HOTFIX
  27. self.environmentType = EnvironmentTypeHotFix;
  28. #endif
  29. }
  30. #endif

Therefore, when the macro definition formal environment exists, the environment cannot be switched manually. It is used for the release version for ordinary users, but other macro definition environments can be switched to the formal environment.

Half a pit

In addition, manual switching of custom environments is implemented in the base class, while other environment configurations are implemented in the protocol, which is inconsistent with the configuration of other environment addresses.

It can be understood that the base class here is to provide the returned value, and the protocol is for the flexibility of the return value. Since the address configuration of the custom environment does not require flexibility, it is naturally better to put it in the base class. The idea is the general direction, and the implementation is flexible. If you have to put it in the protocol, it is fine. It is nothing more than assigning and pasting the same code several times. However, I don’t like to see the same code the most, so I put it in the base class. If there is a better solution, please provide it.

2. Scalability

The model provides high scalability, adding more configurations for different servers, such as encryption methods, data parsing methods... As mentioned earlier, sometimes it takes time to achieve a unified specification, and compatibility becomes a requirement. At this time, the personalized settings of different servers can be declared and implemented in the protocol, and the base class only needs to provide a return value.

Network layer data transfer (request and response)

Network layer data transmission

Client, BaseEngine/DataEngine, RequestDataModel data transfer

In my understanding, network requests occur in two steps: one is data organization, and the other is generating and initiating requests. Based on this idea, I split the Client and Engine, and then split the URLRequestGenerator from the Client. The Engine is split into the lower-level BaseEngine and the DataEngine for different businesses.

From BaseEngine to Client, and then to URLRequestGenerator, data is transferred, request parameters and return parameters, so there is RequestDataModel

RequestDataModel

  1. @interface YAAPIBaseRequestDataModel : NSObject
  2. /**
  3. * Network request parameters
  4. */  
  5. @property (nonatomic, strong) NSString *apiMethodPath; //Network request address  
  6. @property (nonatomic, assign) YAServiceType serviceType; //Server ID  
  7. @property (nonatomic, strong) NSDictionary *parameters; //Request parameters  
  8. @property (nonatomic, assign) YAAPIManagerRequestType requestType; //Network request method  
  9. @property (nonatomic, copy) CompletionDataBlock responseBlock; //Request landing callback  
  10. // upload  
  11. // upload file  
  12. @property (nonatomic, strong) NSString *dataFilePath;
  13. @property (nonatomic, strong) NSString *dataName;
  14. @property (nonatomic, strong) NSString *fileName;
  15. @property (nonatomic, strong) NSString *mimeType;
  16. // download  
  17. // download file  
  18. //progressBlock  
  19. @property (nonatomic, copy) ProgressBlock uploadProgressBlock;
  20. @property (nonatomic, copy) ProgressBlock downloadProgressBlock;
  21. @end  

It can be seen that the RequestDataModel attributes are all necessary parameters for initiating and returning network requests. The benefits of doing so are really huge. I wonder if you have ever had such a scenario: because of the different request parameters, many method interfaces are exposed, and in the end, the same method is still called. Once there are too many methods, you don’t even know which method should be called. I have encountered this, so now my network request is called like this:

  1. //No callback, no other parameters, only one dataModel, saving all your methods  
  2. [[YAAPIClient sharedInstance] callRequestWithRequestModel:dataModel];

Generate NSURLRequest like this:

  1. NSURLRequest *request = [[YAAPIURLRequestGenerator sharedInstance] generateWithYAAPIRequestWithRequestDataModel:requestModel];

You can see that the YAAPIClient class and YAAPIURLRequestGenerator class in my demo have at least methods. Fewer methods means simple and clear logic, which is easy to read. The number of lines of code for both classes is 120 lines, which implements the initiation and landing of network requests. Can you imagine?

Another benefit of RequestDataModel is high scalability. Have you ever encountered a situation where the network layer needs to add or delete a parameter, which causes the calling method to be modified, and then many places need to modify the method? With RequestDataModel, you only need to add or delete parameters, and only need to modify the method body. Modifying the method body and modifying the method name and method body at the same time are two completely different workloads. Haha, it feels a bit like selling tiger skin plasters. This is indeed my proud innovation.

Client

The client performs two operations: one is to generate an NSURLRequest, the other is to generate and initiate an NSURLSessionDataTask, and also expose the cancel operation to the Engine.

URLRequestGenerator generates NSURLRequest. URLRequestGenerator will process and parse dataModel to generate NSURLRequest for the corresponding server.

Then the Client generates NSURLSessionDataTask through NSURLRequest

Client and URLRequestGenerator are both singletons

  1. - ( void )callRequestWithRequestModel:(YAAPIBaseRequestDataModel *)requestModel{
  2. NSURLRequest *request = [[YAAPIURLRequestGenerator sharedInstance]
  3. generateWithRequestDataModel:requestModel];
  4. AFURLSessionManager *sessionManager = self.sessionManager;
  5. NSURLSessionDataTask *task = [sessionManager
  6. dataTaskWithRequest:request
  7. uploadProgress:requestModel.uploadProgressBlock
  8. downloadProgress:requestModel.downloadProgressBlock
  9. completionHandler:^(NSURLResponse * _Nonnull response,
  10. id _Nullable responseObject,
  11. NSError * _Nullable error)
  12. {
  13. //Request landing  
  14. }];
  15. [task resume];
  16. }

The cancellation interface refers to the design of casa master, and uses NSNumber *requestID to bind the task, so I won’t introduce it in detail.

BaseEngine/DataEngine

Engine or APIManager is neither discrete nor intensive in my design.

Theory of Casa Master

Intensive API calls actually mean that all API calls have only one class, and this class receives the API name, API parameters, and callback landing point (which can be target-action, block, delegate, or other landing points) as parameters. Then, it executes methods like startRequest, and it will take off and call the API based on these parameters, and then land at the specified landing point after obtaining the API data. For example:

  1. [APIRequest startRequestWithApiName:@ "itemList.v1" params:params success: @selector (success:) fail: @selector (fail:) target:self];

Discrete API calls are like this: one API corresponds to one APIManager, and then the APIManager only needs to provide parameters to take off. The API name and landing method have been integrated into the APIManager. For example:

  1. @property (nonatomic, strong) ItemListAPIManager *itemListAPIManager;
  2. // getter  
  3. -(ItemListAPIManager *)itemListAPIManager
  4. {
  5. if (_itemListAPIManager == nil) {
  6. _itemListAPIManager = [[ItemListAPIManager alloc] init];
  7. _itemListAPIManager.delegate = self;
  8. }
  9. return _itemListAPIManager;
  10. }
  11. // When using it, just write:  
  12. [self.itemListAPIManager loadDataWithParams:params];

I won’t talk about their respective advantages, but several questions arise from this:

1. The use of dictionaries for parameter transfer is unknown to the network layer, and the business layer needs to pay attention to changes in interface fields, which is actually unnecessary.

2. Discrete APIs will cause Manager explosion

3. The intensive type will make the cancellation operation inconvenient

4. Cancellation operation is not required for every interface. If it is written as partially discrete and partially intensive, the overall structure of the code... I am a person with obsessive-compulsive disorder and I can't stand such code.

So my design mainly solves the above problems

1. The DataEngine for the business layer only passes in the necessary parameters and does not use dictionaries , such as

  1. @interface SearchDataEngine : NSObject
  2. + (YABaseDataEngine *)control:(NSObject *)control
  3. searchKey:(NSString *)searchKey
  4. complete:(CompletionDataBlock)responseBlock;
  5. @end  

Let’s ignore control for now, it is for automatic cancellation and will be introduced later.

searchKey is the search keyword

This is what happens when calling

  1. self.searchDataEngine = [SearchDataEngine control:self searchKey:@ "keyword" complete:^(id data, NSError *error) {
  2. if (error) {
  3. NSLog(@ "%@" ,error.localizedDescription);
  4. } else {
  5. NSLog(@ "%@" ,data);
  6. }
  7. }];

2. I divide DataEngine by business layer , such as BBSDataEngine, ShopDataEngine, UserInforDataEngine... Each DataEngine contains all the network request interfaces of their respective businesses, so there will be no DataEngine explosion. For example, our project has more than 300 interfaces, and there are more than a dozen DataEngine after splitting. If discrete API design is used, the picture is too beautiful and I dare not look at it??

3.BaseEngine provides cancellation operation

Each interface generates a BaseEngine instance, which holds the requestID returned by the Client, so the cancellation operation can be performed. This is a simple usage scenario.

  1. # import   "ViewController.h"  
  2. # import   "SearchDataEngine.h"  
  3. @interface ViewController ()
  4. @property (nonatomic, strong) YABaseDataEngine *searchDataEngine;
  5. @end  
  6. @implementation ViewController
  7. - ( void )viewDidLoad {
  8. [ super viewDidLoad];
  9. // Do any additional setup after loading the view, typically from a nib.  
  10. [self.searchDataEngine cancelRequest];
  11. self.searchDataEngine = [SearchDataEngine control:self searchKey:@ "keyword" complete:^(id data, NSError *error) {
  12. if (error) {
  13. NSLog(@ "%@" ,error.localizedDescription);
  14. } else {
  15. NSLog(@ "%@" ,data);
  16. }
  17. }];
  18. }
  19. @end  

4. The returned YABaseDataEngine instance ViewController does not have to be held. You can hold it when you need to cancel the operation.

This design integrates the advantages of intensive and discrete types, and solves the disadvantages of intensive and discrete types.

How to automatically cancel network requests

When a page request is flying in the air, the user waits for a long time and becomes impatient, clicks back, and the ViewController is popped and recycled. At this time, the landing point of the request is gone. This is a very dangerous situation. If the landing point is gone, it is easy to crash.

Mr. casa said to cancel the network request in the dealloc of BaseDataEngine. I think so too, but Mr. casa said to bind BaseDataEngine to ViewController, so that when ViewController is destroyed, BaseDataEngine will be destroyed as well. I agree with this, but I cannot accept adding BaseDataEngine variables to ViewController to save BaseDataEngine under any circumstances. Moreover, some ViewControllers will initiate two or three network requests. Do I have to add two or three variables? The code invasion is too large, so here I secretly used a trick and used runtime to add a dictionary to ViewController to save requestID and BaseDataEngine. In this way, it is not necessary for ViewController to write variables to hold BaseDataEngine, so the above DataEngine appears to pass the control in.

Bind when making a request

  1. [control.networkingAutoCancelRequests setEngine:self requestID:self.requestID];

Delete when the request is completed

  1. [weakControl.networkingAutoCancelRequests removeEngineWithRequestID:engine.requestID];

What the previous great man in the company did

Although the control method is very convenient, it is a lot of work to add a control field to all existing interfaces. If the existing interface is passed to DataEngine using a dictionary, here is a method used by a great person in a company. It is also possible to use the memory address, add the memory address to the dictionary, and use the memory address as the key binding. If you pass the key parameters directly like me, instead of using a dictionary, it will not work.

  1. NSString *memoryAddress = [NSString stringWithFormat:@ "%p" , self];

Network layer error handling

To be honest, I was struggling for a long time about where to put the error handling. I also discussed it with my colleagues in the company for a long time. Finally, I decided on a plan, which is for your reference only.

We divide error handling into two steps: error parsing and error UI display.

You can see that the data returned by the interface I designed is the standard id data, NSError *error, so my idea is that the Client should handle the error well, no matter it is a network timeout error or incorrect data format, the error should be fully parsed and the error code should be defined. The upper layer will use the code to make specific UI display according to the needs, because some interface errors require users to click to confirm, and some page errors are just a flashing prompt box, and the error is handed over to BaseEngine or DataEngine to handle the errorUI, so I defined a set of errorUI enumerations, and when BaseEngine gets the error, it will display the error.

Summarize

The design of the architecture is more about ideas. I hope that you can take the essence and discard the dross from the ideas we provide, and you will always design the architecture that best suits your project.

In addition, there may be many controversial points in my design. I have mentioned some of them in the article. If you have any other ideas, we can discuss them.

1. About blocks

When it comes to choosing between block and delegate, I prefer block for one reason only: the structure of block is easier to read. I think this one advantage is enough to outweigh all its disadvantages. It can be said that I rarely use delegate in my current projects.

When should you customize the delegate? When your callbacks at different times exceed 2 times (not including 2 times), 3 callbacks depend on the situation. If there are fewer logics to be processed, use block, if there are more, use delegate. Once it exceeds 3 times, basically don't consider block. I hope everyone will not have prejudice against block. Delaying the life cycle can be solved with a macro definition. By the way, strongSelf is given. If you are not willing to use such a convenient macro, then it is really not suitable for using block, and no one can save you.

  1. #define WEAKSELF typeof(self) __weak weakSelf = self;
  2. #define STRONGSELF typeof(weakSelf) __strong strongSelf = weakSelf;

2. What kind of data is delivered to the ViewController? Model or data

Is there anything controversial about this? With DataEngine, you have the final say on what kind of data to deliver.

The underlying BaseEngine and Client are of course more suitable for data. At the DataEngine layer, you can deliver whatever data you want, depending on the needs of the business layer. Some interfaces do not contain models at all. It is nonsense to unify all interfaces to return models. So my suggestion is to unify the specifications based on the actual situation of the interface. Because some interfaces in our design do not require models, they will return data in the future.

3. Optimization

My design is just a basic idea, and I know there are still many points for optimization.

This is where everyone shows off their skills. It's not that I'm being secretive, but the current project doesn't have many optimization points for the network layer, so I didn't do too much. There are too many sensitive codes in the part I did, and I really can't break it down. But I can tell you a small optimization point. The errorUI processing can be considered as a queue. For example, a pop-up box that requires the user to click OK, and the content is the same, just put it in the queue and display it only once.

4. Why the business layer does not use RequestDataModel

Model is an object. The lower layer is mainly used for data transmission, so it is fine to use model. However, when it comes to the business layer, the concept is more about method calls, and the method definitions are more targeted. At this time, it is not appropriate to use model. It is just like a supermarket. When buying goods, they use containers to pull goods. Everything is packed together, and when it arrives at the counter, it will be classified and displayed one by one.

<<:  Being able to write programs is not enough. Here are 5 skills that all outstanding software engineers in Silicon Valley have.

>>:  The correct approach to algorithm analysis

Recommend

You have acquired countless skills, but why have you achieved nothing?

[[163186]] A few days ago, I saw a sentence from ...

Brand Marketing: 8 Failure Experiences of New Brands!

I have been writing less recently, which has led ...

Come, choose a dinosaur as a "mount" ~

Excerpted from: "Inside and Outside the Clas...

Dr. Mok comments on the trends of 2015: The Internet of Things is still a mess

According to foreign media The Verge, Dr. Mo, a v...