说在前面:
许久没更新,最近整理就文件的时候,突然翻到两年前换工作时整理的思维导图,包含了原理八股文,网络,算法,以及架构,重构解决方案等,基本上面试必问的一些项目以及原理都包含在内了。当然了这里整理的很多内容都只是帮助当时的我回忆知识脉络,并没有深入说明,还需要读者自己去查阅资料深入理解。希望能也帮助到最近准备换工作的小伙伴梳理汇总知识点。思维导图高清原图下载地址
欢迎关注一下我的 Github: https://github.com/CYXiang
后续会更新一些AI、Flutter等相关文章。
iOS原生相关
Objective-C 底层原理
-
OC对象
-
NSObject本质
- NSObject底层是结构体,有一个 Class isa 指针
- 创建一个NSObject对象系统分配16字节(至少16),只使用了8字节(用于存放isa),objc源码
-
复杂对象本质
-
包含父类成员结构+子类成员属性
- 内存对齐,结构体的大小必须使最大成员变量的倍数
-
-
属性和方法
- 实例对象存放成员变量,不存放方法(存一份在类对象就够了)
-
内存分配注意点
- 内存分配方式内存对齐、桶、堆内存、16的倍数。目的是内存优化(gnu 内存对齐)<sizeof(a) 是个运算符,编译就确定了,不是函数>
-
对象分类三类
-
instance 实例对象
- isa指针、成员变量值
-
class 类对象
- isa指针、superclass指针、类的属性信息(@property)、类的对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar)
-
meta-class 元类对象
- object_getclass([NSObject class]);
- 与class类对象结构体一样
- isa指针、superclass指针、类的类方法信息(class method)
-
-
isa指向哪里?
- instance的isa指向class、class的isa指向meta-class、meta-class的isa指向基类的meta-class
- 调用对象方法轨迹:isa找到class,方法不存在,就通过superclass找父类的方法,再不存在就找基类..
-
superclass指向哪里?
- class的superclass指向父类的class(如果没有父类,superclass指针为null)、meta-class的superclass指向父类的meta-class(基类的meta-class的superclass指向基类的class)
- 调用类方法轨迹:isa找meta-class,方法不存在,就通过superclass找父类
-
OC的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值存放在instance对象中
-
-
KVO
-
本质是什么?
- 利用Runtime动态生成一个子类,让instance对象的isa指向这个子类NSKVONotifying_XXX
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
- _NSSetXXXValueAndNotify函数内部触发监听器的监听方法
-
如何手动触发KVO?
- 手动调用willChangeValueForKey: 和 didChangeValueForKey:
-
-
KVC
-
1、KVC赋值会触发KVO吗?
- 会!
-
赋值原理
- 赋值顺序:①-(void)setKey; ②-(void)_setKey; 查看accessInstanceVariablesDirectly> ③_key; ④_isKey; ⑤key; ⑥isKey
-
取值原理
- 取值顺序:① getKey、key、 isKey、_key方法 ②查看accessInstanceVariablesDirectly ③按照
_key、_isKey、key、isKey顺序查找成员变量
- 取值顺序:① getKey、key、 isKey、_key方法 ②查看accessInstanceVariablesDirectly ③按照
-
-
Cateogry
通过runtime动态将分类合并到类/元类对象中
-
实现原理
- 1、通过Runtime加载某个类所有的Category数据
- 2、把所有Category的方法、属性、协议数据,合并到一个大数组中(后面参与编译的Category数据,会在数组的前面)
- 3、将合并后的分类数据(方法、属性、协议),插入到类原来的数据前面
-
Category与Class Extension的区别?
- Class Extension在编译时数据就包含在类信息中
- Category是在运行时才会将数据合并到类信息中
-
+load方法
在runtime加载类、分类时调用,在程序运行过程中只调用一次
-
调用顺序
- 1、先调用类的+load。(按照编译先后顺序调用,先编译先调用。调用子类的+load之前会先调用父类的+load)
- 2、在调用分类的+load。(按照编译先后顺序调用,先编译先调用)
-
+initialize方法
+initialize方法会在类第一次接收到消息时调用
-
调用顺序
- 先调用父类的+initialize,再调用子类的+initialize(先初始化父类,再初始化子类,每个类只会初始化一次)
-
关联对象
-
原理
- 1、关联对象并不是存储在关联对象本身内存中
- 2、关联对象存储在全局统一的一个AssociationManage中
- 3、设置关联对象为null,就是移除关联对象
-
-
block
-
原理/本质
- 本质是OC对象,内部也有个isa指针
- 封装了函数调用以及函数调用环境的OC对象
-
block的类型(三种,内存存储区域不同)
-
NSGlobalBlock
- 数据区域Data区(与全局变量一起)
-
NSStackBlock
- 栈(需要手动销毁)
-
NSMallocBlock
- 堆(自动销毁)
-
-
block的变量捕获(capture)
-
为了保证block内部能够正常访问外部变量,有个变量捕获机制
-
局部变量(捕获!)
- auto类型(自动变量,离开作用域就销毁):值传递
- static类型(还能访问内存):指针传递
-
全局变量(不捕获!)
- 直接访问
-
-
-
__block修饰符
-
用于解决修改block内部无法修改auto变量值的问题(不能修饰全局变量、静态变量 static)
- 编译器会把__block变量包装成一个对象
-
-
__block内存管理
1、当block在栈上时,并不会对__block变量产生强引用
-
2、当block被copy到堆时
- 会调用block内部的copy函数
- copy函数内部会调用_Block_object_assign函数
- _Block_object_assign函数会对__block变量形成强引用(retain)
block循环引用问题?
-
-
Runtime
-
objc_msgSend执行流程三大阶段
-
1、消息发送
- 先找自身缓存,自身缓存找不到找父类方法缓存找,找不到在父类方法列表查找,以此类推,找到就缓存到自身缓存中。找不到进入阶段2 ↓
-
2、动态方法解析
- ①调用+resolvenInstanceMethod: 或者+resolvenClassMethod: 方法来动态解析 ,进行动态添加方法
- ②标记为已经动态解析 YES
- ③回到阶段1、消息发送,因为已经动态加了方法且已标记为YES(如果没有添加,进入阶段3)
-
3、消息转发
①调用(id)forwordingTargetForSelector:(SEL)aSelector 返回转发对象,(返回nil就走下一步)
-
②-返回方法签名(返回nil就报错,不为空就→)
- 不为空调用 -(void)forwardInvocation:(NSInvocation *)anInvocation
-
-
-
RunLoop
-
三种模式
NSDefalultRunLoopMode
UITrackingRunLoopMode
-
NSRunLoopCommonMode
- 并不是一个真的模式
-
运行逻辑
- 1、通知Observers:进入Loop;通知Observers:即将处理Timers;通知Observers: 即将处理Sources
- 2、处理Block;处理Source0(可能再次处理Block)
- 3、如果存在Source1,跳转到5
- 4、通知Observers,开始休眠(等待消息唤醒)
- 5、通知Observers,结束休眠(被某个消息唤醒)①处理Timer ②处理GCD ③处理Source1
- 6、根据前面的执行结果决定如何操作
-
NSTimer失效问题
- NSTimer在默认模式下,切换到NSRunLoopCommonMode
-
线程保活
- 1、添加Source; addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode]
- 2、加while(weakSelf&&!weakSelf.isStopped) 循环, 执行currentRunLoop runMode:beforeDate:
-
利用RunLoop监控卡顿
-
怎么才算卡顿?
- 1、进入睡眠前方法执行时间过长而导致无法进入睡眠
- 2、线程唤醒后接收消息时间过长尔无法进入下一步
-
如何监控卡顿?
-
关注两个阶段(进入睡眠之前和唤醒之后的两个loop状态定义的值)
-
1、kCFRunLoopBeforeSources
- 触发Source0回调
-
2、kCFRunLoopAfterWaiting
- 接收mach_port消息
-
-
-
如何监听
- 1、创建一个CFRunLoopObserverContext观察者
- 2、将观察者添加到主线程RunLoop的Common模式下观察。
- 3、再创建一个持续的子线程专门用来监控主线程的RunLoop状态
- 4、一旦发现进入睡眠前的kCFRunLoopBeforeSource状态或唤醒后的kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判断为卡顿,再dump出堆栈信息
-
-
-
多线程
pthread
NSThread
-
GCD
-
同步 sync
- 没有开启线程,串行执行任务
-
异步 async
- 并且是并发队列,才会开启新线程并发执行任务
-
Semaphore信号量
- 控制最大并发数量,也可用来线程同步(把信号量设 1 )
-
-
死锁
- ①使用sync函数
- ②往当前串行队列中添加任务
队列组
-
NSOperation
- 封装GCD
-
iOS线程同步方案(加锁、GCD串行队列)
-
OSSpinLock 自旋锁
- (不安全了不建议使用,优先反转问题)
-
os_unfair_lock
- OSSpinLock的替代方案
-
pthread_mutex
- pthread_mutex_signal 激活一个等待该条件的线程
-
dispatch_semphore
- 信号量
-
dispatch_queue(DISPATCH_QUEUE_SERIAL)
- GCD串行队列
-
pthread_mutex 的OC封装
NSLock
-
NSRecursiveLock
- 递归锁,保证能够递归调用
NSCondition
-
NSConditionLock
- 带条件的lock(生产者消费者模式)
-
@synchronized
- 性能最差
-
-
读写安全方案
-
1、多读单写(异步读,同步写),用于文件数据读写操作
-
pthread_rwlock 读写锁
- 互斥锁,等待锁的过程会进入休眠
-
dispatch_barrier_async 异步栅栏调用
- 传入的并发队列必须是dispatch_queue_create创建的(不是就没效果)
- 如果传入的是一个串行或全局并发队列,那这个函数等同于dispatch_async效果
-
-
-
内存管理
-
定时器
- GCD是最准确的,与RunLoop无关
-
内存布局
保留内存
-
代码段(__TEXT)
- 编译后的代码
-
数据段(__DATA)
- 字符串常量
- 已初始化数据
- 未初始化数据
-
堆(heap)
- 通过alloc、malloc、calloc等动态分配的空间
-
栈(stack)
- 函数调用开销,局部变量的开销
内核区
Tagged Pointer
MRC
copy
-
weak 原理
- 将弱引用存入到哈希表内,当对象销毁时就从表中取出弱引用并清除 (运行时操作)
-
ARC 原理
- 利用LLVM+Runtime,LLVM自动生成插入retain,release代码
autoRelease
-
UI层基本原理
-
事件传递机制
- 当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程
- 事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件
- 拦截事件 hitTest:withEvent:
-
UI绘制原理
在layer内部会创建一个backing store,我们可以理解为CGContextRef上下文。
-
判断layer是否有delegate:
- 如果有delegate,则会执行[layer.delegate drawLayer:inContext](这个方法的执行是在系统内部执行的),然后在这个方法中会调用view的drawRect:方法,也就是我们重写view的drawRect:方法才会被调用到。
- 如果没有delegate,会调用layer的drawInContext方法,也就是我们可以重写的layer的该方法,此刻会被调用到。
最后都由CALayer把绘制完的backing store(可以理解为位图)提交给GPU。
-
异步绘制原理
-
[UIView setNeedsDisplay]方法的时候,不会立马发送对应视图的绘制工作
- 调用[UIView setNeedsDisplay]后
- 然后会调用系统的同名方法[view.layer setNeedsDisplay]方法并在当前view上面打上一个脏标记
- 当前Runloop将要结束的时候才会调用[CALyer display]方法,然后进入到视图真正的绘制工作当中
-
是否知道异步绘制?如何进行异步绘制?
- 基于系统开的口子[layer.delegate dispayLayer:]方法
- 并且实现/遵从了dispayLayer这个方法,我们就可以进行异步绘制
-
-
离屏渲染
-
触发离屏渲染的场景
- 采用了光栅化的 layer (layer.shouldRasterize)
- 使用了 mask 的 layer (layer.mask)
- 需要进行裁剪的 layer
- 设置了组透明度为 YES,并且透明度不为 1 的layer
- 高斯模糊
- 添加了投影的 layer (layer.shadow*)
- 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等
使用Instruments的不同工具来测试性能
-
LLDB
-
动态调试
- 子主题 1
Clang + LLVM
-
Clang编译步骤
-
1、预处理
- 头文件替换,宏替换,预编译指令替换
-
2、词法分析
- 输出token流
-
3、语法分析
- 生成AST抽象语法树
-
4、CodeGen
CodeGen 负责将语法树丛顶至下遍历,翻译成LLVM IR
-
生成中间代码IR,与RunTime桥接
- ARC:分析对象引用关系,将objc_storeStrong/objc_storeWeak等ARC代码插入
- 根据修饰符strong/weak/copy/atomic合成@property 自动实现的 setter/getter
-
-
LLVM后端
- 优化 IR
- LLVM BitCode字节码
-
如何使用Clang做静态分析
-
OCLint
- 基本覆盖了具有通用性的规则,主要包括语法上的基础规则、Cocoa 库相关规则、一些约定俗成的规则、各种空语句检查、是否按新语法改写的检查、命名上长变量名短变量名检查、无用的语句变量和参数的检查
-
Clang 静态分析器
- scan-build 是用来运行分析器的命令行工具
-
Infer(Facebook 开源的、使用 OCaml 语言编写)
- 空指针访问
- 资源泄露
- 内存泄露
-
iOS签名机制
- 保证安装到用户手机上的APP都是经过Apple官方允许的
- 生成CertificateSigningRequest.certSigningRequest文件
iOS安全
-
代码混淆
-
源码的混淆
- 类名
- 方法名
- 协议名
-
字符串加密
越狱相关
- 越狱检测
Swift
性能优化
CPU与GPU
-
CPU
- 对象的创建销毁、对象属性的调整、布局计算、文本计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
-
GPU
- 纹理的渲染、
启动优化
-
启动速度监控
1、定时抓取主线程上方法调用堆栈,计算一段时间里各个方法的耗时(Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式)
-
2、对objc_msgSend方法进行hook
- 原理解释
- 如何使用
-
方案与实践
-
iOS冷启动阶段思路
-
System Interface
加载主二进制、启动dyld、加载动态库以及libSystem初始化
- 避免链接不使用的框架
- 减少动态库的加载
- 减少OC类,分类等
-
runtime Init
执行 +load和staic initializer初始化函数等
- 避免在+load里操作
- 延迟加载+load
- 减少staic initializer(C++ 静态全局变量)
UIKit init
-
Application init
实例化UI Application 和 UI Application Delegate、处理生命周期回调、首帧渲染直到首页渲染完成
- 减少或延迟各种SDK的初始化
-
initial Frame Render
- 减少视图层级和视图数量
- 懒加载View
- 变AutoLayout为手动frame布局等
-
Extended
- 去掉viewDidLoad和viewWillAppear中不必要的逻辑,少做事不做事
-
-
方案
-
低成本高收益方案
- 生命周期延迟
- +load治理
- 动态库下线
- 二进制重排
- 首页预加载
-
深入优化方案
- 动态库懒加载
- staic initlializer治理
- 编译期写入I/O
- 任务编排
-
-
-
流程规范与监控
-
规范
- 1、新增或修改任务要有足够的理由,必须经过严格的code review
- 2、首页渲染完成前不允许监听生命周期
- 3、不允许新增+load耗时方法
- 4、不允许新增C++ initialize
- 5、新增动态库必须经过评估
- 6、任务项相对上个版本有5ms以上的增长时,必须进行修改
监控
-
卡顿排查与解决
-
UI界面卡顿优化(滑动掉帧等)
- 尽量减少CPU GPU资源消耗
崩溃类型的卡顿排查(线程卡顿)
耗电排查与解决
-
1、如何获取电量
- (1)引入 IOPowerSources.h、IOPSKeys.h 和 IOKit
- (2)把 batteryMonitoringEnabled 置为 true
-
2、如何诊断电量问题
- (1)通过 task_threads 函数,获取所有的线程信息数组 threads以及线程总数 threadCount
- (2)thread_basic_info 里有一个记录 CPU 使用百分比的字段 cpu_usage
- (3)遍历所有线程,去查看是哪个线程的 CPU 使用百分比过高。(某个线程的 CPU 使用率长时间都比较高,可能有问题)
-
3、如何优化电量
-
(1)避免让 CPU 做多余的事情。对于大量数据的复杂计算,应该把数据传到服务器去处理
必须要在 App 内处理复杂数据计算,可以通过 GCD 的 dispatch_block_create_with_qos_class 方法指定队列的 Qos 为 QOS_CLASS_UTILITY,将计算工作放到这个队列的 block 里。在 QOS_CLASS_UTILITY 这种 Qos 模式下,系统针对大量数据的计算,以及复杂数据处理专门做了电量优化。
-
(2)I/O 操作也是耗电大户,优化I/O操作
业内的普遍做法是,将碎片化的数据磁盘存储操作延后,先在内存中聚合,然后再进行磁盘存储。碎片化的数据进行聚合,在内存中进行存储的机制,可以使用系统自带的 NSCache 来完成。
NSCache 是线程安全的,NSCache 会在到达预设缓存空间值时清理缓存,这时会触发 cache:willEvictObject: 方法的回调,在这个回调里就可以对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 操作的次数减少了,对电量的消耗也就减少了
-
包瘦身
官方 App Thinning
-
图片资源优化
-
无用图片移除,图片压缩
- LSUnusedResources
TinyPng或者ImageOptim、转webp
-
-
代码瘦身
删除无用功能代码(A/B测试结果删除)
-
源代码瘦身
-
LinkMap 结合 Mach-O 找无用代码
- AppCode(人工二次确认)
-
运行时检查类是否真正被使用过
- 通过isInitialized,判断一个类是否初始化过
-
重复代码删除(Clang插件)
-
可执行文件瘦身
- 编译器优化
架构设计
组件化
-
协议式
-
协议式架构设计主要采用的是协议式编程的思路
- 在编译层面使用协议定义规范,实现可在不同地方,从而达到分布管理和维护组件的目的
-
缺陷
- 协议式编程缺少统一调度层,导致难于集中管理
- 协议式编程接口定义模式过于规范,从而使得架构的灵活性不够高。当需要引入一个新的设计模式来开发时,我们就会发现很难融入到当前架构中,缺乏架构的统一性。
-
-
中间者
-
优势
- 拆分的组件都会依赖于中间者,但是组间之间就不存在相互依赖的关系
- 其他组件都会依赖于这个中间者,相互间的通信都会通过中间者统一调度
- 在中间者上也能够轻松添加新的设计模式,从而使得架构更容易扩展
- 中间者架构的易管控带来的架构更稳固,易扩展带来的灵活性
-
实现方案
-
CTMediator
-
CTMediator 本质就是一个方法,用来接收 target、action、params,对于调用者来说十分不友好
- 通过响应者给 CTMediator 做的 category 或者 extension 发起调用
- category 或 extension 以函数声明的方式,解决了参数的问题
- 不会直接依赖 CTMediator 去发起调用,而是直接依赖 category Pod 去发起调用
解耦的精髓在于业务逻辑能够独立出来,并不是形式上的解除编译上的耦合(编译上解除耦合只能算是解耦的一种手段而已)。更多的还是需要在功能逻辑和组件划分上做到同层级解耦,上下层依赖清晰
-
-
URLRoutor
-
缺陷
- 本地间调用无法传递非常规参数,复杂参数的传递方式非常丑陋
- 必须要在app启动时注册URL响应者
- 新增组件化的调用路径时,蘑菇街的操作相对复杂
-
-
-
MVC
MVVM
- 双向绑定(RAC,RSSwift)
设计模式
系统化思维
五大设计原则
- 单一功能原则:对象功能要单一,不要在一个对象里添加很多功能
- 开闭原则:扩展是开放的,修改是封闭的
- 里氏替换原则:子类对象是可以替代基类对象的
- 接口隔离原则:接口的用途要单一,不要在一个接口上根据不同入参实现多个功能
- 依赖反转原则:方法应该依赖抽象,不要依赖实例。iOS 开发就是高层业务方法依赖于协议
23种设计模式实现原理
- 子主题 1
网络协议相关
IP层
TCP/UDP
-
TCP
- TCP是一个传输层协议,提供端到端(Host-To-Host) 数据的可靠传输
- 支持全双工,是一个连接导向的协议(面向连接的)
-
UDP
- 目标是在传输层提供直接发送报文(Datagram)的能力
HTTP/HTTPS
-
为什么可以相信一个 HTTPS 网站?
- 当用户用浏览器打开一个 HTTPS 网站时,会到目标网站下载目标网站的证书
- 浏览器会去验证证书上的签名,一直验证到根证书,如果根证书被预装,那么就会信任这个网站
DNS
Socket
-
Socket 是一种编程的模型
- 客户端将数据发送给在客户端侧的Socket 对象,然后客户端侧的 Socket 对象将数据发送给服务端侧的 Socket 对象
- Socket 对象负责提供通信能力,并处理底层的 TCP 连接/UDP 连接
- 对服务端而言,每一个客户端接入,就会形成一个和客户端对应的 Socket 对象,如果服务器要读取客户端发送的信息,或者向客户端发送信息,就需要通过这个客户端 Socket 对象
-
Socket 还是一种双向管道文件
- 操作系统将客户端传来的数据写入这个管道,也将线程写入管道的数据发送到客户端
算法
数组&链表
堆栈&队列
面试题:【判断字符串括号是否合法】
-
单调栈
-
递增栈
- 小数消除大数
-
递减栈
- 大数消除小数
-
-
优先队列
- 正常进,安装优先级出
- 实现机制:1、Heap(堆)(Binary、Binomial、Fiboncci)
哈希表
-
Map/Set
- 【有效的字母异位词】
- 【两数之和】
- 【三数之和】
树
-
二叉树
-
反转二叉树
- 遍历二叉树
-
二叉搜索树
字典树
递归&分治
动态规划
- 贪心算法
- 买卖股票
- 背包问题
LRU Cache
Bloom Filter(布隆过滤器)
斐波那契数列
Flutter
底层基本实现
Bloc与响应式
容器化&配置化
组件化
性能优化与实践
安全与密码学
单向散列函数(哈希函数)
- SHA-1、MD5(已不安全)
- SHA-256、SHA-384、SHA-512(目前流行)
加密算法
-
对称加密
-
序列算法(优先使用)
- ChaCha20、AES-256、AES-128
分组算法
-
非对称加密
亮点与疑难解决
动态化
容器化
配置化
持续集成
fastlane
RunTime无埋点方案
产品主要想知道:页面进入次数、页面停留时间、点击事件的埋点(用来计算曝光率、转化率)
运行时方法替换方式进行埋点(AOP)
- 写一个运行时方法替换的类 SMHook
- 利用运行时接口将方法的实现进行了交换,原方法调用时就会被 hook 住,从而去执行指定的方法
- 每个 UIViewController 生命周期到了 ViewWillAppear 时都会去执行 insertToViewWillAppear 方法
事件唯一标识区分不同埋点
- NSStringFromClass([self class]) 方法来取类名,区别不同的 UIViewController
- action 选择器名 NSStringFromSelector(action)” +“视图类名 NSStringFromClass([target class])”组合成一个唯一的标识
- 通过视图的 superview 和 subviews 的属性,我们就能够还原出每个页面的视图树
- 复用机制 UITableViewCell 用 indexPath
RunLoop运行步骤
1、通知 observers:RunLoop 要开始进入 loop 了。紧接着就进入 loop
2、开启一个 do while 来保活线程。通知 Observers:RunLoop 会触发 Timer 回调、Source0 回调,接着执行加入的 block
通知 Observers:RunLoop 的线程将进入休眠(sleep)状态
4、进入休眠后,会等待 mach_port 的消息,以再次唤醒
5、唤醒时通知 Observer:RunLoop 的线程刚刚被唤醒了
6、RunLoop 被唤醒后就要开始处理消息了
启动优化方案
main() 函数执行前
- 加载可执行文件(App 的.o 文件的集合)
- 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定
- Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等
- 初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量
main() 函数执行后(appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成)
- 首屏初始化所需配置文件的读写操作
- 首屏列表大数据的读取
- 首屏渲染的大量计算等
首屏渲染完成后(加载时长)
- 减少视图层级和视图数量
- 懒加载View
- 变AutoLayout为手动frame布局等
- 预加载(缓存首页,骨架屏等)
- 去掉viewDidLoad和viewWillAppear中不必要的逻辑,少做事不做事
APM系统
启动优化、卡顿监听、崩溃监听、性能监控
GNUStep 源码
LLD链接器
链接器最主要的作用,就是将符号绑定到地址上
使用 dyld 加载动态库
调用 +load 方法是通过 runtime 库处理的
Injection for Xcode 动态调试
- 1、Injection 会监听源代码文件的变化
- 2、如果文件被改动了,Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库,也就是 .dylib 文件
- 3、编译、打包成动态库后使用 writeSting 方法通过 Socket 通知运行的 App
更安全的方法交换库 Aspects
通过 Runtime 消息转发机制来实现方法交换的库
它将所有的方法调用都指到 _objc_msgForward 函数调用上
按照自己的方式实现了消息转发,自己处理参数列表,处理返回值
最后通过 NSInvocation 调用来实现方法交换
事件总线技术 Promise
PromiseKit
通过简单、清晰、规范的 Promise 接口将异步的数据获取、业务逻辑、界面串起来,对于日后的维护或重构都会容易很多
Source0与Source1
Source0
- 不能主动触发事件
- 使用时,你需要先调用CFRunLoopSourceSignal,将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp来唤醒RunLoop,让其处理这个事件
Source1
- 主动触发事件。其中它有一个mach_port_t
TCP 最核心的价值是提供了可靠性,而 UDP 最核心的价值是灵活
HTTP 协议 1.1 和 2.0 都基于 TCP,而到了 HTTP 3.0 就开始用 UDP
TCP与UDP区别
目的差异
- TCP 协议的核心目标是提供可靠的网络传输
- UDP 的目标是在提供报文交换能力基础上尽可能地简化协议轻装上阵
可靠性差异
- TCP 核心是要在保证可靠性提供更好的服务。TCP 会有握手的过程,需要建立连接
- UDP 并不具备以上这些特性,它只管发送数据封包,而且 UDP 不需要 ACK
连接 vs 无连接
- TCP 是一个面向连接的协议,传输数据必须先建立连接
- UDP 是一个无连接协议,数据随时都可以发送,只提供发送封包(Datagram)的能力
传输速度
- UDP 协议简化,封包小,没有连接、可靠性检查等,因此单纯从传输速度上讲,UDP 更快
场景差异
-
TCP 场景
- 远程控制(SSH)
File Transfer Protocol(FTP)
邮件(SMTP、IMAP)等
点对点文件传出(微信等)
-
UDP 场景
- 网络游戏
音视频传输
DNS
Ping
直播
APP如何加载?
iOS系统架构
- 用户体验层,主要是提供用户界面。这一层包含了 SpringBoard、Spotlight、Accessibility
- 第二层是应用框架层,是开发者会用到的。这一层包含了开发框架 Cocoa Touch
- 第三层是核心框架层,是系统核心功能的框架层。这一层包含了各种图形和媒体核心框架、Metal 等
- 第四层是 Darwin 层,是操作系统的核心,属于操作系统的内核态。这一层包含了系统内核 XNU、驱动等
XNU 怎么加载 App?
- iOS 的可执行文件和动态库都是 Mach-O 格式,所以加载 APP 实际上就是加载 Mach-O 文件
- 加载 Mach-O 文件,内核会 fork 进程,并对进程进行一些基本设置,比如为进程分配虚拟内存、为进程创建主线程、代码签名等。用户态 dyld 会对 Mach-O 文件做库加载和符号解析
好架构定义
高可用
高性能
易扩展
在功能逻辑和组件划分上做到同层级解耦,上下层依赖清晰,这样的结构才能够使得上层组件易插拔,下层组件更稳固
组件化架构
1、业务完全解耦,通用功能下沉
组件
2、每个业务都是一个独立的 Git 仓库,每个业务都能够生成一个 Pod 库,最后再集成到一起
组件分层
- 底层可以是与业务无关的基础组件,比如网络和存储等
- 中间层一般是通用的业务组件,比如账号、埋点、支付、购物车等
- 最上层是迭代业务组件,更新频率最高
多团队之间如何分工?
- 基建团队,负责业务无关的基础功能组件和业务相关通用业务组件的开发
- 每个业务都由一个专门的团队来负责开发
- 基建团队人员应该是流动的,从业务团队里来,再回到业务团队中去
监控崩溃与采集
崩溃类型
-
信号可捕获到
KVO、数组越界、返回类型不匹配NULL
-
多线程问题
- 在子线程中进行 UI 更新可能会发生崩溃。多个线程进行数据的读取操作,因为处理时机不一致,比如有一个线程在置空数据的同时另一个线程在读取这个数据,可能会出现崩溃情况
-
野指针
- 指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃
-
信号不可捕获
-
后台任务超时
-
iOS 后台保活
-
Background Task (3 分钟)
- 系统提供了 beginBackgroundTaskWithExpirationHandler 方法来延长后台执行时间,可以解决你退后台后还需要一些时间去处理一些任务的诉求
-
-
-
主线程卡顿超阈值
-
主线程无响应
- 如果主线程超过系统规定的时间无响应,就会被 Watchdog 杀掉
-
-
内存打爆
-
JetSam 机制
- 操作系统为了控制内存资源过度使用而采用的一种资源管控机制
-
通过内存警告获取内存限制值
-
didReceiveMemoryWarning
- 强杀掉 App 之前还有 6 秒钟的时间
-
-
定位内存问题信息收集
-
谁分配的内存?定位到函数
- 用 fishhook 去 Hook “malloc_logger”函数,分析统计
-
-
-
监控方案
-
第三方的
- Fabric或Bugly
-
第三方开源自建服务器
-
PLCrashReporter
- 第三方开源库捕获崩溃日志,然后上传到自己服务器上进行整体监控的
-
A/B测试方案(SkyLab)
三个部分
-
策略服务,为策略制定者提供策略
- 决策流程、策略维度
-
A/B 测试 SDK,集成在客户端内
-
客户端SDK:SkyLab
使用的是 MMKV 保存策略
SkyLab 对外的调用接口使用的是 Block ,来接收版本 A 和 B 的区别处理。
-
如何做人群测试桶划分
- 随机分配方式,将分配结果通过 MMKV 进行持续化存储,确保测试桶的一致性
-
日志系统,负责反馈策略结果供分析人员分析不同策略执行的结果
服务端返回A/B实验
性能监控
线下性能
- Energy Log 就是用来监控耗电量的
- Leaks 就是专门用来监控内存泄露问题的
- Network 就是用来专门检查网络情况
- Time Profiler 就是通过时间采样来分析页面卡顿问题
线上监控(不要侵入到业务代码、采用性能消耗最小的监控方案)
-
CPU 使用率的线上监控(App 作为进程运行起来后会有多个线程,每个线程对 CPU 的使用率不同。各个线程对 CPU 使用率的总和,就是当前 App 对 CPU 的使用率)
- thread_info.h 根据当前 task 获取所有线程
- 遍历所有线程来获取单个线程的基本信息
- thread_basic_info 结构体获取CPU 使用率的字段:cpu_usage
- 累加这个字段就能够获取到当前的整体 CPU 使用率
-
FPS 线上监控
- 通过注册 CADisplayLink 得到屏幕的同步刷新率
- 记录每次刷新时间,然后就可以得到 FPS
-
内存使用量的线上监控
- 内存信息存在 task_vm_info
- 类似于对 CPU 使用率的监控,我们只要从这个结构体里取出 phys_footprint 字段
启动优化监控方案
定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时(Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式)
对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时
使用RunLoop监控卡顿
原因
- 复杂 UI 、图文混排的绘制量过大
- 在主线程上做网络同步请求
- 在主线程做大量的 IO 操作
- 运算量过大,CPU 持续高占用
- 死锁和主子线程抢锁
RunLoop基本原理
- 用来监听输入源,进行调度处理的。这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调
- RunLoop 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件
- 当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