Understanding the nature of the components and route iOS

Blog update
2019-6-1-- adds Description: Two -> (c) with respect to whether the internal call to go through the questions URI; increased and revised the description picture.

Foreword

Although iOS and routing component of the topic in the industry had a long talk, but it looks like a lot of people misunderstand, I do not even understand the "component", "module", "routing", "decoupling" means.

Related Bowen also find many, in fact, in addition to those few artists write, with little reference value, besides the famous point of view is not completely correct. Architecture often need to weigh business scenarios, learning cost, development efficiency, so the program can objectively explain the architecture but with some subjective, with modification of some personal characteristics are particularly vulnerable people upside down.

So to keep a clear mind, dialectical attitude towards the problem, the following is the industry's comparative reference value of the article:
iOS application architecture to talk about the components of the scheme
road components of the mushroom Street App in
iOS componentized - routing design ideas analysis
Category characteristics iOS components of the application control
iOS assembly scheme Exploration

This article is the author iOS understanding of the components and routing, and strive to be more objective and concise way to explain the pros and cons of various options, welcome criticism.

DEMO this article

A difference module assembly

2909132-54eeed1d3ec7da9b.png
figure 1
  • "Component" to emphasize that the multiplexing, which is directly dependent on the individual modules or components, infrastructure, which typically does not contain or contains a weak business services belonging to vertical stratification (such as network component requests, download component image).
  • "Modules" to emphasize that package, it refers more functionally independent service module, part of the lateral stratification (such as shopping cart module, individual central module).

So everyone from the implementation of the "components" of the terms of the objectives, called "modular" seems more reasonable.

But the significance of "component" and "module" is the previous definition of the concept of "iOS componentized" has been prejudiced, so only need to understand that "iOS components of" more of a decoupling between doing business modules on the line .

Second, the routing of significance

It must first be clear that not only refers to the routing interface jump, also include data acquisition, etc. Almost all businesses.

(A) simple routing

Internally called the way

Web route to follow, the initial iOS native routing looks like this:

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

Obvious shortcomings: URI string does not characterize the original type iOS system, corresponding to the document to read the modules, a large number of hard-coded.

Code implementation is probably:

+ (void)gotoURI:(NSString *)URI {
    解析 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"]) {
        ...
    }
}

The image of a little:


2909132-1be88fc349a44a92.png
figure 2

After get the URI, and there is always converted to the target parameter ( aim/params) logic before actually calling the native module. Obviously, for internal calls, the URI parsing this step is superfluous (casa said that the issue in a blog).

The method of routing simplified as follows:

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

Very simple to use:

[Mediator gotoDetailWithName:@"xx"];

Thus, the method parameter list will be able to substitute additional documentation, and compiled checks.

URI way how to support external calls

So for external calls, just add URI parsing adapter will solve the problem for them:


2909132-fe09f17173deea06.png
image 3

Where to write routing method

Unified call routing class easy to manage and use, so usually need to define a Mediatorclass. Taking into account the different modules maintainers need to be modified Mediatorto add the routing method, there may be a conflict workflow. Therefore, the use of decorative patterns, add a category for each module is a good practice:

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

Routing method then writes the corresponding module on the corresponding category.

The role of a simple route

Here the package, lifting the direct coupling between the service module, and yet they indirectly coupled (because Routing need to import specific business):


2909132-88a45ce1829ac994.png
Figure 4

However, a simple routing without concern coupling problems, even such a simple process also has the following advantages:

  • A clear list of parameters, easy to use caller.
  • When unlock the coupling between business operations change the interface module may need to change the external call would not change the code.
  • Even business change, routing methods have to change, thanks to inspect compiler can be positioned directly call the position to make changes.

(B) support for dynamic routing of calls

Dynamic invocation, by definition is a change occurs in the call path without updating the App. For example, click A to B trigger to jump interface, at some point they need to click on to jump to A C interface.

To ensure minimum granularity of dynamic invocation, you need complete information about the target business, such as the above said aimand paramsthat the objectives and parameters.

You then need a set of rules, this rule from two sources:

  • From the configuration server.
  • Some local decision logic.

Predictable dynamic invocation

+ (void)gotoDetailWithName:(NSString *)name {
    if (本地防护逻辑判断 DetailController 出现异常) {
        跳转到 DetailOldController
        return;
    }
    DetailController *vc = [DetailController new];
    vc.name = name;
    [... pushViewController:vc animated:YES];
}

Developers need to clearly know "a business" and supports dynamic invocation target dynamic invocation is "a business." In other words, this is a "false" dynamic call, write code logic is dead, but the trigger point is dynamic only.

Automated dynamic invocation

Imagine that the above method + (void)gotoDetailWithName:(NSString *)name;can support automatic dynamic call it?

The answer is no, to achieve true "automation", you must meet one condition: the need for a cut of all routing methods.

The purpose of this section is to intercept and route target parameters, then do dynamic scheduling. AOP mention you might think Hook technology, but for the following two routing methods:

