Bo2SS

Bo2SS

1 基础篇(上)

《带你领略 iOS 知识体系的全貌》这个系列分享自己在极客时间专栏《iOS 开发高手课》的学习心得,作者「戴铭」将专栏一共分成了四个板块:1)基础篇,2)应用篇,3)原理篇,4)原生与前端共舞篇。

我也按照作者的划分整理自己的学习笔记,在这个阶段自己偏向于吸收专栏的精华,往后阶段再慢慢增加自己的输出,以见证自己的成长。

今天要分享的是第一个板块:基础篇

image

00 | 开篇#

2007 年乔布斯发布了第一代 iPhone—— 它重新定义了很多人对于手机的认知,同时也是移动互联网时代的开端。

2008 年 7 月 WWDC 苹果全球开发者大会上,苹果宣布 App Store 正式对外开放 —— 这意味着属于开发者的移动互联网时代真正开始了。

相比于寻找移动端下一个热点是什么?不如静下心来好好消化掉这几年浪潮留下的关键技术,在此基础上再去理解各种 “新技术”,必然会驾轻就熟。

作者戴铭热爱分享,喜欢将平时学习和工作中的经验分享到戴铭的博客上,也会将一些技术总结通过代码发到戴铭的 GitHub上。

01 | 建立你自己的 iOS 开发知识体系#

对于 iOS 的知识体系,作者划分为了基础、原理、应用开发、原生与前端四大板块,并送上了一张思维导图:

image

总的来说,不要看到啥就去学啥,而应该有目的、成体系地去学习,效果才会更好,进而可以从容地应对技术的更新迭代。

02 | App 启动速度怎么做优化与监控?#

App 启动过程#

一般情况下,App 的启动分为冷启动和热启动。

  • 冷启动:一次完整的启动过程。 App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。
  • 热启动:表面上打开 App 的过程。在 App 的进程还在系统里的情况下,用户重新启动进入 App 的情况。

用户能感知到的启动慢,都发生在主线程上。而 App 的冷启动主要包括三个阶段:

  1. main () 函数执行前;
  2. main () 函数执行后;
  3. 首屏渲染完成后。

main () 函数执行前#

主要过程

  • 加载可执行文件,即 App 的.o 文件的集合;
  • 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定;
  • Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
  • 初始化,包括执行 +load () 方法、调用 attribute ((constructor)) 修饰的函数、创建 C++ 静态全局变量。

优化点

  • 减少动态库加载(数量上,苹果公司建议最多使用 6 个非系统动态库);
  • 减少加载启动后不会去使用的类或者方法;
  • +load () 方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize () 方法替换掉(在一个 +load () 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗,积少成多,其对启动速度的影响会越来越大);
  • 控制 C++ 全局变量的数量。

main () 函数执行后#

主要过程:从 main () 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成。如:首页的业务代码都是在这个阶段执行的,包括首屏初始化所需配置文件的读写、首屏列表大数据的读取、首屏渲染的大量计算等操作。

优化思路:从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 启动必要的初始化功能,而哪些是只需要在对应功能开始使用时才需要初始化的。

首屏渲染完成后#

主要过程:didFinishLaunchingWithOptions 方法作用域内执行首屏渲染之后的所有方法执行完成。其主要进行非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等操作。

优化点:优先处理会卡住主线程的方法,否则会影响到用户后面的交互操作。

明白了 App 启动阶段需要完成的工作后,接下来就有针对性地进行功能级别方法级别的启动优化了。

功能级别的启动优化#

简单来说,就是在 main () 函数开始执行后到首屏渲染完成前的阶段,只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。

方法级别的启动优化#

检查首屏渲染完成前主线程上有哪些耗时方法,将没必要的耗时方法滞后或者异步执行。

如何进行方法耗时的监控呢?主要有两种手段

1)定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式。其特点是精度不高,但也够用。

2)对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时。其特点是非常精确,但只能针对 Objective-C 的方法(对于 c 方法和 block,倒也可以使用 libffi 的 ffi_call 来达成 hook,但编写维护相关工具的门槛较高)。

基于第 2 种方式监控耗时的完整代码,见GCDFetchFeed(开源项目),其使用方法:

在需要检测耗时的地方调用 [SMCallTrace start],结束时调用 stop 和 save 就可以打印出方法的调用层级和耗时了,还可以设置最大深度和最小耗时检测,来过滤不需要看到的信息。

objc_msgSend相关知识:

  • 源码见苹果公司的开源网站
  • 它是 Objective-C 里方法执行的必经之路,能够控制所有的 Objective-C 方法,所以 hook 了它,就可以 hook 全部的 Objective-C 方法;
  • 执行逻辑:先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针,经过异常错误处理后,最后跳到对应函数的实现。
  • 用汇编语言写的原因:1)它的调用频次最高,在它上面进行的性能优化能够提升整个 App 生命周期的性能,而汇编语言在性能优化上属于原子级优化,能够把优化做到极致;2)其他语言难以实现未知参数跳转到任意函数指针的功能;
  • 如何 hook 它,可以参考 Facebook 的开源库fishhook—— 实现在 iOS 上运行的 Mach-O 二进制文件中动态地重新绑定符号。

参考资料:

汇编语言入门教程—— 阮一峰

03 | Auto Layout 简介#

Cassowary是 Auto Layout 用到的布局算法。

