IOSiOS开发攻城狮的集散地iOS学习笔记

组件化在蘑菇街

2018-04-21  本文已影响137人  CoderHG

零、说点什么吧

周末是一个轻松的日子,于是决定写点什么。名字取得比较大,暂时也没有想到应该怎么命名。刚刚开始仔细的看了一下 MGJRouter 中的代码,所以一边看,也就一边的做点笔记。现在看完了,整理了一下就发出来了。都是以我看代码的顺序来描述的,建议各位大神自行下载代码细细品读!!

也是因为看了一圈的组件化、我更不知道 组件化 到底是什么。尽然这样、那就忘记组件化,看看在代码方面是如何实现组件化的。仅仅是通过 Demo 看大神们的代码到底使用了什么技术点。

蘑菇街的代码,在这里:MGJRouter 下载下来一起看看吧。

本介绍,仅仅是针对 Demo 的代码。

一、Demo 的概要

打开项目 MGJRouterDemo,看到两个控制器 DemoListViewController 与 DemoDetailViewController,结合项目运行起来的效果很好的能看出这两个控制器的实现逻辑。

1.1 List 控制器的列表数据源

当前就控制器的数据源是 titleWithHandlers 与 titles,仔细研究发现 DemoListViewController+registerWithTitle: handler: 被调用于 DemoDetailViewController 中的 +load 方法中。
看完这个逻辑,发现了一个特点:在整个APP 的运行中仅创建了一个 DemoDetailViewController 对象,是一个 target 与 block 相结合的一个优秀技巧。很值得学习与借鉴。

1.2 Detail 控制器的实现

这个有点绕、但是经典就是经典。首先你会发现在 .h 文件中几乎什么都没有,但是能做出不同的显示。这个还是要归功于 +load 中 target 与 block 的巧妙使用。

二、MGJRouter 核心实现

先瞄一眼 MGJRouter 的头文件,清一色的 Class 方法,对于一个工具 Class,我也喜欢这么去设计。首先是因为这样使用特别的方便,其次尽然是工具就尽量不要拖泥带水的搞一些属性在那里。当然、技术是不能一概而论的,但是很多的时候也会发现很多的小伙伴根本不会写工具。关于 MGJRouter 还有一个巧妙的设计, 那就是本质是一个单例,只是没有被暴露。在我看这个代码之前,我还以为这种方式是我的独创,我之前也会这么干。看到一个单例的第一个正常反应是感觉看一下是否重写了类似 -init 这样的方法。可喜可贺,MGJRouter 是一个单纯的单例,没有重写这些方法。

大概看了一下头文件中各个方法的简单注释介绍,看完之后就可以开始分析具体的实现了。具体的入口的都在 DemoDetailViewController 中。

demoBasicUsage 方法

这个方法的原型如下:

- (void)demoBasicUsage
{
    [MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
        [self appendLog:@"匹配到了 url,以下是相关信息"];
        [self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];
    }];
    
    [MGJRouter openURL:@"mgj://foo/bar"];
}

上面的代码中做了两件事:注册与调用。在这里我们要注意的是,在很多的时候我们在看优秀的代码的时候,可以先看方法,然后猜想一下大神的逻辑是什么。看到上面的代码,我的猜想是:通过一个 URL 注册信息,后期会通过这个 URL 找到注册的这些信息。,那么问题来了,到底是通过什么样的规则来注册呢?这些信息注册到哪里了呢?带着这些问题,我们开始打断点、并运行代码。

这里需要说明一点:这里仅仅是一个示例,并不是说注册就应该与具体的 openURL:写到一起。如果说在实际的项目开发中写到一起,那还叫 组件化 么?

2.1 注册实现

当代码运行起来之后,你的断点可能会在这里停留一会了:


pathComponentsFromURL:

我们先来看一看 pathComponentsFromURL: 方法中都做了什么事:

- (NSArray*)pathComponentsFromURL:(NSString*)URL
{

    NSMutableArray *pathComponents = [NSMutableArray array];
    if ([URL rangeOfString:@"://"].location != NSNotFound) {
        NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
        // 如果 URL 包含协议,那么把协议作为第一个元素放进去
        [pathComponents addObject:pathSegments[0]];
        
        // 如果只有协议,那么放一个占位符
        URL = pathSegments.lastObject;
        if (!URL.length) {
            [pathComponents addObject:MGJ_ROUTER_WILDCARD_CHARACTER];
        }
    }

    for (NSString *pathComponent in [[NSURL URLWithString:URL] pathComponents]) {
        if ([pathComponent isEqualToString:@"/"]) continue;
        if ([[pathComponent substringToIndex:1] isEqualToString:@"?"]) break;
        [pathComponents addObject:pathComponent];
    }
    return [pathComponents copy];
}

这个方法,就是对一个字符串做一个解析操作,那么是如何解析的呢?我先把本 Demo 中用到的解析结果,在这里 Copy 一下:
mgj://foo/bar 对应的是:@[@"mgj", @"foo", @"bar"]
mgj://category/家居 对应的结果是:@[@"mgj"]
mgj://search/:query 对应的结果是:@[@"mgj", @"search", @":query"]
mgj:// 对应的结果是:@[@"mgj", @"~"] 是不是有一种通配符的感觉,对、没错。

