This paper focuses on how componentization communicates with each other

Componentized communication scheme

At present, the mainstream mainly has the following three ways:

  • 1. URL routing

  • 2, target – the action

  • 3. Protocol matches

URL routing

At present, most routing tools on iOS are based on URL matching, or based on naming conventions, using the Runtime method for dynamic invocation

The advantage of these dynamic schemes is that they are simple to implement, while the disadvantage is that they need to maintain string tables, or rely on naming conventions, and cannot expose all problems at compile time, requiring errors to be discovered at run time.

The URL routing mode is mainly MGJRouter represented by Mogujie

The realization idea is as follows:

  • Component modules are instantiated when the App starts, and then these components register urls with the ModuleManager, sometimes without instantiation, using class registration

  • When component A needs to call component B, the URL is passed to the ModuleManager and the parameters are passed as GET with the URL, similar to openURL. The ModuleManager is then responsible for scheduling component B and finally completing the task.

/ / 1, a registered a URL MGJRouter. RegisterURLPattern (" app: / / home ") {(info) and in print (" info: \ (info) ")} / / 2, call routing MGJRouter. OpenURL (" app: / / home ")Copy the code

Advantages of URL routing

  • Highly dynamic, suitable for apps that often carry out operational activities, such as e-commerce

  • Routing rules of multiple platforms can be centrally managed

  • 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

In addition to CTMediator, there are the following tripartite frameworks

  • routable-ios
  • JLRoutes
  • HHRouter

target-action

This solution is based on the OC runtime/category feature to dynamically fetch modules, such as NSClassFromString to fetch classes and create instances, and NSInvocation to dynamically invoke methods via performSelector + NSInvocation

Its main representative framework is CASatwy’s CTMediator

The realization idea is as follows:

  • 1. Add a new interface to the route using the classification, and obtain the corresponding class in the interface through a string

  • 2. Create an instance through Runtime and call its methods dynamically

CTMediator{@objc func A_showHome()->UIViewController? { let params = [ kCTMediatorParamsKeySwiftTargetModuleName: "CJLBase_Example" ] if let vc = self.performTarget("A", action: "Extension_HomeViewController", params: params, shouldCacheTarget: false) as? UIViewController{return vc} return nil} //******* NSObject { @objc func Action_Extension_HomeViewController(_ params: [String: Any])->UIViewController{let home = HomeViewController() return home}} ******* CTMediator.sharedInstance().A_showHome() { self.navigationController? .pushViewController(vc, animated: true) }Copy the code

The reference relationship between its modules is shown in the figure below

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, internally uses dictionaries to pass parameters, and to some extent has the same problems as URL routing

  • 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 source code analysis

  • Called through the classificationperformTargetCame toCTMediatorThe concrete implementation of, namelyperformTarget:action:params:shouldCacheTarget:, mainly through the passed name, find the correspondingtargetaction
- (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 thesafePerformAction:target:params:Realization, mainly throughinvocationforParameter passing + 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) {... } / /... Omit other types of judgment}Copy the code

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 core ideas

  • 1. Calls between modules have changed from directly calling corresponding modules to calling Service, avoiding direct dependence.

  • 2. Distribution of App life cycle, the coupling is logically split in AppDelegate, and each module exists independently in the form of micro-application.

Here is an example (intended to use swift, but has some problems, temporarily use OC) :

******** 1. Register [[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]]; #import "bhService. h" id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];Copy the code

advantages

  • 1. Use the interface call to realize the type safety of parameter passing

  • 2. Directly use the protocol interface of the module without repeated encapsulation

disadvantages

  • 1. Create all objects with a framework in a different way, i.e. no external parameters are supported

  • Create object with OC Runtime, swift is not supported

  • 3, only protocol and class matching, does not support more complex creation methods and dependency injection

  • 4. It is not guaranteed that the protocol used must have a corresponding module, nor can it be directly determined whether a protocol can be used to obtain modules

In addition to BeeHive, there is Swinject

Register the BeeHive module

