有贊 App 如何實現動態域名

作者:楊彬 & 李子

部門:社交電商

一、概述

在移動開發中,網絡層面的監控一直是非常有必要的,比如統計網絡接口的失敗率、重定向網絡請求、網絡 Request 增加公共 header 頭、實現動態域名等等。經常會遇到 App 某些域名因爲一些原因在某些地區 DNS 解析異常,因此我們需要將這些有問題的域名進行動態替換,讓用戶可以正常的訪問接口,正常使用我們的 App。

二、具體方案

動態域名其實就是網絡請求的 URL 的 Host 實現動態替換的能力,我們可以從監聽、攔截網絡請求方面入手來達到動態域名替換的目的。有贊目前的 App 大都使用 Weex、Flutter、H5 進行跨平臺開發,在技術選擇上我們儘量做到統一,沉澱出一套通用能力。由於 Weex 網絡請求採用原生橋接的方式,因此對於 Weex 和 Native 的網絡請求,只需要對 Native 端網絡請求做處理,最終採用攔截 Native 網絡請求的方式,Flutter 和 H5 會在後文介紹。

2.1 配置中心結合 Native

有贊配置中心平臺是爲了滿足 App 靈活開關配置類需求開發的統一管理平臺,可以對差異的功能劃分不同的組件,給運營人員和開發人員發佈新配置的功能,結合長連接能力,能夠達到實時獲取配置效果。那我們的思路就是利用配置中心的能力,結合 Native 網絡攔截方法實現 App 動態域名能力,流程如下圖所示:

整個方案存在一個問題,我們可以設想一下,萬一配置中心的域名 DNS 解析異常,我們該如何去做?

2.2 配置中心備用域名

我們從運維那邊申請了配置中心的備用域名,在配置中心請求接口做了降級策略,這樣就可以保證配置中心接口在極端情況下也可以正常訪問,整體方案流程如下圖所示:

三、Native 技術方案

3.1 iOS

在 iOS 開發中. 常用到的網絡請求三方庫有 AFNetworking 和 Alamofire,它們的底層是基於蘋果提供的 NSURLConnection、NSURLSession 網絡庫接口進行了封裝,那麼想要攔截到網絡請求,就需要使用官方提供的處理 URL 數據的類 NSURLProtocol 。

3.1.1 NSURLProtocol

An abstract class that handles the loading of protocol-specific URL data. 蘋果官方文檔這樣介紹 NSURLProtocol,一個處理加載協議特定 URL 數據的抽象類,看起來像是一個協議,其實這是一個類,支持創建該子類來支持自定義網絡請求,先看看 URL Loading System 架構圖:

在每一個 HTTP 請求開始,URL 會加載系統創建的 NSURLProtocol 對象處理對應的 URL 請求,根據文檔我們只需要創建一個子類繼承自 NSURLProtocol,通過 registerClass:方法註冊我們自定義的網絡協議類,來實現網絡攔截的目的。那麼,我們需要解決的問題就是使用自定義的 NSURLProtocol 來處理 App 所有的網絡請求,蘋果官方文檔中 CustomHTTPProtocol 介紹瞭如何自定義 NSURLPtotocol 來實現網絡攔截。回到之前的問題,我們如何使用 NSURLProtocol 攔截 Http 請求?只需要判斷對於那些請求 request 需要處理;對於需要處理的 request 做出哪些處理;再將響應請求的數據傳遞給調用者。

這裏我們將基於 NSURLSession 爲例來說明如何進行自定義網絡攔截,達到動態域名替換的目的。

3.1.2 返回需要控制的請求

在 NSRULPtotocol,要知道哪些網絡請求是需要被攔截,通過重寫 canInitWithRequest:比如我們可以攔截全部的 http/https 請求。

3.1.3 自定義 request

每一次請求都會有一個 NSURLRequest 實例,上述方法會拿到所有的請求對象,我們就可以根據對應的請求選擇是否處理該對象請求經過 + canInitWithRequest:方法過濾之後,我們得到了所有要處理的請求,接下來需要對請求進行一定的操作。這裏對請求不做任何修改,直接返回。

