In recent days, I have been investigating the implementation scheme and technology selection of component communication, routing mode and target-action mode in the market. Due to the hard coding problem, I am worried that the subsequent maintenance of hard coding may cost a lot of energy. Besides, it is difficult to check whether there are any mistakes in the compilation period of the communication mode based on Runtime. This can cause runtime problems, so the Pass is dropped. Currently in our project, the hop between VC’s is carried out by routing. In fact, the internal hop is instantiated by reflecting the Class from the string, and the interface provided to JS is plug-in based on Runtime. The principle is similar, which is to get the real type at runtime through hard coding and then call it. Although these two schemes can decouple modules, in practice, we found that route loss and plug-in loss did occur during regression testing due to code conflicts solved by colleagues. So this time I directly investigated the way to build services based on Protocol (Service refers to Protocol) and summarized my views.

The following two frameworks are mainly analyzed. These two frameworks are typical component communication built based on Service, and there are many practical skills inside. Through these two frameworks, we will explore the principle of component communication built by Service. Autonavi, Tmall, and a bunch of awesome apps are all based on this approach (and there may be others I haven’t touched yet).

Ali: BeeHive

Awesome: Bifrost.

The basic principle of

There are many articles on the web about componentization, and they are very detailed, so it’s not the focus of this article. This paper mainly analyzes the advantages and disadvantages of the two frameworks in component communication, as well as some personal considerations for the use of technology selection.

Bifrost has been praised for comparing it to a rainbow bridge, connecting and communicating between components. The code is fairly clear and simple. The cool idea goes something like this:

  • 1: Each Service component defines a Module and a Service. The Module is used to realize some external functions, and the Service is used to define the interface of the component to provide external services.
  • 2: Again through a management class, in+loadMethod to register the mapping between them in the dictionary.
  • 3: When the app starts, all modules are instantiated. In fact, all modules are instantiatedThe singleton, supports synchronous and asynchronous initialization, and supports loading priorities.

In this way, if other modules want to obtain Module instances, they only need to use its Service as the key to manage the dictionary registered in the class, so as to realize the dependency relief between components. The general call process is as follows:

id<xxxService> module = [[Bifrost moduleByService:@protocol(xxxService)] doSomething:xxx];
Copy the code
+ (id<BifrostModuleProtocol> _Nullable)moduleByService:(Protocol*_Nonnull)serviceProtocol {// mapping String NSString *protocolStr = NSStringFromProtocol(serviceProtocol); . ModuleDict [protocolStr]; // Class Class Class = BFInstance. ModuleDict [protocolStr]; Id instance = [class sharedInstance];return instance;
}
Copy the code

BeeHive and its idea is actually roughly the same, relatively more code, relatively detailed features, the general idea is as follows:

  • 1: From the source point of view, BeeHive believes that every class that needs to provide interfaces to other components can register a Service, which is designed to provide services externally, where registration, relatively flexible. For example, if A class of component A needs to provide an interface to component B, the class of component A needs to provide A Service(defining the interface) to component B, and then register the Service and the class with BeeHive. Then component B or any other component only needs to reference Service. BeeHive pulls all services together for other components to reference.
  • BeeHive registers the mapping between Modules and services in a variety of ways, either way eventually registering the dictionary through the management class singleton.
  • 3: Module instances support singleton and multi-instance initialization. During the process of obtaining Module instances, it can also be cached in the dictionary, so that the instance can be directly invoked. From the source code through recursive lock to ensure that in the case of multi-threaded access, sequential access data security. The general code flow is as follows:
id<xxxServiceProtocol> module = [[BeeHive shareInstance] createService:@protocol(xxxServiceProtocol)];
Copy the code
- (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];
}
Copy the code
- (id)createService:(Protocol *)service withServiceName:(NSString *)serviceName shouldCache:(BOOL)shouldCache { ... NSString *serviceStr = serviceName; // Support cache, first go to the cache to find, if there is no return, continue to go downif (shouldCache) {
        id protocolImpl = [[BHContext shareInstance] getServiceInstanceFromServiceName:serviceStr];
        if (protocolImpl) {
            returnprotocolImpl; }} // Find the module Class name string in the management Class dictionary and convert it to Class NSString *serviceImpl = [[self servicesDict] objectForKey:NSStringFromProtocol(service)];if(serviceImpl.length > 0) { Class implClass = NSClassFromString(serviceImpl); } // If singleton is implementedif ([[implClass class] respondsToSelector:@selector(singleton)]) {
        if ([[implClass class] singleton]) {
            if([[implClass class] respondsToSelector:@selector(shareInstance)] class] shareInstance];elseimplInstance = [[implClass alloc] init]; // Set cache so store itif (shouldCache) {
                [[BHContext shareInstance] addServiceWithImplInstance:implInstance serviceName:serviceStr];
                return implInstance;
            } else {
                returnimplInstance; }}} // Unimplemented singleton returns multiple instances directlyreturn [[implClass alloc] init];
}
Copy the code
  • BeeHive also has some logic to decouple the AppDelegate, which will not be expanded here.

Contrast selection

To summarize, the two frameworks are similar in principle, but there are some minor details that need to be clarified.

The Module partition

