preface

Although the topic of iOS componentization and routing has been discussed for a long time in the industry, it seems that many people have misunderstood it, and even do not understand the meaning of “component”, “module”, “routing” and “decoupling”.

Related blog also quite many, in fact, in addition to those written by a few famous, there is very little reference value, and the views of famous experts are not completely correct. Architecture is often a trade-off between business scenarios, learning costs, development efficiency, etc., so it is easy to put the cart before the horse when an architectural solution is objectively explained but somewhat subjective, with some personal embellishment.

So to keep a clear mind, dialectical attitude to view the problem, the following is the industry more valuable reference article: IOS componentization — Route Design Analysis Category feature application and control in iOS componentization Exploration of iOS componentization scheme

This article is mainly the author’s understanding of iOS componentization and routing, and tries to explain the advantages and disadvantages of various schemes in a more objective and concise way. Criticism and comments are welcome.

In this paper, the DEMO

First, the difference between components and modules

  • “Component” emphasizes reuse, which is directly depended on by each module or component. It is infrastructure, which generally contains no business or weak business and belongs to vertical layer (such as network request component, image download component).
  • “Module” emphasizes encapsulation and refers more to functional independent business modules that are horizontally layered (e.g., shopping cart module, personal hub module).

So for the purpose of “componentization”, it seems more reasonable to call it “modularization”.

However, “component” and “module” are the meanings defined by predecessors, and the concept of “iOS componentization” has been preconceived, so it is only necessary to understand that “iOS componentization” is more about decoupling between business modules.

Second, the significance of routing

First of all, it’s important to make it clear that routing is not just about interface hopping, but also about almost anything like data retrieval.

(1) Simple routes

Internal invocation

Emulating Web routing, the original iOS native routing looks like this:

[Mediator gotoURI:@"protocol://detail? name=xx"];
Copy the code

The disadvantages are obvious: String URIs do not represent native types in iOS, and there is a lot of hard coding to do by reading the usage documentation for the corresponding modules.

The code implementation is roughly as follows:

+ (void)gotoURI:(NSString *)URI {NSString *aim =... ; NSDictionary *parmas = ... ;if ([aim isEqualToString:@"Detail"]) {
        DetailController *vc = [DetailController new];
        vc.name = parmas[@"name"];
        [... pushViewController:vc animated:YES];
    } else if ([aim isEqualToString:@"list"]) {... }}Copy the code

Look good:

Once you get the URI, there is always the logic to convert it to targets and parameters (AIM/Params) and then actually call the native module. Obviously, for internal calls, the URI parsing step is gilding the lily (Casa has written about this in his blog).

The routing method is simplified as follows:

+ (void)gotoDetailWithName:(NSString *)name {
    DetailController *vc = [DetailController new];
    vc.name = name;
    [... pushViewController:vc animated:YES];
}
Copy the code

It’s easy to use:

[Mediator gotoDetailWithName:@"xx"];
Copy the code

This way, the argument list of a method can be substituted for additional documentation and checked by the compiler.

How to support external URI calls

Then for external calls, just add an adapter for URI resolution to them to solve the problem:

Where do I write the routing method

The Unified routing invocation class is easy to manage and use, so you usually need to define a Mediator class. Considering that the maintainers of different modules need to modify mediators to add routing methods, workflow conflicts may exist. So it’s a good practice to add a category for each module using decorator patterns:

@interface Mediator (Detail)
+ (void)gotoDetailWithName:(NSString *)name;
@end
Copy the code

The routing method of the corresponding module is then written to the corresponding classification.

Functions of simple routes

The encapsulation here removes the direct coupling between business modules, but they are still indirectly coupled (because the routing class needs to import the concrete business) :

However, a simple route does not need to worry about coupling, and even such a simple process has the following benefits:

  • A clear argument list for easy use by the caller.
  • Decouple business modules so that the interface may not change when the business changes and the code may not change when external calls are made.
  • Even if the business changes, the routing method must change, thanks to the compiler’s inspection, it can directly locate the call location to make the change.

(2) Support dynamic call routing

Dynamic invocation, as the name suggests, is when the invocation path changes without updating the App. For example, click A to trigger the jump to INTERFACE B, and at some point, click A to jump to interface C.

To ensure minimal granularity of dynamic invocation, you need complete information about the target business, such as AIM and Params, the targets and parameters mentioned above.

Then you need a set of rules that come from two sources:

  • Configuration from the server.
  • Some local judgment logic.

Predictive dynamic invocation

