技术概要

320 阅读27分钟

CCache原理

优点:
满足我们追求的无侵入、无影响现有的业务的要求,无入侵、且开发人员无感知。
确实能大幅度地提升编译速度,最快时提高一倍以上的编译时间。
不需要对项目作出大调整,只需部署相关环境和一些脚本支持。
不需要改变开发工具链。同一个目录下,CCache 的缓存命中率相对稳定。
对我们项目中有存在些问题点:
在未有缓存的情况下,首次打包编译的时间比原来的翻近一倍,原来20+min,首次将近40+min,在资源紧张的情况下,甚至是70min+。
修改一些引用较多的文件(如公共库、底层库改动),容易造成大范围的缓存失效,速度会变得比原来未使用ccache时更慢。
多个项目相同的组件不支持缓存共享,我们有多个分支打包的需求,修改目录名称后,缓存即失效。
我们机器的Ccache最大的缓存上限约18GB,且Debug/Release区别缓存,项目占用5GB+的缓存,多个项目、多个分支很容易超出上限,一台机器不能同时支持多个项目。
对机器硬盘读写要求高,如不是全部固态硬盘,速度影响大。
CCache 不支持 Clang Modules,系统框架例如 AVFoundation、CoreLocation等, Xcode 不会再帮你自动引入,会导致编译失败。
CCache 目前不支持 Swift
CCache 不支持 PCH 文件

App启动

启动过程:

Dyld-Runtime-main三步走
1.当手指点击APP图标时,会创建一个进程
2.然后调用Dyld(动态连接器)接管后续的工作
3.加载可执行文件-dyld会调用 instantiateFromLoadedImage 函数将二级制文件加载到内存 (二进制文件常被称为 image,包括可执行文件、动态库等)
加载过程分三步:
(1)合法性检查,可执行文件是否在当前CPU架构下运行
(2)选在imgaeLoader加载可执行文件。
(3)注册image信息
4.link函数-
(1)首先调用 recursiveLoadLibraries,递归加载程序所需的动态链接库。
(2)Rebase && Bind
因为地址空间加载随机化(ASLR,Address Space Layout Randomization)的缘故,二进制文件最终的加载地址与预期地址之间会存在偏移,所以需要进行 rebase 操作,对那些指向文件内部符号的指针进行修正,在 link 函数中该项操作由 recursiveRebase 函数执行。rebase 完成之后,就会进行 bind 操作,修正那些指向其他二进制文件所包含的符号的指针,由 recursiveBind 函数执行。
当 rebase 以及 bind 结束时,link 函数就完成了它的使命
Rebase 修正内部(指向当前mach-o文件)的指针指向
Bind 修正外部指针指向
5.Dyld 在 bind 操作结束之后,会发出 dyld_image_state_bound 通知,然后与之绑定的回调函数 map_2_images 就会被调用,就是Runtime启动过程(可以看源码)
它主要做以下几件事来完成 Objc Setup: 简称初始化Objective C Runtime
1、调用map_images进行读取可执行文件的 DATA 段内容,找到与 objc 相关的信息
2、注册 Objc 类一个TableVIew里面
3、Category中的方法注册到对应的类中
4、确保 selector 的唯一性
到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理成功加载到内存中,被runtime 所管理。

6.Initializers Objc SetUp 结束后,Dyld 便开始运行程序的初始化函数,该任务由 initializeMainExecutable 函数执行。整个初始化过程是一个递归的过程,顺序是先将依赖的动态库初始化,然后在对自己初始化。初始化需要做的事情包括:

  1. 调用 Objc 类的 + load 函数
  2. 调用 C++ 中带有 constructor 标记的函数
  3. 非基本类型的 C++ 静态全局变量的创建 main 当初始化结束之后,可执行文件才处于可用状态,之后 Dyld 就会去调用可执行文件的 main 函数,开始程序的运行。

二进制插桩重排:

  用户使用时并不会使用到全部内存,如果 App 一启动就全部加载到内存中会浪费很多内存空间。 虚拟内存技术 的出现就是为了解决这个内存浪费问题。 App 启动后会认为自己已经获取到整个 App 运行所需的内存空间,但实际上并没有在物理内存上为他申请那么大的空间,只是生成了一张 虚拟内存和物理内存关联的表
  mach-o代码段:
  __TEXT 代码段,包括头文件、代码和常量。只读不可修改。
  __DATA - 数据段,包括全局变量, 静态变量等。可读可写。
  __LINKEDIT - 如何加载程序, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息。只读不可修改。

