iGBlog

iGuan7u

iOS 出身的小伙子,对 Objective-C、Swift、以及 Javascript 有浓厚的兴趣。热衷使用原生实现所有功能,厌恶一切的跨平台开发技术。喜欢分享工作过程中遇到的问题以及日常工作中遇到的新技术。希望这个博客能给你带来一点启发。

复盘 2018

度过了进入公司的第三年,需要开始深切的回顾自己的工作历程,认真考虑自己的职业生涯了。
从进入公司的第一年开始,Leader 就已经认真地跟我说明了:“进入职场的三年尤其重要,这可能直接间接地确定你以后的职业生涯。”一眨眼,三年转瞬即逝,不太好确定自己在过去三年奠定了什么基础,既有满足,也有懊悔。这里与其说复盘 2018,不如复盘 2015-2018 吧。

2015-2016

毕业刚刚进入公司,对技术颇有追求。全身心投入到项目中,对自己的每一个换行、每一个分号都有严格的要求,期待自己能写出完美代码。只是后来发现,这种心理对于项目并无太大意义,这是后话了。

Clang Static Analyzer

第一年 Leader 安排了一个小项目,在项目中接入代码静态分析的检查,我第一次接触到 clang,相应的,了解到 LLVMAST,老实说,对于刚进入职场的新人来说,这段项目经验并不友好。鉴于大学阶段对于大型项目接触经验较少,接触这个项目时真的是束手无策,C++ 的语法虽然说并不陌生,可是当看到真实项目中的指针、模版,毫无亲切感。
记得当时的项目要求中,希望在 clang static analyzer 中添加自定义的检测规则,国内在这方面的分享文档简直少得可怜,所以百度搜索基本毫无结果。官网上有英文文档,对于添加自定义规则的功能也有提及,可是却没有详细说明规则实现的原理、暴露的 API 接口、静态检测的流程,对新手友好度远不及 github 上热门的开源项目。无可厚非,有这方面需求的开发人员,一般对编译器有深入的了解,静态检测虽是开箱即用的工具,可是内部涉及的原理,语法树、编译上下文都是需要优秀开发者所创造的结晶,实在非一个新手开发者所能轻易消化理解。
假若当初能一直坚持这个项目的钻研,或许现在对代码底层的认知会更为扎实了。

Fauxpas

那时候在项目中除了接入 clang 静态分析以外,还使用了一个国外的代码检测应用 Faux Pas for Xcode ,这个应用除了能提供基础的静态分析规则外,还提供了一些实用的分析规则,并且能自行配置需要使用的规则。另外,这个应用对项目进行分析所需要的时间远小于 Clang Static Analyzer,对项目代码质量要求较高的,强烈推荐使用这个应用。

优点:

  1. 代码检测规则实用;
  2. 可自行配置实用的规则;
  3. 检测结果除了提供页面展示以外,也能在命令行中执行输出 JSON 格式的结果,非常符合接入自动构建系统中;

缺点:

  1. 收费应用;
  2. 无法自定义规则;

持续集成

接触部署 Clang Static Analyzer 以及 Fauxpas 后,让我明白到自动化跟持续集成对于一个项目发展的重要性,也让我明白了测试开发对于一个项目的支持是多么重要。然而其实国内很多团队都对于这方面要求颇有疏忽,虽然我经过切身体验,这方面的支持是相当繁琐并且无趣的。

2016-2017

众所周知,iOS 应用不提供执行应用外部的二进制文件的功能,也就是说,在 iOS 中,从 App Store 下载下来的应用,无论你使用与否,应用中都包含了的所有功能。这点与 Android 应用截然不同。因此一般情况下,iOS 应用包尺寸都会比 Android 应用包要大。
而另一方面,苹果禁止了这方面的功能,导致开发者对于发布苹果应用有着异常谨慎的态度,因为一旦发布出去,其中包含了未被测试发现的问题,开发者只能再次等待漫长的应用审核(后来苹果提供了加速审核的途径,只不过严格限制了使用次数),才能重新上架应用修复问题。