3.1.4 對處理過的 request 進行標記

通過這兩個方法,就已經能夠攔截住 iOS 的網絡請求了,但是我們需要對每個處理過的 request 進行標記,判斷如果這個 request 已經處理過,那麼我們就不再進行處理。

我們在 + canInitWithRequest: 中判斷是否有處理過的標誌,來進行攔截。

 3.1.5 開始網絡請求

當我們處理了這個請求後,就需要我們手動去發送這個網絡請求,即重寫 starLoading 方法。

這裏我簡化了代碼,在這個方法裏面根據配置中心下發的 replaceHost 域名可以對 targetHost 域名進行動態替換,也可以將 request 做一些自定的處理,比如增加統一的 header 頭等處理。

3.1.6 停止相應的請求

取消網絡請求的 task,將 task 置爲 nil。

3.1.7 實現 NSURLSessionTaskDelegate

然後將自定義的 protocol 註冊到 NSURLProtocol 中即可這樣就可以攔截 UIWebView 和自定義的網絡請求了,如果要攔截 AFNetworking、Alamofire 等三方庫請求,我們需要將 NSURLSessionConfiguration 類,用 Method Swizzle 將 protocolClasses 替換成自己定義的 CustomeURLProtocol。以上就是自定義 NSURLProtocol 大體流程,配合上配置中心,我們就可以實現動態域名替換,當然你還可以做以下事情:

3.2 Android

3.2.1 對 OKHttp 插樁

Android 端 App 基本使用 OKHttp 和 HttpUrlConnection/HttpsUrlConnection 兩種框架來進行網絡請求,因此只需要對以上兩種網絡請求做插樁來達到網絡請求攔截的效果。方案圖如下:

3.2.2 插樁實現

拿到 OkHttpClient 之後可以設置很多屬性如:

**  3.2.3 UrlConnection 插樁**

通過以下方式插樁可以拿到 URLConnection 的入參 URL,因此也可以動態控制域名。

3.2.4 使用

a、build.gradle 添加引用** b、app/build.gradle 添加代碼掃描配置** **c、Application 中主動拉取動態域名配置****d、擴展能力**

四、跨平臺

4.1 Flutter

目前我們使用的 Flutter 網絡請求分爲:圖片下載請求和普通數據網絡請求,數據網絡請求我們採用插件方式,封裝了 Native 的網絡請求庫,不需要做單獨的處理,圖片加載使用的 Flutter 自己的渲染引擎,下面來介紹下 Flutter 圖片下載如何去做動態域名。

4.1.1 Flutter 渲染流程圖

Layer Tree:這個是 dart runtime 輸出的一個樹狀數據結構,樹上的每一個葉子節點,代表了一個界面元素(Button,Image 等等)。

Skia:這個是谷歌的一個跨平臺渲染框架,從目前 iOS 和 Anrdroid 來看,SKIA 底層最終都是調用 OpenGL 繪製。Vulkan 支持還不太好,Metal 還不支持。

Shell:這裏的 Shell 特指平臺特性(Platform)的那一部分,包含 IOS 和 Android 平臺相關的實現,包括 EAGLContext 管理、上屏的操作以及後面將會重點介紹的外接紋理實現等等。

從圖中可以看出,當 Runtime 完成 Layout 輸出一個 Layertree 以後,在管線中會遍歷 Layertree 的每一個葉子節點,每一個葉子節點最終會調用 Skia 引擎完成界面元素的繪製,在遍歷完成後,在調用 glPresentRenderBuffer(IOS)或者 glSwapBuffer(Android) 按完成上屏操作。

基於這個基本原理,Flutter 在 Nativ e 和 Flutter Engine 上實現了 UI 的隔離,書寫 UI 代碼時不用再關心平臺實現從而實現了跨平臺。

4.1.2 外接紋理

