JLRoutes是一个非常简单(还是只有两个文件)好用的URL map库。对于功能区分比较明显的app,比如:HubSpot

HubSpot

点击左侧菜单的每个链接都可以到一个单独的功能里面,非常适合拆分,然后用JLRoutes组合。

JLRoutes本质可以理解为:保存一个全局的Map,key是url,value是对应的block。这样在下面的代码中:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
  return [JLRoutes routeURL:url];
}

如果自己被打开:

NSURL *viewUserURL = [NSURL URLWithString:@"myapp://user/view/joeldev"];
[[UIApplication sharedApplication] openURL:viewUserURL];

JLRoutes就可以遍历这个全局的map,通过url来执行对应的block。

像这种全局的变量,首先想到肯定要用Singleton模式吧。不过和普通的单利稍有区别,这个property不是self,而是一个成员变量。注意dispatch_once的使用。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	routeControllersMap = [[NSMutableDictionary alloc] init];
});

但是注意,JLRoutes中的routeControllersMap可不是刚才提到的全局map,而是namespace。这样,JLRoutes找对应url的block的时候,首先要找namespace,默认的全局namespace是:

static NSString *const kJLRoutesGlobalNamespaceKey = @"JLRoutesGlobalNamespace"

而且每个namespace下存放的不是map而是list。这个list里面存放的是:

@interface _JLRoute : NSObject

@property (nonatomic, weak) JLRoutes *parentRoutesController;
@property (nonatomic, strong) NSString *pattern;
@property (nonatomic, strong) BOOL (^block)(NSDictionary *parameters);
@property (nonatomic, assign) NSUInteger priority;
@property (nonatomic, strong) NSArray *patternPathComponents;

- (NSDictionary *)parametersForURL:(NSURL *)URL components:(NSArray *)URLComponents;

@end

_JLRoute插入到routeControllersMap[kJLRoutesGlobalNamespaceKey]这个list中的时候,用到了插入排序的思想,priority高的在前面。这样enum这个_JLRoute List的时候,如果match pattern,就return,自然就解决了「路径匹配优先级」的问题。

##block的copy

在插入新的规则的时候:

- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary *parameters))handlerBlock {
	_JLRoute *route = [[_JLRoute alloc] init];
	route.pattern = routePattern;
	route.priority = priority;
	route.block = [handlerBlock copy];//注意这里的copy
	route.parentRoutesController = self;
	
	if (!route.block) {
		route.block = [^BOOL (NSDictionary *params) {
			return YES;
		} copy];//注意这里的copy
	}
	//此处代码省略
}

为什么要对block进行copy呢?具体原因可以参考正确使用Block避免Cycle Retain和Crash。简单的说一下,由于block可能引入block外部的变量(比如int a吧),但是a是有作用域的,但是传递block的时候,可能在block引用的地方,超过了a的作用域。所以程序虽然能通过编译,但是运行的时候会提示

Segmentation fault: 11

举个例子:

#  import <Foundation/Foundation.h>
typedef long (^BlkSum)(int, int);
NSArray *test;

void setBlock(){
	int base = 100;
	BlkSum blk2 = ^ long (int a, int b) {
		return base + a + b;
	};
	NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>
	// test=@[[blk2 copy]];
	test=@[blk2 ];
}
int main(int argc,const char *argv[]){
	setBlock();
	NSLog(@"%ld",((BlkSum)(test[0]))(1,2));
}
//运行:gcc -ObjC -framework Foundation test.m && ./a.out

相反,如果用copy的话,就没问题,因为copy会把block引用的外部变量一块copy出去。科学一点的解释是,block有三种位置:

  1. NSGlobalBlock:类似函数,位于text段;
  2. NSStackBlock:位于栈内存,函数返回后Block将无效;
  3. NSMallocBlock:位于堆内存。

其中NSGlobalBlock类型的block和普通函数一样,不引用外部变量,不需要copy。第二种NSStackBlock引用外部变量,会收到外部变量的作用域限制,所以需要copy成NSMallocBlock

##使用JLRoutes

在我们的项目中,即使不用JLRoutes来组合sub app,也建议引入,因为:

  1. 本身很小,可以直接拖进来,不用cocoapods。
  2. 很方便扩展自己的app,比如从触屏版的产品页面打开,或者扫描二维码进来,这样都是一个url,可以很方便的处理这些请求。

JLRoutes不适合的场景:(正在发邮件问limboy)

比如我现在是在产品单页上,点击进入店铺跳转到店铺。这时候很显然是

 [self.navController pushViewController:<#(UIViewController *)#> animated:YES];

因为可以很方便的返回。如果这时候用了:

[[UIApplication sharedApplication] openURL:@"myapp://shop/123"];

跳转到对应的shop问题不大,这时候返回就不好处理了。如果当前页面保留了复杂的用户用户输入、改变,在返回问题更大。


参考文档:

  1. 开发新版花瓣iPhone客户端
  2. 正确使用Block避免Cycle Retain和Crash