+ (void)gotoDetailWithName:(NSString *)name {
    if(The local defense logic determines that the DetailController is abnormal.) {Go to the DetailOldControllerreturn;
    }
    DetailController *vc = [DetailController new];
    vc.name = name;
    [... pushViewController:vc animated:YES];
}
Copy the code

Developers need to explicitly know that “some business” supports dynamic invocation and that dynamic invocation targets “some business”. In other words, this is a “pseudo-” dynamic call, where the code logic is written dead and the trigger point is dynamic.

Automated dynamic invocation

Automated dynamic invocation means that the page to which a route is directed can be automatically changed. For example, the +gotoDetail method can be directed to any other page. The resolution operation can be controlled by the server, such as routing resolution table, dynamic request interface resolution.

Imagine + (void)gotoDetailWithName:(NSString *)name; Can automatic dynamic invocation be supported?

The answer is no, true “automation” requires one condition: you need a slice of all routing methods.

The purpose of this slice is to intercept routing targets and parameters and then perform dynamic scheduling. When referring to AOP, we may think of Hook technology, but for the following two routing methods:

+ (void)gotoDetailWithName:(NSString *)name;
+ (void)pushOldDetail;
Copy the code

You can’t find any similarities between them. It’s hard to hit.

So, get a section of the method I can think of only one: unified routing method entry.

Define a method like this:

- (void)gotoAim:(NSString *)aim params:(NSDictionary *)params {Copy the code

(The technical implementation of how to dynamically call a specific business will be discussed later, but it is not concerned here, just need to know that the specific business can be dynamically located through these two parameters.)

Then, the routing method says:

+ (void)gotoDetailWithName:(NSString *)name {
    [self gotoAim:@"detail" params:@{@"name":name}];
}
Copy the code

Note that @”detail” is the agreed Aim and can dynamically locate specific businesses internally.

The diagram is as follows:

Of course, external calls can be made without internal calls, so it is possible to dynamically locate local resources without being aware of specific business:

Thus, the unified routing method entry must be hard-coded, and the automated dynamic invocation must be hard-coded for this scheme.

So, here’s a classification method + (void)gotoDetailWithName:(NSString *)name; It’s a good idea to wrap up the hard code and hand it over to the business engineers to maintain.

Casa’s CTMediator classification does just that, and this is where the Mogujie componentization scheme can be optimized.

(3) whether the internal call needs to pass through the URI question

Earlier, I expressed the view that internal calls don’t need urIs (see Figure 3 and its evolution).

For those of you who think that an internal call just needs to wrap a layer of syntax sugar (such as a class) on top of a URI that the compiler can check, it looks like this:

Q1: Understandably, there seems to be a compelling reason for this: all routing calls are uniformly URI resolved. So, this URL resolution method acts as an interceptor, which seems to do the dynamic invocation mentioned above?

A1: However, this only supports predictive dynamic calls, that is, you need to specify a specific business and then write some “dead” code that only makes the trigger point dynamic. It doesn’t matter if the code for such predictive dynamic calls is written in the “internal call (syntax sugar)”; it is concentrated in one place as well as scattered around.

Here is the decoupling mode:

Q2: One might argue that the “Figure 3-evolution-URI decoupling” approach enables automated dynamic invocation without the need to import specific business code.

A2: However, wouldn’t that be equivalent to having two interceptors? The decoupled approach to invoking the business itself has a unified entry point. Therefore, it makes no sense to call internally through the uniform URI resolution method interceptor.

Q3: It can also be said that he just wants to do something with a unified URI resolution interception portal, rather than doing automated dynamic calls.

A3: This means that he does not fully decouple the specific business in this interception entry, and the interception does things that are independent of the specific business (which brings us back to Q1). This is the “Figure 3 – evolve-URI” approach. This scenario would seem to be a reason, for example, to record all routing calls without involving specific business modules? But internal calls can be done without URI resolution:

Don’t say this is hard coding, because internal calls are still hard coding after URL parsing.

In my opinion, internal calls that go URI are not necessary. If you must, here are the disadvantages:

  • If the route does not need to be decoupled from the specific service, the URI mode of internal invocation adds meaningless hard coding.
  • The URI resolution rule must be consistent on all three ends. If not, external calls still require additional transformation adapters, more meaningless transformation work, and the rules for “WebView calls” and “external App calls” need to be unified. If all the rules are unified, then the three ends need a lot of communication costs, and any end cannot easily change the rules, and the routing of internal calls is “completely restricted” by external calls.
  • Strings do not support many system native types and urIs are resolved toaim / paramsWhen you may need to convert to native parameters (such as string to NSData), thenInternal call (need to convert NSData to string) -> URI parsing (then convert string to NSData) -> AIM/pamrasObviously the conversion process is redundant. (Casa has also touched on this issue in his blog.)

In software development, “unity” seems to become an obsessive thinking. In fact, only by analyzing the real meaning of specific business scenarios can we better implement the architecture.

If you have any questions, please leave a message at the end of the article.

(4) Route summary

You can see that I talk a lot about routing, but I don’t mention componentization, because routing doesn’t necessarily require componentization.

The design of routing mainly considers whether dynamic calls are needed. The following scenarios are listed:

  • If the native wab page fails, switch to the corresponding WAB page.
  • Wab will switch to the native page to reduce the consumption.

If you don’t want to write this logic “in anticipation” and want all routes to support free switching between Web and native, or relocation of different services, you may need to do automated dynamic routing.

Three, the meaning of componentization

The previous analysis of routing mentioned using goals and parameters (AIM/Params) to dynamically locate technical points to specific businesses. There are actually two ways of thinking about reflection and dependency injection in iOS Objective-C:

  • willaimMake it concreteClassandSEL, using the Runtime runtime to call for specific services.
  • For code, process space is shared, so maintain a global mapping table in advanceaimMaps to a piece of code that performs a specific business when invoked.

To be clear, both approaches have relieved Mediator of its dependence on business modules:

These decoupling techniques are at the heart of iOS componentization.

The main purpose of componentization is to make each business module run independently without interfering with each other, so complete decoupling between business modules is inevitable. At the same time, the separation of business modules is also very careful, and it should pursue functional independence rather than minimum granularity.

(1) Runtime decoupling

A unified entry method is defined for Mediators:

/// This method is an interceptor, - (id)performTarget:(NSString *)target action:(NSString *)action params:(NSDictionary *)params {Class CLS; id obj; SEL sel; cls = NSClassFromString(target);if(! cls) goto fail; sel = NSSelectorFromString(action);if(! sel) goto fail; obj = [cls new];if(! [obj respondsToSelector:sel]) goto fail;#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [obj performSelector:sel withObject:params];
#pragma clang diagnostic pop
fail:
    NSLog(@"Can't find a target, write fault-tolerant logic.");
    return nil;
}
Copy the code

