Deep Link及相关第三方库调研

背景说明


        通知相关的页面跳转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. 可以自定义跳转方式。


参考资料:

Deferred Deep Linking in iOS

URLNavigator Github文档 

JLRoutes Github文档)

你可能感兴趣的:(Deep Link及相关第三方库调研)