+ (void)gotoDetailWithName:(NSString *)name;
+ (void)pushOldDetail;

You can not find similarities between them, it is difficult to hit.

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

Such a method is defined:

- (void)gotoAim:(NSString *)aim params:(NSDictionary *)params {
    1、动态调用逻辑(通过服务器下发配置判断) 
    2、通过 aim 和 params 动态调用具体业务
}

(Dynamic invocation technology on how to achieve specific business later speaks here not control, just need to know where to be able to dynamically locate these two parameters specific business.)

The routing method which so wrote:

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

Note that @"detail"a good agreement Aim, can be dynamically positioned inside the specific service.

Illustrated as follows:


2909132-b70b8e49ffb26ddd.png
Figure 3 - Evolution - decoupling 1

Of course, external calls can not go through internal call, then it can be done without specific business-aware dynamic positioning local resources:


2909132-605087d75be86282.png
Figure 3 - Evolution - decoupling 2

Thus, unified routing method necessitates a hard-coded entry for this program for automation of dynamic invocation necessarily need to hard-code .

Here, then, using a classification method + (void)gotoDetailWithName:(NSString *)name;will wrap up the hard-coding is a good choice, these hard code to the corresponding service engineers to maintain it.

The classification is CTMediator Casa do so, and that's where the mushroom Street, component-based programs can be optimized place.

(C) questions about whether the internal call to go through the URI

It expressed the view in front do not need to go inside to call the URI (please see Figure 3 and its evolution).

Some friends may feel inside only need to call the compiler can check the package layer of syntactic sugar on the set URI (such as a classification), it becomes the next chart like this:


2909132-44395898136f39aa.png
Figure 3 - Evolution - URI.

Or decoupled manner:


2909132-82ccbe00cd613343.png
Figure 3 - Evolution - URI decoupling

Q1: to be understood that such treatment appears to have a reason to convince people: all calls are routed through a unified URI parsing . So, this analytical method is equivalent to a URL blocker , it seems able to do the above-mentioned dynamic invocation ?

A1: However, this can only support dynamic invocation predictable , that is, you need to clear one specific business, then write some "dead" code that can only make the trigger point is dynamic. Then such unpredictable dynamic calling code written in the "Internal call (syntactic sugar)," which can, by without a unified URI resolution does not matter, so there is no difference in the code set and the rest scattered one.

Q2: Some might say, "Figure 3 - Evolution - URI decoupled" approach, does not need to import specific business code, not to achieve a dynamic call automation of?

A2: However, doing so does not have the equivalent of two interceptors up? Call the decoupling of business, itself has a unified entrance of. So, earlier this unified analytical method interceptor URI does not make sense.

Q3:可以又有人说,他只是想通过统一的 URI 解析拦截入口做一些事情,并不是做自动化的动态调用

A3:这也就是意味着,他不会在这个拦截入口中做对具体业务的完全解耦,且拦截做的这些事情与具体业务无关(若与具体业务有关又回到了 Q1 的问题),那么就是“图3 - 演变 - URI”做法。这种场景似乎能成为一个理由,比如记录所有路由调用却又不涉及具体业务模块?但是内部调用不经过 URI 解析也能做到:


2909132-272e9a70a4a8b60e.png
图3 - 演变 - 不解耦.png

不要说这样会产生硬编码,因为内部调用经过 URL 解析仍然有硬编码。

笔者的观点是:内部调用走 URI 方式是不必要的。如果你非要这么做,笔者说一下缺点:

  • 如果路由不需要和具体业务解耦,内部调用走 URI 方式增加了无意义的硬编码。
  • URI 解析这个规则,三端都需要统一。若不统一,外部调用仍然需要额外的转换适配器,多出了无意义的转换工作,且“WebView 调用”和“外部 App 调用”的规则也要统一;若所有的规则都统一,那么三端就需要大量的沟通成本,且任意一端不能轻易的更改规则,内部调用的路由受到了外部调用的“完全制约”。
  • 字符串不支持很多系统原生类型,URI 解析为 aim / params 时可能需要转换为原生参数(比如字符串转 NSData)的工作,那么内部调用 (需要将 NSData 转换为字符串) -> URI 解析 (再将字符串转换为 NSData) -> aim / pamras,明显转换过程多余了。(casa 在博客中也大概说了一下这个问题)

在软件开发中,“统一”似乎成为了一个强迫症思维,其实应该结合具体业务深入场景,分析真正的意义才能更好的实施架构。

可能这部分表述有些抽象,如有疑问欢迎在文末留言(留言还是在简书好一点,方便笔者回复,其它转发平台的回复不一定能及时看到)。

(四) 路由总结

可以发现笔者用了大篇幅讲了路由,却未提及组件化,那是因为有路由不一定需要组件化。

路由的设计主要是考虑需不需要做全链路的自动化动态调用,列举几个场景:

  • 原生页面出现问题,需要切换到对应的 wap 页面。
  • wap 访问流量过大切换到原生页面降低消耗。

可以发现,真正的全链路动态调用成本是非常高的。

