58同城 iOS客户端组件化演变历程---公司也正朝着这个方向努力_58最早界面-程序员宅基地

技术标签: app  

导语: 架构的演进是为业务不断发展服务的,架构不能脱离业务,这是最基本的出发点。58 同城 iOS 客户端随着业务量和用户量的持续增长,架构也是不断受到挑战,采用什么样的架构去适应这些变化,对技术人员来说也是一大考验。58 App 的架构先后经历了纯 Native、引入 Hybrid 框架、底层服务组件化、业务线组件化,即整个 App 组件化的四个阶段。

第一版 App 架构

早在 2010 年 58 同城诞生第一版 iOS 客户端,按照传统的 MVC 模式去设计,纯 Native 页面,这时的功能较为简单,架构也是如此,从上至下分为 UI 展现、业务逻辑、数据访问三层,如图 1 所示。和同期其他公司一样,App 的出发点是为了快速抢占市场,采取“短平快”的方式开发。纯 Native 的 App 在早期业务量不是太大的情况下,能满足业务的需求。

图 1 App 早期架构

第二版架构

Hybrid 框架需求

由于苹果审核周期较长,业务需求不断增大,有些业务如果用 Native 进行开发,工作量大投入人员较多,也不能动态更新,如 58 App 的大类、列表、详情页面。这种情况下,用 HTML5 是比较流行的解决方式,由此产生了第二版架构,如图 2 所示,在 UI 层添加了 HTML5 页面及 Hybrid 交互框架。

图 2 带 Hybrid 的架构

当时 58 App 设计时用于加载 HTML5 的组件是 UIWebView,也只能使用这个(彼时还没有 WKWebView),但实现起来有几个问题是需要解决的:

  1. 怎么解决 Hybrid 中 Web 和 Native 交互问题,如用户点击一个类别,能调起 Native 的一些方法去执行相关页面跳转或写日志。
  2. 如何提高 HTML5 页面的加载速度,HTML5 页面加载时要下载一些 JavaScript、CSS 及图片资源,是比较耗时的。

设置缓存

为了方便描述,本文先介绍如何提高 HTML5 页面加载速度的问题。

对于一些访问比较频繁的页面,如大类列表详情,我们早期采用的都是 HTML5 页面。要加速这些页面的渲染,就要想办法提升资源的加载。那么如何实现呢?首先想到的是使用缓存,我们可以把这些页面的资源内置到 App 中随版本发布。

由于 UIWebView 在发请求的时候都会走 NSURLCache 的这个方法:

- (nullable NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest *)request;

我们可以从 NSURLCache 派生出子类 WBHybrid 
Component,复写 cachedResponseForRequest:方法,在这之中加载 App 的内置资源,具体加载策略可见图 3。

图 3 缓存处理流程

其中,H5ViewController 为 HTML5 载体页面,WBCacheHandler 为专门处理内置资源类,用于加载、查找、下载、保存内置资源。URL 的 query 中设置版本号参数 cachevers 作为资源缓存的标识,其值为数字类型,假设 cachev1,其与内置资源中的版本号如为 cachev2 进行对比,若 cachev2>= cachev1,表示内置资源中是最新数据,直接给请求返回数据;否则下载新的内置资源,同时根据 cachev1- cachev2 的差值进行判断,如设置一个临界值 x,若差值大于 x,则说明内置资源为旧,给请求返回 nil,否则返回内置数据,让请求先用缓存数据,下次启动时再用新数据。

内置数据采用的是一个 bundle 包,如图 4 所示,CacheResources.bundle 为内置包名,里面包含了一个索引文件和若干个内置数据文件,其中索引文件中每项 item 格式为 key、版本号和文件名。

图 4 缓存包结构

想要使用自定义的 NSURLCache,必须在 App 启动时初始化 WBHybridComponent,并进行设置,替换默认的 Cache,注意:这个设置必须在所有请求之前进行,否则设置失效,而是采用默认的 NSURLCache 实例,我们曾经踩过这个坑。