如果对上面的结果有歧义,那么可以从上依次的学习一下这个方法中用到的方法:

NSString 中的 pathComponentsFromURL: 方法

关于这个方法,我给出的一个示例如下:

// 定义一个字符串
NSString* URL = @"Coder";
NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
NSLog(@"%@", pathSegments);
/** 打印结果:
 (
   Coder
 )
 */

// 修改字符串
URL = @"Coder://";
pathSegments = [URL componentsSeparatedByString:@"://"];
NSLog(@"%@", pathSegments);
/** 打印结果:
 (
   Coder,
   ""
 )
 */

// 修改字符串
URL = @"Coder://HG";
pathSegments = [URL componentsSeparatedByString:@"://"];
NSLog(@"%@", pathSegments);
/** 打印结果:
 (
   Coder,
   HG
 )
 */

综上可知:这是一个NSStringNSArray 之间的转换。具体的转换规则在上面的这个示例中已经完全的介绍了。上面的是 NSString 转换成 NSArray ,那么 NSArray 又是如何转成 NSString 的呢?

NSURL 的 pathComponents 方法

关于这个方法,我给出的示例代码如下:

// 定义一个字符串
NSURL* URL = [NSURL URLWithString:@"CoderHG/iOS/8968"];
// 执行方法
NSArray* pathComponents = [URL pathComponents];
// 打印
NSLog(@"%@", pathComponents);
/** 打印结果:
 (
   CoderHG,
   iOS,
   8968
 )
 */

// 修改字符串 带中文
URL = [NSURL URLWithString:@"CoderHG/iOS/8968/朱鸿"];
pathComponents = [URL pathComponents];
// 打印
NSLog(@"%@", pathComponents);
/** 打印结果:
 nil
 */

综上所述:这个方法是针对字符串中带有 / 而言的,一旦 URL 中带有中文,则返回为 nil。

NSString 的 substring 方法

关于这个方法,我给出的示例代码如下:

// 定义一个字符串
NSString* coder = @"coder";

NSString* substringToIndex = [coder substringToIndex:1];
NSString* substringFromIndex = [coder substringFromIndex:3];

NSLog(@"To = %@, From = %@", substringToIndex, substringFromIndex);
/** 打印结果:
 To = c, From = er
 */

// 直接 crash
NSString* crashString = [coder substringToIndex:10];
NSLog(@"%@", crashString);

终上所述:就是一个字符剪切的方法,但是一定要注意 crash 的情况。

addURLPattern: 方法

上一个方法执行结束之后,就会回到这个方法中来, 这个方法很有意思,具体如下:

- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern
{
    NSArray *pathComponents = [self pathComponentsFromURL:URLPattern];

    NSMutableDictionary* subRoutes = self.routes;
    
    for (NSString* pathComponent in pathComponents) {
        if (![subRoutes objectForKey:pathComponent]) {
            subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
        }
        subRoutes = subRoutes[pathComponent];
    }
    return subRoutes;
}

当从 pathComponentsFromURL: 中返回 pathComponents 数据之后, 对 self.routes 做了一系列的赋值处理。其中 routes 是一个关键的字典,就是用来承载注册信息的,具体是如何承载的呢?
主要是的代码就是在哪个 for 语句中,这是又是一个很奇妙的设计方式。第一次注册 mgj://foo/bar 后的样子是这个样子的:

image.png

这个数据结构,看起来 玄之又玄 ,弄了一个字典中的字典。

总之,就是通过 pathComponents 中的元素,在 self.routes 中布了一个局,里面什么都没有,都是空字典。 还有一个特点是,将最后一个字典做了 return 了。欲知有何用途,请看下面分解。

通过上面的方法之后, 会到这个方法中来:

addURLPattern: andHandler:

这个方法的原型是:

- (void)addURLPattern:(NSString *)URLPattern andHandler:(MGJRouterHandler)handler
{
    NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
    if (handler && subRoutes) {
        subRoutes[@"_"] = [handler copy];
    }
}

通过 addURLPattern: 返回的 subRoutes 是根据 URLPatternself.routes 中注册的最后一个字典。
看了这个方法,终于明白返回最后一个字典的原因就是将 注册的 handler 放到通过 _ 作为 key 值放到这个字典中。

到这里,一个简单的注册流程,就走完了。那么问题来了,这个注册流程到底都干了什么事情呢?总结如下:
通过 URLPattern 按照一定的规则解析成一个数组,然后将数组又按照一定的规则布局于 self.routes 中,最后将注册的 handler 放到通过 _ 作为 key 值放到最后一个字典中。

那么问题又来了:注册之后,又该如何使用呢?我的猜想是这样的:
注册仅仅是通过一定的规则,将注册的信息暂存于 self.routes 中,这些信息将服务于后期的 openURL: 操作。具体是如何服务的呢?请看下回分解。