BeeHive manages each module mainly through the BHModuleManager. Only registered modules are managed in the BHModuleManager.

BeeHive provides three different invocation forms, static plist, dynamic registration, and annotation. Module and Service are not associated. Each Service Module can independently implement functions of Module or Service.

1. Annotation method Registration this method is mainly used for Annotation marking through BeeHiveMod macro

//***** use BeeHiveMod(ShopModule) //***** BeeHiveMod macrodefinition #define BeeHiveMod(name) \ class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name""; #define BeeHiveDATA(sectName) __attribute((used, Section ("__DATA,"#sectname" ")) //***** char * kShopModule_mod __attribute((used, section("__DATA,""BeehiveMods"" "))) = """ShopModule""";Copy the code

Here are a few things to say about __attribute

  • The first parameter, used, modifies a function. Used modifies a function, meaning that it will not be optimized under Release even if it is not referenced. Without this modifier, unreferenced segments are removed from the Release environment linker.

  • Specify which segment by using __attribute__((section(“name”))). The data is marked with __attribute__((used)), preventing the linker from optimally removing unused segments and then injecting modules into __DATA

Now that the Module has been stored in a special section of the Mach-O file, how do I get it?

  • Enter theBHReadConfigurationThe method is mainly throughMach-OLocate the stored data segment and place it in an array