Simple to write the code, the principle is very simple, available Demo test. For internal calls, write a class for each module:

@implementation BMediator (BAim) - (void)gotoBAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {  [self performTarget:@"BTarget" action:@"gotoBAimController:" params:@{@"name":name, @"callBack":callBack}];
}
@end
Copy the code

You can see that the message is sent to the BTarget:

@interface BTarget : NSObject
- (void)gotoBAimController:(NSDictionary *)params; 
@end
@implementation BTarget
- (void)gotoBAimController:(NSDictionary *)params {
    BAimController *vc = [BAimController new];
    vc.name = params[@"name"];
    vc.callBack = params[@"callBack"];
    [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
}
@end
Copy the code

Why are categories defined

The purpose of defining a class is to provide a syntax candy that the caller can easily use and that the hard code can hand over to the corresponding business engineer.

Why is a Target defined?

  • Avoid scattered routing logic of the same module, facilitating management.
  • Routing is more than just controller jumps, and some businesses may not be able to put code (for example, network requests require additional classes to be created to accept routing calls).
  • Easy access and rejection of solutions (flexibility).

While some may have doubts about the management of these classes, the following diagram shows their relationship (a block represents a REPO) :

In the “note” arrow in the figure, whether module B needs to introduce its own classified REPO depends on whether it needs to intercept all interface jumps and if so, module B still needs to introduce its own REPO.

The complete scheme and code can be found in Casa’s CTMediator. The design is quite complete and the author has not found any problems.

(2) Block decoupling

There are two simple implementations:

- (void)registerKey:(NSString *)key block:(nonnull id _Nullable (^)(NSDictionary * _Nullable))block {
    if(! key || ! block)return; self.map[key] = block; } - (id)excuteBlockWithKey:(NSString *)key params:(NSDictionary *)params {if(! key)return nil;
    id(^block)(NSDictionary *) = self.map[key];
    if(! block)return nil;
    return block(params);
}
Copy the code

To maintain a global dictionary (Key -> Block), just make sure the closure is registered before the business code runs. It’s easy to think of writing in +load:

@implementation DRegister
+ (void)load {
    [DMediator.share registerKey:@"gotoDAimKey" block:^id _Nullable(NSDictionary * _Nullable params) {
        DAimController *vc = [DAimController new];
        vc.name = params[@"name"];
        vc.callBack = params[@"callBack"];
        [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
        return nil;
    }];
}
@end
Copy the code

The reason for using a separate DRegister class is the same reason why a Target was defined in the previous “Runtime decoupling” section. Also, use a classification to simplify internal calls (this is where the Mushroom Street scheme can be optimized) :

@implementation DMediator (DAim) - (void)gotoDAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {  [self excuteBlockWithKey:@"gotoDAimKey" params:@{@"name":name, @"callBack":callBack}];
}
@end
Copy the code

As you can see, the Block scheme and the Runtime scheme’s REPO architecture are basically the same (see Figure 6), except that the Block has the registration step.

For flexibility, the Demo uses Key -> Block, which makes a lot of code to write inside a Block. If you narrow it down to Key -> UIViewController.class, you can reduce the amount of code registered, but it’s hard to cover all scenarios.

The memory footprint of registration is not a burden, mainly because a large number of registrations can significantly slow startup.

Protocol decoupling

This approach is still registered and stored in a global dictionary (Protocol -> Class).

- (void)registerService:(Protocol *)service class:(Class)cls {
    if(! service || ! cls)return;
    self.map[NSStringFromProtocol(service)] = cls;
}
- (id)getObject:(Protocol *)service {
    if(! service)return nil;
    Class cls = self.map[NSStringFromProtocol(service)];
    id obj = [cls new];
    if ([obj conformsToProtocol:service]) {
        return obj;
    }
    return nil;
}
Copy the code

Define a protocol service:

@protocol CAimService <NSObject>
- (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack;
@end
Copy the code

Implement the protocol with a class and register the protocol:

@implementation CAimServiceProvider
+ (void)load {
    [CMediator.share registerService:@protocol(CAimService) class:self];
}
#pragma mark - <CAimService>
- (void)gotoCAimControllerWithName:(NSString *)name callBack:(void (^)(void))callBack {
    CAimController *vc = [CAimController new];
    vc.name = name;
    vc.callBack = callBack;
    [UIViewController.yb_top.navigationController pushViewController:vc animated:YES];
}
@end
Copy the code

The reason for using a separate ServiceProvider class is the same reason why a Target was defined in the previous Runtime decoupling.

Elegant to use:

id<CAimService> service = [CMediator.share getObject:@protocol(CAimService)];
[service gotoCAimControllerWithName:@"From C" callBack:^{
       NSLog(@"CAim CallBack");
}];
Copy the code

This may seem comfortable without hard coding, but it has a fatal problem — you can’t intercept all routing methods.

This means that this scheme cannot automate dynamic calls.

Ali’s BeeHive is the current best practice. It can write the string of the class to be registered to the Data segment, and then read the registration when the Image is loaded. This action simply puts the registration execution ahead of the +load method, which still slows startup, so this is not intended to speed up the process, but to more elegantly distribute the registration code to specific business parties.

Why Protocol -> Class and Key -> Block need to be registered?

Imagine decoupling means that the caller has only the system native identity. How do you locate the target business? There has to be a mapping. While the Runtime can directly call the target business, the other two methods are to create a mapping table. Of course, Protocol can also not build a mapping table, directly through all the classes, to find the classes that follow this Protocol can also be found, but obviously this is inefficient and insecure.

(4) Weak sign decoupling

See weak symbols for iOS component decoupling.

Componentized summary

For many projects, it is not necessary to implement componentization at the beginning. In order to avoid being stranded when business stability needs to be implemented in the future, it is better to have some forward-looking design at the beginning of the project and reduce the coupling of various business modules as much as possible in the process of coding.

When designing routes, it is important to understand the implementation conditions of the various alternatives as the migration costs of future componentization are minimized. If automated dynamic routing is almost impossible for the project in the future, the Protocol -> Class scheme can be used to remove hard coding; Otherwise, use either the Runtime or Key -> Block schemes, both of which are hard coded to varying degrees but the Runtime does not need to be registered.

After the language

The best way to design a solution is to enumerate all options, identify their strengths and weaknesses, and then make trade-offs based on the business needs. There may be times when the industry’s solutions are not entirely suitable for your project, and some creative improvements are needed.

Instead of saying “it’s the way it should be,” think “Why is it the way it should be?”