Bifrost is based on the appearance mode, and all the call relations between components are realized by appearance classes. One appearance class corresponds to one Service, that is, one component corresponds to one Service. In The opinion of Zan, in this way, complex relations between components are realized by appearance roles, which reduces the coupling degree of the system. It sets all the facade classes, the Module class, as singletons.

“BeeHive” is my position from the source code analysis towards which classes have interfaces need to be used by other components, a Service which classes to register, this class can be a singleton, also can be more cases, but I feel a little flexible appearance for each component defines a class can implement, or may be too much Service files, maintenance difficulties.

In my opinion, the two ideas are basically the same. BeeHive is more flexible in comparison.

The Module registration

Bifrost registrations are all in the +load method, and each Module implements its + Load method and registers services to achieve this mapping.

+ (void)load {
    [Bifrost registerService:@protocol(xxxServiceProtocol) withModule:self.class];
}
Copy the code

BeeHive, by contrast, has several ways of registering, the most novel of which is to add this mapping to the mach-o data segment at compile time through the __attribute() function, and then pull it out and register it into the dictionary at App startup, all in BHAnnotation.

The Module management

The Bifrost in App started, the AppDelegate willFinishLaunchingWithOptions, will all the Module, initialized according to the order, and all for a singleton. In practice, there are at most 20 components, so these singletons don’t cause memory problems. Initialization supports asynchrony. Bifrost actually gets instances when it calls between components that are already initialized singletons.

+ (void)setupAllModules {
    NSArray *modules = [self allRegisteredModules];
    for (Class<BifrostModuleProtocol> moduleClass inmodules) { ... Leave out some codeif (setupSync) {
                [[moduleClass sharedInstance] setup];
            } else{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[moduleClass sharedInstance] setup];  }); }}}Copy the code

When the App starts, BeeHive does not instantiate all modules. Instead, it adds the class name and the corresponding Service name to the dictionary of management classes. When components really need to communicate, it takes the Module class name from the dictionary according to the Service name. During the instantiation process, it can be set to singleton or multiple instances.

- (void)registerService:(Protocol *)service implClass:(Class)implClass {
    ... 
    NSString *key = NSStringFromProtocol(service);
    NSString *value = NSStringFromClass(implClass);
    
    if(key.length > 0 && value.length > 0) { [self.lock lock]; / / in fact only the string stored up not to instantiate [self. AllServicesDict addEntriesFromDictionary: @ {key: value}]. [self.lock unlock]; }}Copy the code

Bifrost sets up all modules as singletons. In practice, I find it very convenient to use this configuration. You can get singletons directly from Service, especially when you need the Module to store some state. However, there is also a problem, because the positioning of Module is the packaging layer of all external exposure and leakage interfaces of the whole component. However, I often need the Module to hold the specific implementation class for some business scenarios. At this time, I will find that the class held by the singleton will be troublesome to release. But I don’t think it’s pretty. So I prefer BeeHive to add multiple implementations of component appearance classes, initialize them as needed, and release them as needed.

Transmission and processing

If we need to pass A model from component A to component B, at least in the BeeHive Demo, if we want to pass the whole model, The need to pass all of the Model fields to component B as parameters makes the interface very long and unintuitive. If component B can fetch the Model directly, component B will easily know where the parameters passed by the interface come from and what they do, which will enhance business relevance. Moreover, it will be very easy to read and write the parameters through the point syntax. Bifrost provides a good idea. It also builds a Service for the Model, which is written in the same Service as the Module, as follows:

@interface GoodsModel : NSObject<GoodsProtocol>

@property(nonatomic, strong) NSString *goodsId;
@property(nonatomic, strong) NSString *name;
@property(nonatomic, assign) CGFloat price;
@property(nonatomic, assign) NSInteger inventory;

@end
Copy the code
#pragma mark - Model Protocols
@protocol GoodsProtocol <NSObject>
- (NSString*)goodsId;
- (NSString*)name;
- (CGFloat)price;
- (NSInteger)inventory;
@end
Copy the code

It’s also easy to use:

id<GoodsProtocol> goods = [BFModule(GoodsModuleService) goodsById:item.goodsId];
Copy the code

BFModule macro definition expansion:

#define BFModule(service_protocol) ((id<service_protocol>)[Bifrost moduleByService:@protocol(service_protocol)])
Copy the code

In general, on the basis of Bifrost, Module management, integration of BeeHive registration method, support multiple cases, in the use of the creation of release and other ideas would be better.

conclusion

As an added bonus, one of the main advantages of the Protocol based approach is that there are compile-time errors, and the compiler checks for missing files, missing references, and I think that’s one of the main reasons a lot of companies use this approach. Id

XXX =… You can get an instance of one of the modules without having to reference the header file of the module, thus achieving intermodule compilation isolation and intermodule communication.

For those of you who are not familiar with this, this is actually consistent with the principle of proxy. When we write a utility class to provide a proxy, do you care which class is calling your utility class? The answer is no, we just need to care if the caller complies with the xxxServiceProtocol and implements the methods, if so we can call them.

Ref:

BeeHive: A Practice in decoupling iOS Modules

“Practice of Componentized (Modular) Architecture Design of Mobile iOS”