热更新

随着时间推移,开发者们逐渐加入研究如何能在不经过版本审核去修复线上问题,或者提供新功能。后来随着 JSPatchwax 等一系列热更新框架兴起,组内、甚至行业内对 iOS 的热更新兴致达到了前所未有的水平。热更新 (hotfix),笔者认为,其实很难定义这门技术究竟是好是坏。毫无疑问,它的出现对于 iOS 开发领域来说,是降低的版本发布的风险,让应用发布后修复问题成为了可能。甚至让无需重新提交 App Store 版本审核,直接让应用提供新功能。在 Android 开发中,这种模式是相当常见的。然而它极大的打乱了版本发布流程,让发布前测试在整个应用开发流程中的重要性降低,容易导致项目管理者无法准备把握项目的开发进度。
在很长一段时间中,笔者以及周围的同事都对热更新抱以恐惧的心态。因为应用提交审核跟发布之间存在着时间差(没错,就是苹果万恶的审核时间,审核加速前这个时间一般长达 7 到 14 天),而在这段时间内,版本肯定是需要继续迭代的。真正需要热更新修复时,代码早已不是对应版本。

因此热更新修复的流程,及其繁琐:

  1. 开发者需要回滚代码,或者 checkout 对应的 tag 分支,查阅旧版本逻辑代码;
  2. 确认问题,使用原生代码修复该问题;
  3. 改用热更新框架语言,重新实现修复逻辑;
  4. 提单,测试进行介入,确认无误后准备上线现网;
  5. 灰度上线;
  6. 回到主干代码,merge 分支对应修复逻辑;
  7. 测试再次介入,确认问题在主干分支中也同样被解决;

而真正令人为此反感的,是这个流程可能随时出现,打断你当前的开发进度。为此笔者跟周围的同事付出了沉重的代价。然而在这个过程中,我们还是作出了一点成绩的。
在热更新方面,JSPatch 一直是我们团队中的备选方案,比起 Javascript,我们选择的 lua 语言方案的 wax patch。然而两种方案都有一个固定缺陷,开发者无法使用原生语言进行 hotfix 开发,因此 JSPatch 的作者开发了一个在线转换的小工具,能够对简单的 Objective-C 语句进行转换,这极大的方便了大部分的开发者,也体现了作者对于开源社区生态建设付出的努力。可是这仅限于简单语句,对于复杂的语句,那个翻译小工具给出的效果不尽如人意。
由此可见,通过简单的字符替换进行语法转换,是在复杂的代码前是无法胜任的。我们使用了 ANTLR,一个语法解析器,将 Objective-C 代码解析成语法树,再根据其语法树转换成 lua 代码,成功解决复杂语法转换的问题。在实践中,我们成功将一个复杂模块全部替换成热更新逻辑,做到动态下发以及热插拔。

后续因为苹果官方严格审查 Patch 等后门,该项目停滞。

跨平台开发

在这一年中,也成功接触了跨平台开发的业务,跨平台开发永远是业内讨论不完的话题,从 HTML5,Codova,到 React-Native,甚至到当前的大热门 Flutter,人们一直在探求跨平台开发的最优解。

Code once, run everywhere.