NSArray<NSString *>* BHReadConfiguration(char *sectionName,const struct mach_header *mhp) { NSMutableArray *configs = [NSMutableArray array]; unsigned long size = 0; Uintptr_t *memory = #ifndef __LP64__ (uintptr_t*)getsectiondata(mhp, SEG_DATA, sectionName, &size); #else const struct mach_header_64 *mhp64 = (const struct mach_header_64 *)mhp; uintptr_t *memory = (uintptr_t*)getsectiondata(mhp64, SEG_DATA, sectionName, &size); #endif unsigned long counter = size/sizeof(void*); For (int idx = 0; idx < counter; ++idx){ char *string = (char*)memory[idx]; NSString *str = [NSString stringWithUTF8String:string]; if(! str)continue; BHLog(@"config = %@", str); if(str) [configs addObject:str]; } return configs; }Copy the code

2. Read the local Pilst file

  • First, you need to set up the path
[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive"; / / optional, default is extracted. The bundle/BeeHive plistCopy the code
  • Create plist file, plist file format is also an array containing multiple dictionaries. There are two keys in the dictionary, one is @”moduleLevel” and the other is @”moduleClass”. Notice that the name of the root array is @” moduleClasses “.

  • Go to the loadLocalModules method and basically take the array from the Plist and add it to the BHModuleInfos array.

// to initialize the context, load Modules and Services -(void)setContext:(BHContext *)context {_context = context; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self loadStaticServices]; [self loadStaticModules]; }); } 👇 // load modules - (void)loadStaticModules { And register with the BHModuleManager BHModuleInfos array [[BHModuleManager sharedManager] loadLocalModules]; // Register all modules and sort them internally by priority [[BHModuleManager sharedManager] registedAllModules]; } 👇 - (void)loadLocalModules {//plist file path NSString *plistPath = [[NSBundle mainBundle] pathForResource:[BHContext shareInstance].moduleConfigName ofType:@"plist"]; // Check if file exists if (! [[NSFileManager defaultManager] fileExistsAtPath:plistPath]) { return; } / / read the entire file] [@ "moduleClasses" : array NSDictionary * moduleList = [[NSDictionary alloc] initWithContentsOfFile: plistPath]; ModuleClasses key [[@"moduleClass":"aaa", @"moduleLevel": @"bbb"], [...]] NSArray<NSDictionary *> *modulesArray = [moduleList objectForKey:kModuleArrayKey]; NSMutableDictionary<NSString *, NSNumber *> *moduleInfoByClass = @{}.mutableCopy; / / to iterate through group [self BHModuleInfos enumerateObjectsUsingBlock: ^ (NSDictionary * _Nonnull obj, NSUInteger independence idx, BOOL * _Nonnull stop) { [moduleInfoByClass setObject:@1 forKey:[obj objectForKey:kModuleInfoNameKey]]; }]; [modulesArray enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {if (! ModuleInfoByClass [[obj objectForKey: kModuleInfoNameKey]]) {/ / stored in BHModuleInfos [self.BHModuleInfos addObject:obj]; } }]; }Copy the code

3, load method registration

This method registers the Module class in the Load method

+ (void)load
{
    [BeeHive registerDynamicModule:[self class]];
}

Copy the code
  • Enter theregisterDynamicModuleimplementation
+ (void)registerDynamicModule:(Class)moduleClass { [[BHModuleManager sharedManager] registerDynamicModule:moduleClass]; } 👇 - (void) registerDynamicModule: (Class) moduleClass {[self registerDynamicModule: moduleClass shouldTriggerInitEvent:NO]; } 👇 - (void)registerDynamicModule:(Class)moduleClass shouldTriggerInitEvent:(BOOL)shouldTriggerInitEvent {[self addModuleFromObject:moduleClass shouldTriggerInitEvent:shouldTriggerInitEvent]; }Copy the code

Its underlying or the same as the first way, will eventually go addModuleFromObject: shouldTriggerInitEvent: method

  • The load method can also be usedBH_EXPORT_MODULEMacro instead of
#define BH_EXPORT_MODULE(isAsync) \ + (void)load { [BeeHive registerDynamicModule:[self class]]; } \ -(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue]; }Copy the code

The BH_EXPORT_MODULE macro can pass in a parameter that indicates whether the Module is loaded asynchronously, if YES, or synchronously if NO.

2. The BeeHive module event

BeeHive provides life cycle events for each module to interact with the BeeHive host environment and sense changes in the module’s life cycle.

BeeHive modules receive events. In the BHModuleManager, all events are defined as the BHModuleEventType enumeration. As shown below, there are two special events: BHMInitEvent and BHMTearDownEvent

Typedef NS_ENUM(NSInteger, BHModuleEventType) {// Set the Module BHMSetupEvent = 0, // Used to initialize the Module, such as the environment, // Used to remove the Module BHMTearDownEvent, BHMSplashEvent, BHMQuickActionEvent, BHMWillResignActiveEvent, BHMDidEnterBackgroundEvent, BHMWillEnterForegroundEvent, BHMDidBecomeActiveEvent, BHMWillTerminateEvent, BHMUnmountEvent, BHMOpenURLEvent, BHMDidReceiveMemoryWarningEvent, BHMDidFailToRegisterForRemoteNotificationsEvent, BHMDidRegisterForRemoteNotificationsEvent, BHMDidReceiveRemoteNotificationEvent, BHMDidReceiveLocalNotificationEvent, BHMWillPresentNotificationEvent, BHMDidReceiveNotificationResponseEvent, BHMWillContinueUserActivityEvent, BHMContinueUserActivityEvent, BHMDidFailToContinueUserActivityEvent, BHMDidUpdateUserActivityEvent, BHMHandleWatchKitExtensionRequestEvent, BHMDidCustomEvent = 1000 };Copy the code

There are three main types

  • 1, system events: mainly refers to Application life cycle events!

The general approach is to change the AppDelegate to inherit from BHAppDelegate

@interface TestAppDelegate : BHAppDelegate <UIApplicationDelegate>

Copy the code
  • 2, application events: official flow chart, including modSetup, modInit, etc., can be used to code the setup and initialization of each plug-in module.

  • 3. Customize events

All of the above events can be handled by calling BHModuleManager triggerEvent:.

- (void)triggerEvent:(NSInteger)eventType { [self triggerEvent:eventType withCustomParam:nil]; } 👇 - (void)triggerEvent:(NSInteger)eventType withCustomParam:(NSDictionary *)customParam {[self handleModuleEvent:eventType forTarget:nil withCustomParam:customParam]; } 👇 #pragma mark-module protocol - (void)handleModuleEvent:(NSInteger)eventType ForTarget :(id<BHModuleProtocol>)target withCustomParam:(NSDictionary *)customParam {switch (eventType) {// initializes the event case BHMInitEvent: //special [self handleModulesInitEventForTarget:nil withCustomParam :customParam]; break; / / destructor event case BHMTearDownEvent: / / special [self handleModulesTearDownEventForTarget: nil withCustomParam: customParam]; break; Default: {NSString *selectorStr = [self.bhSelectorByevent objectForKey:@(eventType)]; [self handleModuleEvent:eventType forTarget:nil withSeletorStr:selectorStr andCustomParam:customParam]; } break; }}Copy the code

As you can see from the above code, with the exception of the BHMInitEvent initialization event and the BHMTearDownEvent removing Module event, All events are called handleModuleEvent: forTarget: withSeletorStr: andCustomParam: method, its internal implementation mainly traversal moduleInstances instance array, Call performSelector: withObject: method corresponding method call

- (void)handleModuleEvent:(NSInteger)eventType forTarget:(id<BHModuleProtocol>)target withSeletorStr:(NSString *)selectorStr andCustomParam:(NSDictionary *)customParam { BHContext *context = [BHContext shareInstance].copy; context.customParam = customParam; context.customEvent = eventType; if (! selectorStr.length) { selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)]; } SEL seletor = NSSelectorFromString(selectorStr); if (! seletor) { selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)]; seletor = NSSelectorFromString(selectorStr); } NSArray<id<BHModuleProtocol>> *moduleInstances; if (target) { moduleInstances = @[target]; } else { moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)]; } // Iterate through the moduleInstances array, Call performSelector: withObject: method corresponding method call [moduleInstances enumerateObjectsUsingBlock: ^ (id < BHModuleProtocol > moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) { if ([moduleInstance respondsToSelector:seletor]) { #pragma clang diagnostic push #pragma clang Diagnostic ignored "-warc-performSelector -leaks" // Perform method calls [moduleInstance performSelector: Seletor withObject:context]; #pragma clang diagnostic pop [[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]]; } }]; }Copy the code

