背景说明
通知相关的页面跳转POCT项目处于后台状态,服务器发推信息到客户端,客户端在通知栏点击消息,进入App并跳转到具体的消息页面。现阶段接收的通知包含:系统消息、个人消息、春雨、七鱼、检测记录等,针对每一种通知都需要进行判断,并处理跳转到对应的VC中。随着项目的不断扩大,需要处理的内容也越来越多,会导致处理消息的函数越来越庞大,针对每一种消息处理的代码重复性极高,代码的可读性大大降低。
本质上,客户端接收服务端推送的数据,根据推送的数据进行页面跳转。如果服务器能够直接告诉客户端需要跳转到具体的某个页面,并显示页面的相应内容,这将大大减少客户端处理消息的函数复杂性。服务端也可以对客户端显示的内容进行统一管理。带着这样的目的,对Deep Link,和实现了vc间用url进行跳转的第三方库进行调研。
Deep Link
Apple的沙盒机制限制APP之间的数据访问,但是深层链接为APP之间的数据共享提供了解决方案。可以在WKWebView、UIWebView、SFSafariViewController中使用http链接启动程序,使APP间的数据传输成为可能。
在iOS9之前,Apple使用Custom URL进行APP间数据共享,在iOS9之后Apple对Deep Link进行优化,使用Unverisal Link替代Custom URL,下面会主要介绍Unverisal Link相较于Custom URL的优势和应该如何在工程中使用Unverisal Link以达到在应用程序已安装的情况下,可以从另一个App启动应用程序,并跳转到具体的VC中。具体可以查看官方文档
Unverisal Link
相较于custom URL schemes,Unverisal Link的优点主要表现在以下几个方面:
- 唯一性。不同于custom URL schemes,Unverisal Link不会被别的APP引用。因为它使用Http,Https为标准与web相关联。
- 安全性。当用户安装项目APP时,iOS会检查APP上传到web服务器上文件,以确保网站允许APP打开url。
- 灵活性。Unverisal Link在APP还没安装的时候也能进行工作。在APP没有安装的时候,会在Safari中打开连线,显示相应内容。
- 简易型。单个URL可以同时在App端和web端使用。
- 私密性。不需要自己的App安装,就能够实现别的应用程序和自己的应用程序进行交流。
通过以下的三个步骤即可实现支持Universal Links。
1. 创建名为apple-app-site-association的文件,包含urls信息的Json格式的内容。以保证应用程序能够处理。
2. 上传apple-app-site-association文件到Https的web服务器中。可以把文件放在根目录下或者放在.well-known的自目录下。
3. 在工程中处理每个Universal links.
第三方库调研
URLNavigator分别是用Swift和OC实现的router相关的第三方库。在接下来的内容中,会对两个库从导入、实现、设计原理的角度来分别介绍这两个库。
URLNavigator
URLNavigator在github上拥有1360+stars,并且已在近期支持Swift4.0版本。(需要加上一句针对这个库具有总结性的内容)
导入URLNavigator
方法1:
pod 'URLNavigator'
方法2:下载源文件,将源文件Sources目录下的两个文件(URLMatcher和URLNavigator)拷贝到工程中即可使用。
使用URLNavigator
将URLNavigator在工程中运用,分两步:
1. 在项目启动时,注册URLNavigator
let navigator = Navigator()
navigator.register("URLNavigator://TextUrl", { (url, value, context) -> UIViewController? in
return TextUrlController(navigator: navigator)
})
2. 在具体需要页面跳转的时候调用URLNavigator的相关方法
navigator.push("URLNavigator://TextUrl")
完成以上两步即可实现通过URLNavigator,使用url进行页面间的跳转。下面会对其中的相关实现原理进行分析。
在整个跳转过程中,我们需要关注的点:
- 如何将viewcontroller与url相关联
- url中的值如何匹配,可以通过url的内容进行页面传值。
- 通过url进行页面跳转。
根据以上三个角度阅读源代码:
viewcontroller与url关联
实际是将url与block相关联,将关联项存储在内存中。因为这个原因,所以需要在didFinishLaunchingWithOptions阶段,对其完成页面的注册操作。
open func register(_ pattern: URLPattern, _ factory: @escaping ViewControllerFactory) {
self.viewControllerFactories[pattern] = factory
}
注册url与viewController相关,需要传入URLPattern和名为ViewControllerFactory的Block,他们的定义如下所示:
public typealias URLPattern = StringURLPattern
虽然是String的类型,但是在传入时需要遵循一定的规则,否则会影响之后url的匹配。具体规则如下:> 在register阶段正确的url格式是
'URLNavigator://TextUrl//'
1. '//'之后到第一个'/'之前,表示的内容类似于viewcontroller的name
2. 通过'/'来分隔每个参数- 使用‘< >’包含数据格式 例如'int'表示数据类型,'id'表示参数名称。现阶段能够接受的数据类型是 'int', '在push阶段关于url的识别
"URLNavigator://TextUrl/1234/sunyicheng"
下面是闭包的内容:
public typealias ViewControllerFactory = (_ url: URLConvertible, _ values: [String: Any], _ context: Any?) -> UIViewController?
url 表示链接地址。
values 表示将url解析之后的数值对。详细的内容会在url值的匹配中说明。
context表示上下文环境。
按照代码所示,注册的时候将pattern和factory的对应关系保存在字典中,因此实现了传入的url和对应viewcontroller的对应关系。
url中的值匹配关于url的匹配我们可能会提出会想。
在传入url和已注册的url是如何进行匹配?怎么从url中拿到对应的值?值的类型包括哪些,是否可以有扩展空间?
open func match(_ url: URLConvertible, from candidates: [URLPattern]) -> URLMatchResult? {
let url = self.normalizeURL(url) 1
let scheme = url.urlValue?.scheme 2
let stringPathComponents = self.stringPathComponents(from :url) 3
for candidate in candidates {
// 判断scheme是否相互匹配
guard scheme == candidate.urlValue?.scheme else { continue } 4
if let result = self.match(stringPathComponents, with: candidate) { 5
return result 6
}
}
return nil
}
1. 获取到标准化的url
2. 提取到scheme
3. 去除掉url中':'前的内容,并且以'/'为分割,获取到一个数组例如> url =URLNavigator://TextUrl/1234/'sunyicheng' 得到的数组是 ["TextUrl","1234","'sunyicheng'"]
4. 判断scheme是否相互匹配,匹配才进行进一步判断
5. 调用self.match方法会通过对3中得到的数组 和 源url中获取到[URLPathComponent]的数组进行比对,获取到result。
URLPathComponent的结构体
enum URLPathComponent {
case plain(String)
case placeholder(type: String?, key: String)
}
6. 关于通过匹配返回是URLMatchResult的结构体。
public struct URLMatchResult {
public let pattern: String
public let values: [String: Any]
}
调用方法实现跳转
上面两部分的内容可以帮助开发人员进行页面注册,同时可以加深对url的理解。下面将介绍在实际使用中,是如何实现页面的跳转。
@discardableResult
public func push(_ url: URLConvertible, context: Any? = nil, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? {
guard let viewController = self.viewController(for: url, context: context) else { return nil }
return self.push(viewController, from: from, animated: animated)
}
@discardableResult
public func push(_ viewController: UIViewController, from: UINavigationControllerType? = nil, animated: Bool = true) -> UIViewController? {
guard (viewController is UINavigationController) == false else { return nil }
guard let navigationController = from ?? UIViewController.topMost?.navigationController else { return nil }
guard self.delegate?.shouldPush(viewController: viewController, from: navigationController) != false else { return nil }
navigationController.pushViewController(viewController, animated: animated) return viewController
}
1. 首先通过传入的url拿到对应的viewcontroller
2. 然后判断该页面是否能够支持跳转
3. 通过navigationController实现跳转
JLRoutesPOCT项目中使用
JLRoutes需要进行如下操作:
1. 导入JLRoutes> pod 'JLRoutes', '~> 2.0.5'
2. 建立桥接头文件 > #import "JLRoutes/JLRoutes.h"
3. Objective—C Bridging Header 关联桥接文件。
4. 在需要使用处> import JLRoutes
JLRoutes注册和使用
let routes = JLRoutes.global()
routes.addRoute("/user/:controller") { (parameters) -> Bool in
// 通过名称转成类名
let namespage = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String
let controllerName = parameters["controller"] as! String
guard let cls: AnyClass = NSClassFromString(namespage+"."+controllerName) else {
print("无法转换controller")
return true
}
guard let clsType = cls as? UIViewController.Type else {
print("无法转换成UIViewController")
return true
}
let nextVC = clsType.init()
let vc = UIViewController.currentViewController()
vc?.navigationController?.pushViewController(nextVC, animated: true)
return true
}
从上述代码来看JLRoutes和URLNavagator的注册是有明显区别:
- JLRoutes将viewcontroller的信息也放在url中。
- JLRoutes将页面间的跳转放在block中进行。开发人员需要手动操作页面跳转。
- JLRoutes如果url的格式是一致的,就不需要再次注册,在一定程度上会减少内存占用,且减少大量的重复代码。具体使用阶段的操作:
let url = "JLRouterTest://user/URLController"
UIApplication.shared.open(URL(string: url)!, options: [:]) { (_) in
}
使用上述代码配合上注册相关的信息,即可完成从当前页面跳转到URLController页面。
JLRoutes实现原理
JLRoutes的核心内容是url内容提取,关于JLRoutes的源码阅读,也将主要从url内容解析的角度出发。
pattern的存储
register都会调用addRoute方法,通过传入的patttern和对应的block组成一个JLRRouteDefinition对象,对象中的实例方法如下所示。再将这个对象保存在数组中。
@interface JLRRouteDefinition : NSObject
/// The URL scheme for which this route applies, or JLRoutesGlobalRoutesScheme if global.
@property (nonatomic, copy, readonly) NSString *scheme;
/// The route pattern.
@property (nonatomic, copy, readonly) NSString *pattern;
/// The priority of this route pattern.
@property (nonatomic, assign, readonly) NSUInteger priority;
/// The handler block to invoke when a match is found.
@property (nonatomic, copy, readonly) BOOL (^handlerBlock)(NSDictionary *parameters);@property (nonatomic, strong) NSArray *patternComponents;
pattern转化成JLRouteDefinition对象
JLRoutes是通过以下的方式将pattern转化成JLRouteDefinition对象。
scheme: 如果不设置scheme,默认schemem名“JLRoutesGlobalRoutesScheme”。设置scheme方便查找,可以对route细分化。不设置,所有的route都放在同一个scheme下,在内容量大的情况下会导致读取的缓慢。
pattern:在register阶段已经进行赋值,不需要别的操作。
priority:优先级,不设置默认0。用途在存入数组中排队顺序,数值越大,在数组中排的位置越靠前。
handlerBlock:在register阶段已经进行赋值,不需要别的操作。
patternComponents:用过pattern进行转化获取到数组。
if ([pattern characterAtIndex:0] == '/') {
pattern = [pattern substringFromIndex:1];
}
self.patternComponents = [pattern componentsSeparatedByString:@"/"];
pattern内容的匹配
通过刚才的注册,知道了在register阶段,会将每一条注册数据存储在JLRouteDefinition对象中。而在实际交互的阶段,JLRoute会将这个内容包裹成JLRRouteRequest对象。
@interface JLRRouteRequest : NSObject
/// The URL being routed.
@property (nonatomic, strong, readonly) NSURL *URL;
/// The URL's path components.
@property (nonatomic, strong, readonly) NSArray *pathComponents;
/// The URL's query parameters.
@property (nonatomic, strong, readonly) NSDictionary *queryParams;```
为了在JLRRouteRequest和JLRouteDefinition匹配成功后有正确的参数,JLRoute设计了一个JLRRouteResponse,包含以下变量:
/// Indicates if the response is a match or not.
@property (nonatomic, assign, readonly, getter=isMatch) BOOL match;
/// The match parameters (or nil for an invalid response).
@property (nonatomic, strong, readonly, nullable) NSDictionary *parameters;
有了上面3个对象的了解,大概能够知道。匹配阶段通过对注册内容进行查找,找到匹配项。并对匹配内容进行拼接,完成匹配pattern的匹配和变量赋值的操作。
BOOL patternContainsWildcard = [self.patternComponents containsObject:@"*"]; **1**
// 如果没有“*”标识,却数量不一致 则直接返回初始化的JLRRouteResponse
if (request.pathComponents.count != self.patternComponents.count && !patternContainsWildcard) { **2**
// definitely not a match, nothing left to do
return [JLRRouteResponse invalidMatchResponse];
}
// bool dictionary的对象 初始化 response 对象
JLRRouteResponse *response = [JLRRouteResponse invalidMatchResponse];
// 字典
NSMutableDictionary *routeParams = [NSMutableDictionary dictionary];
BOOL isMatch = YES;
NSUInteger index = 0;
for (NSString *patternComponent in self.patternComponents) {
NSString *URLComponent = nil;
if ([patternComponent hasPrefix:@":"]) { **3**
// this is a variable, set it in the params
NSString *variableName = [self variableNameForValue:patternComponent];
NSString *variableValue = [self variableValueForValue:URLComponent decodePlusSymbols:decodePlusSymbols];
routeParams[variableName] = variableValue; } else if (![patternComponent
isEqualToString:URLComponent]) **4** {
// break if this is a static component and it isn't a match
isMatch = NO;
break;
}
}
if (isMatch) {
NSMutableDictionary *params = [NSMutableDictionary dictionary];** 5**
[params addEntriesFromDictionary:[JLRParsingUtilities queryParams:request.queryParams decodePlusSymbols:decodePlusSymbols]];
[params addEntriesFromDictionary:routeParams];
[params addEntriesFromDictionary:[self baseMatchParametersForRequest:request]];
response = [JLRRouteResponse validMatchResponseWithParameters:[params copy]];
}
return response;
此处只贴出关于匹配的部分关键代码。关于某些特殊符号(“*”)的使用不在此处扩展。
1. 判断注册的pattern中是否含有“*”;
2. 如果数组大小不一致,且不包含“*”号,则直接返回ismatch=false的JLRRouteResponse对象;
3. 通过注册的patternComponent “:”来作为一个key,拿到对应的repuest中的patternComponent,组成一个键值对;
4. 如果非包含“:”的patternComponent与repuest中的patternComponent不符合,返回不匹配。
5. 创建字典将内容赋值给response。
关于* 号使用的tips:
如果register是的pattern是/a/b/c/*,则在需要匹配的阶段只能是/a/b/c/d/.....而不能是/a/b/d
调用方法实现操作Block
最终实现,只是要将上文匹配得到的params传递给对应的闭包即可。调用方法的整体按以下三个步骤:
1. 将url转换成JLRRouteRequest对象。
2. 将JLRRouteRequest对象和register时创建的JLRouteDefinition对象进行配队,获取到params。(具体过程同pattern匹配的过程)
3. 将params传递给对应的闭包。
相对URLNavigator,JLRoutes的优势:
1. 在注册的阶段,相同类型的parrent不用重复设置,减少内存消耗。
2. 匹配查询阶段,因为可以对scheme进行区分,且加入了优先级的概念,在一定程度上可以减少操作时间。
3. 可以自定义跳转方式。