背景說明
????????通知相關的頁面跳轉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. 可以自定義跳轉方式。