有赞 App 如何实现动态域名
发布于 3 年前 作者 taoli 5005 次浏览 来自 分享

一、概述

在移动开发中,网络层面的监控一直是非常有必要的,比如统计网络接口的失败率、重定向网络请求、网络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大体流程,配合上配置中心,我们就可以实现动态域名替换,当然你还可以做以下事情:

  • 统计网络接口的失败率
  • 重定向网络请求
  • 自定义修改request
  • 自定义返回网络请求的结果

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如何实现动态域名的策略介绍,欢迎各位一起讨论。


感兴趣的商家,可点击 → 免费试用有赞店铺~

回到顶部