学习摘要

319 阅读19分钟

编译原理

主要过程

      预编译             -->                编译                  -->           汇编   -->            链接      
 *展开define/去除注释/加行号       语义检查/生成AST/静态分析(语法检查)/其它分析      翻译成IR/mach-o     查找链接O生成可执行文件*
 --> 拷贝资源文件 -->  编译/链接storyboard  --> 编译Asset --> Coacopods脚本 --> 签名

编译链接过程

  1. 预处理:macro 宏, import 头文件替换及处理其他的预编译指令,产生.i文件。(都是以#号开头)
  2. 编译:把预处理完的一系列文件进行一系列词法、语法、语义分析,并且优化后生成相应的汇编代码,产生.s文件;
  3. 汇编:汇编器将汇编代码生成机器指令,输出目标文件,产生.o文件(根据汇编指令和机器指令的对照表一一翻译就可以了);
  4. 链接:在一个文件中可能会到其他文件,因此,还需要将编译生成的目标文件和系统提供的文件组合到一起,这个过程就是链接。经过链接,最后生成可执行文件。 经过编译和链接,才会把写的代码转换成计算机能识别的二进制指令。

预编译

预处理主要处理规则如下:

  • 删除所有#define,并将所有宏定义展开,在源码中使用的宏定义会被替换为对应代码
  • 将被包含的文件插入到预编译指令(#include)所在位置(这个过程是递归的)
  • 删除所有注释:// 、/* */等
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及编译时能够显示警告和错误的所在行号
  • 保留所有的#pragma编译器指令,因为编译器须要使用它们

当我们无法判断宏定义是否正确或者头文件是否包含时可以查看预编译后的文件来确定问题 ​

App启动过程

App的启动的三个阶段

  1. main()函数执行之前(Pre-main阶段)
  • dylib loading: 加载可执行文件
  • rebase/binding: rebase指针调整和bind符号绑定
  • Objc setup: Objc runtime初始化, Class注册, Category注册, Selector检测等
  • initializer: 初始化, +load()方法执行, C++全局变量初始化,C/C++静态初始化对象和标记为__attribute__(constructor)的方法 这里要提一点的就是,+load方法已经被弃用了,如果你用Swift开发,你会发现根本无法去写这样一个方法,官方的建议是用initialize。区别就是,load是在类装载的时候执行,而initialize是在类第一次收到message前调用。

iOS13之后使用dyld3加载: dyld2是纯粹的in-process,也就是在程序进程内执行的,也就意味着只有当应用程序被启动的时候,dyld2才能开始执行任务。 dyld3则是部分out-of-process,部分in-process。图中,虚线之上的部分是out-of-process的,在App下载安装和版本更新的时候会去执行,out-of-process会做如下事情: i. 分析Mach-o Headers ii. 分析依赖的动态库 iii. 查找需要Rebase & Bind之类的符号 iV. 把上述结果写入缓存 ​

  1. main()函数执行(main执行到设置rootViewController前)
  • 首页Controller的加载, 以及其子Controller的加载
  1. 首屏渲染(设置rootViewController后到didFinishLaunchWithOptions)
  • 初始化一些其他功能(第三方库等)

启动优化

减少bundle -- dylib阶段 减少category -- Rebase & Bind & Runtime 阶段 减少+load和__atribute__((constructor)) -- Initializers阶段 lazy初始化不重要的sdk和逻辑、只初始化必要的vc - main阶段 使用Swift -- 原因 二进制重排 -- 启动过程 辅助排查工具:TimeProfile

多线程和RunLoop

什么是RunLoop

runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
  • runloop: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。
  • runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认创建并启动,子线程的RunLoop需手动创建且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。

RunLoop的作用?

  • 1.保持程序运行
  • 2.处理app的各种事件(比如触摸,定时器等等)
  • 3.节省CPU资源,提高性能。

RunLoop内部是怎么实现的?

  1. RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
  2. 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
  3. 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

runloop的mode是用来做什么的?有几种mode?

  • model:是runloop里面的运行模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode: (1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。 (2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。 (3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。 (4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。 (5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes

Runloop启动过程

1.通知观察者 run loop 已经启动 2.通知观察者将要开始处理Timer事件 3.通知观察者将要处理非基于端口的Source0 4.启动准备好的Souecr0 5.如果基于端口的源Source1准备好并处于等待状态,立即启动:并进入步骤9 6.通知观察者线程进入休眠 7.将线程置于休眠直到任一下面的事件发生 (1)某一事件到达基于端口的源 (2)定时器启动 (3)Run loop 设置的时间已经超时 (4)run loop 被显式唤醒 8.通知观察者线程将被唤醒 9.处理未处理的事件,跳回2 (1)如果用户定义的定时器启动,处理定时器事件并重启 run loop。进入步骤 2 (2)如果输入源启动,传递相应的消息 (3)如果 run loop 被显式唤醒而且时间还没超时,重启 run loop。进入步骤 2 10.通知观察者run loop 结束 ​

什么是虚拟内存

虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操作系统是如何记住这种映射关系的呢,答案就是页表,页表中记录了虚拟内存地址到物理内存地址的映射关系。有了页表就可以将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。 每个进程都有自己的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。

问题:

线程和进程的区别,进程间怎么通信的?

1.进程之间不能共享内存,线程可以 同一个进程中的线程共享了改进程的很多资源,包括:进程虚拟空间、进程代码段、进程共有数据等,因此线程之间更容易相互通信,多线程的运行效率远远高于多进程; 2.系统创建进程的时候要为其分配系统资源,而创建线程则只需要很小一部分,因此多线程比多进程来的更加容易; 3.多线程可以充分利用处理器(双核或者多核),但是当线程数量达到上限的时候,性能就不在提升了; 4.多线程的进程中一个线程崩溃了就会导致进程崩溃,如果是主线程崩溃会导致程序崩溃; 但是多进程中子进程崩溃了不会影响到其它进程,程序稳定性更好; 5.多线程需要控制线程之间的同步,而多进程则需要控制和主进程之间的交互; 6.如果两个进程之间要相互传输大量的数据,会相当影响性能,多进程适合小数据量传输,密集运算; ​

进程间切换和线程间切换的区别、线程切换需要寄存器吗?

进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。 ​

为什么虚拟地址切换很慢:现在我们已经知道了进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB,Translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。

performSelector:afterDelay:在子线程为什么无法执行?

performSelector:afterDelay:为在当前Runloop中延迟3秒后执行selector中方法。 使用该方法需要注意以下事项: 在子线程中调用performSelector: withObject: afterDelay:默认无效,因为子线程的Runloop默认不开启,因此无法响应方法。 所以我们尝试在GCD Block中添加 [[NSRunLoop currentRunLoop]run];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self performSelector:@selector(sureTestMethod:)
                   withObject:params
                   afterDelay:3];
        [[NSRunLoop currentRunLoop]run];
    });

运行代码发现可以正常打印sureTestMethodCall。 这里有个坑需要注意,曾经尝试将 [[NSRunLoop currentRunLoop]run]添加在performSelector: withObject: afterDelay:方法前,但发现延迟方法仍然不调用,这是因为若想开启某线程的Runloop,必须具有timer、source、observer任一事件才能触发开启。

[self performSelector:@selector(sureTestMethod:)
                 onThread:thread
               withObject:params
            waitUntilDone:NO];

使用performSelector:onThread:方法一定要注意所在线程生命周期是否正常,若thread已销毁不存在,而performSelector强行执行任务在该线程,会导致崩溃:

RunLoop有几种模式,特点是什么?

model:是runloop里面的运行模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode: (1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。 (2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。 (3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。 (4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。 (5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes

线程里的内存管理是怎样的?

线程如果中途截断,资源会释放吗?进程呢?

线程不会释放资源,需要维护资源释放,特别是锁;

虚拟内存管理机制?

内存分页

iOS近期的系统中,基于A7和A8处理器的系统,物理内存按照4KB分页,虚拟内存按照16KB分页。基于A9处理器的系统,物理和虚拟内存都是以16KB进行分页 系统将内存页分为三个状态: (1)活跃的内存页(active page):内存页已经被映射到物理内存中,而且近期被访问过,处于活跃状态。 (2)非活跃的内存页(inactive page):内存页已经被映射到物理内存中,但是近期没有被访问过。 (3)可用的内存页(free page):没有关联到虚拟内存页的物理内存页集合。 当可用的内存页降低到一定的阀值时,系统就会采取低内存应对措施,在OSX中,系统会将非活跃内存页交换到硬盘上,而在iOS中,则会触发Memory Warning,如果你的App没有处理低内存警告并且还在后台占用太多内存,则有可能被杀掉。

页表

页表的功能是提供虚拟页面到物理页面的映射。 页表的记录条数与虚拟页面数相同。此外,内存管理单元依赖于页表来进行一切与页面有关的管理活动,这些活动包括判断某一页面号是否在内存里,页面是否受到保护,页面是否非法空间等等。 由于页表的特殊地位,决定了它是由硬件直接提供支持,即页表是一个硬件数据结构。

malloc

OC中,除了使用NSObject的alloc分配内存外,还可以使用c的函数malloc进行内存分配。malloc的内存分配当然也是先分配虚拟内存,然后使用的时候再映射到物理内存,不过malloc有一个缺陷,必须配合memset将内存区中所有的值设置为0。这样就导致了一个问题,malloc出一块内存区域时,系统并没有分配物理内存。然而,调用memset后,系统将会把malloc出的所有虚拟内存关联到物理内存上,因为你访问了所有内存区域。 为了解决这个问题,苹果官方推荐使用calloc代替malloc,calloc返回的内存区域会自动清零,而且只有使用时才会关联到物理内存并清零。

ASLR

ASLR(Address space layout randomization)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

虚拟内存的缺点

虚拟内存也有同样的缺点:硬盘的容量比内存大,但也只是相对的,速度却非常缓慢,如果和硬盘之间的数据交换过于频繁,处理速度就会下降,表面上看起来就像卡住了一样,这种现象称为抖动(Thrushing)。相信很多人都有过计算机停止响应的经历,而造成死机的主要原因之一就是抖动。 ​

线程的同步与互斥怎么实现?锁有几种方式?

juejin.cn/post/684490… 总共13种锁

1. os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10 macos10.12开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等

2. pthread_mutex_t pthread_cond_t

互斥锁及条件变量为POSIX接口,见《unix进程间通信.md》

3. NSLock NSRecursiveLock

NSLock是对pthread_mutex普通锁的封装。pthread_mutex_init(mutex, NULL);默认属性为 PTHREAD_MUTEX_NORMAL NSLock 遵循 NSLocking 协议,使用方法与pthread_mutex类似

4. NSCondition

NSCondtion是对pthread_mutex和pthread_cond的封装

5. NSCondtionLock

NSConditionLock是对NScondition的进一步封装

6. synchronized

@synchronized使用

使用注意事项

慎用@synchronized(self)

使用self对于外部可以修改使用的对象地址,容易外部混合使用@synchronized及其他锁导致_死锁_问题

7. pthread_rwlock 读写锁

pthread_rwlock经常用于文件等数据的读写操作

8. atomic属性

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁(使用了自旋锁);它并不能保证使用属性的过程是线程安全的(eg.一个属性array,atomic的话只能保证在外面set和get的时候线程安全,但是不能保证array addObject、removeObject线程安全); ​

GCD和NSThread、OperationQueue区别是什么?

知道协程吗?

参考

信号量的机制:生产者和消费者

内存

文章

1. 自动释放池的前世今生

a. 整个 iOS 的应用都是包含在一个自动释放池 block 中的; b. @autoreleasepool {} 被转换为一个 __AtAutoreleasePool 结构体,这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法; c. AutoreleasePoolPage 是一个 C++ 中的类:

objc-autorelease-AutoreleasePoolPage 它在 NSObject.mm 中的定义是这样的:

class AutoreleasePoolPage {
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};
  • magic 用于对当前 AutoreleasePoolPage完整性的校验
  • thread 保存了当前页所在的线程

每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)

b. 主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理;在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage

总结:

自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中 调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息

问题:

堆和栈的区别

C++内存结构

block原理

参考 参考2

自动释放池什么时候释放?

  • 当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池

算法和数据结构

问题: 虚函数的实现?虚函数表,存放在哪里? 多态的实现? malloc和new的区别 虚拟内存和物理内存的区别 vector和list的区别 索引,实现的数据结构(B-tree),b+树与b树和哈希表的比较 负载均衡怎么实现的? ​

算法: 给定一个升序序列和目标值,旋转序列,找出目标值的下标,要求O(logn) 实现镜像二叉树 设计模式知道吗?(单例,工厂,观察者)、装饰器模式了解吗? 求二叉树的左视图 一个排序链表,奇数位置正向排序,偶数位置逆向排序 经典股票售卖问题I & II ​

网络

问题: 说一下拥塞控制 tcp的三次握手,四次挥手 七层模型 QUIC原理 SSL机制 DNS域名解析的过程 ​

包大小

DATA迁移:避免加密导致文件无法被压缩 无效资源:图片、类-静态扫描 + 代码覆盖检查工具、三方SDK大小管控+下线 ​

KVO和KVC

Key-Value Coding

键值编码。它是一种不通过存取方法,而通过属性名称字符串间接访问属性的机制。
1.首先查找有无<property>,set<property>,is<property>等property属性对应的存取方法,若有,则直接使用这些方法;
2.若无,则继续查找_<property>,_get<property>,*set<property>等方法,若有就使用;
3.若查询不到以上任何存取方法,则尝试直接访问实例变量<property>,* <property>;\
4.若连该成员变量也访问不到,则会在下面方法中抛出异常。**之所以提供这两个方法,就是让你在因访问不到该属性而程序即将崩掉前,供你重写,在内做些处理,防止程序直接崩掉。
`valueForUndefinedKey:`和`setValue:forUndefinedKey:`方法。

Key-Value Obersver,

键值观察。它是观察者模式的一种衍生。基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,会自动的通知观察者。这里所谓的通知是触发观察者对象实现的KVO的接口方法
KVO的原理:KVO是怎么实现的?

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中被观察属性的 setter 方法,在setter方法里使其具有通知机制。因此 ,要想KVO生效,必须直接或间接的通过setter方法访问属性(KVC的setValue就是间接方式)。直接访问成员变量KVO是不生效的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

重新的setter方法里到底干了什么,而使其就有了通知机制呢?其实只是在setter方法里,给属性赋值的前后分别调用了两个方法

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

- (void)didChangeValueForKey:(NSString *)key;会调用

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context;

MVVM

www.ruanyifeng.com/blog/2015/0…