使用 Auto Layout 一定要注意多使用 Compression Resistance Priority 和 Hugging Priority,利用优先级的设置,让布局更加灵活,代码更少,更易于维护,可以参考Auto Layout 相关的 Demo

在前端出现了 Flexbox 这种高级的响应式布局思路后,苹果公司基于 Auto Layout 又封装了一个类似 Flexbox 的 UIStackView,用来提高 iOS 开发响应式布局的易用性。

PS:目前工程一般使用基于 Auto Layout 封装的第三方库,如Masonry—— 相关博客。

04 | 项目大了人员多了,架构怎么设计更合理?#

目标:将业务完全解耦,将通用功能下沉,每个业务都是一个独立的 Git 仓库,每个业务都能够生成一个 Pod 库,最后再集成到一起。

简单架构向大型项目架构演进中,就需要解决三个问题:

  1. 模块粒度应该如何划分?对于 iOS 这种面向对象编程的开发模式来说,我们首先应该遵循 SOLID 原则。
  2. 如何分层?建议不要超过三个:底层可以是与业务无关的基础组件,比如网络和存储等;中间层一般是通用的业务组件,比如账号、埋点、支付、购物车等;最上层是迭代业务组件,更新频率最高。
  3. 多团队如何协作?团队分工要灵活,不把人员隔离固化,导致做的东西相互都不用;然后要围绕着具体业务进行功能模块提炼,去解决重复建设的问题,在这个基础上把提炼出的模块做精做扎实。

作者心目中好的架构

组件间关系协调但没有固定的标准,协调的优劣成为了衡量架构优劣的一个基本标准。

在实践中,一般有两种架构设计方案:

1)协议式:采用协议式编程思路,在编译层面使用协议定义规范,实现可在不同地方,从而达到分布管理和维护组件的目的。这种方式也遵循了依赖反转原则,是一种很好的面向对象编程的实践。

2)中间者:采用中间者统一管理的方式,控制 App 整个生命周期中组件间的调用关系。

在考虑架构设计时,我们更多的还是需要在功能逻辑组件划分上做到同层级解耦,上下层依赖清晰,这样的结构才能够使得上层组件易插拔,下层组件更稳固。而中间者架构模式更容易维护这种结构,中间者的易管控和易扩展性,使得整体架构能够长期保持稳健与活力。所以,中间者架构更是作者心目中好的架构

案例分享

ArchitectureDemo——Github,其在中间者架构的基础上增加了对中间件、状态机、观察者、工厂模式的支持,此外,在使用上还支持链式调用。

参考资料

iOS 应用架构谈 开篇——Casatwy

05 | 链接器:符号是怎么绑定到地址上的?#

带着疑问学习:自己参与的项目里,为什么有的编译起来很快,有的却很慢;编译完成后,有的启动得很快,有的却很慢?

这篇要讲的链接器呀,它最主要的作用,就是将符号绑定到地址上。下面我们先从编译器讲起~

iOS 开发为什么使用编译器#

iOS 编写的代码是先使用编译器把代码编译成机器码,然后直接在 CPU 上执行。

之所以不使用解释器来运行代码,是因为苹果公司希望程序的执行效率更高、运行速度更快。

相反,解释器可以在运行时执行代码,该过程具有动态性,程序运行后能够通过更新代码随时改变程序的逻辑。

现在苹果公司使用的编译器是 LLVM,相比于 Xcode 5 版本前使用的 GCC,编译速度提高了 3 倍。同时,苹果公司也反过来主导了 LLVM 的发展,让 LLVM 可以针对苹果公司的硬件进行更多的优化。

LLVM 本质上是编译器工具链技术的一个集合:编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器(LLVM 中的 lld 项目)会将项目中的多个 Mach-O 文件合并成一个。

编译过程

  1. 首先,你写好代码后,LLVM 会预处理你的代码,比如把宏嵌入到对应的位置。
  2. 然后,LLVM 会对代码进行词法分析和语法分析,生成 AST(抽象语法树,结构上比代码更精简,遍历起来更快)。
  3. 最后, AST 会生成 IR(中间表示),它是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。

编译时链接器做了什么?#

链接器的作用,就是完成变量、函数名和其地址绑定这样的任务,篇首提到的符号,就可以理解为变量名和函数名。

并且,链接器在整理函数的符号调用关系时,会理清有哪些函数是没被调用的,并自动去除掉。该过程中,链接器会以 main 函数为源头,跟随每个引用,并将其标记为 live。跟随完成后,那些未被标记 live 的函数,就是无用函数。然后,链接器可以通过打开 Dead code stripping 开关,来开启自动去除无用代码的功能(这个开关是默认开启的)。

动态库链接#

链接的共用库分为静态库和动态库:

  • 静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新;
  • 动态库是运行时链接的库,使用 dyld 就可以实现动态加载。

使用 dyld 加载动态库,有两种方式:1)有程序启动加载时绑定,2)符号第一次被用到时绑定。为了减少启动时间,大部分动态库使用的都是采用第 2 种方式。

常用工具:nm 工具 - 查看符号表、otool 工具 - 找符号所需库。

希望通过这个系列《带你领略 iOS 知识体系的全貌》,后续的深度挖掘就靠自己了,可不要浅尝辄止哦~


投票:春节假期你主要做什么呢?

A. 走访亲戚

B. 旅游

C. 组局

D. 看剧

E. 阅读

F. 敲代码

G. 其它

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。