一、C/C++跨平台开发


一次编码,到处编译。将写好的代码copy到其它操作系统,也能够编译运行,而不用去修改哪怕一行代码。

是的,大家都希望能跨平台开发以免重复劳动。业界也有各种各样的跨平台解决方案,使用跨平台语言C/C++来开发跨平台底层组件也是常见的做法之一,这里主要介绍我们团队在业务中怎么设计自己的跨平台方案,并且应用到具体业务中思考了哪些问题。

二、业务与跨平台


再高大上的技术和设计,最终都应该为业务贡献,不能纯粹为了技术而复杂化或者自high。我们也不例外,为什么要用跨平台的技术方案,得从业务说起。

YunOS发展至今,已经有越来越多的智能终端搭载YunOS操作系统,各种各样不同类型的智能终端如手机、TV、手表、车载等都会使用YunOS。而在业务上,我们团队主要负责YunOS-云服务的客户端,有目录服务、相册服务、文件存储&传输服务、长连接服务、网关服务,这些业务大部分需要在不同的智能终端上实现。

我们业务的有如下几个特点:

  1. 多操作系统支持,包括YunOS、Android、iOS、嵌入式操作系统等
  2. 输出形式也会根据操作系统有所不同,如对外合作会以SDK形式输出,对于YunOS系统则是系统底层服务
  3. 核心逻辑主要在网络协议、传输优化、数据处理上,和UI无关

基于这些业务特点,再加上我们团队(C++较擅长)的特点,很自然就能想到要使用C/C++跨平台技术来实现:一套代码,以不同形式输出到不同的操作系统,大幅降低我们的维护成本。

三、跨平台设计


相信很多人都看过chromium的工程,chromium就是一个很成熟的跨平台完整方案,一套核心代码,在windows、mac、linux、android、iOS都能编译使用。我们的整体工程也是基于了chromium的一些底层库和工具之上进行设计的:

整体的设计从下到上:

  1. Chromium Utility: Chromium工程里的一些平台隔离的的基础能力库和相关开源库
  2. Cloud Framework: 对第三方开源库的一些基础组件封装,并且根据业务抽象通用的基础能力,如线程管理、日志管理、网络处理等
  3. Cloud Biz: 业务层,各个业务的核心逻辑代码
  4. Output: 各个业务不同的输出,如SDK和System Service

3.1 跨平台工程配置

我们工程使用的是chroimum的gyp(generate your project)来管理的,这里有链接地址。gyp的好处有:

  • 配置各个平台共性的工程配置,如源码文件、编译宏、工程依赖等
  • 配置各个平台差异性的工程配置,如Android、iOS差异的源码文件、编译器参数等
  • 自动化生成平台toolchain支持的project,生成平台相关的工程

目前chroimum提供了新的工程管理工具 - gn,由于历史原因暂时还没用上

gyp并不是万能的,对于自家系统YunOS来说,最终我们要把代码放到系统里一起编译,所以还是会有相关系统的编译配置文件。

3.2 平台输出

我们的核心代码逻辑是使用C/C++写的,最终也会封装和暴露一层C/C++的API。但如果只是单纯暴露一层C/C++的API,对业务接入方来说是不可接受的,所以需要对不同平台做一次平台相关的封装和打包输出。

针对不同的平台,我们会有不同的封装和桥接层:

  1. YunOS: 在YunOS中,我们的服务属于系统service,只需要使用相关JS绑定接口,在JS环境中注册相关JS API,转接到C++ API即可。
  2. Android: 在Android环境,最终输出形式是jar + so + res,需要编写相关Java接口,然后通过JNI转调到C++ API。
  3. iOS: 在iOS中输出给第三方的包是一个framework,需要编写Objective-C接口,再转调到底层C++ API。
  4. Embbed: 嵌入式系统中只支持C语言,从底层到接口都是纯C,最终需要把代码放到系统里一起编译。

关于嵌入式平台,由于代码是纯C的,不能和其它平台的C++代码复用,但是一些核心库如MQTT协议库,可以被C++复用

3.3 平台代码抽象和隔离

在平台输出中,API层是使用平台相关方式封装的,除此之外,在底层的核心逻辑中也会有不少平台相关代码。比如获取当前设备电量,需要调用各个平台Framework提供的接口获取。

相比使用#ifdef SYSTEM编译宏的方式来实现平台相关逻辑隔离,我们更愿意使用文件级别的代码隔离:即通过抽象和定义平台相关功能接口(如GetBatteryLevel),再由不同的平台代码编译(通过gyp配置在不同平台使用不同源文件编译)来实现相关接口。

这样做的好处是我们更容易维护平台相关代码,如果增加新的平台兼容只需要添加新的平台实现文件,在里面无需考虑其它平台的代码也不用担心碰到它们。

四、开发环境&代码管理


跨平台开发意味着源代码可以在任何一个平台编译、运行,因此我们在任何一个平台也需要有相应的开发环境:

  1. for YunOS: 使用Sublime或者自家IDE开发,使用定制的YunOS内部编译环境机器来编译
  2. for Android: 使用Sublime开发,在Ubuntu 12.04上通过ninja来编译
  3. for iOS: 使用xcode开发,与正常的iOS应用一样编译
  4. for Embbed: 使用Sublime开发,使用定制的YunOS内部编译环境机器来编译

4.1 主开发环境

