说在前面:
许久没更新,最近整理就文件的时候,突然翻到两年前换工作时整理的思维导图,包含了原理八股文,网络,算法,以及架构,重构解决方案等,基本上面试必问的一些项目以及原理都包含在内了。当然了这里整理的很多内容都只是帮助当时的我回忆知识脉络,并没有深入说明,还需要读者自己去查阅资料深入理解。希望能也帮助到最近准备换工作的小伙伴梳理汇总知识点。思维导图高清原图下载地址
欢迎关注一下我的 Github: https://github.com/CYXiang
后续会更新一些AI、Flutter等相关文章。
思维导图梳理
iOS原生相关
Objective-C 底层原理
OC对象
NSObject本质
NSObject底层是结构体,有一个 Class isa 指针
创建一个NSObject对象系统分配16字节(至少16),只使用了8字节(用于存放isa),objc源码
复杂对象本质
包含父类成员结构+子类成员属性
内存对齐,结构体的大小必须使最大成员变量的倍数
属性和方法
实例对象存放成员变量,不存放方法(存一份在类对象就够了)
内存分配注意点
内存分配方式内存对齐、桶、堆内存、16的倍数。目的是内存优化(gnu 内存对齐)
对象分类三类
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顺序查找成员变量
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 操作的次数减少了,对电量的消耗也就减少了
(3)苹果维护了一个电量优化指南“Energy Efficiency Guide for iOS Apps”
包瘦身
官方 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 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件
当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