可是这似乎一直是一个伪命题。因为只要有不少于一家厂商存在于这个世界,它总会想尽办法创造不同于其他厂家的独异之处。差异,这是亮点,也是卖点。在手机行业,乃至于所有行业,厂家都在想方设法找到自己的亮点。因此,笔者认为跨平台方案永远都在路上。
而在笔者负责的项目中,跨平台开发的业务逻辑一直都是一个顽疾,所有人都敬而远之,而它的重要性又使得所有同事不得不深入理解那其中的“奥妙”。那是其中包含了 chormium 的 base library,该库是提供跨平台的多进程方案,另外模块中还使用了 cURL,用作网络协议的请求。这方案在初期应该是取得了不错的收益,在 Windows、macOS、iOS、Android 统一使用这套模块方案,base 库极大的消除了平台的差异性,cURL 解决了不同网络协议的传输问题。
然而当笔者接触到这个模块的时候,情况已经截然不同了。由于用户对桌面端的需求逐渐减少,Windows、macOS 平台下的客户端已经处于无限期停止开发的状态,Android 平台由于实在无法忍受不能断点调试 C++ 模块的弊端,已经动用大量人力开始进行原生代码的转移,仅剩 iOS 端一直使用该模块。其实跨平台的优势在此时早已荡然无存,弊端却逐渐显现。该模块年久失修,基础库早已落后太多个版本,使得更新风险越来越大,越发难以修改。
所以其实选择一个合适的跨平台方案,是尤为重要的,不能单单只看当前方案的成熟度,同时也要考虑开源社区中普遍开发者对于该方案的态度、后期的维护成本,新人上手难度等。当然,这可能已经属于一个架构师需要考虑的了。

2017-2018

终于到了这一年了。其实在这一年中,笔者已经很少接触 iOS 开发了,由于这一年需要开发新的项目-一个前端相关的项目,所以笔者被指派到开发新的项目中。本以为说接触一个新的项目就是从零开始,移动端开发的经验毕竟与前端开发并不太吻合,可是当自己亲身切换到新的领域中时,才发现说过于经历的东西其实都会成为你伴随终身的财富。在这里,希望看到这篇文章的读者都能明白,不要害怕陌生的领域,也不要对当前耳熟的东西感到无趣。
在这一年中,开始深入接触到了 Javascript,开始不久就已经看到这么一句话:

Any application that can be written in JavaScript, will eventually be written in JavaScript. —— Jeff Atwood

与 Objective-C 不同,它真的是一门如日中天的语言,用日新月异去形容都不足为奇,从简单的 XMLHttpRequest,发展到 jQuery、React.js、Vue.js、Angular.js,Javascript 的发展速度如此迅猛,以至于刚去接触这个领域的笔者都是瞠目结舌的。然而,得益于 Javascript 的人气,很多优秀的设计思想都能在其中落地,同时可以迅速接受用户们的验证,正如单项数据流这种设计思想,在 Vue.js 框架中得到逻辑,迅速受到了开发者们的赞赏,从而极大地推动了这种思想应用到其他领域中。

这一年中,笔者基本都在维护一个名为 Online Document 的项目,项目中使用到了 Vue.js, pReact, Babel.js, Eslint, Webpack 等技术,在项目初期,很大部分时间都耗费在项目配置中。有经验的读者应该都深有同感,Webpack 的配置是极其繁琐而有难以避免的,然而一旦配置成功后,项目发展阶段就无需变更。这种一次性的体验难以归纳成为系统性的知识。