Http&https工作原理

Https 证书传递流程

  1. 客户端发起https请求。
  2. 服务端的配置。采用https协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。这套证书其实就是一对公钥和私钥,如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
  3. 传送证书,证书中包含了很多信息,如证书的颁发机构,过期时间,公钥等。
  4. 客户端解析证书。这部分工作是由客户端的TLS来完成的,首先会验证证书是否有效,比如颁发机构,过期时间等,具体过程参考上图。如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随机值(这个随机值相当于客户端的私钥),然后用证书中的公钥对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
  5. 传送加密信息。这部分传送的是用公钥加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
  6. 服务端解密信息。服务端用私钥解密后,得到了客户端传过来的随机值,然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥(随机值)通过某种算法混合在一起,这样除非知道私钥(随机值),不然无法获取内容,而正好客户端和服务端都知道这个私钥(随机值),所以只要加密算法够彪悍,私钥(随机值)够复杂,数据就够安全。
  7. 传输加密后的信息。这部分信息是服务端用私钥(随机值)加密后的信息,可以在客户端被还原。
  8. 客户端解密信息。客户端用之前生成的私钥(随机值)解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

三次握手

  TCP(Transmission Control Protocol) 传输控制协议 TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接 位码即tcp标志位,有6种标示:
SYN(synchronous建立联机)
ACK(acknowledgement 确认)
PSH(push传送)
FIN(finish结束)
RST(reset重置)
URG(urgent紧急)
Sequence number(顺序号码)
Acknowledge number(确认号码)
establish 建立,创建
  所谓三次握手(Three-Way Handshake)即建立TCP连接,是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
  (1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
  (2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
  (3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

数据结构

数组和链表的区别

数组的优点
  随机访问性强
  查找速度快
数组的缺点
  插入和删除效率低
  可能浪费内存
  内存空间要求高,必须有足够的连续内存空间。
  数组大小固定,不能动态拓展
链表的优点
  插入删除速度快
  内存利用率高,不会浪费内存
  大小没有固定,拓展很灵活。
链表的缺点
  不能随机查找,必须从第一个开始遍历,查找效率低

  • 链表在iOS中的应用
    AutoreleasePool:
    • AutoreleasePool中的 AutoreleasePoolPage 是以双向链表的形式连接起来的.
      objc_autoreleasePoolPush方法被调用时会牵扯到POOL_SENTINEL、hotPage等,
      1.有hotPage且未满时, page->add
      2.当hotPage已满时它会从传入的 page 开始遍历整个双向链表,直到:查找到一个未满的 AutoreleasePoolPage使用构造器传入 parent 创建一个新的 AutoreleasePoolPage在查找到一个可以使用的 AutoreleasePoolPage 之后,会将该页面标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。
      3.如果当前内存中不存在 hotPage,就会调用 autoreleaseNoPage 方法初始化一个AutoreleasePoolPage。
class AutoreleasePoolPage {    
   magic_t const magic;//magic 用于对当前 AutoreleasePoolPage
   完整性的校验    id *next;//指向了下一个为空的内存地址   
   pthread_t const thread;//thread 保存了当前页所在的线程  
   AutoreleasePoolPage * const parent;//parent 和 child 就是用来构造双向链表的指针 
   AutoreleasePoolPage *child;  
   uint32_t const depth; 
   uint32_t hiwat;
};

  (1)alloc/new/copy/mutableCopy等持有对象的方法,不会加入autoreleasePool;其他不持有对象的方法通过objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue来判断是否需要加入autoreleasePool,这是编译器的优化。
  (2)iOS5及之前的编译器,关键字__weak修饰的对象,会自动加入autoreleasePool;iOS5及之后的编译器,则直接调用的release,不会加入autoreleasePool。
  (3)id指针和对象指针(id *NSError **),会自动加上关键字__autorealeasing,加入autoreleasePool。
  NSNotification: NSNotificationCenter是中心管理类,实现较复杂。总的来讲在NSNotificationCenter中定义了两个Table,同时为了封装观察者信息,也定义了一个Observation保存观察者信息。这里主要是来保存观察者信,并且发送通知时逐个遍历调用其中方法。Observation的结构如图

typedef struct  Obs {
  id        observer;   /* 保存接受消息的对象*/  
  SEL       selector;   /* 保存注册通知时传入的SEL*/  
  struct Obs    *next;      /* 保存注册了同一个通知的下一个观察者*/ 
  struct NCTbl  *link;  /* 保存改Observation的Table*/
} Observation;

以Object为Key,用链表来保存所有的观察者,并且以这个链表为Value。

NSArray&NSMutbleArray

哈希表

  哈希表(hash table,也叫散列表),是根据键(key)直接访问访问在内存储存位置的数据结构。
  哈希冲突当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。冲突就难免会发 生。另外,当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新记录,不仅发生冲突,而且还会发生溢出
  用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定 的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的 地址则表明表中无待查的关键字,即查找失败。
  拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。

  • Hash在iOS中的应用
      1.Weak表
      2.Runloop

iOS多线程

在我们实际开发的过程中,经常会遇到多任务处理的情况,我们期望任务的处理顺序按开发者的意愿来进行,而不是刻板的按照某一种方法或原则,由此,调度任务时间顺序的需求应运而生,我们管这个需求的解决方案叫多线程。
1、实现多个请求完成刷新UI的方法(gcdgroup、信号量、rac zip
2、实现多个请求顺序执行的方案(RACContactdispatch_barrier_async

iOS多线程:GCD

iOS界面渲染

离屏渲染

  在上面的渲染流水线示意图中我们可以看到,主要的渲染操作都是由CoreAnimation的Render Server模块,通过调用显卡驱动所提供的OpenGL/Metal接口来执行的。通常对于每一层layer,Render Server会遵循“画家算法”,按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销)。
”画家算法“,把每一层依次输出到画布
  然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作。
  如果要绘制一个带有圆角并剪切圆角以外内容的容器,就会触发离屏渲染。我的猜想是(如果读者中有图形学专家希望能指正):
  将一个layer的内容裁剪成圆角,可能不存在一次遍历就能完成的方法 容器的子layer因为父容器有圆角,那么也会需要被裁剪,而这时它们还在渲染队列中排队,尚未被组合到一块画布上,自然也无法统一裁剪
此时我们就不得不开辟一块独立于frame buffer的空白内存,先把容器以及其所有子layer依次画好,然后把四个角“剪”成圆形,再把结果画到frame buffer中。这就是GPU的离屏渲染

离屏渲染+UI优化

  • 1 各种圆角阴影渲染避免用drawrect(CPU)cornerRadius等 用CAShapeLayer(GPU)UIBezierPath(GPU)来替代。
  • 2 利用runloop来实现在scrollview滑动的时候不加载图片,从而优化滑动帧数。
  • 3 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit
  • 4 设置layeropaque值为YESGPU不用考虑多色图层层叠混色的问题),减少复杂图层合成尽量使用不包含透明(alpha)通道的图片资源(opaque =YES: GPU将不会做任何的计算合成,不需要考虑它下方的任何东西(因为都被它遮挡住了),而是简单从这个层拷贝。这节省了GPU相当大的工作量。由此看来,opaque属性的真实用处是给绘图系统提供一个性能优化开关!,)
  • 5 尽量设置layer的大小值为整形值
  • 6 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  • 7 很多情况下用户上传图片进行显示,可以让服务端处理圆角
  • 8 使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPathCoreGraphics框架)画出来圆角图片
  • 9 适当的时候使用shouldRasterize开启光栅化、当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。

OC-RunTime

Class结构

typedef struct objc_class *Class;

struct objc_class {
    Class isa; // 指向metaclass  
    
    Class superclass;  // 指向父类Class
    const char *name;  // 类名 
    uint32_t version;  // 类的版本信息
    uint32_t info;      // 一些标识信息,标明是普通的Class还是metaclass
    uint32_t instance_size;     // 该类的实例变量大小(包括从父类继承下来的实例变量);
    struct old_ivar_list *ivars;    //类中成员变量的信息
    struct old_method_list **methodLists;   类中方法列表
    Cache cache;    查找方法的缓存,用于提升效率
    struct old_protocol_list *protocols;  // 存储该类遵守的协议  
}

函数调用过程

判断 receiver(objc_msgSend的第一个参数self) 是否为 nil。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉
从 cache 中查找 SEL 对应的 IMP,若找到,则执行,否则
从对象的方法链表中根据 SEL 去查找 IMP,找到后加入到缓存。查找过程如下:
  从对象的 Class 的 method_list 寻找 selector,如果找到,填充到缓存中,并返回 selector,否则
  从对象的超类的 method_list 寻找,并依次往上寻找,直到找到 selector,填充到缓存中,并返回 selector,否则
  调用 _class_resolveMethod,动态方法决议(这里进入消息转发),若找到 selector 对应的方法实现 IMP,不缓存,方法返回,否则
  转发这个 selector,否则
报错,抛出异常

消息转发机制

  • 类的动态方法解析   在该类和父类中没有找到该方法的实现则会执行 +(BOOL)resolveClassMethod:(SEL)sel+(BOOL)resolveInstanceMethod:(SEL)sel 方法。在这两个方法中利用黑魔法runtime 动态添加方法实现。
  • 备用接受者   在类中实现- (id)forwardingTargetForSelector:(SEL)aSelector 方法 并返回一个备用接受者对象
  • 完整的消息转发   在该类中实现-(void)forwardInvocation:(NSInvocation*)anInvocation- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector方法

应用

  • 动态扩展属性
  • 交换方法用于统一处理某个方法
  • 遍历类属性--映射解析
  • 修改isa指针,自己实现kvo
  • 利用消息转发机制实现crash防护

组件化开发

  所谓的组件化,通俗理解就是将一个工程分成各个模块,各个模块之间相互解耦,可以独立开发并编译成一个独立的 APP 进行调试,然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候,便于各个开发者之间分工协作、同步开发;被分割出来的模块又可以在项目之间共享,从而达到复用的目的。组件化有诸多好处,尤其适用于比较大型的项目。

RunLoop

概念

  RunLoop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

  • RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
  • Runloop的创建过程:[NSRunLoop currentRunLoop]-->CFRunLoopGetCurrent -->_CFRunLoopGet0 (在这个方法里面先是去全局字典里面去找线程(key)对应的runloop对象,若果有就取出返回,如果没有就创建一个runloop对象并以线程为key存到全局字典里面)。
  • 主线程的RunLoop会在初始化全局字典时创建
  • 子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
  • RunLoop会在线程销毁时销毁
  • RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 RunLoop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
  • CoreFoundation源码(包括runloop)

应用

  • RunLoop保证线程的长久存活
  • 保证NSTimer正常运转。
  • 滚动视图流畅性优化。滑动的时候不加载图片
  • 监测iOS卡顿   创建一个 CFRunLoopObserverRef,并提供 runLoopObserverCallBack 记录 Runloop 的 CFRunLoopActivity 变化 向 main Runloop 添加该 observer 开启异步线程,并以指定间隔持续监测 CFRunLoopActivity 连续 3 次检测到 kCFRunLoopBeforeSources 或者 kCFRunLoopAfterWaiting 时,认为当前处于卡顿状态,触发 卡顿 的数据收集

iOS关键字大全

  • strong与retain:主要区分在修饰block,strong在修饰block的时候就相当于copy,而retain修饰block的时候就相当于assign,这样block会出现提前被释放掉的危险
  • weak和assign:weak比assign多了一个功能,前者会在引用的对象被释放的时候将该属性置为nil
  • copy与retain: copy与retain处理流程一样,先对旧值release,再copy出新的对象,而retain是指针拷贝,拷贝一份原来的指针,释放原来指针指向的对象的内容,再令指针指向新的对象内容,简单来说就是一个是内容拷贝,一个是指针拷贝。
  • const与宏:
    1.编译时刻不同,宏属于预编译 ,const属于编译时刻
    2.宏能定义代码,const不能,使用宏定义过多的话,随着工程越来越大,编译速度会越来越慢,const只会编译一次,缩短编译时间。
    3.宏不会检查错误,const会检查错误
  • extension和category:
    1.extension和category在编译期决议,它就是类的一部分,但是category则完全不一样,它是在运行期决议的

MVC、MVP、MVVM浅析

从MVC、MVP到MVVM,实际上是模型和视图的分离过程。MVC中模型和视图没有完全分离,造成Activity代码臃肿,MVP中通过Presenter来进行中转,模型和视图彻底分离,但由于V和P互相引用,代码不够优雅。ViewModel通过Data Binding实现了视图和数据的绑定,解决了这种MVP的缺陷。但是数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。

iOS静态库和动态库

  • 静态库:
      链接时会被完整的复制到可执行文件中,被多次使用就有多分拷贝。如下图,抖音和微信的可执行文件加载静态库时,每个可执行文件链接时都要加载这份静态库。
  • 动态库
    系统动态库:
      链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用,节省内存。
    自己打包的动态库:
      自己打包的动态库,则是在应用程序里的,但是与静态库不同,它不在可执行文件中。

KVC原理

  • setValue:forKey:
      以set<Key>:_set<Key>的顺序查找对应命名的setter方法,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换);
      如果没有发现setter方法,但是accessInstanceVariablesDirectly类属性返回YES,则按_<key>_is<Key><key>is<Key>的顺序查找一个对应的实例变量。如果发现则将value赋值给实例变量;
      如果没有发现setter方法或实例变量,则调用setValue:forUndefinedKey:方法,默认抛出一个异常,但是一个NSObject的子类可以提出合适的行为。
  • valueForKey:
      通过getter方法搜索实例,以get<Key>, <key>, is<Key>, _<key>的顺序搜索符合规则的方法,如果有,就调用对应的方法;
      如果没有发现简单getter方法,并且在类方法accessInstanceVariablesDirectly是返回YES的的情况下搜索一个名为_<key>_is<Key><key>is<Key>的实例;
      如果返回值是一个对象指针,则直接返回这个结果;如果返回值是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回;如果返回值是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回;
      在上述情况都失败的情况下调用valueForUndefinedKey:方法,默认抛出异常,但是子类可以重写此方法。

Xcode编译过程

预处理

  • 宏替换
  • 头文件引入(#include,#import)
  • 处理条件编译指令 (#if,#else,#endif)

词法解析

  • 这一步把源文件中的代码转化为特殊的标记流,源码被分割成一个一个的字符和单词,在行尾Loc中都标记出了源码所在的对应源文件和具体行数,方便在报错时定位问题.

语法分析

  • 这一步是把词法分析生成的标记流,解析成一个抽象语法树(abstract syntax tree -- AST),同样地,在这里面每一节点也都标记了其在源码中的位置.

静态分析

  • 类型检查
  • 其他分析

中间代码生成和优化

  • 接下来 LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,最后输出汇编代码

汇编

  • 在这一阶段,汇编器将上一步生成的可读的汇编代码转化为机器代码。最终产物就是 以 .o 结尾的目标文件。使用Xcode构建的程序会在DerivedData目录中找到这个文件

链接

  • 这一阶段是将上个阶段生成的目标文件和引用的静态库链接起来,最终生成可执行文件,链接器解决了目标文件和库之间的链接

设计模式七大原则

单一职责原则 SRP:

一个类只负责一项职责,假设某个类 P 负责两个不同的职责,职责 P1 和 职责 P2,那么当职责 P1 需求发生改变而需要修改类 P,有可能会导致原来运行正常的职责 P2 功能发生故障.

开放-关闭原则 OCP:

开放-关闭原则表示软件实体 (类、模块、函数等等) 应该是可以被扩展的,但是不可被修改.例:3参数扩展4参数方法

里氏替换原则 LSP:

里氏替换原则的重点在不影响原功能,而不是不覆盖原方法。里氏替换原则告诉我们,当使用继承时候,类 B 继承类 A 时,除添加新的方法完成新增功能 P2,尽量不要修改父类方法预期的行为。

依赖倒转原则 DIP:

依赖倒转原则的核心思想就是面向接口编程,高层模块不应该依赖低层模块,二者都应该于抽象。进一步说,抽象不应该依赖于细节,细节应该依赖于抽象。

接口隔离原则 ISP:

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

迪米特法则 LOD:

迪米特法则又称为 最少知道原则,它表示一个对象应该对其它对象保持最少的了解。通俗来说就是,只与直接的朋友通信。

组合/聚合复用原则 CRP:

组合/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分; 新的对象通过向这些对象的委派达到复用已有功能的目的。

iOS签名原理

Weak的实现原理

  Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
weak 的实现原理可以概括一下三步:
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。