设计原则
单一职责原则(
SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)、依赖倒置原则(DIP)、迪米特法则(LoD)
数据类型:
1.基本数据类型:
NSInteger、NSUInteger、CGFloat、枚举类型、BOOL类型。2.指针数据类型:类:
NSString、NSArray等;id类型:delegate等。3.构造类型:结构体:
struct、联合体(共用体):union。4.
NSArray(有序集合),NSSet(无序集合、自动去重、哈希表、散列算法查找元素较快)。5.
@property本质:ivar(实例变量) +setter+getter,增加属性做了什么:
在
ivar_list中添加一个成员变量的描述;在
method_list中增加setter与getter方法的描述;在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量;
setter与getter方法的实现,从偏移量位置赋值/取值,偏移量指针类型做了强转;6.属性修饰符:
assign:修饰基本数据类型,引用计数不变;strong:强引用;weak:弱引用;
copy:修饰不可变类型属性;readwrite:读、写;readonly:只读;nonatomic:非原子性访问;atomic:原子性访问;
对象分类:
1.
NSObject对象:至少8个字节内存(存放isa指针),实际16或者16的倍数.2.实例对象:
alloc的对象;存储信息:isa指针/其他成员变量.3.类对象:存储信息:
isa/superclass指针、类的属性/对象方法/协议/成员变量信息.4.元类:每个类有且只有一个;存储信息:
isa/superclass指针、类的类方法信息.
Runtime:
动态运行时系统,运行时创建类/调用方法/访问属性等。实现动态消息传递和类型识别。
核心机制 - 消息机制,
objc_msgSend()函数调用,通过isa指针找到其类对象。
方法查找:
1.快速查找:接收者的类的缓存(
cache_t)中查找方法。若命中缓存,跳转执行结束。2.慢速查找:缓存未命中,类的
class_rw_t的方法列表查找,找到缓存到cache_t,若未找到通过super_class指针向上查找,直至根类(如NSObject)。
方法查找终止:
父类为
nil或找到_objc_msgForward,进入动态方法解析 → 快速转发 → 慢速转发三阶段。
_objc_msgForward是函数指针:发送消息未实现,进行消息转发,若直接调用_objc_msgForward会直接走消息转发。
具体转发流程:
1.动态方法解析(Resolve):
- 调用
+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法)动态添加方法实现(返回:YES->方法查找,NO->下一阶段)。
2.快速转发(Fast Forwarding):
- 调用
forwardingTargetForSelector:找一个能响应消息的对象转发(返回:非nil->消息转发;nil->下一阶段)。
3.慢速转发(Slow Forwarding):
- 调用
methodSignatureForSelector:获取方法签名(返回值类型、参数类型)(返回:nil->触发崩溃(unrecognized selector);有效签名->调用forwardInvocation:在该方法中自定义消息处理逻辑)
IMP、SEL、Method的区别和使用场景:
Method(方法结构体)包含SEL(方法选择器)和IMP(方法实现指针)
1.
SEL(选择器):方法调用(objc_msgSend(person, @selector(run)))、判断方法是否存在([person respondsToSelector:@selector(run)])等。
2.
IMP(实现):直接调用方法(跳过消息查找)
3.
Method(方法):获取方法详情(如method_getName(method)获取SEL、method_getImplementation(method)获取IMP)、动态添加方法(class_addMethod)
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量;能向运行时创建的类中添加实例变量;
1.编译后的类已经注册在
runtime中,类结构体中实例变量的链表和内存大小已经确定,同时runtime会调用class_setIvarLayout或class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量2.运行时创建的类是可以添加实例变量,调用
class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
实际应用遵循规则:
1.在
+load中使用dispatch_once。2.优先使用
class_addMethod避免覆盖父类实现。3.为交换方法添加前缀(如
aop_)防止命名冲突。4.避免
Hook高频方法(如dealloc),可能引发性能问题。
具体应用:
1.方法交换:
class_getInstanceMethod()、method_exchangeImplementations()- 控制器生命周期做埋点2.关联对象:分类添加存储属性:
objc_setAssociatedObject()、objc_getAssociatedObject()、objc_removeAssociatedObjects()3.动态类创建和方法添加:
objc_allocateClassPair()、class_addMethod()/class_addIvar()、objc_registerClassPair()4.获取类属性:
class_copyPropertyList()-json转model、属性归档/解归档5.解决
NSTimer循环引用问题:中间类基于NSProxy-methodSignatureForSelector()、forwardInvocation()6.反射机制:
NSClassFromString, 组件化应用
数据结构 - runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等):
1.
isa指针:isa_t(union)维护对象与类的关系,确保对象和类可以通过isa指针找到对应方法、实例变量、属性、协议等。指向关系如下:
isa链路:实例对象->isa->类对象->isa->元类->isa->根元类->isa->自身继承链:
class->superclass->父类的class(无父类superclass->nil)元类->
superclass->父类的元类;基类的元类->superclass->基类的class2.对象:
objc_object结构体,包括一个isa指针。3.类对象:
objc_class结构体,继承自objc_object,包含:isa,superclass,方法缓存,方法列表/属性/协议等。4.元类:
meta_class,存储类方法(单一职责)
class_rw_t 和 class_ro_t 的区别:
1.
class_rw_t:运行时可对类拓展,存储了运行时修改类的方法,属性,协议等2.
class_ro_t:存储的大多是类在编译时就已经确定的信息。3.存储类的方法、属性、协议等信息,
class_rw_t(二维数组),class_ro_t(一维数组)
category如何被加载的,+load方法的加载顺序,同名方法的加载顺序:
category加载:
1.运行时把
category的实例方法、属性、协议添加到类对象。类方法、属性、协议添加到元类。2.二维数组存储方法列表:[[后编译方法],[先编译方法],[原类方法]]
+load方法加载顺序:
1.先父类,再子类,结构体对象
loadable_class存到表loadable_classes。2.类本身(
Xcode中Build Phases中的Compile Sources的文件顺序加载的)3.分类(后编译先调用),结构体对象
loadable_category存到表loadable_categories。
同名方法加载顺序:
1.
category的同名方法并不会替换掉原类方法,而是插入到方法列表前端2.方法调用(后编译先调用),原类方法只有在移除所有
category才会调用
面试题:+load 和 +initialize 的区别?
+initialize是通过objc_msgSend进行调用的,而+load是找到函数地址直接调用的
+load方法:
1.
+load方法会在Runtime加载类、分类时调用,程序运行过程中只调用一次2.先加载原始类,再加载分类的
+load方法3.当父类和子类都实现
+load方法时, 调用顺序:先父类再子类。4.当子类未实现
+load方法时,不会调用父类的+load方法5.多个类都实现
+load方法,与Compile Sources中出现的顺序一致
+initialize方法:
1.当类第一次被使用的时候就会调用(创建类对象的时候)
2.
+initialize程序运行过程中只会被调用一次, 无论使用多少次这个类3.先调用父类的
+initialize再调用子类的+initialize4.当子类未实现
+initialize方法时,会把父类的实现继承过来调用一遍,再此之前父类的+initialize方法被优先调用一次5.当有多个
Category都实现了+initialize方法覆盖类中的方法,只执行一个(会执行Compile Sources列表中最后一个Category的+initialize方法)
分类和扩展有什么区别?可以分别用来做什么?分类局限性?分类结构体成员?
1.分类是在运行时把分类信息合并到类信息中,扩展在编译时就把信息合并到类中
2.分类声明的属性,只会生成
getter/setter方法的声明,不会自动生成成员变量和getter/setter方法的实现,而扩展会3.分类不可用为类添加实例变量,而扩展可以
4.分类可以为类添加方法的实现,而扩展只能声明方法,而不能实现
分类:添加方法,属性,协议 扩展:添加成员变量、属性、方法(只是声明)
1.分类无法添加实例变量,但可通过关联对象进行实现,
2.分类的方法若和类中原本的实现重名,会覆盖原本方法的实现(并不是真正的覆盖)
3.多个分类的方法重名,会调用最后编译的那个分类的实现
原理:底层结构是struct category_t,运行时将对象方法、类方法、属性、协议信息
,合并到类信息中(类对象、元类对象中)
关联对象相关:
双层哈希表:
AssociationsHashMap:以对象地址为键,映射到ObjectAssociationMap。
ObjectAssociationMap:以关联键为键,存储ObjectAssociation(含_policy和value)。
实现 weak 属性:Block 包裹弱引用;Weak 容器类
Weak 的实现原理与 SideTable 结构解析:
weak是hash表结构,key是所指对象的地址,value是weak的指针数组
实现原理:
1、初始化时:调用
objc_initWeak函数,初始化一个新的weak指针指向对象的地址。2、添加 引用时:调用
objc_storeWeak()函数,更新指针指向,创建对应弱引用表。3、释放时,调用
clearDeallocating函数。
根据对象地址获取所有
weak指针地址的数组遍历这个数组把其中的数据设为
nil,把这个
entry从weak表中删除,最后清理对象的记录。
SideTable 的结构:
struct SideTable {
spinlock_t slock; // 保证原子操作的自旋锁
RefcountMap refcnts; // 引用计数的值
weak_table_t weak_table; // 存放weak指针的哈希表
};
AutoreleasePool 的原理与数据结构:
iOS中管理临时对象内存的机制,通过延迟释放对象,避免频繁调用release。
核心原理:
1.作用:收集调用
autorelease的对象,在Autoreleasepool销毁时,对池内所有对象调用release。2.触发时机:
主线程:
RunLoop的每个循环周期结束时(如kCFRunLoopBeforeWaiting),自动销毁并重建Autoreleasepool;子线程:需手动创建
@autoreleasepool{},否则对象可能无法及时释放;手动销毁:
@autoreleasepool{}代码块执行完毕时,池内对象被release。
数据结构:
Autoreleasepool基于双向链表实现,核心结构是AutoreleasePoolPage。
操作流程:
1.创建
Autoreleasepool:调用objc_autoreleasePoolPush(),压入POOL_BOUNDARY(哨兵对象),返回其地址;2.对象调用
autorelease:调用objc_autorelease(),将对象指针存入当前Page的top位置,top自增;若当前Page满,创建新Page并继续存储;3.销毁
Autoreleasepool:调用objc_autoreleasePoolPop(),从top向下遍历,对每个对象调用release,直到遇到哨兵对象,并调整top指针。
属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗
内部实现原理:
1、自动生成
getter/setter方法,调用objc_getProperty和objc_setProperty方法2、加锁机制:方法内部使用
os_unfair_lock加锁,保证读写的原子性和线程安全。3、通过
key(对象地址+属性偏移量)获取同一把锁,保证同一属性的读写互斥。
不能保证绝对线程安全:
对可变数组进行操作,添加或者移除对象,是不在
atomic的负责范围之内的,所以给被atomic修饰的数组添加或者移除对象是没办法保证线程安全的。
实现线程安全的替代方案:
1、同步锁:
@synchronized(self){}2、串行队列:
dispatch_queue_create()、dispatch_async()3、并发队列 +
Barrier(栅栏):dispatch_barrier_async()
iOS 中内省的几个方法有哪些?内部实现原理是什么
1.
class方法:读取对象的isa指针2.
isMemberOfClass:比对对象的isa指针与传入的类对象地址3.
isKindOfClass:遍历对象的类继承链,递归比对父类指针直到匹配或抵达根类NSObject4.
respondsToSelector:遍历类的方法列表,若未找到,递归检查父类链5.
conformsToProtocol:检查类的协议列表或其继承链中的协议
Block:
介绍:将函数及其执行上下文封装起来的对象,
Block的调用即是函数调用
block的内部实现,结构体是什么样的:
1.
__block_impl基础结构体,包含:isa指针、标志位和保留字段。2.
__main_block_impl_0具体结构体继承自__block_impl,包含:描述信息和捕获的外部变量。
一个int变量被 __block 修饰与否的区别?block的变量截获:
未使用__block修饰:
1.
block会以值拷贝方式捕获int变量,生成的结构体中会直接存储该值的副本2.不允许在
block内部修改原始变量值,会报错:Variable is not assignable
使用__block修饰:
1.结构体包装该变量
2.
block捕获的是指向该结构体的指针,通过指针间接访问原始变量3.允许在
block内部修改原始变量值,修改会反映到外部
变量截获:
1.静态局部变量:指针形式截获局部静态变量,不需要
__block修饰符2.局部变量:需要
__block修饰符,基本数据类型:截获其值;对象类型:连同所有权修饰符一起截获3.静态全局变量:不截获,不需要
__block修饰符4.全局变量:不截获,不需要
__block修饰符
block在修改NSMutableArray时,需不需要添加__block:
修改数组内容(不添加
__block):增删改操作,捕获对象指针直接修改内容重新赋值数组(添加
__block):重新赋值,__Block_byref_结构体包装变量
block怎么进行内存管理的,可以使用stong修饰吗,循环引用产生原因,怎么解决:
Block内存管理涉及类型、存储位置、变量捕获机制:
1.
_NSGlobalBlock(全局Block),全局数据区,未捕获外部变量生成。2.
_NSStackBlock(栈Block),栈内存,捕获局部变量或者成员属性生成。3.
_NSMallocBlock(堆Block),堆内存,强指针引用或显示调用copy后生成。
MRC环境下:栈Block手动copy到堆:[block copy];堆Block手动release。
ARC环境下:栈Block自动copy到堆(全局Block除外);堆Block:引用计数为0时自动释放。
Block是否使用strong修饰:
1.
MRC环境(手动内存管理)不可使用strong修饰:strong修饰仅对Block进行retain操作,不会将栈Block拷贝到堆内存,当栈Block超出作用域被销毁后,继续调用会导致野指针访问,引发崩溃。必须使用copy修饰,显式将栈Block拷贝到堆内存,确保生命周期可控。2.
ARC环境(自动内存管理):strong和copy效果一样(除全局Block)会将栈Blcok拷贝到堆内存
循环引用:当对象A持有Block,而Block内部又强引用了对象A,形成双向强引用链,导致双方都无法释放,内存泄漏。解决方法如下:
1.使用
__weak弱引用(打破强引用环,避免Block捕获对象时产生强引用)2.弱引用对象可能被提前释放,需要临时持有对象,则需配合
__strong强引用(仅在Block执行期间强引用)
典型应用场景:
异步回调:网络请求完成更新
UI,要确保回调执行时控制器未被释放动画
Block:执行动画期间需保持试图对象有效
NSNotification:
NSNotification(结构设计、存储机制、name&observer&SEL之间的关系等):
结构设计:
1.
NSNotification(不可变通知模型)
- 包含
name(标识)、object(发送对象)、userInfo(附加数据)-
postNotificationName:object:userInfo:方法创建并发送2.
NSNotificationCenter(单例模式管理通知)
核心数据结构:
NCTable包含三个储存表
wildcard(链表结构),存储未指定name、object的观察者
nameless(哈希表),存储仅指定object的观察者
named(双层哈希表),存储指定name的观察者(第二层以object为key)3.
Observation(存储单元结构体)
- 包含
observer(观察者对象)、selector(回调方法)、next(链表指针)等
存储与映射关系:
1.
name-observer-SEL关联如下:
通过
NCTable的哈希表建立三级映射:第一层:
name作为named表的key第二层:
object作为子哈希表的key第三层:
Observation链表存储同一(name+object)组合的所有观察者2.特殊存储规则:
当
object=nil时,系统自动生成唯一的key存储观察者未指定
name的通知会存入nameless或者wildcard表
通知派发流程:
1.同步执行机制
通过
performSelector:同步调用观察者的回调方法,可能阻塞发送线程异步方案使用
NSNotificationQueue或者指定NSNotificationQueue2.线程安全性:
默认通知回调在发送线程执行,需手动切换至主线程更新
UI
iOS9+观察则使用weak引用,未移除不会crash(iOS8-会因为野指针崩溃)
通知发送:默认同步
调用
postNotification:方法时,发送线程会阻塞等待所有观察者回调方法执行完毕,才会继续执行后续代码观察者回调的执行线程与发送通知的线程完全一致(在主线程发送,回调必须在主线程执行)
同步性引发的风险:
1.主线程卡顿:观察者回调耗时较长(网络请求、复杂计算),会导致界面无响应
2.时序依赖问题:多个观察者的回调按注册顺序同步执行,若存在依赖逻辑可能因执行顺序出错
异步发送方案:
1.
NSNoticationQueue指定发送方式 -NSPostWhenIdle/NSPostASAP(异步发送);NSPostNow(同步发送)2.
GCD异步派发:在观察者回调内主动切换线程(dispatch_async)3.
Block注册方式:使用addObserverForName:object:queue(指定方式):usingBlock(回调)
NSNotificationQueue是异步还是同步发送?在哪个线程响应:
发送方式:由NSPostingStyle决定是同步还是异步
1.
NSPostNow- 同步发送:通知立即发送,阻塞当前线程直到观察者的回调完成2.
NSPostASAP- 异步发送:通知加入队列,下一个RunLoop周期发送(不阻塞当前线程)3.
NSPostWhenIdle- 异步发送:通知加入队列,等待RunLoop空闲时发送(无用户交互时)
响应线程:与enqueueNotification调用线程一致
1.默认主线程响应:调用
enqueueNotification时位于主线程,观察者回调必须在主线程执行2.子线程触发:在子线程响应,观察者回调在同一子线程执行,需手动切换至主线程更新UI
注意:
1.
RunLoop依赖:异步发送需要RunLoop驱动,若线程无活跃的RunLoop,则通知无法发送2.线程安全实践:推荐使用
Block注册方式 显式 指定回调队列,确保线程可控。
NSNotificationQueue和Runloop的关系:
1.
RunLoop驱动通知发送:NSNotificationQueue的异步发送(NSPostWhenIdle/NSPostASAP)需由RunLoop触发,通知被存入队列后,RunLoop会在特定状态(空闲/下一个循环周期)从队列中取出通知并调用NSNotificationCenter同步发送2.主线程默认开启
RunLoop,因此在主线程可以直接使用异步发送3.子线程需手动开启
RunLoop:使用[[NSRunLoop currentRunLoop] run]开启,否则通知会积压在队列中无法发出
如何保证通知接收的线程在主线程:
1.指定主队列注册:使用
addObserverForName:object:queue(设置为主队列):usingBlock(回调主线程更新)
// 强制主线程回调的注册方式
id observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"Event" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
// 安全更新 UI
}];
2.手动切换至主线程:使用传统
addObserver:selector:name:object:注册时,在回调中通过GCD显式切换至主线程
- (void)handleNotification:(NSNotification *)note {
dispatch_async(dispatch_get_main_queue(), ^{
// 主线程更新UI或者执行逻辑
})
}
3.
RunLoop端口通信:在主线程RunLoop添加Mach Port,通过端口代理回调(主线程执行)触发通知发送,确保接收为主线程
[[NSRunLoop mainRunLoop] addPort:[NSPort port]]
RunLoop:
作用:(主线程RunLoop自主开启的原因)
1、保持程序的持续运行(
runloop保证主线程不会被销毁和程序的持续运行)。2、处理
App中的各种事件(如:触摸事件、定时器事件、选择器事件)。3、节省
CPU资源,提高程序性能(有事情就做事情,没事情就休息 (其资源释放))。4、负责渲染屏幕上的所有
UI。
原理:基于CFRunLoop的循环机制,通过CFRunLoopMode管理不同事件源(Sources/Timers/Observers),在休眠和唤醒间切换。
数据结构:
1.
CFRunLoop:pthread、currentMode、modes、commonModes、commonModelItems(Observer,Timer,Source集合)构成2.
CFRunLoopMode:运行模式-name、source0、source1、observers、timers构成。
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
UITrackingRunLoopMode:跟踪用户交互事件
UIInitializationRunLoopMode:刚启动App时的第一个Mode
GSEventReceiveRunLoopMode:接受系统内部事件
kCFRunLoopCommonModes:伪模式3.
CFRunLoopSource:输入源/事件源
source0:用户触发的事件,需手动唤醒线程(内核态->用户态)
source1:基于port,包含一个mach_port和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。具备唤醒线程的能力4.
CFRunLoopTimer:定时源,在预设的时间点唤醒RunLoop执行回调5.
CFRunLoopObserver:观察者
流程原理:内部是一个 do-while 循环
1.通知
Observer:即将进入Loop2.通知
Observer:将要处理Timer3.通知
Observer:将要处理Source0-> 处理Source04.如果有
Source1,跳转到第7步5.通知
Observer:线程即将休眠 -> 休眠,等待唤醒(Source0/Timer/手动唤醒)6.通知
Observer:线程刚被唤醒7.处理唤醒时收到的消息,跳回步骤
2(Timer/Source1)8.通知
Observer:即将退出Loop
多线程
为什么要有?多线程是用来干什么:
1.主线程用来更新
UI/处理用户触摸事件,不能将耗时操作在主线程执行,会造成界面卡顿2.将耗时操作放在另一个线程中去执行,多线程就是为了防止主线程堵塞,增加运行效率的方法
iOS开发中有多少类型的线程?分别对比:
1.
NSThread:仅当需要直接线程控制时使用
手动控制线程生命周期(启动、停止)和同步机制(加锁)
启动流程:
start()->创建pthread->main()->[target performSelector:]->exit()2.
GCD:适合大多数异步任务(网络请求、图像处理)
提供串行队列(任务顺序执行)和并发队列(任务并行执行)
自动管理线程池,优化多核
CPU性能,避免开发者直接操作线程支持异步/同步任务提交和主线程回调
3.
NSOperation/NSOperationQueue:需任务依赖或取消机制时(批量下载)
- 支持高级操作(任务依赖、取消暂停队列、优先级)设置
GCD有哪些队列,默认提供哪些队列:
1.主线程串行队列:
dispatch_get_main_queue()2.全局并行队列:
dispatch_get_global_queue()3.自定义串行队列:
dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);4.自定义并行队列:
dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
常用API:
1.队列创建/获取:
dispatch_queue_create、dispatch_get_global_queue、dispatch_get_main_queue2.队列优先级调整:
dispatch_set_target_queue3.同步/异步执行:
dispatch_sync、dispatch_async4.延迟执行:
dispatch_after5.单次 执行:
dispatch_once6.任务组管理:
dispatch_group_create、dispatch_group_async、dispatch_group_notify、dispatch_group_wait7.栅栏任务:
dispatch_barrier_async8.快速迭代:
dispatch_apply9.信号量:
dispatch_semaphore_create、dispatch_semaphore_wait、dispatch_semaphore_signal10.其他:
dispatch_io、dispatch_source_create
使用场景:
1.图片异步加载:
dispatch_async+ 全局队列,完成后主队列更新UI2.多任务同步:
dispatch_group3.资源竞争控制:
dispatch_semaphore限制并发线程数
GCD主线程 & 主队列的关系:
1.主队列只在主线程中被执行的,而主线程运行的是一个
runloop,不仅仅只有主队列的中的任务,还会处理UI的布局和绘制任务2.主队列不具备开启新线程的能力
3.主队列+同步提交:阻塞当前线程(主线程),死锁
4.主队列+异步提交:任务加入主队列尾部排队,不阻塞当前线程(UI更新)
任务和队列不同组合:
1.并发队列+异步:开启新线程,并发执行任务
2.串行队列+异步:开启新线程,串行执行任务
3.并发队列+同步:不开启新线程,串行执行任务
4.串行队列+同步:不开启新线程,串行执行任务
5.主队列+异步:不开启新线程,串行执行任务
6.主队列+同步 - 主线程:死锁(同步任务和主队列任务相互等待)
7.主队列+同步 - 其他线程:不开启新线程,串行执行任务
不同任务+不同队列组合,以及队列中嵌套队列:
1.『并发队列+异步』嵌套『同一个并发队列』+同步:不开启新线程,串行执行任务
2.『并发队列+异步』嵌套『同一个并发队列』+异步:开启新线程,并发执行任务
3.『并发队列+同步』嵌套『同一个并发队列』+同步:不开启新线程,串行执行任务
4.『并发队列+同步』嵌套『同一个并发队列』+异步:开启新线程,并发执行任务
5.『串行队列+异步』嵌套『同一个串行队列』+同步:死锁
6.『串行队列+异步』嵌套『同一个串行队列』+异步:开启新线程,串行执行任务
7.『串行队列+同步』嵌套『同一个串行队列』+同步:死锁
8.『串行队列+同步』嵌套『同一个串行队列』+异步:开启新线程,串行执行任务
GCD如何实现同步:
1.在某队列开启同步线程:
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block)2.障碍锁(栅栏)的方式同步:
dispatch_barrier_sync()3.任务组方式:
dispatch_group_create() + dispatch_group_wait()4.快速迭代:
dispatch_apply()5.信号量:
dispatch_semaphore_create() + dispatch_semaphore_wait()6.条件锁:
NSConditionLock & NSCondition7.
@synchronized和dispatch_once():单位时间内只允许一个线程进入临界区8.递归锁:
NSRecursiveLock
dispatch_once实现原理:
通过原子操作 + 状态标记实现高效、线程安全的单次执行,避免重复初始化
什么情况下会死锁:
死锁需同时满足互斥、请求保持、不可剥夺、循环等待四个条件,常见于多线程资源竞争或
GCD队列使用不当时。避免核心是破坏任一条件,如按序申请资源、异步提交任务或使用信号量控制
典型死锁场景(GCD):
1.主线程同步提交任务到主队列:主队列串行执行,
dispatch_sync阻塞主线程2.同一串行队列嵌套同步提交:外层任务占用队列,内层同步任务需等待队列释放,形成循环等待
3.线程
A持有锁1后申请锁2,线程B持有锁2后申请锁1
有哪些类型的线程锁,分别介绍下作用和使用场景:
1.
OSSpinLock:自旋锁,ios10废弃;性能极高但存在优先级反转问题(计数器操作)2.
os_unfair_lock:互斥锁,替代OSSpinLock,解决优先级反转问题(内存缓存操作)3.
pthread_mutex:互斥锁,线程获取不到锁时会休眠(共享资源保护)4.
pthread_mutex(recursive):递归锁(递归函数调用)5.
NSLock:互斥锁,封装pthread_mutex(代码同步)6.
NSRecursiveLock:递归锁(递归操作保护)7.
pthread_rwlock:读写锁(配置数据缓存)8.
@synchronized:互斥锁9.
pthread_cond_t:条件锁10.
NSCondition/NSConditionLock:条件锁,封装pthread_cond_t
NSOperationQueue中的maxConcurrentOperationCount默认值:
1.默认值:
-1,系统自行决定最优并发数(接近设备CPU核心数)2.其他值:
1,强制串行执行;>1,指定固定并发数;0,禁止执行任何操作;
NSTimer、CADisplayLink、dispatch_source_t 的优劣:
1.
NSTimer:简单;依赖RunLoop,不精确,循环引用风险(低频任务)2.
CADisplayLink:中等;精确度极高,循环引用风险(UI动画/渲染)3.
dispatch_source_t:复杂;精确度极高(高性能后台任务)
NSTimer不准确的原因:
1.
RunLoop不同模式,由A模式切换到B模式,A模式下的NSTimer就会暂停,直到切回到A模式2.任务阻塞:主线程中进行大量耗时操作,
NSTimer的触发时间延迟3.系统资源竞争:
CPU负载过高,优先处理重要任务,NSTimer不能按照设定的时间间隔触发
NSTimer和CADisplayLink循环引用统一处理方案:
继承
NSProxy在消息转发里替换target
进程和线程的区别:
进程:资源分配最小单位/开销大/完全隔离/独立内存空间(堆、栈、代码区)
线程:
CPU调度基本单位/开销小/无隔离/共享进程内存,私有栈空间
为什么线程崩溃会导致进程退出?
同一进程所有的线程共享内存空间,非法内存访问会破坏整个进程的地址空间
如何实现进程间的通信?
XPC:Apple官方推荐的IPC机制(隔离性强)
App Groups:通过UserDefaults或文件共享数据
Mach Ports:底层内核通信机制
UI视图相关:
ViewController生命周期:
1
.initWithCoder:(NSCoder *)aDecoder:通过nib文件初始化时触发2.
loadView:加载view3.
viewDidLoad:view加载完毕4.
viewWillAppear:控制器的view将要显示5.
viewWillLayoutSubviews:控制器的view将要布局子控件6.
viewDidLayoutSubviews:控制器的view布局子控件完成7.
viewDidAppear:控制器的view完全显示8.
viewWillDisappear:控制器的view即将消失的时候9.
viewDidDisappear:控制器的view完全消失的时候10.
dealloc控制器销毁
LayoutSubviews调用时机:
添加视图/视图重新布局(
frame改变)/手动调用setNeedsLayout、layoutIfNeeded
drawRect调用时机:loadView和ViewDidLoad之后调用
UIView和CALayer关系:
1.
UIView负责响应事件CALayer负责绘制UI2.
UIView对CALayer封装属性 我们一般访问的framecenter等属性 其实内部会访问相应的CALayer属性 但是设置阴影 圆角 还是得靠直接访问CALayer的属性来完成3.每个
UIView都持有一个CALayer并且是CALayer的代理
CPU和GPU:
CPU:对象创建和销毁/属性调整/布局计算/文本计算和排版/图片格式转换和解码/图像的绘制
GPU:负责纹理的渲染
屏幕撕裂:
1.单一缓存模式下,帧缓冲区只有一个缓存空间,图片经过
CPU->内存->GPU->的渲染过程2.
CPU和GPU的协作过程中出现了偏差,GPU应该完整的绘制图片,但是工作慢了只绘制出图片上半部分。此时CPU又把新数据存储到缓冲区,GPU继续绘制的时候下半部分就变成了新数据。造成了两帧同时出现了一部分在屏幕上,看起来就撕裂了。
解决方案:双缓冲区 + 垂直同步信号
1.解决上一帧和下一帧的覆盖问题,需要使用不同的缓冲区,通过两个图形缓冲区的交替来解决。
2.出现速度差的时候,就把下一帧存储在后备缓冲区,绘制完成后再切换帧。
3.当绘制完最后一个像素点就会发出这个垂直同步信号通知展示完成。
掉帧(卡顿):
产生原因:CPU和GPU渲染计算耗时过长
1.屏幕正在展示A帧的时候,
CPU和GPU会处理B帧。2.
A帧展示完成该切换展示B帧的时候B帧的数据未准备好。3.没办法切换就只能重复展示
A帧,感官上就是卡了,这就是掉帧的问题
解决方案:降低视图层级、提前或减少在渲染期的计算
离屏渲染:
图形绘制操作不在当前屏幕显示缓冲区进行,而是需要在
CPU或GPU创建额外的缓冲区完成渲染,在拷贝到屏幕缓冲区的过程。
常见场景:
1.圆角+裁剪:需计算图层和其他视图的叠加区域(无需透明背景,可用中间镂空图片替代)
2.阴影效果(未指定
path):需获取图层完整形状计算阴影(显式 指定shadowPath)3.图层遮罩、光栅化、抗锯齿、自定义
drawRect:绘制复杂图形
优化策略与实践:
1.避免动态圆角裁剪:预渲染为圆角图片
2.替代方案实现阴影:带阴影的图片、添加专用的阴影图层
3.光栅化正确使用:适合静态
cell(缓存复用)、动态内容禁止使用4.异步绘制复杂内容:在后台线程绘制
事件响应链和传递链:
响应者链:
1.程序启动:
UIApplication的nextResponsers设置为AppDelegate。2.创建
UIWindow:UIWindow的nextResponser设置为UIApplication。3.根
VC初始化:根VC的nextResponser设置为UIWindow。4.
view初始化:view的nextResponser设置为根VC。5.
view添加subView:subView的nextResponser设置为superView。
传递链:
1:该层级是否能够响应(不透明/打开交互响应/未隐藏)
2:判断该点是否在
view内部3:如果在那么遍历子
view继续返回可响应的view,直到没有。// 先判断点是否在
View内部,然后遍历subViews
hitTest: withEvent:和pointInside: withEvent:
APP如何接收到触摸事件的:
系统响应阶段:
1.触碰屏幕,系统进程(
IOKit)将触摸事件封装成事件对象(IOHIDEvent)2.通过
mach端口传递给SpringBoard进程(系统桌面进程)3.
SpringBoard进程接收到触摸事件,触发主线程runloop的source1事件源的回调
APP响应阶段:
1.
mach端口接收SpringBoard传递的触摸事件,主线程的RunLoop被唤醒,触发source1回调2.触发
source0回调,事件封装成UIEvent对象添加到UIApplication的事件队列中
事件分发与响应链处理:
1.从队列中取出事件,通过调用
hitTest:withEvent:方法从视图层级(UIApplication→UIWindow→ 子视图)中寻找最佳响应者2.使用
pointInside:withEvent:判断触摸点是否在视图范围内,从最顶层向下遍历找到合适的响应视图3.事件随后在响应链中传递(从父控件到子控件),允许手势识别器或响应者对象处理
KVC:Key-Value Coding,即键值编码。
赋值流程:
1.找类的
set方法,找不到去找_set方法2.找不到根据
accessInstanceVariablesDirectly方法返回(默认为YES)。
返回
YES,按照_key/_isKey/key/isKey顺序找属性赋值,没有上述属性调用setValue:forUndefinedKey方法(自己实现一下,否则报错)返回
NO时,直接调用setValue:forUndefinedKey方法
取值流程:
1.首先取值会按
getKey、key、isKey、_key的顺序取2.找不到根据
accessInstanceVariablesDirectly返回值
返回
YES时,按照_key/_isKey/key/isKey顺序找属性取值,没有上述属性调用valueForUndefinedKey:方法(自己实现一下,否则报错)返回
NO时,直接调用valueForUndefinedKey:
KVO:Key-Value Observing,即键值观察。
底层机制:
1.利用
RuntimeAPI动态生成一个子类,继承实例对象isa原来指向的类2.修改实例对象的
isa指针,让其指向这个新的子类;3.当修改实例对象属性时,调用属性的
set方法,去isa指向的类(新子类)找实现方法;4.全新的子类会重写
set方法,会调用__NSSetXXXValueAndNotify函数5.重写
class方法
使用方法:
1.注册观察者:
addObserver:forKeyPath:options:context(包含任意数据):2.属性变化通知:
observeValueForKeyPath:ofObject:change:context:3.移除观察者:
removeObserver:forKeyPath:context:
手动关闭KVO、手动触发KVO:
1.
+(BOOL)automaticallyNotifiesObserversForKey手动关闭KVO2.
willChangeValueForKey、didChangeValueForKey手动触发KVO
imageName & imageWithContentsOfFile区别:
1.
imageNamed:系统内存缓存/支持Assets和mainBundle加载/不随对象释放/小尺寸高频使用/非线程安全(建议主线程调用)2.
imageWithContentsOfFile:无缓存/仅支持mainBundle/随对象销毁释放/大尺寸低频使用/可安全用于后台线程
内存管理:
1.栈区(
stack):存储临时创建的局部变量和函数参数等,作用域执行完毕被系统回收,地址由高到低分布。2.堆区(
heap):存储动态分配的内存段(通过调用alloc等函数),默认ARC管理,MRC模式下需手动释放,地址由低到高分布。3.全局静态区:存放全局变量 和 静态变量,程序结束后由系统释放;主要分为:
BBS区:存放未初始化的全局变量 和 静态变量。数据区:存放已初始化的全局变量 和 静态变量。
4.常量区:存放的是常量,如常量字符串,程序结束后由系统释放。
5.代码区:由于存放程序代码。
内存管理机制:ARC(自动内存计数)、手动内存计数、内存池。
1.自动内存计数
ARC:由Xcode自动在App编译阶段,在代码中添加内存管理代码。2.手动内存计数
MRC:遵循谁创建,谁释放,谁引用,谁管理的原则。3.内存释放池
Release Pool:内存池的释放:自动和手动。自动释放受runloop机制影响。
网络相关:
网络的七层协议:
物理层,网卡。数据单位是
bit数据链路层,交换机,负责节点间的通信传输,保障数据传输可靠性。数据单位是帧
网络层,创建节点之间的传输逻辑链路,通过路由实现不同局域网间的通信。数据单位是数据包
传输层,建立了主机端到端服务,处理数据包错误和保证次序。
会话层,维护两个结点间的传输连接,确保点到点传输不中断,以及管理数据交换。
表示层,设备的数据格式和网络标准格式的转换
应用层,提供各种网络服务,比如文件服务器、数据库服务与其他网络软件服务。
什么是 HTTP/HTTPS/TCP / UDP/ Socket:
http(超文本传输协议):应用层协议
运行在
TCP之上,明文传输,无状态连接;
https(SSL+HTTP协议构建):
1.需要到
ca申请证书,安全性的ssl加密传输协议。2.可进行加密传输、身份认证。
TCP(数据传输协议):
1.面向连接的(通信之前先建立连接,确保双方在线:三次握手、四次挥手)
2.可靠传输(在网络正常的情况下,数据不会丢失)
3.面向字节流、流量控制(滑动窗口协议)、拥塞控制(慢开始/拥塞避免、快恢复/快重传)
UDP(用户数据报协议):
无连接、尽最大努力交付数据、面向报文(既不合并也不拆分)
Socket(对TCP/IP的封装):
1.本身并不是协议,是一个调用接口
2.应用层的编程接口
HTTP和HTTPS的三次握手与四次挥手:
三次握手过程:ISN 是动态生成的
1.客户端给服务端发一个
SYN报文,并指明初始化序列号ISN(c),客户端处于SYN_SENT状态。2.服务器收到
SYN报文,以自己的SYN报文应答,指定自己的初始化序列号ISN(s),把客户端的ISN+1作为ACK值,表示已收到,服务器处于SYN_REVD状态。3.客户端收到
SYN报文,会发送一个ACK报文,把服务器的ISN+1作为ACK值,表示已收到,客户端处于establised状态。服务器收到
ACK报文之后,也处于establised状态,此时,双方建立链接。
三次握手作用:
1.确认双方的接受能力、发送能力是否正常。
2.指定自己的初始化序列号,为后面的可靠传送做准备。
3.如果是
https协议的话,还会进行数字证书的验证以及加密密钥的生成到。
四次挥手:
1.客户端发送一个
FIN报文,报文中会指定一个序列号。客户端处于CLOSED_WAIT1状态。2.服务端收到
FIN之后,会发送ACK报文,且把客户端的序列号值+ 1作为ACK报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT2状态。3.如果服务端也想断开连接了,和客户端的第一次挥手一样,发给
FIN报文,且指定一个序列号。此时服务端处于LAST_ACK的状态。4.客户端收到
FIN之后,一样发送一个ACK报文作为应答,且把服务端的序列号值+ 1作为自己ACK报文的序列号值,此时客户端处于TIME_WAIT状态。需要过一阵子以确保服务端收到自己的ACK报文之后才会进入CLOSED状态服务端收到
ACK报文之后,就处于关闭连接了,处于CLOSED状态。
HTTPS的SSL加密方式:
1.对称加密:加密与解密用的是同样的密钥:
AES,DES,3DES等2.非对称加密:密钥成对,分为公钥和私钥,公钥加密需要私钥解密,私钥加密需要公钥解密:
RSA算法
HTTPS传输数据的过程?
1.使用
HTTPS需要保证服务端配置好安全证书2.客户端发送请求到服务端,服务端返回证书和公钥
3.客户端拿到证书验证安全性,然后生成一个随机数,用公钥加密随机数后发送给服务端
服务端用私钥解密得到随机数,再用这个随机数作为私钥对传输的数据加密,最后发送数据
4.客户端拿到传输的数据后用之前生成的随机数对其解密获取数据
SSL建立连接的过程是什么?
1.首先客户端向服务器发送自身的
SSL版本以及加密参数给服务器2.服务器返回自己的
SSL版本和参数以及数字证书包括了服务器的公钥3.客户端生成浏览器会话秘钥通过公钥进行加密返回给服务器,服务器通过私钥解密出会话秘钥
4.客户端再发送一个报文通知服务器以后通过该会话秘钥进行加密传输,并发送加密报文表示我方
SSL链接建立完成5.服务器也回复相同的表示自己也建立连接完成
GET和POST请求的区别:
1.
POST比GET更安全,GET把请求参数拼接到URL后面,POST把请求参数放在请求体里面2.
GET比POST请求速度快,因为少了一次确认过程
HTTP的header都有哪些?
1.请求消息格式:请求方法、
URL、协议版本2.响应消息格式:状态响应码、协议版本
网络请求的依赖关系:当一个接口的请求需要依赖于另一个网络请求的结果:
方法1:在上一个网络请求的响应回调中进行下一网络请求的激活
方法2:线程:
NSOperation操作依赖和优先级,操作B依赖于操作方法3:
GCD信号量方法4:
GCD group
数据结构与算法:
【冒泡排序】:重复比较相邻2个元素并交换,最值(大)在末尾,时间复杂度:最差 O(n²),平均 O(n²)
for (NSUInteger i = 0; i < arr.count - 1; i++) {
for (NSUInteger j = 0; j < arr.count - 1 - i; j++) {
if (arr[j]>compare:arr[j+1]) {
[arr exchangeObjectAtIndex:j withObjectAtIndex:j+1];
}
}
}
【选择排序】:从未排序部分选出最小/大元素,与未排序部分第一个元素交换位置。重复此过程直到所有元素有序,时间复杂度:O(n²)
for (NSUInteger i = 0; i < arr.count - 1; i++) {
NSUInteger minIndex = i;
// 在未排序部分查找最小值
for (NSUInteger j = i + 1; j < arr.count; j++) {
if (arr[j]> arr[minIndex]) {
minIndex = j;
}
}
// 将最小值交换到已排序部分末尾
if (minIndex != i) {
[arr exchangeObjectAtIndex:i withObjectAtIndex:minIndex];
}
}
【折半/二分查找】:优化查找时间(不用遍历全部数据)
1> 数组必须是有序的、必须已知min和max(知道范围)
2> 动态计算mid的值,取出mid对应的值进行比较
3> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
4> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
int findKey(int *arr, int length, int key) {
int min = 0, max = length - 1, mid;
while (min <= max) {
mid = (min + max) / 2;
if (key > arr[mid]) {
min = mid + 1;
} else if (key < arr[mid]) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}
【插入排序】未排序元素逐个插入已排序子集的正确位置,时间复杂度:最差 O(n²),平均 O(n²)
func insertionSort(_ array: inout [Int]) {
for i in 1..<array.count {
let key = array[i]
var j = i - 1
while j >= 0 && array[j] > key {
array[j + 1] = array[j]
j -= 1
}
array[j + 1] = key
}
}
【快速排序】:递归,选取基准值将数组分为左右子集(左小右大),再合并,
时间复杂度:平均 O(n log n),最差 O(n²)(基准选择不当)
- (void)quickSort:(NSMutableArray *)arr left:(NSInteger)left right:(NSInteger)right {
if (left < right) {
NSInteger pivot = [self partition:arr left:left right:right];
[self quickSort:arr left:left right:pivot - 1];
[self quickSort:arr left:pivot + 1 right:right];
}
}
- (NSInteger)partition:(NSMutableArray *)arr left:(NSInteger)left right:(NSInteger)right {
NSNumber *pivot = arr[right];
NSInteger i = left - 1;
for (NSInteger j = left; j < right; j++) {
if ([arr[j] intValue] <= [pivot intValue]) {
i++;
[arr exchangeObjectAtIndex:i withObjectAtIndex:j];
}
}
[arr exchangeObjectAtIndex:i + 1 withObjectAtIndex:right];
return i + 1;
}
【字符串反转】:
void char_reverse (char *cha) {
// 定义头部指针
char *begin = cha;
// 定义尾部指针
char *end = cha + strlen(cha) -1;
while (begin < end) {
char temp = *begin;
*(begin++) = *end;
*(end--) = temp;
}
}
【单向链表是否有环】:时间复杂度:O(n)
1.利用NSSet集合的元素去重特性,遍历把所有节点加入到集合和数组中,如果集合元素数小于数组元素数,则有环
2.快慢双指针法,快指针一次走两步,慢指针一次走一步,如果有环必会相遇
+ (BOOL)hasCycle:(ListNode *)head {
if (!head || !head.next) return NO;
ListNode *slow = head;
ListNode *fast = head.next;
while (fast && fast.next) {
if (slow == fast) return YES;
slow = slow.next;
fast = fast.next.next;
}
return NO;
}
【计算二叉树的高度】:递归方法,二叉树的高度等于其左右子树高度的较大值加1。时间复杂度为O(n)
+ (NSInteger)heightOfTree:(TreeNode *)root {
if (!root) return 0;
NSInteger leftHeight = [self heightOfTree:root.left];
NSInteger rightHeight = [self heightOfTree:root.right];
return MAX(leftHeight, rightHeight) + 1;
}
【A和B的值交换】:
中间变量:
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
加法:
void swap(int a, int b) {
a = a + b;
b = a - b;
a = a - b;
}
异或:
void swap(int a, int b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
【最大公约数】:
直接遍历法:
int maxCommonDivisor(int a, int b) {
int max = 0;
for (int i = 1; i <=b; i++) {
if (a % i == 0 && b % i == 0) {
max = I;
}
}
return max;
}
辗转相除法:
int maxCommonDivisor(int a, int b) {
int r;
while(a % b > 0) {
r = a % b;
a = b;
b = r;
}
return b;
}
性能优化:
1.启动时间优化:懒加载、多线程处理耗时任务
2.响应时间优化:异步处理耗时任务、避免主线程阻塞、减少视图层级复杂度
3.网络性能优化:减少网络请求次数、合并请求、使用缓存、压缩数据传输量
4.数据库存储优化:选择合适的数据库、避免频繁的数据读写操作
5.动态库优化:减少不必要的依赖库、使用静态和动态库的合理组合、
6.图像性能优化:使用合适大小的图片资源、压缩图片文件、使用
webP或者HEIC格式7.安全性能优化:代码混淆、增加反编译难度、加密存储敏感数据、防止数据篡改和注入攻击
8.第三方库优化:精简第三方库、选择高性能替代库
9.算法优化:避免重复计算(缓存中间结果/
Hash查找替代全遍历)、空间换时间10.定量消耗优化:网络请求、避免后台持续运行、减少不必要的定位和推送
如何做启动优化:
APP的冷启动可以概括为4大阶段:
1.
dyld动态库加载
启动
APP时,dyld会先装载APP的可执行文件,同时会递归加载所有依赖的动态库当
dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理2.
Runtime初始化
Runtime会调用map_images进行可执行文件内容的解析和处理在
load_images中调用call_load_methods,调用所有Class和Category的+load方法进行各种
objc结构的初始化(注册Objc类 、初始化类对象等等)调用
C++静态初始化器和__attribute__(())修饰的函数3.进入
main函数,开始整个应用的生命周期
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库,由
Runtime负责加载成objc定义的结构
所有初始化工作结束后,
dyld就会调用main函数接下来就是
UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法4.初始化帧渲染,到
viewDidAppear执行完,用户可见可操作。
启动优化:
减少冷启动时间:应用进程不在系统中,需要分配新的进程来启动(冷启动),优化方法:
1.合并动态库,减少不必要的
framework。删除无用代码、无用的静态变量。2.不必须在
+load方法中做的事情延迟到+initialize中。3.避免使用
attribute(()),将要实现内容放在初始化配合dispatch_once使用。4.减少非基本类型的
C++静态全局变量的个数。
main函数代理阶段:优化方法:
1.删除不必要的初始化代码。
2.延迟加载不必要的模块和功能。
3.使用异步加载和懒加载来提高启动速度。
如何做卡顿优化:(比如tableView卡顿优化)
卡顿优化在CPU层面:
1.使用轻量级对象,如
CALayer代替UIView,避免不必要的事件处理2.不要频繁地调用
UIView的相关属性,比如frame、bounds、transform等3.提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
4.
Autolayout会比直接设置frame消耗更多的CPU资源5.图片的
size最好刚好跟UIImageView的size保持一致6.控制一下线程的最大并发数量、尽量把耗时的操作放到子线程
7.文本处理(尺寸计算、绘制)、图片处理(解码、绘制)
卡顿优化在GPU层面:
1.合并多张图片为一张图片显示,减少短时间内大量图片的显示
2.控制纹理尺寸不超过
4096*4096,避免占用CPU资源进行处理3.尽量减少视图数量和层次、减少透明的视图
4.尽量避免出现离屏渲染
如何做耗电优化:
优化
CPU使用:通过分析Time Profiler或sysdiagnose的数据,找出CPU占用高的函数或方法,并进行优化,如减少循环次数、使用异步操作等。优化网络使用:减少网络请求次数,合并请求,使用缓存等方式来降低网络耗电。
优化定位使用:根据需求选择合适的定位精度,避免过高的精度导致的额外能耗。
优化
GPU使用:减少不必要的图形渲染操作,避免过度绘制等。
如何做网络优化:
1.减少请求带宽:
使用高效的数据交互模式。
在可能的情况下使用预先压缩的数据。
压缩每一个请求与响应负载。
2.降低请求延迟:
最小化
DNS查询的数量,可以使用DNS预下载进行更早的DNS解析。最大程度减少应用发起连接数,请求结束不要关闭
HTTPS连接,使用域分片等方式来减少SSL握手时间。在单个
TCP连接上发送HTTP请求,以管道形式发送HTTP请求,避免多个TCP连接的开销。
UITableView的重用机制?
1.
UITableView为每个单元格指定一个重用标识符来节省内存2.当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队列中,等待被重用。
3.新单元格从屏幕外滑入屏幕内,从重用队列查找可重用单元格,有就用,没有就创建。
造成tableView卡顿的原因
1.没有使用
cell的重用标识符,使用addView给cell动态添加view2.
cell的重新布局、cell中控件的数量过多、没有提前计算并缓存cell的属性及内容3.使用了
ClearColor,无背景色,透明度为04.不同场景使用合适的刷新方法
5.加载网络数据,下载图片,没有使用异步加载,并缓存
第三方库:
SDWebImage:(二级缓存策略,内存(NSCache)和磁盘)
1、
setImageWithURL:先显示占位图,然后SDWebImageManager根据URL开始处理图片。2、
SDWebImageManager-downloadWithURL:下载前SDImageCache从内存缓存查找,如果有图片缓存,回调到UIImageView+WebCache等前端展示图片。3、内存缓存没有,生成
NSInvocationOperation添加到队列根据URLKey从硬盘查找,如果有添加到内存缓存中,进而回调展示图片。4、硬盘缓存没有,
SDWebImageDownloader开始下载图片,下载完成后交给SDWebImageDecoder做图片解码处理。5、回调展示图片,内存缓存和硬盘缓存同时保存。
AFNetworking 中如何运用 Runloop?
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source ,所以
1.
[runLoop run]之前先创建了一个新的NSMachPort添加进去了。2.此处添加
port只是为了让RunLoop不至于退出,并没有用于实际的发送消息。3.当需要后台线程执行任务时,通过
performSelector:将任务扔到后台线程的RunLoop
FMDB:支持多线程吗?它是如何实现的!
支持多线程。它有
FMDatabaseQueue类,并不是队列,继承NSObject。
内部创建串行队列处理
inDatabase/inTransaction传入的Block,在主线程或后台调用inDatabase/inTransaction时,实际上是同步的。
FMDatabaseQueue这样设计是为了避免并发访问数据库时造成的线程安全问题,所有的数据库访问都是同步执行,比@synchronized与NSLock效率高。
FMDB的性能优化建议?
使用事务(
beginTransaction/commit)批量插入数据。避免频繁打开/关闭数据库连接。常用查询添加索引
设计模式:
MVC:M:业务数据, V:视图,负责展示 C:控制器,负责协调M、V
C作为M和V之间的连接, 负责响应视图事件,界面的跳转,view的声明周期,获取业务数据, 然后将处理后的数据输出到界面上做相应展示, 在数据有更新时,C需要及时提交相应更新到界面展示。View和Model之间没有直接的联系
MVP:M:业务数据, V:视图, P:协调器,业务处理层
P:业务逻辑的处理者,作为M、V的桥梁。当获取到数据后进行相应处理, 处理完成后会通知绑定的View数据有更新,View收到更新通知后从P获取格式化好的数据进行页面渲染。相比较MVC,MVP把业务逻辑和View的展示分离开
MVVM:M:业务数据, V:视图, VM:视图模型,展示逻辑
相比较
MVC,View/ViewController不直接引用Model,而是通过ViewModel,ViewModel负责用户交互逻辑,视图显示逻辑,发起网络请求等
单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。
UIApplication(应用程序实例类)、NSNotificationCenter(消息中心类)、NSFileManager(文件管理类)、NSUserDefaults(应用程序设置)、NSURLCache(请求缓存类)、NSHTTPCookieStorage(应用程序cookies池)
观察者模式:KVO是典型的观察者模式,观察某个属性的状态,状态发生变化时通知观察者。
委托模式:代理+协议的组合。实现1对1的反向传值操作。
工厂模式:通过一个类方法,批量的根据已有模板生产对象。
swift相关知识点
OC与Swift的异同:
1.
Swift是强类型(静态)语言,有类型推断,OC弱类型(动态)语言2.
Swift面向协议编程,OC面向对象编程3.
Swift注重值类型,OC注重引用类型4.
Swift支持泛型,OC只支持轻量泛型(给集合添加泛型)5.
Swift支持静态派发/动态派发(函数表派发、消息派发),OC支持动态派发(消息派发)6.
Swift支持函数式编程(高阶函数)7.
Swift的协议不仅可以被类实现,也可以被Struct和Enum实现8.
Swift有元组类型、支持运算符重载9.
Swift支持命名空间、支持默认参数、Swift比OC代码更简洁
类(class) 和 结构体(struct) 有什么区别:
1.
Struct不支持继承,Class支持继承2.
Struct是值类型,Class是引用类型3.
Struct使用let创建不可变,Class使用let创建可变4.
Struct无法修改自身属性值,函数需要添加mutating关键字5.
Struct不需要deinit方法,因为值类型不关系引用计数,Class需要deinit方法6.
Struct初始化方法是基于属性的