殊途同归的iOS终端与Linux后台开发技术
前言
因为作者是从后台岗位转通道到移动开发岗位,所以深知作为一个后台开发人员对想了解终端开发技术的渴望,也明白作为一个终端开发者对后台开发技术的好奇。通过阅读本文,你会发现前后台技术之间的本质都是殊途同归的。文章重点还是侧重于讲解终端技术,后台技术没有细讲,只是涉及了对应概念。
注:本文中提到的终端都统指iOS终端
iOS与Linux之间的关系
首先来看iOS与Mac OS X之间的关系:从本质上说iOS实际上就是Mac OS X,但是两者也有一些显著的区别。区别如下几点:
1. iOS的GUI是SpringBoard,OS X是Aqua(进程叫Finder,而Lion之后的LaunchPad实际上是SpringBoard反相移植到Mac上的)。
2. iOS系统限制更严格,没有root访问权限(使用用户名为mobile),而且只能访问自己目录里的数据(苹果自己的应用除外)。
3. iOS属于封闭的移动操作系统,无法直接使用任何硬件设备,只能通过苹果提供的框架来访问硬件功能。其中的数字签名+沙盒(底层是BSD 的Mandatory Access Control 技术,简单来说就是沙盒模式下的进程调用系统调用会通过在内核态中的强制访问控制层的策略过滤这个进程是否可以调用此系统调用来达到隔离和限制的作用)+entitlement技术再配合应用商店的审核机制,保证了iOS应用的安全性。
小提示:越狱后的iOS在安装了cydia后你会发现你可以使用openssh,Linux /bin目录下能找到的基础工具,top ps uname等命令,重要的是你拥有了root。
Mac OS X分两部分:一部分是图形用户接口( Carbon and Cocoa APIs or the Quartz Compositor and Aqua user interface),另外一部分是Darwin内核。 结构图:
接下来让我们来看Darwin吧。Darwin是一个换掉了底层实现的类Unix操作系统。包含开放源代码的XNU核心(集成了Mach3.0微核心),在硬件抽象、任务调度、内存管理和保护作为基础,使用freeBSD的UNIX进程模型、安全策略、POSIX线程模型(Pthread)和网络协议栈(BSD Socket API)。(NT Kernel Window内核也是Mach核心的变种,不过BSD部分由微软自己实现了)。这样的设计使它实现了POSIX 兼容,使得可以将为其他类Unix系统编写的代码不做改变就可以编译移植到Darwin中。因为使用了Mach-O的二进制格式,能够提供多种不同CPU架构的二进制格式到一个单一的文件(例如Intel x86和ARM)。
Linux也属于类Unix操作系统(Linux发行版如CentOS,SUSE,Ubuntu等都是以linux内核加上提供的支撑的系统工具和库或应用软件的统称,Android也属于Linux发行版),他们都相当程度地继承了原始 UNIX 的特性,有许多相似处,并且都在一定程度上遵守POSIX规范。我们公司自己的TLinux就是使用linux2.6内核为基础深度定制的内核,并结合SUSE或CentOS安装在大量的服务器上为业务提供可靠的服务。
根据上文中,对iOS和Linux的简述,可以看出他们同来自于Unix,基本属于同母异父:
1.对于后台同事,运用C/C++,遵循POSIX API和Socket API实现的代码可以很好的运行在你的APP中(微信网络组件所使用的技术和后台技术非常类似,同时支持iOS和Android)。
2.对于终端同事,在苹果的大树下,是非常幸福的。和后台同事相比,虽然都可以算在Unix上开发,但通过Xcode写代码和Clang+LLVM+LLDB编译调试与后台的VIM+GCC+GDB对比,我的眼泪都快流出来了(其实后台也有很多大神是在Mac或者Windows下编码调试的)。
开发语言和开发工具的对比
后台:各种语言通吃,主要的有C/C++,Python,PHP等。开发调试工具主要是Vim+GCC+GDB+Shell脚本+加各种如nm,lsof,ps,tcpdump等工具。我身边的很多同事也使用windows下的Source Insight来替代vim写代码(架设Samba服务,映射服务器磁盘到windows中,对文件的修改是同步的)。
iOS终端:主要使用Objective-C结合C/C++,当然现在也可以使用Swift语言来编写app了。 开发调试上Xcode是最佳的选择,编码+调试+性能测试+项目配置,只要是工程所涉及到的基本上都囊括在XCode中了。
相互联系:从开发语言上看,两者的不同仅仅是Objective-C语言上(苹果的SDK 接口是Objective-C和Swift)。如果你精通C/C++语言,那么你只需要一周时间即可掌握Objective-C。那么再来看看编译调试工具:
下图是早前某一个版本Xcode中的编译选项,可以看到可选分别有:GCC 4.2, LLVM GCC 4.2,LLVM compiler 2.0。(最新版本只能选Apple LLVM 6.0)
GCC
GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划的关键部分,亦是自由的类Unix及苹果电脑 Mac OS X 操作系统的标准编译器。GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。之后也变得可处理 Fortran、Pascal、Objective-C、Java, 以及 Ada与其他语言。
Clang
GCC系统庞大而笨重,而Apple大量使用的Objective-C在GCC中优先级很低。此外GCC作为一个纯粹的编译系统,与IDE配合得很差。加之许可证方面的要求,Apple无法使用LLVM 继续改进GCC的代码质量。于是,Apple决定从零开始写 C、C++、Objective-C语言的前端 Clang,完全替代掉GCC。Clang只支持C,C++和Objective-C三种C家族语言,现在也支持了Swift语言。Clang 是一个 C++ 编写、基于 LLVM、发布于 LLVM BSD 许可证下的 C/C++/Objective C/Objective C++ 编译器。 由于 Clang 在设计上的优异性,使得 Clang 非常适合用于设计源代码级别的分析和转化工具。Clang 也已经被应用到一些重要的开发领域,如 Static Analysis 是一个基于 Clang 的静态代码分析工具。
LLVM
LLVM 是 Low Level Virtual Machine 的简称,这个库提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。简而言之,可以作为多种语言编译器的后台来使用。Clang 对源程序进行词法分析和语义分析,并将分析结果转换为 Abstract Syntax Tree ( 抽象语法树 ) ,最后使用 LLVM 作为后端代码的生成器。
下面这张图将显示GCC、LLVM-GCC、LLVM Compiler这三个编译选项的不同点了:可以看出三个不同选项的区别主要就是词法语法语义分析的前端,和代码编译链接和优化的后端所使用的技术不同:
Mach-O VS ELF
iOS和OS X下的可执行文件是Mach-O格式,而Linux下的可执行文件是ELF格式。上文中也提到了这种区别的原因:”OS X的内核中遗留了NeXTSTEP的Mach微内核架构,对于OS X的系统调用,有隐藏的Mach API,也包括了通常使用的BSD API。如果移除了Mach API系统调用,那几乎所有的应用程序都没法运行了。基于微内核的消息传递架构可能很优雅,但是效率还是比Linux宏内核慢。如果Mach被OS X移除了,那么Mach-O二进制格式也没有存在的理由了,当然移除的工作量是巨大的,但是带来的好处也是巨大的。移除之后等于铺平了转换支持ELF格式的道路,加上OS X已经兼容POSIX标准,那么就可以将所有类Unix系统的二进制可执行文件不经修改就移植到OS X中。“——此段引用自一本好书《深入解析Mac OS X & iOS操作系统》——作者Jonathan Levin。强烈建议阅读使认识更加深入。
这里还需要谈到一个概念:OS X中通用二进制格式。终端开发的同事肯定注意到了,为什么通过xcode提交一个应用包就可以同时支持i386,X86_64,armv7等架构,但是二进制文件的大小就是成倍增大? 这个就是通用二进制格式的作用了:通过在二进制文件头加入magic code和支持的架构数,cpu类型,偏移位置让系统载入合适的对应的可执行二进制。在mac下可以尝试使用命令 file 可执行文件名 查看二进制格式和支持的架构。如果你使用的是mac,可以使用命令 xxd -l 4 可执行文件名 查看到magic code:Mach-O: 0xfeedface/0xcefaedfe (32-bit), 0xfeedfacf/0xcffaedfe (64-bit) , ELF: 0x7f454c46
GDB VS LLDB
理解了上文中的编译选项问题,那调试选项就不难理解了。 Xcode同样可以选择使用GDB或者LLDB来调试程序。两者在使用上基本类似,不同点简单来说就是:LLDB比较高级,号称领先GDB一代。从平时使用的感觉来说LLDB对调试多线程支持得较好,而目前XCode默认的调试器就是LLDB。
总结:通过上文的介绍,在开发语言和编译调试工具上来说本质上是互通的,同时终端同事可以仔细观察XCode的项目编译选项,头文件路径,连接库路径,编译选项指定等,都可以找到Makefile的影子,以文本方式打开proj工程文件,会更清晰的看到Xcode是如何组织源代码的。而后台工程的编译都是通过Makefile(还有很多类似AutoConf,AutoMake的工具)来编译链接,然后使用shell脚本来完成部署。(性能测试,检测内存泄漏等方面上文没有涉及到,一句话说明:对于终端开发,Xcode中集成了所有的工具;而后台开发则需要借助很多如gprof和Valgrind等工具;抓包tcpdump通用。)
主要技术异同对照
1. 进程,线程:
iOS中,所有应用都是单进程,并发处理依赖多线程来处理。但是在服务器程序中,可以表现为单进程形式,也可以是多进程形式,由于线程安全以及竞争带来的复杂性和切换性能问题导致很少使用多线程方式;而且因为将任务步骤或者说是业务逻辑拆分后已经实现了类似并发的效果,所以对多线程化的需求并不强烈。但是接下来,我依然会针对iOS下进化过的多线程并发编程来向各位展示这个革命性的技术:
iOS中可以选择使用POSIX pthread API来创建和管理线程,在Objective-C中苹果提供了对该API的封装-NSThread,使开发者使用起来更加简单易用。但是在应用中直接操作线程却是难以接受的(同后台开发一样,会遇到线程安全和竞争切换性能等复杂的问题),一旦操作不当,线程中再调用sdk框架的代码,底层也许又会创建一个线程,因为他并不知道你已经创建好了线程,这样会造成整个应用不可控,极其不稳定。为了解决这个问题,苹果开发了基于队列的并发编程API:Grand Central Dispatch(GCD),来解决上面的问题。GCD是一个系统级(内核)的并行化框架,使用了工作队列线程池的设计,建立在最基本的线程之上。通过GCD,开发者不用再跟线程直接打交道了,而是采用下图代码的方式即可添加子线程或主线程需要执行的代码块。
GCD在后端管理着一个线程池。GCD不仅决定着哪个线程(block代码块)将被执行,它还可以根据系统资源对线程池中的线程进行管理。这样做让开发者远离了线程管理,真正使应用可控。接下来的GCD架构图结合上图的代码,你就可以明白dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEAFAULT) 与 dispatch_get_main_queue()的区别了:
对于开发者来说,系统拥有Serial Queue和Concurrent Queue,也就是串行队列和并行队列。加入到串行队列中的block任务块一定是按照加入的顺序先进先执行。而并行队列执行的顺序是不确定的,是通过线程竞争执行的。上图中有一个Serial Queue也属于Concurrent Queue这又怎么理解呢?其实很简单,在串行队列中的block任务一定是串行执行,但是开发者可以创建多个串行队列,而对于多个串行队列来说,他们之间是并发竞争的关系。另外需要注意的一点就是主线程Main Thread只对应了一个默认的Serial Queue串行队列。上文代码中的dispatch_get_main_queue()接口就是拿到主线程的这个串行队列。dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEAFAULT)拿到的队列是系统的并行队列,如果需要使用串行队列,是需要自己创建的(dispatch_queue_create(“my serial queue", NULL);)。参数还可指定队列的优先级,High Priority,Default Priority,Low Priority,Background Priority。一般使用default优先级,根据具体任务可调整。
对于后台同事需要思考的一个问题就是:线程池,或进程池,或者内存池这些技术都是我们非常熟悉的,苹果令人敬佩的最重要的一点就是他能把这些使用门槛较高的难以控制的技术抽象和包装成一个极其简便及其易用和稳定技术提供给应用层的开发者,这也是我在iOS开发中感受到的后台开发中所缺少的东西:非常友好的让我做懒人。总结起来就是无论是终端还是后台,技术总是服务于人的,要从用户的角度思考我们的接口设计是否友好,使用是否简易方便,系统是否能轻松运营。简单到极致就是重剑无锋的感觉了。
上文中说到了GCD技术,让这个技术用起来如此简单,使程序的可读性如此清晰,那么还需要介绍其中的另一个技术:Block闭包编程。
Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性。用维基百科的话来说,Block是Apple Inc.为C、C++以及Objective-C添加的特性,使得这些语言可以用类lambda表达式的语法来创建闭包。在Python这类脚本语言中早就支持了lambda的闭包语法,而iOS中的block技术从本质上来说就是后台技术中常用的函数指针回调技术,就是将需要执行的函数通过指针的方式注册进事件处理逻辑中,当对应事件发生后,通过调用函数指针以及传递参数来实现。在没有使用block技术之前的函数指针的代码可读性是很差的,编码组织结构一旦组织不好,在看代码的时候你会完全找不到北,完全被转晕。使用了block技术之后,再结合gcd多线程编程,让你的并发多线程代码也能顺序的阅读,大大提高了代码的可读性和可维护性。总结起来Block技术就是苹果对C语言中函数指针回调技术的封装。下图就让我们清楚的看到,Block语法的代码经过编译器处理后其实就是转换成了函数指针的逻辑:
源代码:
经过编译器处理后的中间代码:
总结:苹果的用户体验无论是在做产品,还是在开发技术上,都贯彻了把事情做到极致的思想,非常值得我们学习。公司要想做好连接一切的平台,那么提供给外部开发者使用的sdk或者技术也需要在体验上做到极致,目前来看还是不够好,比如微信公众号开发者模式。
2. 内存与存储:
(1) 内存
iOS对应用的内存使用是有比较严格的限制,如果使用内存过大或者增长异常是会被iOS系统的守护进程直接kill -9 干掉而没有任何机会。这里网上有一个很有意思的问题:为什么iOS上默认是不开启swap内存交换选项来让应用可以使用更多的内存(越狱后可以开启),但是又不从系统中去掉,而是继续保留着但是并不打开?这里又出现了一个技术再一次印证了苹果把事情做到极致的哲学:ARC(自动引用技术)。在ARC还没有出现之前,iOS上的应用都需要开发者手动管理内存,这无形中给很多开发者提高了不少难度来管理内存,稍有不慎应用就会崩溃掉。ARC技术从本质原理上来说是比较简单的,编译器自动分析代码后在合适的位置插入释放内存的代码。不同于GC垃圾回收技术,ARC是真正意义上的不用的内存立即释放。所以对于iOS开发者来说,在iOS平台上开发应用是非常友好的。从LLVM编译器,XCode IDE,以及后期的开发调试一系列的工具和技术都是为了极简化的抽象了底层的技术,提供给开发者一个非常友好的开发环境,使开发者只需要关心应用层面的逻辑。这一点我一直强调无论是终端还是后台开发都应该学习和贯彻的思想。
(2) 存储
终端基本上都是使用sqlite数据库,因为在稳定的前提下占用资源极小。我们公司之前的后台系统中绝大多数是使用mysql数据库。而目前越来越多的业务开始使用K/V存储+分布式存储的方案。
总结:
存储对于后台开发来说是重中之重,所有数据最终都会落地到存储,并且需要给业务提供稳定高性能的数据查询服务,所以追求最卓越的存储方案和性能对于后台开发来说是没有尽头的。后台同事在稳定服务,容灾,数据迁移,存储扩容,自动运营等诸多方面所面对的问题都是非常的大而难,需要不断的设计新的方案来满足业务不断的变化。对于终端同事,更多的是在更高效的使用内存,优化和提高存储和数据查询的性能方面做功夫,核心目的是为用户提供最稳定流畅的体验。
3. 网络通信和协议
iOS终端拿微信举例:
(1) 网络通信:使用了Socket TCP 长连接或 HTTP 短连接,对于后台来说这两种方式只是接入的方式不同(统称接入层),逻辑层大部分是通过调用对应业务CGI来处理逻辑。(其中同步和通知以及一部分特殊业务是通过长连接进行数据通信,其中Android的心跳和消息通知依赖长连接。而iOS微信退后台之后某一刻可能长连接被系统断掉或者说杀掉微信进程后也可以通过苹果的APNS服务推送消息,所以并不依赖此长连接来处理消息通知)。
(2) 通信协议:微信大量使用的是google的protobuf,一小部分业务使用的是xml。我所了解到的目前很多小应用是采用json协议,可能的原因是因为功能上可能需要和web网页兼容,使用难度上比protobuf简单,数据大小比xml小,解析速度也比xml快。
后台服务程序:
(1) 网络通信:后台服务进程本机之间的通信大部分都是使用域Socket(Mysql就是)或者共享内存消息队列加Socket通信(公司内IEG的TBUS和SNG的SPP组件)。
(2)通信协议: 接入服务器上的网络通信和协议主要还是看具体的业务,如果前端需要同时接入和支持终端应用,web,pc客户端,例如微云,通常是使用HTTP + protobuf或者json的设计。如果是游戏服务器,也要区分端游和手游。总体来说就是Socket TCP 和 HTTP TCP。数据经过接入层后再分发到真正的逻辑服务器。
总结
无论是终端开发或后台开发,对协议的设计和协议工具的建设都是十分重要的,协议设计得好,不但可以减少流量,并能方便的扩展业务。协议工具建设得好,不但能大大减小各终端和后台之间协议不同和出错的概率,还能自动化根据配置生成和更新协议,对整个业务是有好处的。(在protobuf没有出来之前,相信很多同事都手动打包解包过,写过一大堆处理协议的代码。进步一些的使用xml或者其他标记性语言作为协议载体,再进步一些的使用自己建设的二进制协议组件,比如IEG的TDR)。对于网络通讯,终端同事几乎都不会直接接触到socket层,但是了解熟悉其中的原理和知识,对开发的应用是百利而无一害,真正的做到一切尽在掌握。HTTP协议无论是终端还是后台同事都应该熟悉并且了解,移动互联网时代,太多的是HTTP。微云最近推出的预览office文档功能(之前和qq邮箱一样使用yongzhong库全部转换成一个静态的html页面后显示)就是使用WOPI协议(Web Application Open Platform Interface Protocol)一种基于HTTP协议的服务交互协议让我们的Linux服务程序与微软MS服务程序交互从而实现office文件的预览甚至编辑。可见HTTP协议作为一种标准在不同的服务器之间搭建起了一个很便捷易理解的桥梁。
4. 不得不说的事件循环处理:iOS RunLoop VS Server Logic Runloop
在iOS的主线程中有一个RunLoop(NSRunLoop)在循环处理输入源(Input sources)的异步事件,和定时器(Timer sources)同步事件。输入源包括了可以自定义输入源,和界面事件和网络事件的Port输入源。新创建的线程系统不会默认加入RunLoop,需要开发者根据需要自己加入。每一个RunLoop都可运行在不同的模式,Default模式几乎包含了所有的输入源,在拖动界面时会处于Event Tracking模式下,此模式下会限制输入事件处理(比如在拖动界面时,以Default模式创建的Timer是不会被Fire触发的)。最后还有一种Common模式,这是一个伪模式,加入这个模式相当于包括了上述提到或者没有提到的大部分模式,所以即使在Event Tracking模式下,如果Timer是创建于Common模式下,也能正常触发。下图是整个RunLoop事件循环的介绍图:
看了上图只有,有必要解释图中Port的概念。Port全称应该叫做Mach Port,上文中提到过类似的概念,Mach Port就是OS X上的进程间的通信机制,和Mach内核的设计有关。那么我们对界面的操作又是怎么传到我们应用的RunLoop中处理的呢?结合上文提到的iOS上处理界面的进程SpringBoard,大胆的猜想一下:当用户操作设备触摸屏时,整个消息的传递顺序是按照 硬件->驱动->内核->I/O Kit->SpringBoard界面处理进程->当前活动进程的RunLoop 顺序传递,而我们的应用中的主线程RunLoop收到来自SpringBoard进程通过Mach Port IPC机制发送的事件,并在事件循环中不断的循环处理。
理解了iOS下的RunLoop事件循环机制,我们再来看看经典的后台服务器事件循环:下图引用自一篇KM文章,详细的游戏服务器设计请参看此文。下图箭头左边是单进程串行处理方式,箭头右边是异步处理方式。不知道大家发现没有,这两种服务器的事件处理设计和iOS中的RunLoop事件处理设计有异曲同工之处,除了针对的对象不同,其设计和抽象思想都是一样的。
这里还必须提到的一个概念就是I/O多路复用,比较经典的是poll/select 和升级版epoll,有兴趣的不清楚的同事可以查阅KM相关文章或者google搜索。上文中的RunLoop事件循环简单来看也是一个I/O多路复用技术。
结语
文章虽然有点长,如果你仔细阅读,不难发现,无论是终端技术还是后台技术都是相通的,本质都是类似的。如果你在工作中善于发现,你会发现双方还有很多很多数不清的相同点。如何把复杂的系统复杂的技术做简单,把简单做到极致是我们开发者需要不断去思考不断去追求的东西。