// URLCache初始化
WBHybridComponent *hybridComp = [[WBHybridComponent alloc] initWithMemoryCapacity:MEM_CAPACITY diskCapacity:DISK_CAPACITY diskPatch:nil];
[NSURLCache setSharedURLCache:hybridComp]

基于 AJAX 的 Hybrid 框架

对于前面所列的第一个问题,我们是要设计一个 Web/Native 的 Hybrid 框架。交互主要包括两部分内容,一是 Native 调用 Web,这个比较简单,直接通过 UIWebView 的 stringByEvaluatingJavaScriptFromString:执行一段 JS 脚本,并返回执行结果,本文主要分享 Web 调 Native 的方法。

对于 Web 调 Native 交互的方式,我们采用异步 AJAX 进行,创建一个 XMLHttpRequest 对象,执行 send()进行异步请求,Native 拦截。

xmlhttp.onreadystatechange = function() {
    
    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
      // 处理返回数据
    }
  };
  xmlhttp.open("GET", "nativechannel://?paras=...”, true);
  xmlhttp.send();

由于 XMLHttpRequest 的方式是进行页面局部刷新,并不能被 UIWebViewDelegate 代理的 - (BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType 方法拦截到,设计到这里又出现了新问题,如何让 Native 能拦截到 AJAX 请求呢?

经过一番调研,我们找到了用于缓存的 NSURLCache,对于 UIWebView 中的所有请求(包括 AJAX 请求)都会走 NSURLCache。因此,我们决定采用复用缓存中的 WBHybridComponent 拦截 AJAX 请求,具体 Web 调 Native 的交互设计如图 5 所示。

图 5 Hybrid 框架处理流程图

其中,H5ViewController 为 HTML5 的载体页,WBWebView 是 UIWebView 派生类。WBWebView 中通过 AJAX 发出的异步请求,在 WBHybridComponent 中被拦截,再通过 WBHybridJSHandler 中的 dic 表找到对应的 WBActionAnalysis 对象,然后在 WBActionAnalysis 中分析异步请求传过来的协议,取出 action 字段,再根据 action 值找到 delegate 即 H5ViewController 中对应的方法。

AJAX 发出的请求我们约定为:nativechannel://?paras=<json 协议>,WBHybridComponent 在拦截时判断 URL 中是否为 nativechannel 的协议头,如果是则为 Web 调起 Native 操作,需要进行后续 Native 处理;否则放过进行其他处理。<json 协议> 的简化格式如图 6 所示,这是二手车大类页点击二手车类目 Web 调 Native 时 AJAX 传过来的协议。

图 6 Web 调 Native 传输协议

改进的 Hybrid 框架

前面我们设计的 Hybrid 框架,通过创建 XMLHttpRequest 对象发送 AJAX 请求的方式能达到 Web 调 Native 的目的,也可以满足业务上的需求,在一段内发挥了重要作用。但随着时间的推移,这个 Hybrid 框架暴露出了一些问题,如下所示。

  1. 我们发现 App 中存在大量的内存泄露,经查罪魁祸首竟是 UIWebView。调研发现 UIWebView 中执行 XMLHttpRequest 异步请求时会有内存泄露,网上也有人探讨过这个问题,参考博文:http://blog.techno-barje.fr//post/2010/10/04/UIWebView-secrets-part1-memory-leaks-on-xmlhttprequest/

  2. Hybrid 交互方式与缓存都使用 NSURLCache 的派生类 WBHybridComponent 执行拦截,其初衷也是用于缓存。我们的 Hybrid 框架将两者耦合在一起,这对于后期的开发和性能优化工作会带来不少隐患。

  3. 我们在 Hybrid 交互的时候维护了一个

//创建iFrame元素
variFrame= document.createElement("iframe");
//设置iFrame加载的页面链接
iFrame.src= "nativechannel://?paras=<json协议>";
//向dom tree中添加iFrame元素,以触发请求
document.body.AppendChild(iFrame);
//请求触发后,移除iFrame
iFrame.parentNode.removeChild(iFrame);
iFrame = null;</json协议>

由于 iframe 方式是整个页面刷新,所以能执行 UIWebViewDelegate 的回调方法 - (BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType。我们可以直接在这个方法中拦截 Web 的调起,iframe 方式处理流程如图 7 所示。

图 7 iframe 的 Hybrid 交互方式

通过 iframe 的方式,我们 App 极大地简化了 Hybrid 框架的交互流程,同时也解决了内存泄露、与缓存功能耦合、消耗不必要的内存空间等问题。

第三个版本架构

随着业务的进行,一些新的技术需求来了,比如有些基础模块可以从 App 中独立出来进行多应用间的复用;需要为转转 App 提供一个日志 SDK;为违章查询等 App 提供登录的 Passport SDK;为其他 App 提供一个可定制化的分享组件等等。

App 拆分组件

这时我们迫切地需要在工程代码层面对原来的 App 进行拆分、组件化开发,如图 8 所示。

图 8 第三版架构

我们将 App 拆分成三层,从下至上依次是基础服务层、基础业务层、主业务层:

  1. 基础服务层里的组件是与业务无关的,供上层调用,每个组件为一个工程,如网络、数据库、日志等。这里面有些组件是整个公司的其他 App 也在使用,如乐高日志,我们对外提供一个 SDK,与文档一起放在代码服务器上供其他团队使用。并将 58 App 中用到的所有第三方库都集中起来存放到一个专门的工程中,也便于更新维护。
  2. 基础业务层里的组件是与业务相关的,供主业务层使用,每个组件是一个工程,如登录、分享、推送、IM 等,我们把 Hybrid 框架也归在业务层。其中登录组件我们做成 Passport SDK,供公司其他 App 集成调用。
  3. 主业务包括 App 首页、个人中心、各业务线业务和第三方接入业务,业务线业务主要包括发布、大类、列表、详情。

集成管理组件

工程拆分完后,就是工程集成了,我们用 Cocoapods 将各工程集成到一起编译运行和打包,对于每一个工程配置好.podspec 文件。在配置 podfile 文件时,当用于本地开发时,我们通过 path 的方式进行集成,不用临时下载工程代码,如下所示。

pod proj, :path => '~/58_ios_libs/proj’

在进行 Jenkins 打包时,我们通过 Git 方式将代码实时下载:

pod proj, :git => '[email protected]:58_ios_team/proj.git',:branch => '1.0.0'

GitLab 服务进行代码管理

我们在局域网搭建一个 GitLab 服务,用于管理所有工程代码,并设置好开发组及相应的权限。通过 GitLab 还可以实现提交代码审核、代码合并请求及工程分支保护。

第四版架构

随着 58 App 用户量的剧增,各业务线业务迅速增长,对 58 App 又提出了新需求,如为加快大类列表详情页面的渲染速度,需要将原来这些 HTML5 页面 Native 化;再如各业务线要定制列表详情和筛选样式。面对如此众多需求,显然原来的架构已经满足不了,那就需要我们进一步改进客户端架构,将主业务层进一步拆分。

主业务层拆分

我们对主业务层进行一个拆分,拆分后的整体架构如图 9 所示,其中每一个模块为一个工程,也是一个组件。

图 9 第四版架构

我们将首页、发布、发现、消息中心、个人中心及第三方业务等都从主业务层拆分出来成为独立工程。同样将房产、二手、二手车、黄页、招聘等业务线的代码从原工程里面剥离出来,每个业务线独立一工程,将列表和详情分别剥离出来并进行 Native 化,为上层业务线定制功能提供接口。

业务线拆分的时候我们遵循以下几个原则:

  1. 各业务线之间不能有依赖关系,因为我们的业务线在开发的整个过程中都是独立运行的,不会含有其他业务线代码。
  2. 非业务线工程不能对各业务线有依赖关系,即所有业务线都不集成进 App 也要能正常编译。
  3. 各业务线对非业务线工程可以保留必要的依赖,如业务线对列表组件的依赖。

在拆分过程中我们也采取了一些策略,如在拆分招聘业务线时,先把招聘业务线从集成后的工程中删除,进行编译,会出现各种编译错误,说明是有工程对招聘业务线代码进行依赖。如何解决这些依赖关系呢?我们主要是解决相互依赖关系,招聘业务线对非业务线工程肯定是有一定的依赖关系,这个先保留,我们要解决的是其他组件甚至可能是其他业务线对招聘的依赖。我们总结了下,主要用了以下几种方式:

  1. 将依赖的文件或方法下沉,如有些文件并不是招聘业务线专用的,可以从招聘中下沉到其他工程,同样有些方法也可以下沉。
  2. Runtime,这种方式比较普遍,但也不需要所有地方都用,毕竟其维护成本还是比较高的。
  3. Category 方式,如个人中心组件中方法 funA 要调用招聘组件中的方法 funB,但 funB 的实现是要依赖招聘内部代码,这种情况下个人中心是依赖招聘业务线的,理论上招聘可以依赖个人中心,而不应该反过来依赖。解决办法是可以在个人中心添加一个类,如 ClassA,里面添加方法 funB,但实现为空,如果带返回值可以返回一个默认值,再在招聘中添加一个 ClassA 的类别 ClassA+XX,将原来招聘中的方法 funB 放入 ClassA+XX,这样如果招聘集成进来,就会执行 ClassA+XX 中的 funB 方法,否则执行个人中心自己的 funB 方法。

跳转总线

总线包括 UI 总线和服务总线,前者主要处理组件间页面间的跳转,尤其是在主业务层,UI 总线用得比较频繁。服务总线主要处理组件间的服务调用,这里主要讲跳转总线。在主业务层,被封装成的各个组件需要通过 UI 总线进行页面跳转,我们设计了一个总分发中心和子分发中心的模式进行处理,如图 10 所示。

图 10 UI 跳转总线

主业务层每个组件内都有一个子分发中心,它的处理逻辑由各组件内来进行,但必须实现一些共同的接口,且这个子分发中心需要进行注册。当组件内需要进行 UI 跳转时,调用总分发中心,将跳转协议传入总分发中心,总分发中心根据协议中组件标识(如业务线标识)找到对应的目标组件子分发中心,将跳转协议透传到对应的子分发中心。接下来的跳转由子分发中心去完成。这样的方式极大降低了组件间的耦合度。

UI 总线中的跳转协议我们原来用 JSON 形式,后来统一调整为 URL 的方式,将 m 调起、浏览器调起、push 调起、外部 App 调起和 App 内跳转统一处理。

新统跳协议 URL 格式如下:

wbmain://jump/job/list? ABMark=markID&params=

其中,wbmain 为 58 App 的 scheme,job 为招聘业务线标识,list 为到列表页,ABMark 为 AB 测跳转用的标识 ID,后面会细讲,params 为传过来的一些参数,如是否需要动画,push 还 present 方式入栈等。为了兼容老协议,我们将原来协议中的一部分内容直接透传到 params 中。

AB 测跳转

对于指定跳转 URL,有时跳转的目标页面是不固定的,如我们的发布页面,有 HTML5 和 React Native 两套页面,如果 React Native 页面出了问题,可以将 URL 做修改跳到 HTML5 页面。具体方案是服务器下发一个路由表,每个表项有一个 ID 和对应新的跳转 URL,每个表项设置有过期时间。跳转的 URL 可以带有 AB 测跳转用的标识 ID,即 markID。如果有这个标识,跳转时就去与路由表中的表项匹配,如果命中就改用路由表中的 URL 跳转,否则还用原来的 URL 执行跳转,大概流程如图 11 所示。

图 11 AB 测跳转流程图

静态库方案

为了提高整个 App 的编译速度,我们为每个工程配置一个对应的库工程,里面预先由源码工程编译出来一个对应的静态库,如图 12 所示。

图12 源码库与静态库对应关系

开发人员可以将权限内的源码和静态下载到本地,按需进行源码和库混合集成,如对于招聘业务线 RD,我们只需关心招聘业务线源码工程,不需要其他业务线的源码或静态库,剩下的工程可以选择全部用静态库进行集成。

对于 Jenkins 打包平台,我们也可以根据需求适当在源码和静态库之间做选择。对于一些特殊的工程,如第三方库工程 ThirdComponent,一般也不会变,可以直接接入对应的静态库工程 ThirdComponentLib。

总结

业务在不断变化,需求持续增多,技术也在不断地更新,我们的架构也需要不断进行调整和升级,架构的演进是一项长期的任务。

作者简介: 曾庆隆,58 同城 iOS 客户端架构师。专注于 App 移动架构研发,主要负责 58 同城 App 的架构以及性能优化,主导了 App 组件化相关的架构升级优化。

文章转来学习参考使用,没有任何商业用途,如有侵权,请联系本篇博客作者。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chichengjunma/article/details/70256951

智能推荐

HDU 3746 Cyclic Nacklace-程序员宅基地

文章浏览阅读45次。题意:给你几组字符串,每组添加多少个字符能够构成循环.题解:最小循环节,注意讨论的三种情况,题上刚好给了这三种情况(要是不给我这弱鸡又考虑不全了) 1 #include <iostream> 2 #include <cstring> 3 #include <string> 4 #include <algorithm&g...

深度学习基础--正则化与norm--L2归一化、L2正则化、L2范数的区别_norm_l2-程序员宅基地

文章浏览阅读2.9k次。L2归一化、L2正则化、L2范数的区别  1)归一化是将数据变到一定的区间内,故是x除以||x||_2。  2)正则化是在优化时所使用的概念,称为正则化方法,而不是指某种具体的数据运算,概念比归一化要高一层。  3)L2范数指的是公式意义上的||x||_2。..._norm_l2

