iOS开发音频课---笔记---基础原理(一)

简介

戴铭老师的《iOS开发高手课》仰慕已久,早就在群里被小伙伴们安利,自己没有学过,岂不落后了
本着学过不做笔记 = 忘记的初衷,做了些笔记,以便更好的学习与吸收

本课程仅是自己学习的笔记,还是希望各位小伙伴多多支持戴铭老师,购买正版


01-建立自己的iOS开发知识体系

iOS 的知识体系,包括了基础、原理、应用开发、原生与前端四大模块。

基础模块

按照 App 的开发流程(开发、调试测试、发布、上线)
在这里插入图片描述

应用开发

主要是关注一些经典库

主流的一些库的源码建议还是看看

在这里插入图片描述

原理模块

iOS 开发原理,主要就是系统内核 XNU、AOP、内存管理和编译的知识
在这里插入图片描述

原生与前端

在这里插入图片描述
对于新的技术如何去看,很重要,先不要急着深入到开发细节中了,那样你会迷失在技术海洋中
你需要先建立好自己的知识体系,打好基础,努力提升自己的内功,然后找好指明灯,这样才能追着目标航行

  • 在学习知识的道路上,我的建议是求精、求深,基础打牢,以不变应万变。
  • 在工作上,则要注重开发效率,避免不必要地重复造轮子,理解原理和细节,同时开阔眼界,紧跟技术前沿。
  • 不要急着看到啥就去学啥,有目的、有体系地去学习,效果才会更好
  • 即使工作再忙,你也要找时间成体系地提升自己的内功,完善自己,然后反哺到工作上,让工作效率和质量达到质的提升,进而从容应对技术的更新迭代。

学习如逆水行舟,不进则退,戴铭老师这句话说的最有道理,即使工作再忙,也要学习,活到老,学到老,只有每天学习,才能不断进步,不断适应这个社会


02-App启动速度怎么做优化与监控

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

  • 冷启动是指, App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
  • 热启动是指 ,App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少。

这里主要讲的是 冷启动的优化
App 的启动时间,指的是从用户点击 App 开始,到用户看到第一个界面之间的时间。
总结来说,App 的启动主要包括三个阶段:

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

main()函数执行前

系统主要会做下面几件事情:

  • 加载可执行文件(App的.o文件的集合)
  • 加载动态库,进行rebase指针调整和bind符号绑定

rebase指针调整和bind符号绑定,一脸懵逼

  • Objc运行时的初始处理,包括Objc相关类的注册、category注册、selector唯一性检查等
  • 初始化,包括执行+load()方法、attribute((constructor)) 修饰的函数的调用、创建C++静态全局变量

attribute((constructor)) 修饰的函数的调用,这个不太懂

我们可以做的优化有:
  • 减少使用动态库
  • 减少加载启动后不会使用的类或者方法
  • +load()方法里的内容,可以放到首屏渲染完成后再执行,或者使用+initialize()方法替换掉
  • 控制C++全局变量的数量

我理解的就是全局变量

mian()函数执行后

指的是从 main() 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成。

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

首屏渲染完成后

首屏渲染后的这个阶段,主要完成的是,非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等。
这个阶段就是从渲染完成时开始,到 didFinishLaunchingWithOptions 方法作用域结束时结束。

了解了App启动过程后,我们可以从两个方面去优化:

功能级别的启动优化

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

方法级别的启动优化

检查首屏渲染完成前主线程上有哪些耗时方法,将没必要的耗时方法滞后或者异步执行。
通常情况下,耗时较长的方法主要发生在计算大量数据的情况下,具体的表现就是加载、编辑、存储图片和文件等资源。

问:如何监控App的启动速度?
  • 定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时
    开发类似工具成本不高,可以快速集成到App中,缺点是:精确度不够高
  • 对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时
    hook 方法的意思是,在原方法开始执行时换成执行其他你指定的方法,或者在原有方法执行前后执行你指定的方法,来达到掌握和改变指定方法的目的。
    该方法对检查结果比较准确

objc_msgSend 本身是用汇编语言写的,这样做的原因主要有两个:

  • 一个原因是,objc_msgSend 的调用频次最高,在它上面进行的性能优化能够提升整个 App 生命周期的性能。而汇编语言在性能优化上属于原子级优化,能够把优化做到极致。所以,这种投入产出比无疑是最大的。
  • 另一个原因是,其他语言难以实现未知参数跳转到任意函数指针的功能。

03 | Auto Layout 是怎么进行自动布局的,性能如何?

