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文檔)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容