A preliminary study on componentization
Why componentization
- Decoupling between modules
- Module reuse
- Improve team collaboration and development efficiency
- Easy unit testing
What items do not need to be componentized
- The project is small, the interaction between modules is simple, less coupling
- A module is not referenced by multiple external modules and is simply a small module
- Modules do not need to be reused, and code is rarely modified
- Small team size
Componentized layered design
The underlying components typically include the underlying class libraries, classifications, macro definition files, and so on
Note: Componentized layering is really about interface isolation to achieve dependencies between layers, and only upper-level dependencies on lower-level dependencies project common code resources sink, and horizontal dependencies sink best
Cocoapods creates private libraries
The process for creating a private library is as follows:
Write a Demo to illustrate the process
1. On the terminal, go to the path where you want to create the pod library project and create the pod library. The command is as follows:
plz@plzdeMacBook-Pro:~ better$ cd desktop
plz@plzdeMacBook-Pro:desktop better$ pod lib create PrivateHelloWorld
Copy the code
When you press Enter, the terminal will ask you a few questions
What language do you want to use?? [Swift/ObjC] > ObjC # create a demo project in your project. I chose Yes Would you like to include a demo application with your library? [Yes/No] > Yes # Which testing frameworks will you use? [Specta/Kiwi/None] > None # [Yes/No] > No # What is your class prefix? > HGCopy the code
The Pod private library is successfully created. Normally the project will be opened automatically when it is successfully created
2. Install the CocoaPods project. Go to the PrivateHelloWorld folder you just created and click in
3, Add you want to add the code file (copy and paste)
Pod Install PrintHelloWorld
# to Example directory BetterdeMacBook - Pro: desktop better $CD/Users/better/desktop/PrivateHelloWorld/Example # CocoaPods project installation BetterdeMacBook-Pro:Example better$ pod install --no-repo-updateCopy the code
Go to the Example folder and open the project by clicking on the file with the suffix xcworkspace.
Edit the CocoaPods configuration file (podSpec)
You can see that in the PrintHelloWorld folder
This file can be edited in a variety of ways, such as Xcode, Text editor, Sublime Text series, and Atom. I opened it with a Text editor, which is ugly but convenient
From here on, you can write any website at all. You are advised to write on the homepage of your project, but be sure to change it, otherwise you will get an error by default because there is no default url
S.ource needs to fill a private git address library, github charges, I choose gitLab
Copy the private library address and replace the s.ource address in the. Podspec file
The configuration is complete
Moving to our Example file again, pod updates
BetterdeMacBook-Pro:Example better$ pod update --no-repo-update
Copy the code
Open the project and see if it succeeded
5. Add PrintHelloWorld, run the test to open the project, and import printhelloWorld.h in byViewController.m
In order to ensure the correctness of the project, the POD file configuration is ok, we need to verify before submitting
Use terminal to move to our project path
BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/PrivateHelloWorld
Copy the code
At this point, we are done importing the source code, verifying that the project works, and verifying the POD configuration file locally
7. When the project is released, the tag 0.0.1 terminal is moved to the project file to execute git related commands
Betterdemacbook-pro :PrivateHelloWorld better$git remote add Origin betterdemacbook-pro :PrivateHelloWorld better$git remote add Origin https://gitee.com/Better_Y/PrintHelloWorld.git # add file betterdemacbook-pro :PrivateHelloWorld better$git add. And write the description Betterdemacbook-Pro :PrivateHelloWorld Better $git commit -a -m "First submitted version is 0.0.1" # -- allow-suggested -histories # git The pull origin MAste will fail. refusing to merge Merge master # The reason is that the branch master on the remote warehouse Origin and the local branch Master are considered different repositories by Git and so can't be merged directly, Need to add -- Allow-Suggested - Histories BetterdemacBook-Pro :PrivateHelloWorld Better $git Pull Origin Master -- Allow-Suggested - Histories # Push To betterdemacBookpro :PrivateHelloWorld Better $git Push on the Master branch of the Code Cloud's PrintHelloWolrd project Betterdemacbook-pro :PrivateHelloWorld better$git tag 0.0.1 # Push to remote branch Betterdemacbook-pro :PrivateHelloWorld Better $git Push Origin 0.0.1Copy the code
The steps for creating a Git private library are the same as those for the code cloud
Execute the Specs creation commands at the terminal
BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo add PrintSpecs https://gitlab.com/peiliuzhen/testspecs.git
Copy the code
Now, we can publish directly
# PrintSpecs is just add above management library name # PrivateHelloWorld podspec PrintHelloWorld project filename suffix for podspec inside BetterdeMacBook-Pro:PrivateHelloWorld better$ pod repo push PrintSpecs PrivateHelloWorld.podspecCopy the code
After the successful release, we can go to the code cloud to see if PrivateSpecs’ Git project has been successfully submitted
At this point, our private library release is complete
9. Verify private library releases
Create a new privateDemo project, create a Podfile file, and install Podfile as follows
Platform :ios,'8.0' target 'privateDemo' do pod 'PrintHelloWorld',:git => 'https://gitlab.com/peiliuzhen/privatehelloworld.git' endCopy the code
Open terminal and execute POD installation instruction:
BetterdeMacBook-Pro:~ better$ cd /Users/better/Desktop/privateDemo
BetterdeMacBook-Pro:privateDemo better$ pod install --no-repo-update
Copy the code
Let’s open the PrivateDemo project directory and take a look
At this point, verify our private library release is complete!!
Componentize basic operations
Extract, release and pack
Using a common list page example:
Cell encapsulation extraction:
Componentized module processing
Module coupling
The module of decoupling
Component communication
Module to module communication
Open source componentized framework recommendation (MJRouter, CTMediator, BeeHive)
There are three main componentization methods at present
-
URL routing
-
target-action
-
Protocol match
Here are the representative frameworks corresponding to the three approaches
MJRouter (URL Router)
Common iOS routing tools generally use URL matching, or use runtime method to dynamically call according to the convention.
The advantage of this approach is that it is easy to implement, but the disadvantage is that the string table needs to be maintained, or the problem cannot be exposed at compile time according to the dependent naming convention, and the error is discovered at run time.
The typical framework of URL routing mode is The Open source MGJRouter of Mogujie, as well as JLRoutes and HHRouter
MGJRouter implementation analysis:
- App startup instantiates each component module or registers it with class, and the component reports to
ModuleManager
registeredUrl
- When component A needs to call component B, the
ModuleManager
Pass the URL, parameters can be concatenated after the URL or passed in a dictionary, similaropenURL
. And then byModuleManager
Responsible for scheduling component B and finally completing the target
The registration and call codes are as follows:
[MGJRouter registerURLPattern:@" MGJ ://foo/bar" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]); }]; [MGJRouter openURL:@" MGJ ://foo/bar"]; [MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) { NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]); // @{@"user_id": @1900} }]; [MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];Copy the code
Advantages of URL routing
-
It is highly dynamic and suitable for apps that often carry out operation activities. For example, Mogujie itself belongs to the e-commerce industry
-
This helps manage routing rules on multiple platforms in a unified manner
-
Easy to adapt to URL schemes
Disadvantages of URl routing
-
There are limited ways to pass arguments, and no compiler can do parameter type checking, so all arguments are converted by string
-
Applies only to interface modules, not to generic modules
-
The format of the parameter is not clear, it is a flexible dictionary, and there needs to be a place to look up the format of the parameter.
-
Do not support the storyboard
-
Rely on string hard coding, difficult to manage, mushroom street to do a background management.
-
There is no guarantee that the module being used exists
-
The decoupling capability is limited, and the “registration”, “implementation” and “use” of URL must use the same character rules. Once any party makes modification, the code of other parties will become invalid, and it is difficult to reconstruct
CTMediator (target – the action)
The target-action solution is based on the OC runtime and category feature to get the module dynamically. For example, the class is obtained from string and the instance is created by NSClassFromString. The method is dynamically called by performSelector + NSInvocation. The typical framework is CTMediator
Implementation analysis:
-
Add a new interface to the route using the classification, and get the corresponding class through a string in the interface
-
The instance is created through Runtime and its methods are invoked dynamically
The code for page skipping is as follows:
#import "CTMediator+A.h" @implementation CTMediator (A) - (UIViewController *)A_Category_Swift_ViewControllerWithCallback:(void (^)(NSString *))callback { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"callback"] = callback; params[kCTMediatorParamsKeySwiftTargetModuleName] = @"A_swift"; return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO]; } - (UIViewController *)A_Category_Objc_ViewControllerWithCallback:(void (^)(NSString *))callback { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"callback"] = callback; return [self performTarget:@"A" action:@"Category_ViewController" params:params shouldCacheTarget:NO]; } @end //******* #import "target_a.h" #import "aviewController.h" @implementation Target_A - (UIViewController *)Action_Category_ViewController:(NSDictionary *)params { typedef void (^CallbackType)(NSString *); CallbackType callback = params[@"callback"]; if (callback) { callback(@"success"); } AViewController *viewController = [[AViewController alloc] init]; return viewController; } - (UIViewController *)Action_Extension_ViewController:(NSDictionary *)params { typedef void (^CallbackType)(NSString *); CallbackType callback = params[@"callback"]; if (callback) { callback(@"success"); } AViewController *viewController = [[AViewController alloc] init]; return viewController; } //******* 3, use // objective-c -> Category -> objective-c UIViewController *viewController = [[CTMediator sharedInstance] A_Category_Objc_ViewControllerWithCallback:^(NSString *result) { NSLog(@"%@", result); }]; [self.navigationController pushViewController:viewController animated:YES];Copy the code
advantages
-
Classification allows you to explicitly declare interfaces for compilation checks
-
The implementation is lightweight
disadvantages
-
It is necessary to add every interface in Mediator and target, and the code is cumbersome when modularizing
-
It still hardcodes strings in categories and internally uses dictionaries to pass parameters
-
There is no guarantee that the module being used exists, and after target is modified, the user can only detect the error at run time
-
Too many Target classes may be created
CTMediator core source code analysis
PerformTarget to CTMediator calling through the classification of the specific implementation, namely performTarget: action: params: shouldCacheTarget:, mainly by passing in the name, Find the corresponding target and action
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { if (targetName == nil || actionName == nil) { return nil; } // When used in swift, you need to pass in the target name of the corresponding project, Otherwise you will find the view controller nsstrings * swiftModuleName = params [kCTMediatorParamsKeySwiftTargetModuleName]; // generate target NSString *targetClassString = nil; If (swiftModuleName. Length > 0) {// Swift stringWithFormat:@"% @.target_ %@", swiftModuleName, targetName]; } else {//OC in target filename join targetClassString = [NSString stringWithFormat:@"Target_%@", targetName]; } / / the cache lookup target NSObject * target = [self safeFetchCachedTarget: targetClassString]; Target if (target == nil) {Class targetClass = NSClassFromString(targetClassString); Target = [[targetClass alloc] init]; NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; Sel action = NSSelectorFromString(actionString); If (target == nil) {// If (target == nil) {// If (target == nil) {// If (target == nil) {// If (target == nil) { In practice, you can give a fixed target in advance to be used at this time, Then process this request of [self NoTargetActionResponseWithTargetString: targetClassString selectorString: actionString originParams:params]; return nil; } / / whether to cache the if (shouldCacheTarget) {[self safeSetCachedTarget: target key: targetClassString]; } / / whether the response sel the if ([target respondsToSelector: action]) {/ / dynamic invocation methods return [self safePerformAction: the action target: the target params:params]; } else {// This is where the unresponsive request is handled. If there is no response, try calling the notFound method corresponding to target to handle SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else {// This is also where unresponsive requests are handled. If notFound is not present, this demo will return directly. In practice, you can use the fixed target above mentioned. [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params]; @synchronized (self) { [self.cachedTarget removeObjectForKey:targetClassString]; } return nil; }}}Copy the code
Enter safePerformAction: target: params: implementation, mainly through the invocation parameters + message forwarding
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {// get method signature NSMethodSignature* methodSig = [target methodSignatureForSelector:action]; if(methodSig == nil) { return nil; Const char* retType = [methodSig methodReturnType]; //void type if (STRCMP (retType, @encode(void)) == 0) {... } / /... Other types can read CTMediator source code}Copy the code
BeeHive (Protocol Class)
Protocol matching is implemented as follows:
-
1. Dictionary match protocol with the corresponding class
-
2. Create an instance dynamically by using protocol to get the class
A typical tripartite framework for Protocol is Alibaba’s BeeHive. BeeHive borrowed from the Spring Service, Apache DSO architecture concept, using AOP+ extension App life cycle API form, the business functions, basic functional modules in modular way to solve complex problems in large applications, and between modules in the form of Service call, complex problems will be segmtioned, Modularize services in an AOP manner.
BeeHive source code has not been further explored, want to know can refer to the link: BeeHive – an iOS module decoupling practice
Attached references:
Building a complete iOS componentization solution: How to decouple modules for interfaces