Auto Layout包含了布局在运行时的生命周期等一整套布局引擎系统,用来统一管理布局的创建、更新和销毁
这一整套布局引擎系统叫作 Layout Engine ,是 Auto Layout 的核心,主导着整个界面布局。

每个视图在得到自己的布局之前,Layout Engine 会将视图、约束、优先级、固定大小通过计算转换成最终的大小和位置。
在 Layout Engine 里,每当约束发生变化,就会触发 Deffered Layout Pass,完成后进入监听约束变化的状态。当
再次监听到约束变化,即进入下一轮循环中。

Layout Engine 在碰到约束变化后会重新计算布局,获取到布局后调用 superview.setNeedLayout(),然后进入 Deferred Layout Pass。

Layout Engine 会从上到下调用 layoutSubviews() ,通过 Cassowary 算法计算各个子视图的位置,算出来后将子视图的 frame 从 Layout Engine 里拷贝出来。


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

  • 当业务需求量和团队规模达到一定程度后,任何一款 App 都需要考虑架构设计的合理性。
  • MVC 是很好的面向对象编程范式,非常适合个人开发或者小团队开发。

对于 iOS 这种面向对象编程的开发模式来说,我们应该遵循以下五个原则,即 SOLID 原则

  • 单一功能原则:对象功能要单一,不要在一个对象里添加很多功能。
  • 开闭原则:扩展是开放的,修改是封闭的。
  • 里氏替换原则:子类对象是可以替代基类对象的。
  • 接口隔离原则:接口的用途要单一,不要在一个接口上根据不同入参实现多个功能。
  • 依赖反转原则:方法应该依赖抽象,不要依赖实例。iOS 开发就是高层业务方法依赖于协议。

重新梳理组件之间的逻辑关系,进行改造
而对于组件间如何分层这个问题,我认为层级最多不要超过三个,你可以这么设置:

  • 底层可以是与业务无关的基础组件,比如网络和存储等;
  • 中间层一般是通用的业务组件,比如账号、埋点、支付、购物车等;
  • 最上层是迭代业务组件,更新频率最高。

组件化是解决项目大、人员多的一种很好的手段
在实践中,一般分为了协议式和中间者两种架构设计方案。

协议式架构设计主要采用的是协议式编程的思路
另一种常用的架构形式是中间者架构。它采用中间者统一管理的方式,来控制 App 的整个生命周期中组件间的调用关系。

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

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

在不同项目中,有的项目编译的快,有的项目跑的慢,这是为什么呢?
带着疑问,我们开始这节内容

其实,编译快慢与:编译和启动时连接器有关

编译、连接、运行


编译器

iOS 编写的代码是先使用编译器把代码编译成机器码,然后直接在 CPU 上执行机器码的
C、C++、OC
我们看一下iOS没有使用到的解释器:

解释器

解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码(Bytecode)),然后一句一句地执行目标代码。
解释器,是在运行时才去解析代码,这样就比在运行之前通过编译器生成一份完整的机器码再去执行的效率要低。

  • 使用解释器,写的程序跑起来后不用重新启动,就可以看到代码修改后的效果,这样就缩短了调试周期。
  • 使用解释器,程序发布后,你还可以随时修复问题或者增加新功能,用户也不用一定要等到发布新版本后才可以升级使用
    python、shell

编译器与解释器的优缺点:

  • 采用编译器生成机器码执行的好处是效率高,缺点是调试周期长。
  • 解释器执行的好处是编写调试方便,缺点是执行效率低。

iOS开发使用的是什么编译器?

苹果使用的编译器是LLVM

  • LLVM 是编译器工具链技术的一个集合。而其中的 lld 项目,就是内置链接器。
  • 编译器会对每个文件进行编译,生成 Mach-O(可执行文件);
  • 链接器会将项目中的多个 Mach-O 文件合并成一个。
LLVM的编译过程主要包括:
  • 首先,你写好代码后,LLVM 会预处理你的代码,比如把宏嵌入到对应的位置。
  • 预处理完后,LLVM 会对代码进行词法分析和语法分析,生成 AST 。AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 IR(中间表示)。
  • 最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。
编译时,连接器做了什么?

Mach-O 文件里面的内容,主要就是代码和数据
代码是函数的定义;
数据是全局变量的定义,包括全局变量的初始值。
不管是代码还是数据,它们的实例都需要由符号将其关联起来

链接器的作用,就是完成变量、函数符号和其地址绑定这样的任务
而这里我们所说的符号,就可以理解为变量名和函数名。

链接器最主要的作用,就是将符号绑定到地址上

链接器为什么还要把项目中的多个 Mach-O 文件合并成一个?