【杂七杂八】excel中根据RTL信号位宽生成拼接取位_rtl语法 位宽拼接-程序员宅基地

文章浏览阅读398次。前言作为一个不务正业的芯片前端,总会遇到掉奇奇怪怪的需求,就比如题目这个啊,我写完之后就觉得非常的拗口。那么具体的需要是啥呢?就是比如说有了下面这个excel表:信号名 width sig0 3 sig1 10 sig2 14 sig3 20 sig4 8 要直接做一列生成前面几个信号在整体信号中的取位信息,简单来说就是这样:信号名 width local sig0 3 [2:0] sig1 10_rtl语法 位宽拼接

go WaitGroup的坑-程序员宅基地

文章浏览阅读2.3k次。go WaitGroup的使用请参考笔者的另外一篇博客go WaitGroup的使用示例这里重点讲一下WaitGroup的注意点,以免被坑示例代码如下:package mainimport ( "log" "sync")func main() { wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(..._waitgroup的坑

升级libc.so.6和libstdc++.so.6方法_deepin 升级libstdc++6-程序员宅基地

文章浏览阅读8.2k次。解决"libc.so.6: version `GLIBC_2.14' not found"问题转载自https://www.cnblogs.com/Mrhuangrui/p/7766554.html试图运行程序,提示"libc.so.6: version `GLIBC_2.14' not found",原因是系统的glibc版本太低,软件编译时使用了较高版本的glibc引起的:问题Ce..._deepin 升级libstdc++6

快速学习STL-程序员宅基地

文章浏览阅读386次。STL概述STL的一个重要特点是数据结构和算法的分离。尽管这是个简单的概念,但这种分离确实使得STL变得非常通用。例如,由于STL的sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包括链表,容器和数组。要点STL算法作为模板函数提供。为了和其他组件相区别,在本书中STL算法以后接一对圆括弧的方式表示,例如sort()。STL另一个重要特性是它不是面向_学习stl

随便推点

Java核心技术·卷I(原书第12版)_java核心技术第十二版pdf-程序员宅基地

文章浏览阅读1w次。他是《Java核心技术》两卷本的作者,也是《重要的,第二版》(Addison-Wesley,2018)的Core Java SE 9和《重要的,第二版》(Addison-Wesley,2017)的作者。读者应在充分理解Java语言和Java类库的基础上,灵活应用Java提供的高级特性,包括面向对象编程、反射和代理、接口和内部类、异常处理、泛型编程、集合框架、事件监听器模型、图形用户界面设计和并发。☉第四章介绍了面向对象的两大基石——封装的重要概念,以及Java语言实现封装的机制——类和方法;_java核心技术第十二版pdf

【Gradle】Gradle配置全局阿里云镜像仓库_android studio gradle全局使用阿里云镜像 详细步骤-程序员宅基地

文章浏览阅读2.5k次。一、参考资料Gradle配置阿里云仓库_梁海江的博客-程序员宅基地Gradle的配置操作以及配置阿里云镜像和整合本地Maven仓库 - 简书_android studio gradle全局使用阿里云镜像 详细步骤

Maven手动导入jar包到自己仓库_maven手动导入jar包到仓库-程序员宅基地

文章浏览阅读1k次。1、保证settings.xml文件中的 localRepository 是自己需要引入的仓库。_maven手动导入jar包到仓库

(一)数据科学_聚类 样本外-程序员宅基地

文章浏览阅读419次。数据科学技术1 数据科学概念2 数理统计技术2.1 描述性统计分析2.2 统计推断与统计建模1 数据科学概念数据科学是一个发现、解释数据中的模式并用于解决问题的过程。数据科学可以从数据中获取知识,为行动提出建议的方法、技术和流程,以完成商业或工业上的目标。下图所示流程为数据科学的工作范式。反过来即为建模步骤。数据学是数据科学的基础。数据学研究数据本身,研究数据的各种类型、状态、属性及变化规律;数据科学是为科学研究的数据方法。2 数理统计技术2.1 描述性统计分析2.2 统计推断与统计建模_聚类 样本外

练习时长两年半的网络安全与防火墙_两年半网站-程序员宅基地

文章浏览阅读460次。下边基于这次攻击演示我们介绍一下网络安全的一些常识和术语。资产任何对组织业务具有价值的信息资产,包括计算机硬件、通信设施、IT环境、数据库、软件、文档资料、信息服务和人员等。网络安全网络安全是指网络系统的硬件、软件及其系统中的数据受到保护,不因偶然的或者恶意的原因而遭到破坏、更改、泄露,系统连续可靠正常地运行,网络服务不中断的状态。从广义上说,网络安全包括网络硬件资源及信息资源的安全性。_两年半网站

springboot 集成 hibernate,不集成 jpa,使用 HQL 或者 sql 操作数据库的例子_spring6+hibernate5 非jpa-程序员宅基地

文章浏览阅读1.8k次。springboot 默认使用 jpa 操作数据库。我在网上搜到了一个 springboot 集成 hibernate 使用 sql 操作数据库的例子。如果要使用 hql,就需要为实体类配置 .hbm.xml 映射文件。我把这个例子项目修改了一下,使它也可以支持 hql 和事务,附上修改后的项目的源码:源码的链接: https://pan.baidu.com/s/1RHB8Ak23_p39WuBQnubS1w 提取码: 8re5springboot 集成 hibernate ..._spring6+hibernate5 非jpa

推荐文章

热门文章

相关标签