以上是注册的整个流程,接下来看一下在调用 openURL 中又做了什么事?

2.2 openURL 流程

代码一路执行,最终到这里看到了一个莫名的转换,先来看看:


image.png

字符串的 stringByAddingPercentEscapesUsingEncoding: 方法,就这个方法,我给出的示例代码如下:

NSString* URL = @"鸿哥最帅";
NSLog(@"%@", URL);
// 打印结果: 鸿哥最帅

URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@"%@", URL);
// 打印结果: %E9%B8%BF%E5%93%A5%E6%9C%80%E5%B8%85

URL = [URL stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSLog(@"%@", URL);
// 打印结果:鸿哥最帅

这个方法就是一个字符转义用的,主要是处理中文的情况。

下一步,跳进这个方法中来:


image.png

这里需要注意的是,这个方法传进来的 URL 是经过转义的。在往下,是一个 for 循环。主要就是查看当前传进来的 URL 是否被注册过,如果在 self.routes 任意一个节点中没有找到像匹配的节点,那么就直接返回 nil。在这个 for 循环中的技术点,数组排序的很简单,可以忽略,先来看一下这个方法:

+ (BOOL)checkIfContainsSpecialCharacter:(NSString *)checkedString {
    NSCharacterSet *specialCharactersSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
    return [checkedString rangeOfCharacterFromSet:specialCharactersSet].location != NSNotFound;
}

看方法的命名就知道,这个方法是用来检查 checkedString 是否包含特殊字符串的 ** specialCharacters(/?&.),的确,NSCharacterSet** 这东西挺厉害的,至少面试的时候面试官可能会这样的问:你用过 OC 中的集合么?如果说你回答了字典与数组,那么就等于面试官什么都没有问,如果说你回答了 NSSet,在强调一下 NSCharacterSet,恐怕面试官会爱上你的。关于这个,我给出的示例代码如下:

NSCharacterSet *specialCharactersSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
NSString* text = @"CoderHG";
NSRange rang = [text rangeOfCharacterFromSet:specialCharactersSet];
NSLog(@"%zd, %zd", rang.location, rang.length);
// 打印结果: 9223372036854775807, 0

text = @"Coder?HG";
rang = [text rangeOfCharacterFromSet:specialCharactersSet];
NSLog(@"%zd, %zd", rang.location, rang.length);
// 打印结果: 5, 1

text = @"C/?oder/?HG?";
rang = [text rangeOfCharacterFromSet:specialCharactersSet options:NSBackwardsSearch];
NSLog(@"%zd, %zd", rang.location, rang.length);
// 打印结果: 11, 1

关于 NSURLComponents,我的示例代码如下:

NSString* url = @"http://coder.com?coder=iOS&name=HG";
// Extract Params From Query.
NSArray<NSURLQueryItem *> *queryItems = [[NSURLComponents alloc] initWithURL:[[NSURL alloc] initWithString:url] resolvingAgainstBaseURL:false].queryItems;

NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
for (NSURLQueryItem *item in queryItems) {
    parameters[item.name] = item.value;
}

NSLog(@"%@", parameters);
/** 打印结果:
{
    coder = iOS;
    name = HG;
}
 */

明白了,原来是为了获取 URL 后面的参数的。想当年某些人使用一个一个的截取,然后一个一个的去拼接的。

2.3 注销操作

注销方法 +deregisterURLPattern:
主要是这个方法:

- (void)removeURLPattern:(NSString *)URLPattern
{
    NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:[self pathComponentsFromURL:URLPattern]];
    
    // 只删除该 pattern 的最后一级
    if (pathComponents.count >= 1) {
        // 假如 URLPattern 为 a/b/c, components 就是 @"a.b.c" 正好可以作为 KVC 的 key
        NSString *components = [pathComponents componentsJoinedByString:@"."];
        NSMutableDictionary *route = [self.routes valueForKeyPath:components];
        
        if (route.count >= 1) {
            NSString *lastComponent = [pathComponents lastObject];
            [pathComponents removeLastObject];
            
            // 有可能是根 key,这样就是 self.routes 了
            route = self.routes;
            if (pathComponents.count) {
                NSString *componentsWithoutLast = [pathComponents componentsJoinedByString:@"."];
                route = [self.routes valueForKeyPath:componentsWithoutLast];
            }
            [route removeObjectForKey:lastComponent];
        }
    }
}

在注册分析的过程中已经发现,所谓的注册就是通过 URL 按照一定的规则将消息存于 self.routes 中,那么销毁正好相反。

至此整个流程也就差不多了, 还有没介绍到的,其实都有包括了。

三、想法

看完之后,也发现在 MGJRouter 中也没有什么高深莫测的技术点,但是这是一个很经典的代码。经典于设计思想、经典于使用小技术解决大问题。

建议大家下载源码仔细品读,会让你收获更多!

很多的时候我们不是不懂技术,而是不知道如何是使用我们已知的技术。

谢谢!
上一篇 下一篇

猜你喜欢

热点阅读