项目中文件之间的变量和接口函数都是相互依赖的,只有通过链接器将项目中生成的多个 Mach-O 文件的符号和地址绑定起来,才能在调用其他文件中的实现函数的时候,找到该调用函数的地址。

链接器在链接多个目标文件的过程中,会创建一个符号表,用于记录所有已定义的和所有未定义的符号。
链接时如果出现相同符号的情况,就会出现“ld: dumplicate symbols”的错误信息;
如果在其他目标文件里没有找到符号,就会提示“Undefined symbols”的错误信息。

连接器对代码主要做了哪些事?
  • 去项目文件里查找目标代码文件里没有定义的变量。
  • 扫描项目中的不同文件,将所有符号定义和引用地址收集起来,并放到全局符号表中。
  • 计算合并后长度及位置,生成同类型的段进行合并,建立绑定。
  • 对项目中不同文件里的变量进行地址重定位。

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

动态库连接

在真实的 iOS 开发中,你会发现很多功能都是现成可用的,不光你能够用,其他 App 也在用,比如 GUI 框架、I/O、网络等。链接这些共享库到你的 Mach-O 文件,也是通过链接器来完成的。

链接的共用库分为静态库和动态库
静态库是编译时链接的库,需要链接进你的 Mach-O 文件里,如果需要更新就要重新编译一次,无法动态加载和更新;

加载动态库有两种方式:

  • 一种是,在程序开始运行时通过 dyld 动态加载。通过 dyld 加载的动态库需要在编译时进行链接,链接时会做标记,绑定的地址在加载后再决定。

  • 第二种是,显式运行时链接(Explicit Runtime Linking),即在运行时通过动态链接器提供的 API dlopen 和 dlsym 来加载。这种方式,在编译时是不需要参与链接的。
    不过,通过这种运行时加载远程动态库的 App,苹果公司是不允许上线 App Store 的,所以只能用于线下调试环节。

而动态库是运行时链接的库,使用 dyld 就可以实现动态加载

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。
也就是说,这些符号会显示为“未定义”,
但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

dlopen 会把共享库载入运行进程的地址空间,载入的共享库也会有未定义的符号,这样会触发更多的共享库被载入。
dlopen 也可以选择是立刻解析所有引用还是滞后去做。
dlopen 打开动态库后返回的是引用的指针
dlsym 的作用就是通过 dlopen 返回的动态库指针和函数符号,得到函数的地址然后使用。

使用 dyld 加载动态库,有两种方式:
有程序启动加载时绑定

符号第一次被用到时绑定

为了减少启动时间,大部分动态库使用的都是符号第一次被用到时再绑定的方式。

加载过程开始会修正地址偏移,iOS 会用 ASLR 来做地址偏移避免攻击,确定 Non-Lazy Pointer 地址进行符号地址绑定,加载所有类,最后执行 load 方法和 Clang Attribute 的 constructor 修饰函数。

每个函数、全局变量和类都是通过符号的形式定义和使用的,当把目标文件链接成一个 Mach-O 文件时,链接器在目标文件和动态库之间对符号做解析处理。

dylib 这种格式,表示是动态链接的,编译的时候不会被编译到执行文件中,在程序执行的时候才 link,这样就不用算到包大小里,而且不更新执行程序就能够更新库。

dyld 做了这么几件事:
  • 先执行 Mach-O 文件,根据 Mach-O 文件里 undefined 的符号加载对应的动态库,系统会设置一个共享缓存来解决加载的递归依赖问题;
  • 加载后,将 undefined 的符号绑定到动态库里对应的地址上;
  • 最后再处理 +load 方法,main 函数返回后运行 static terminator。
    调用 +load 方法是通过 runtime 库处理的
    在代码里可以发现 Class 的 +load 是先执行的,然后执行 Category 。

编译阶段由于有了链接器,你的代码可以写在不同的文件里,每个文件都能够独立编成 Mach-O 文件进行标记。编译器可以根据你修改的文件范围来减少编译,通过这种方式提高每次编译的速度。

了解了这种链接机制,你也能够明白,文件越多,链接器链接 Mach-O 文件所需绑定的遍历操作就会越多,编译速度也会越慢。

了解程序运行阶段的动态库链接原理,会让你更多地了解程序在启动时做的事情,同时还能够对你有一些启发。

比如,在开发调试阶段,是不是代码改完以后可以先不去链接项目里的所有文件,只编译当前修改的文件动态库,通过运行时加载动态库及时更新,看到修改的结果。这样调试的速度,不就能够得到质的提升了么。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值