要使用跨平台开发,必须要全部掌握每个平台的相应开发工具和编译环境。但一般来说,会选定一个平台作为主开发环境,当然这个可以根据个人喜好或者项目情况来定。就我个人来说,因为电脑是macbook,并且xcode对C++很友好,所以会使用xcode作为主开发环境。

选好主开发环境后,每有新的功能开发都会优先在主开发环境进行编码。比如我会在xcode上调通了,把代码提交之后,再到别的平台去调试和验证。

4.2 代码管理

之前听过几次从Facebook过来的大牛赵海平的讲座,其中有一点印象深刻:Facebook整个公司的代码都在一个金字塔上,所谓一个金字塔可以理解成一个代码仓库或者一个代码分支,不同的项目和业务代码都在一起。

一个金字塔,可以大幅提高代码的复用性,提高代码的统一性。因此,我们各个平台的业务代码都在一个git仓库的一个主分支上,每个人都可以在上面进行修改:

通过gyp配置,可以把所有代码有机地组织在一起,除此之外,还需要有各个项目的发布分支,以用于各个业务发布时隔离业务之间的影响:

每个业务需要有一个自己的发布分支,业务测试时有bug的时候直接在发布分支修复,发布成功后把代码合回主分支。

五、质量保证&自动化


在我们业务中,输出的是SDK或者SystemService,对此常用的测试方法有:

  1. 编写单元测试、接口测试用例,对输出接口进行各种情况的测试,并且通过提升覆盖率来保证质量
  2. 编写测试Demo,通过增加测试用例来对SDK进行集成测试,保证真实环境下的可用性

我们最终输出的接口会根据不同平台而不同(Java、ObjC、JavaScript、C++),因此如果要为每个平台的接口编写接口测试成本将会很高,同时平台相关的桥接层代码逻辑并不复杂,更多是一层很薄的透传,出于如此考虑,最终我们只针对C++ API编写了测试用例。而在交付之前,我们会拿相关平台的Demo来自测和提测,确保集成测试通过才能正式交付:

5.1 持续集成&自动化测试

在持续集成工具上,我们最终选用了chromium使用的buildbot。buildbot部署简单,易于上手而且可定制性强,能自己简单地扩展。

buildbot是master-slave(s)模型,可以在不同的编译环境上配置使用。通过自定义step,可以让buildbot帮我们完成自动化单元测试和各个平台的编译打包:

如上,主要分为几个step:

  1. 代码变更或者计划任务触发
  2. 在Linux上编译unittest,并且运行,运行后输出代码覆盖率,如果单元测试有Fail则退出
  3. 在YunOS编译机上把代码拷贝到相应位置,进行系统编译,如果编译失败则Fail退出
  4. 在Mac编译机上运行xcodebuild编译,输出的iOS framework归档,若失败则退出
  5. 在Android编译机上运行ninja工具编译,编译时打入版本信息和签名,输出的相关SDK和symbol文件归档,若失败则退出

对于Android,最终输出的SDK和相应symbol文件会自动归档。当有crash日志反馈,可以通过crash日志中的版本号反查对应symbol,然后通过addr2line反查堆栈。我们也在buildbot上扩展了自动化工具,让crash反查流程自动化。

通过buildbot,可以用自动化的单元测试来提升质量保证,而且每次都会输出代码覆盖率。还能降低某个平台编译通过,但是导致别的平台编译失败的可能。

六、其它


除了跨平台一般要考虑的问题外,这里再补充下我们业务中考虑到的一些其它细节点。

6.1 数据埋点

我们知道,目前集团内部的客户端的统计都需要使用阿里云的无线数独,SDK内部的API调用次数、API RT、错误code统计等等,也统一上报到这个平台。我们找wdm申请了SDK专用的数据上报appkey,用C++封装了wdm的相关数据上报协议,实现了相关上报逻辑,后台BI平台收据数据通过odps计算结果并展示。

6.2 Android HotPatch

对于Android平台来说,我们的输出形式是jar + so,而且核心逻辑都在so里,所以我们还增加了Android下,自动更新so的能力。原理也很简单,启动SDK时检测是否有新版本so,如果有下载到private data目录下,下次启动时会优先加载指定更新目录的so。但是这种方式的限制也很明显,接口无法修改,即使新增的接口上层也无法使用,所以只能用于内部逻辑调整的场景。

6.3 最终生成体积

我们把依赖chromium三方库中不需要的进行了裁减,最终编译出来的二进制体积根据业务不同而不同,整体范围在2M - 4M。

七、The End


跨平台方案有很多,业绩也有很多文章介绍其它的跨平台方案。我们以开发效率为出发点,建造了包括整体设计、工程管理、开发环境、代码质量的跨平台方案。

C/C++跨平台方案也不是万能的,要结合特定业务场景下思考是否能使用。像其它业务场景如界面交互定制化的跨平台方案,会有其它的如weex、鸟巢、react native这样的方案。

实际上这里面涵盖了很多细节设计和平台相关隔离的模块,还有每个业务的跨平台设计,篇幅有限不再展开了。对于C/C++跨平台方案解决业务问题有兴趣的同学可以联系我一起交流下,而且目前我们团队在招人,对跨平台或者多端技术感兴趣的同学也可以联系我! rijian.lrj@alibaba-inc.com



评论需要翻墙 for disqus