Note: all modules here must be BHModuleProtocol compliant or they will not receive messages for these events.

3. Call the BeeHive module

In BeeHive, the BHServiceManager manages each Protocol. BHServiceManager manages only registered protocols.

There are three ways to register Protocol, which correspond to Module registration

1. Annotation registration

/ / * * * * * * 1, the Annotation by BeeHiveService macro marks BeeHiveService (HomeServiceProtocol BHViewController) / / * * * * * * 2, and # define a macro definition BeeHiveService(servicename,impl) \ class BeeHive; char * k##servicename##_service BeeHiveDATA(BeehiveServices) = "{ \""#servicename"\" : \""#impl"\"}"; //****** Char * kHomeServiceProtocol_service __attribute((used, section("__DATA,""BeehiveServices"" "))) = "{ \"""HomeServiceProtocol""\" : \"""BHViewController""\"}";Copy the code

2. Read the local PList file

  • First, as with Module, you need to set the path first
[BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";

Copy the code
  • Set the plist file

  • Also register services in setContext

// Load services -(void)loadStaticServices {[BHServiceManager sharedManager]. EnableException = self.enableException; [[BHServiceManager sharedManager] registerLocalServices]; } 👇 - (void)registerLocalServices {NSString *serviceConfigName = [BHContext shareInstance].Serviceconfigname; / / the file path nsstrings * plistPath = [[NSBundle mainBundle] pathForResource: serviceConfigName ofType: @ "plist"]. if (! plistPath) { return; } NSArray *serviceList = [[NSArray alloc] initWithContentsOfFile:plistPath]; [self.lock lock]; For (NSDictionary *dict in serviceList) {NSString *protocolKey = [dict objectForKey:@"service"]; NSString *protocolImplClass = [dict objectForKey:@"impl"]; if (protocolKey.length > 0 && protocolImplClass.length > 0) { [self.allServicesDict addEntriesFromDictionary:@{protocolKey:protocolImplClass}]; } } [self.lock unlock]; }Copy the code

3, load method registration

To register the Protocol in the Load method, register the Protocol by calling registerService:service: in BeeHive

+ (void)load { [[BeeHive shareInstance] registerService:@protocol(UserTrackServiceProtocol) service:[BHUserTrackViewController class]]; } 👇 - (void)registerService:(Protocol *)proto service:(Class) serviceClass {[[BHServiceManager sharedManager] registerService:proto implClass:serviceClass]; }Copy the code

At this point, the three methods are created

The Protocol for

Protocol differs from Module in that Protocol has one more method than Module that returns a Protocol instance object

- (id)createService:(Protocol *)proto; { return [[BHServiceManager sharedManager] createService:proto]; } 👇 - (id)createService:(Protocol *)service {return [self createService:service withServiceName:nil]; } 👇 - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName {return [self createService:service withServiceName:serviceName shouldCache:YES]; } 👇 - (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache {if (! serviceName.length) { serviceName = NSStringFromProtocol(service); } id implInstance = nil; // Check whether protocol is already registered if (! [self checkValidService:service]) { if (self.enableException) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"%@ protocol does not been registed", NSStringFromProtocol(service)] userInfo:nil]; } } NSString *serviceStr = serviceName; // If there is a cache, Directly from the cache for the if (shouldCache) {id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName: serviceStr];  if (protocolImpl) { return protocolImpl; Class implClass = [self serviceImplClass:service]; if ([[implClass class] respondsToSelector:@selector(singleton)]) { if ([[implClass class] singleton]) { if ([[implClass Class] respondsToSelector:@selector(shareInstance)]) // Create a singleton. Else implInstance = [[implClass alloc] init]; If (shouldCache) {/ / cache the [[BHContext shareInstance] addServiceWithImplInstance: implInstance serviceName: serviceStr]; return implInstance; } else { return implInstance; } } } return [[implClass alloc] init]; }Copy the code

CreateService first checks if Protocol is registered. It then takes the corresponding Class from the dictionary and creates a singleton if the shareInstance method is implemented, or an instance if it is not. If singleton is implemented, we can further cache implInstance and serviceStr in the servicesByName dictionary of the BHContext. This can then be passed with context

  • Enter theserviceImplClassImplementation, from which you can see that protocol and classes are passedThe dictionaryThe binding,protocolAs akey.serviceImp(Class name) asvalue
- (Class)serviceImplClass:(Protocol *)service
{
    //通过字典将 协议 和 类 绑定,其中协议作为key,serviceImp(类的名字)作为value
    NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];
    if (serviceImpl.length > 0) {
        return NSClassFromString(serviceImpl);
    }
    return nil;
}

Copy the code

Module & Protocol

Here is a brief summary:

  • For Module: array storage

  • For Protocol: Bind Protocol to the class using a dictionary. Key is Protocol and value is serviceImp, which is the name of the class

Auxiliary class

  • BHConfig class: a singleton with an NSMutableDictionary config property that maintains dynamic environment variables as a complement to BHContext

  • The BHContext class is a singleton with two NSMutableDictionary properties, modulesByName and servicesByName. This class is mainly used to store context information. For example in the application: didFinishLaunchingWithOptions: when you can initialize a lot of context information

// Save information [BHContext shareInstance]. Application = Application; [BHContext shareInstance].launchOptions = launchOptions; [BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive"; / / optional, default is extracted. The bundle/BeeHive plist [BHContext shareInstance]. ServiceConfigName = @ "BeeHive. Bundle/BHService";Copy the code
  • BHTimeProfiler class: A Profiler for computing time performance

  • The BHWatchDog class opens a thread and listens for the main thread to block

Refer to the link

  • BeeHive – an elegant but still improving decoupling framework
  • BeeHive, an iOS module decouple practice