而后,Online Document 被要求抽离成为一个 SDK,作为一个 Web 离线应用加入到另外一个项目中,在开始,笔者乐观的评估为其实只需要在应用层捕捉 request,然后返回应用离线的数据即可,类似于 iOS 中的 NSURLProtocol。可是在后面的过程中,了解到问题并没有如此简单。

  • 在离线环境下,返回上一次的最新数据;在有网环境中,优先展示本地的离线数据,然后等最新数据返回成功后,在将其渲染到页面中;

    1. JS 逻辑中,无论如何,优先请求本地的缓存数据,这里需要对请求作出特殊标识,让 Native 层能够正确识别该请求,并返回本地数据;
    2. JS 逻辑中,根据 navigator.onLine属性,可以判断当前的网络环境,如果目前是有网状态下,发起正常的网络请求,获取最新的应用数据,并刷新页面展示;
    3. Native 逻辑中,当最新网络请求成功返回数据后,需要记录其 request 的 URL,以及其 response 的 data,以用于下一次的离线数据相应;
  • 离线环境下需要能进行正常的编辑操作,并且在有网环境下将编辑操作重新同步到服务器中

    1. 由于编辑操作都是通过 WebSocket 同步到服务器的,该通讯方式不适用于使用 NSURLProtocol 进行捕捉,因此编辑操作只能依赖 H5 提供的缓存功能,在 JS 逻辑中进行维护
    2. H5 提供了几种常用的持久化技术,我们在这里逐一分析一下:
      1. sessionStorage :适用于保存会话期间的数据,例如一次性登陆态信息等,在网络窗口关闭后会被清除,该技术不适用保存文档操作信息,因为我们需要在下次文档打开后仍能正常访问该数据;
      2. localStorage:区别于 sessionStorage,该存储数据能够在下次网页打开后仍能够正常访问,其缺点为仅适用于保存简单类型的数据,不能保存对象实例;
      3. Web SQL DataBase:该技术为 H5 提供 SQL 操作客户端数据库的 API,可是该方案已经被 W3C 官方在 2011 年宣布不再进行维护,属于废弃过时的技术方案
      4. Indexed DataBase:作为 Web SQL DataBase 的后继者,该存储技术被各大开发者广泛介绍,其类似于 NoSQL 形式的操作方式也极大的方便了 Web 开发者的使用,然而在移动端中的支持度稍有不足,在 iOS 8-9.2 版本的 WebView 中,尚未完全支持 Indexed DB,因此无法使用这项优秀的技术
        折中取舍,我们最终选择的 localStorage 作为文档操作的存储 方式。
    3. 因为 webView 随时可能因为用户的操作而销毁,为了避免操作数据的丢失,文档编辑的操作信息必须要优先保存 localStorage,等操作信息成功同步到服务器后,再将 localStorage 的对应操作信息去掉,降低数据丢失的风险。
    4. 离线的环境下,展示文档的信息需要同时结合文档离线返回的快照信息,以及离线环境下的操作记录,才会构造出最终正确的文档内容;在在线环境下,还需要在请求到服务器最新的文档快照信息,再次加入离线环境下的操作信息,构造出真正的文档内容。
  • 作为客户端的功能,该表现上需要尽可能的流畅,接近原生体验。考虑到 H5 的操作流畅,iOS 开发者应该是本能性地会想到 WKWebView 的,该控件能提供高达 60 Fps 的渲染速度,同时得益于使用了与 Safari 相同的 Javascript Core,该控件下 JS 的执行效率有了极大的提升。然而在开发过程中,我们否决了使用 WKWebView 的建议,分析如下:

    • 考虑到我们应用中使用 NSURLProtocol,因为 WKWebView 真实的执行环境是独立于应用的不同进程,因此单纯配置 NSURLProtocol 并不能捕捉到 WKWebView 的请求,虽然可以通过调用其私用 API 强制将 WKWebView 的请求传递到 NSURLProtocol 中,这里出现的跨进程数据传递必定有不可忽视的性能损耗。同时,由于苹果的设计缺陷,即便请求强制经过 NSURLProtocol,可是 WKWebView 并没有将 request body 传递过来,导致 POST 请求会无法正常执行。
    • 虽然 WKWebView 的执行效率极高,可是可能是因为独立于应用的进程的原因,使得 WKWebView 的初始化速度极慢,其经过 alloc 方法,到真正发起 request 请求,需要接近 0.5 秒的时间,而 UIWebView 则不存在这个问题。这里应该产品体验,初始化页面的白屏时间过长无法接受,因此这里放弃使用 WKWebView。

老实说,经过了这个项目的磨练,算是捡起了前端开发大门的敲门砖,不同于客户端开发,前端开发这个领域有太多新奇的东西,Javascript 这门语言也为这个世界增加了很多有意思的东西。虽然这其中有太多坑,填坑过程中也太过于痛苦(这里必须说一下,iOS WebView 中,只要涉及到跟计算键盘高度的,都是一场灾难),可是真的为开发者成长生涯铺上了一个极其重要的垫脚石。

现在,终于可以开始 Enjoy the new beginning!