上圖是前文提到的 LayerTree 的一個簡單架構圖,每一個葉子節點代表了 dart 代碼排版的一個控件,可以看到最後有一個 TextureLayer 節點,這個節點對應的是 Flutter 裏的 Texture 控件(這裏的 Texture 和 GPU 的 Texture 不一樣,這個是 Flutter 的控件)。當在 Flutter 裏創建出一個 Texture 控件時,代表的是在這個控件上顯示的數據,需要由 Native 提供。以 iOS 端爲例,TexttureLayer 節點的最終繪製整體過程可以分爲三步:調用 external_texture copyPixelBuffer,獲取 CVPixelBuffer CVOpenGLESTextureCacheCreateTextureFromImage 創建 OpenGL 的 Texture 將 OpenGL Texture 封裝成 S KImage,調用 Skia 的 DrawImage 完成繪製。

通過外接紋理的方式,實際上 Flutter 和 Native 傳輸的數據載體就是 PixelBuffer,Native 端的數據源將數據寫入 PixelBuffer,Flutter 拿到 PixelBuffer 以後轉成 OpenGLES Texture,交由 Skia 繪製。

4.1.3 ShareGroup

App 中使用 OpenGL 來渲染都會有兩個線程,一個負責加載資源,一個負責渲染的方式。這兩個線程會共用一個 EAGLContext。Flutter 在 EAGLContext 的處理上採用兩個線程彼此通過 ShareGroup 來共享紋理數據。在 Flutter 創建的 Context 時,將它們的 ShareGroup 透出。在 Native 通過 OpenGL 渲染的模塊創建 Context 時,在 Native 側保存好這個 ShareGroup ,這樣當 Native 創建 Context 時,都會使用這個 ShareGroup 進行創建,這樣就實現了 Native 和 Flutter 之間的紋理共享。

4.1.4 原生處理

總結整個方案,通過外接紋理的方式,Flutter 就可以很容易繪製出大型圖片加載庫 SDWebImage 等,本質是共用了一套緩存,將圖片網絡加載的工作轉移到了 Native 端,從而實現了圖片 URL 動態域名的需求,至於網絡請求,Flutter 完全可以使用網絡庫插件,本質也是調用 Native 的網絡庫。

4.2 H5

上面介紹的 Native 方法對於 H5 請求來說並不能做到攔截網絡,比如 iOS 基於 NSURLProtocol 實現自定義攔截網絡請求,並不能攔截 WKWebView 的網絡請求,市面上也有很多方法可以攔截 WKWebVIew 網絡請求。我們這邊的方案是讓前端來對域名進行動態配置,如果檢測到域名訪問異常,就激活配置中心,替換新的域名讓商家能夠正常的訪問,整體的業務流程設計如下圖所示:

五、總結與展望

未來將攔截網絡請求的效果達到最大化,可以監控網絡 Request 和 Response;可以做到統計接口失敗率;可以做到 App 內部統計一些接口訪問量;App 內所有特定請求增加公共的 header;可以返回自定義的 Response 等等,簡單來講就是網絡數據的收發,都可以監控並做自定義操作。

本文章講述了 Native、Flutter、H5 端實現動態域名的技術方案。iOS 端採用繼承 NSURLProtocol 來實現對網絡攔截、Android 端採用插樁來達到網絡請求攔截,最終都配合配置中心動態下發域名來達到動態域名的目的。Flutter 端則採用外接紋理的方式,Native 和 Flutter 通過 PixelBuffer 作爲載體來達到共用緩存的目的,通過圖片加載插件,將下載圖片的操作橋接到 Native 端,最終也可以實現動態域名的目的。H5 則採用類似配置中心的下發配置統一收口網絡請求策略來達到動態域名的目的,這三種方案結合依賴可以覆蓋有贊 App 的所有網絡請求場景。以上是有贊 App 如何實現動態域名的策略介紹,歡迎各位一起討論。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EkWLdd_OXtSnSRqX0x5TTQ