三、组件化的意义

前面对路由的分析提到了使用目标和参数 (aim/params) 动态定位到具体业务的技术点。实际上在 iOS Objective-C 中大概有反射依赖注入两种思路:

  • aim转化为具体的ClassSEL,利用 runtime 运行时调用到具体业务。
  • 对于代码来说,进程空间是共享的,所以维护一个全局的映射表,提前将aim映射到一段代码,调用时执行具体业务。

可以明确的是,这两种方式都已经让Mediator免去了对业务模块的依赖:

2909132-9b98897879f37c79.png
图5

而这些解耦技术,正是 iOS 组件化的核心。

组件化主要目的是为了让各个业务模块独立运行,互不干扰,那么业务模块之间的完全解耦是必然的,同时对于业务模块的拆分也非常考究,更应该追求功能独立而不是最小粒度。

(一) Runtime 解耦

为 Mediator 定义了一个统一入口方法:

/// 此方法就是一个拦截器,可做容错以及动态调度
- (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(@"找不到目标,写容错逻辑");
    return nil;
}

简单写了下代码,原理很简单,可用 Demo 测试。对于内部调用,为每一个模块写一个分类:

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

可以看到这里是给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

为什么要定义分类

定义分类的目的前面也说了,相当于一个语法糖,让调用者轻松使用,让 hard code 交给对应的业务工程师。

为什么要定义 Target “靶子”

  • 避免同一模块路由逻辑散落各地,便于管理。
  • 路由并非只有控制器跳转,某些业务可能无法放代码(比如网络请求就需要额外创建类来接受路由调用)。
  • 便于方案的接入和摒弃(灵活性)。

可能有些人对这些类的管理存在疑虑,下图就表示它们的关系(一个块表示一个 repo):


2909132-39939c62feff480e.png
图6

图中“注意”处箭头,B 模块是否需要引入它自己的分类 repo,取决于是否需要做所有界面跳转的拦截,如果需要那么 B 模块仍然要引入自己的 repo 使用。

完整的方案和代码可以查看 Casa 的 CTMediator,设计得比较完备,笔者没挑出什么毛病。

(二) Block 解耦

下面简单实现了两个方法:

- (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);
}

维护一个全局的字典 (Key -> Block),只需要保证闭包的注册在业务代码跑起来之前,很容易想到在+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

至于为什么要使用一个单独的DRegister类,和前面“Runtime 解耦”为什么要定义一个Target是一个道理。同样的,使用一个分类来简化内部调用(这是蘑菇街方案可以优化的地方):

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

可以看到,Block 方案和 Runtime 方案 repo 架构上可以基本一致(见图6),只是 Block 多了注册这一步。

为了灵活性,Demo 中让 Key -> Block,这就让 Block 里面要写很多代码,如果缩小范围将 Key -> UIViewController.class 可以减少注册的代码量,但这样又难以覆盖所有场景。

注册所产生的内存占用并不是负担,主要是大量的注册可能会明显拖慢启动速度。

(三) Protocol 解耦

这种方式仍然要注册,使用一个全局的字典 (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;
}

定义一个协议服务:

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

用一个类实现协议并且注册协议:

@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

至于为什么要使用一个单独的ServiceProvider类,和前面“Runtime 解耦”为什么要定义一个Target是一个道理。

使用起来很优雅:

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

This program does not need to hard-code looks very comfortable, but it has a fatal problem --- can not intercept all the routing methods.

This also means that the program can not do automated dynamic invocation.

Ali BeeHive is the best practice. Registration section which register can be written to the Data segment category string, and then read out when the Image Register is loaded. This operation is only to register execution put +loadbefore the method will still slow down the startup speed, so I do not see the value of this optimization.

Why Protocol -> Class and Key -> Block need to register?

Imagine decoupling means that the caller identification system that only native, how to locate the target business?
There must be a mapping.
The runtime can directly call the target business, only two other ways to establish a mapping table.
Of course Protocol way may not establish a mapping table, directly through all the classes, to find both follow the protocol class can be found, but obviously this is inefficient and unsafe.

Components of summary

For many projects, not the beginning, of the components need to be implemented, in order to avoid a loss in time in the future stability of business need to be implemented in the beginning of the project some of the best-looking design, while the encoding process should try to reduce the various business coupling module.

When designing the route to minimize the cost of future migration of components, it is understood the various embodiments conditions embodiment is important. If the project is almost impossible to do in the future automation of dynamic routing, use the Protocol -> Class program will be able to remove hard-coded; otherwise, or use the Runtime or Key -> Block program, both of which have varying degrees of hard-coded but does not require registration Runtime .

Afterword

When designing a program, the best way is to exhaust all programs, respectively, to identify strengths and weaknesses, and then based on business needs, trade-offs. Industry solutions may sometimes not entirely suitable for their own projects, this time on the need to do some creative improvements.

Do not always say "it should be this way," and think "Why so."

Guess you like

Origin blog.csdn.net/weixin_33739627/article/details/91018777