iOS 进阶知识总结(四)
3~5年开发经验的 iOS工程师 应该知道的内容~本文总结以下内容
- 内存管理
- 野指针处理
- autoreleasePool
- weak
- 单例、通知、block、继承和集合
导航
- 对象
- 类对象
- 分类
- runtime
- 消息与消息转发
- KVO
- KVC
- 多线程
- 锁
- runloop
- 计时器
- 视图渲染和离屏渲染
- 事件传递和响应链
- crash处理和性能优化
- 编译流程和启动流程
- 内存管理
- 野指针处理
- autoreleasePool
- weak
- 单例、通知、block、继承和集合
- 网络基础
- AFNetWorking
- SDWebImage
内存管理
堆和栈区的区别
- 栈
- 栈由系统分配和管理
- 栈的内存增长是向下的
- 栈内存速率比堆快
- 栈的大小一般默认为1M,但可以在编译器中设置
- 操作系统中具有专门的寄存器存储栈指针,以及有相应的硬件指令去操作栈内存分配
- 堆
- 堆由开发者申请和管理
- 堆的内存增长是向上的
- 堆内存速率比栈慢
- 内存比较大,一般会达到4G
- 忘记释放会造成内存泄漏
堆为什么默认4G?
- 系统是32位的,最多只支持32位的2进制数来表示内存地址
- 2^32 = 4G,没法表示比4G更大的数字了,所以寻址只能寻到 4G
机器内存条16G,虚拟内存只有4G,岂不是浪费?
- 虚拟内存大小和物理内存大小无关
- 虚拟内存是物理内存不够用时把一部分硬盘空间做为内存来使用
- 由于硬盘传输的速度要比内存传输速度慢的多,所以使用虚拟内存比物理内存效率要慢
一个进程的地址和物理地址之间的关系是什么?
- CPU能够访问到的是进程中记录的逻辑地址,
- 使用页式内存管理方案,逻辑地址包括页号和页内偏移量
- 页号可以在页表中查询得到物理内存中划分的页
- 找到页以后用进程的起始地址拼接上页内偏移量可以得到实际物理地址
这样有什么更快的方法去计算物理地址?
- TLB快表
同一个进程里哪些资源是线程间共享的,哪些是独有的。
- 堆:所有线程共有的
- 栈:单个线程私有的
哪些变量保存在堆里,哪些保存在栈里
- 指针在栈里,对象在堆里,指针指向对象。
什么是野指针?
- 指向被释放/回收对象的指针。
如何检测野指针?
引用Bugly
工程师陈其锋的思路,fishhook free
函数,把释放的空间填入0x55
。XCode
的僵尸对象填充的就是0x55
。这样可以使偶现的野指针问题问题(对象释放仍被调用)变为必现,方便排查。
bool init_safe_free() {
_unfreeQueue = ds_queue_create(MAX_STEAL_MEM_NUM);
orig_free = (void(*)(void*))dlsym(RTLD_DEFAULT, "free");
rebind_symbols((struct rebinding[]){{"free", (void*)safe_free}}, 1);
return true;
}
void safe_free(void* p){
size_tmemSiziee=malloc_size(p);
memset(p,0x55, memSiziee);
orig_free(p);
return;
}
但是如果上述内存被重新填充了可用数据,就无法检测到了。
所以其实可以直接在替换的free函数
中做更多的操作。
用哈希表记录需要别释放的对象,但实际上并不释放,只是把里面的数据替换成0x55,该指针再被调用时就会crash。
在发生内存警告的时候再清理一部分内存。
这种改动不可以出现在线上版本,只能用于排查crash。
DSQueue* _unfreeQueue=NULL;//用来保存自己偷偷保留的内存:1这个队列要线程安全或者自己加锁;2这个队列内部应该尽量少申请和释放堆内存。
int unfreeSize=0;//用来记录我们偷偷保存的内存的大小
#define MAX_STEAL_MEM_SIZE 1024*1024*100//最多存这么多内存,大于这个值就释放一部分
#define MAX_STEAL_MEM_NUM 1024*1024*10//最多保留这么多个指针,再多就释放一部分
#define BATCH_FREE_NUM 100//每次释放的时候释放指针数量
//系统内存警告的时候调用这个函数释放一些内存
void free_some_mem(size_t freeNum){
size_t count=ds_queue_length(_unfreeQueue);
freeNum=freeNum>count?count:freeNum;
for (int i=0; i<freeNum; i++) {
void* unfreePoint=ds_queue_get(_unfreeQueue);
size_t memSiziee=malloc_size(unfreePoint);
__sync_fetch_and_sub(&unfreeSize,memSiziee);
orig_free(unfreePoint);
}
}
void safe_free(void* p){
#if 0//之前的代码我们先注释掉
size_t memSiziee=malloc_size(p);
memset(p, 0x55, memSiziee);
orig_free(p);
#else
int unFreeCount=ds_queue_length(_unfreeQueue);
if (unFreeCount>MAX_STEAL_MEM_NUM*0.9 || unfreeSize>MAX_STEAL_MEM_SIZE) {
free_some_mem(BATCH_FREE_NUM);
}else{
size_t memSiziee=malloc_size(p);
memset(p, 0x55, memSiziee);
__sync_fetch_and_add(&unfreeSize,memSiziee);
ds_queue_put(_unfreeQueue, p);
}
#endif
return;
}
bool init_safe_free()
{
_unfreeQueue=ds_queue_create(MAX_STEAL_MEM_NUM);
orig_free=(void(*)(void*))dlsym(RTLD_DEFAULT, "free");
rebind_symbols1((struct rebinding[]){{"free", (void*)safe_free}}, 1);
return true;
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
free_some_mem(1024*1024);
}
简单谈一下内存管理
- 通过引用计数管理对象的释放时机。创建的时候引用计数+1,出现新的持有关系的时候引用计数+1。当持有对象放弃持有的时候引用计数-1,当对象的引用计数减至0的时候,就要把对象释放。MRC模式需要手动管理引用计数。ARC模式引用计数交由系统管理
- 自动释放池
AutoReleasePool
是OC
的一种内存自动回收机制,回收统一释放声明为autorelease
的对象;系统中有多个内存池,系统内存不足时,取出栈顶的池子把引用计数为0的对象释放掉,回收内存給当前应用程序使用。 自动释放池本身销毁的时候,里面所有的对象都会做一次release
。
autoreleasepool
的使用场景
- 创建了大量对象的时候,例如循环的时候
autoreleasePool
的数据结构
autoreleasePool
底层是AutoreleasePoolPage
- 可以理解为双向链表,每张链表头尾相接,有
parent
、child
指针 - 每次初始化调用
objc_autoreleasePoolPush
,会在首部创建一个哨兵对象作为标记,释放的时候就以哨兵为止 - 最外层池子的顶端会有一个
next
指针。当链表容量满了(4096字节,一页虚拟内存的大小),就会在链表的顶端,并指向下一张表。
autoreleasePool
什么时候被释放?
ARC中所
有的新生对象都是自动添加autorelese
的@atuorelesepool
解决了大部分内存暴增的问题。autoreleasepool
中的对象在当前runloop
循环结束的时候自动释放。
子线程中的autorelease
变量什么时候释放?
- 子线程中会默认生成一个
autoreleasepool
, 当线程退出的时候释放。
autoreleasepool
是如何实现的?
@autoreleasepool{}
本质是一个结构体autoreleasepool
会被转换成__AtAutoreleasePool
__AtAutoreleasePool
里面有objc_autoreleasePoolPush
、objc_autoreleasePoolPop
两个关键函数- 最终调用的是
AutoreleasePoolPage
的push
和pop
方法 push
是压栈,pop
是出栈,pop
的时候以哨兵作为参数,对所有晚于哨兵插入的对象发送release
消息进行释放
放入@autuReleasePool
的对象,当自动释放池调用drain
方法时,一定会释放吗
drain
和release
都会促使自动释放池对象向池内的每一个对象发送release消息来释放池内对象的引用计数release
触发的操作,不会考虑对象是否需要release
,drain
会在自动释放池向池内对象发送release
消息的时候,考虑对象是否需要release
- 对象是否释放取决于引用计数是否为0,池子是否释放还是取决于里面的所有对象是否引用计数都为0。
@aotuReleasePool
的嵌套使用,对象内存是如何被释放的
- 每次初始化调用
objc_autoreleasePoolPush
,会在首部创建一个哨兵对象作为标记 - 释放的时候就会依次对每个pool里晚于哨兵的对象都进行
release
- 从内到外的顺序释放
ARC环境下有内存泄漏吗?举例说明
- 有。例如两个
strong
修饰的对象相互引用。 block
中的循环引用NSTimer
的循环引用delegate
的强引用- 非
OC
对象的内存处理(需手动释放)
出现内存泄漏,该如何解决?
- 使用
Instrument
当中的Leak
检测工具 - 使用僵尸变量,根据打印日志,然后分析原因,找出内存泄漏的地方
ARC
对reatain & release
优化了什么
- 根据上下文及阴影关系,减少了不必要的
retain
和release
- 例如
MRC
环境下引用一个autorelease
对象,对象会经历new -> autorelease -> retain -> release
,但是仅仅只是引用而已,中间的autorelease
和retain
操作其实可以去除,所以ARC
就是把这两步不需要的操作优化掉了
MRC转成ARC管理,需要注意什么
- 去掉所有的
retain,release,autorelease
NSAutoRelease
替换成@autoreleasepool{ }
块assign
修饰的属性需要根据ARC规定改写dealloc
方法来管理一些资源释放,但不能释放实例变量,dealloc
里面去掉[super dealloc]
,ARC下父类dealloc
由编译器来自动完成Core Foundation
的对象可以用CFRetain,CFRelease
这些方法- 不能在使用
NSAllocateObject、NSDeallocateObject
void * 和 id
类型的转换,oc对象和c对象的转换需要特定函数
实际开发中,如何对内存进行优化呢?
- 使用ARC管理内存
- 使用
Autorelease Pool
- 优化算法
- 避免循环引用
- 定期使用
Instrument
的Leak
检测内存泄漏
结构体对齐方式
struct {
char a;
double b;
int c;
}
char 1
short 2
int 4
float 4
long 8
double 8
new和malloc的区别
new
调用了实例方法初始化对象,alloc + init
malloc
函数从堆上动态分配内存,没有init
delete
和free
的区别
delete
是一个运算符,做了两件事- 调用析构函数
- 调用
free
释放内存
free()
是一个函数
内存分布,常量是存放在哪里(重点!)
- 栈区
- 堆区
- 全局静态区
- 代码区
weak
weak是怎么实现的
weak
通过SideTable
实现,SideTable
里面包含了一个锁,一个引用计数表,一个弱引用表weak
关键字修饰的对象会被记录到弱引用表里面weak_table_t
里面有一个数组记录多个弱引用对象(weak_entry_t
),每个weak_entry_t
对应一个被弱引用的OC对象weak_entry_t
里面有记录弱引用指针的数组,存放的是weak_referrer_t
,每个weak_referrer_t
对应一个弱引用指针- 创建的时候判断是否已经创建了
weak_entry_t
,有的话就把新的weak_referrer_t
插入数组,没有的话就创建weak_referrer_t
和weak_entry_t
一起插入到表里。 - 添加的时候还会进行容量判断,如果超过3/4就会容量乘以2进行扩容。
SideTable
最多只能存储64个节点
为什么需要多张SideTable
每个对象都有可能被弱引用,如果都存在一个表里,不同线程、不同操作对这个单表频繁的加锁和解锁,这样处理起事务更容易出现问题。
weak对象为什么可以自动置为nil
dealloc
的过程里面有一步是调用clear_weak_no_lock
,会取出弱引用表遍历每个弱引用对象置为nil
dealloc -> rootDealloc -> object_dispose -> obj_desturctInstance -> clearDeallocating -> clearDeallocating_slow -> weak_clear_no_lock
单例
什么是单例
- 只有一个实例对象。而且向整个系统提供这个实例。
你实现过单例模式么? 你能用几种实现方案?
+ (instancetype)shareInstance {
static ShareObject *share = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
share = [[super allocWithZone:NULL] init];
});
return share;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self shareInstance];
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
单例怎么销毁
dispatch_once
当onceToken
为0的时候才会被调用,调用完成后onceToken
会被置为-1- 必须把
onceToken
变成全局的,在需要的时候重置为0
+ (void)removeShareInstance {
//置0,下次调用shareInstance才会再次创建对象
onceToken = 0;
_sharedInstance = nil;
}
不使用dispatch_once
如何实现单例
- 重写
allocWithZone:
方法;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static id instance = nil;
@synchronized (self) {
if (instance == nil) {
instance = [super allocWithZone:zone];
}
}
return instance;
}
项目开发中,你用单例都做了什么?
- 用户登录后,用
NSUserDefaults
存储用户信息,采用单例封装方便全局访问 - IM聊天管理器使用单例,方便全局访问
Block
什么是block
?
- 闭包,可以获取其它函数局部变量的匿名函数。
block
的内部实现
block
是个对象,block
的底层结构题也有isa
,这个isa
会指向block
的类型block
的底层结构体是__main_block_impl_0
,存储了下列数据- 方法实现的指针
impl
block
的相关信息Desc
- 如果有捕获外部变量,结构体内还会存储捕获的变量。
- 方法实现的指针
- 使用
block
时就会根据impl
找到方法所在,传入存储的变量调用。
block
的类型
block
有3种类型,可以通过调用class方法或者isa指针查看具体类型
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
,存在全局区__NSStackBlock__ ( _NSConcreteStackBlock )
,存在栈区__NSMallocBlock__ ( _NSConcreteMallocBlock )
,存在堆区
int
变量被 __block
修饰与否的区别?
block
对未经__block
修饰的int
变量的引用是值拷贝,在block中是不能改变外部变量的。- 通过
__block
修饰后的int
变量,block
对这个变量的引用是指针引用。它会生成一个结构体复制这个变量的指针,从而达到可以修改外部变量的作用。
block
在修改NSMutableArray
,需不需要添加__block
- 不需要,不改变数组指针的指向,只是添加数组内容
block
捕获外部局部变量实际上发生了什么?__block
又做了什么?
block
捕获外部变量的时候,会记录下外部变量的瞬时值,存储在block_impl_0
结构体里__block
所起到的作用就是只要观察到该变量被block
所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block
内部也可以修改外部变量的值。- 总之,
block
内部可以修改堆中的内容, 不可以直接修改栈中的内容。
在ARC
和MRC
下block
访问对象类型的变量时,有什么区别
ARC
环境会根据外部变量是__strong
还是__weak
修饰进行引用计数管理,达到强引用或弱引用的效果MRC环
境下,block
属于栈区,外部变量是auto
修饰的,不手动copy
的话变量就不会被block
强引用。
block
可以用strong
修饰吗
MRC
环境下,不可以。strong
只会把block
进行一次retain
操作,栈上的block
不会被复制到堆区,依旧无法共享ARC
环境下,可以。block
在堆区,而且block
的retain
操作也是通过copy
完成
block
为什么用copy
修饰?
MRC
环境,block
创建在栈区,只要函数作用域消失就会被释放,外部再去调用就会崩溃。通过copy
修饰可以把它复制到堆区,外部调用也没问题,从而解决了这个问题。ARC
环境,block
创建在堆区,用strong
和copy
都一样。block
的retain
操作也是通过copy
完成。以前用copy
就一直延续了。
block
在什么情况下会被copy
- 主动调用
copy
方法 - 当
block
作为返回值时 - 将
block
赋值给__strong
指针时 block
作为GCD API
的方法参数时block
作为Cocoa API
方法名含有usingBlock
的方法参数时
block
的内存管理
block
通过block_copy
、block_release
两个方法管理内存NSGlobalBlock
,使用retain、copy、release
都不会不会改变引用计数,copy
方法不会复制,只会返回block
的指针NSStackBlock
,使用retain、release
都不会改变引用计数,使用copy
会把block
复制到堆区NSMallocBlock
,使用retain、copy
会增加一次引用,使用release
会减少一次引用- 被
block
引用到外部变量,如果block
存在堆区或者被复制到堆区,变量的引用计数+1,block
释放后-1
解决循环引用时为什么要用__strong、__weak
修饰
- 在
block
外部使用__weak
修饰外部引用对象,可以打破互相持有造成的循环引用 - 在
block
中使用__strong
修饰外部引用对象,block
强持有外部变量,可以防止外部变量被提前释放
在Masonry
的block
中,使用self
,会造成循环引用吗?如果是在普通的block
中呢?
- 不会,因为这是个栈
block
,没有延迟使用,使用后立刻释放 - 普通的
block
会,一般会使用强引用持有,就会触发copy
操作
在普通的block
中只使用下划线属性去访问,会造成循环引用吗
- 会,和调用
self.
是一样的
NSNotification
消息通知的理解
- 通知(
NSNotification
)支持一对多的信息传递方式 - 使用时先注册绑定接收通知的方法,然后通知中心创建并发送通知
- 不再监听时需要移除通知
实现原理(结构设计、通知如何存储的、name & observer & SEL
之间的关系等)
Observation
是通知观察对象,存储通知名、object
、SEL
的结构体NSNotificationCenter
持有一个根容器NCTable
,根容器里面包含三个张链表wildCards
,存放没有name & object
的通知观察对象(Observation
)nameless
,存放没有name
但是有object
的通知观察对象(Observation
)named
,存放有name & object
的通知观察对象(Observation
)
- 当添加通知观察的时候,
NSNotificationCenter
根据传入参数是否齐全,创建Observation
并添加到不同链表- 创建一个新的通知观察对象(
Observation
) - 如果传入参数包含名称,在
named
表里查询对应名称,如果已经存在同名的通知观察对象,将新的通知观察对象插入其后,如果不存在则添加到表尾。存储结构为链表,节点内先以name
作为key,一个字典作为value
。如果通知参数带有object
,字典内以object
为key
,以Observation
作为value
。 - 如果传入的参数如果只包含
object
,在nameless
表查询对应名称,将新的通知观察对象插入其后,如果不存在则添加到表尾。存储结构为链表,节点内以object
为key
,以Observation
作为value
。 - 如果传入参数没有
name
也没有object
,直接添加到wildCards
表尾。结构为链表,节点内存储Observation
。
- 创建一个新的通知观察对象(
通知的发送是同步的,还是异步的
- 通知的接收和发送是在一个线程里,实际上发送通知都是同步的,不存在异步操作
- 通知提供了枚举设置发送时机
NSPostWhenIdle
,runloop
空闲的时候发送NSPostASAP
,尽快发送,会穿插在事件完成的空隙中发送NSPostNow
,立刻发送或合并完成后发送
NSNotificationCenter
接受消息和发送消息是在一个线程里吗?如何异步发送消息
- 是的
- 异步发送,也就是延迟发送,可以使用
addObserverForName:object: queue: usingBlock:
NSNotificationQueue
是异步还是同步发送?在哪个线程响应
- 异步发送,也就是延迟发送
- 在同一个线程发送和响应
NSNotificationQueue
和runloop
的关系
NSNotificationQueue
只是把通知添加到通知队列,并不会主动发送NSNotificationQueue
依赖runloop
,如果线程runloop
没开启就不生效。NSNotificationQueue
发送通知需要runloop
循环中会触发NotifyASAP
和NotifyIdle
从而调用NSNotificationCenter
NSNotificationCenter
内部的发送方法其实是同步的,所以NSNotificationQueue
的异步发送其实是延迟发送。
如何保证通知接收的线程在主线程
- 1、在主线程发送通知
- 2、使用
addObserverForName: object: queue: usingBlock
方法注册通知,指定在主线程处理
页面销毁时不移除通知会崩溃吗
- iOS9之前会,因为强引用观察者
- iOS9之后不会,因为改为了弱引用观察者
多次添加同一个通知会是什么结果?多次移除通知呢
- 多次添加,重复触发,因为在添加的时候不会做去重操作
- 多次移除不会发生崩溃
下面的方式能接收到通知吗?为什么
// 注册通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 发送通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
- 不能
- 这个通知存储在
named
表里,原本记录的通知观察对象内部会用object
作为字典里的key
,查找的时候没了object
无法找到对应观察者和处理方法。
其他模式
继承与组合的优缺点
- 继承
- 通过父类派生子类
- A继承自B,可以理解为A是B的某一种分支,B的变化会对A产生影响
- 优点:
- 易于使用、扩展继承自父类的能力
- 缺点:
- 都是白盒复用,父类的细节一般会暴露给子类
- 父类修改时,除非子类自行实现,否则子类会跟随变化
- 优点:
- 组合
- 设计类的时候把需要组合的类(成员)的对象加入到该类(容器)中作为成员变量。
- 例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分
- 容器类(头)仅能通过被包含对象(眼耳口鼻)的接口来对其进行访问。
- 优点:
- 黑盒复用,因为被包含对象的内部细节对外是不可见。
- 封装性好,每一个类只专注于一项任务,实现上的相互依赖性比较小。
- 缺点:
- 导致系统中的对象过多
- 为了组成组合,必须仔细地对成员的接口进行定义
- 优点:
工厂模式是什么,工厂模式和抽象工厂的区别
- 工厂模式,定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
- 抽象工厂,使用了工厂模式后工厂提供的能力非常多,需要分类这些工厂,就可以根据工厂的共性进行抽象合并。
- 抽象工厂其实就是帮助减少工厂数量的,前提条件就这些工厂要具备两个及以上的共性。
原型模式是什么
- 通过复制原型实例创建新的对象。
- 需要遵循
NSCoping
协议 并重写copyWithZone
方法
转载自:https://juejin.cn/post/7076028177570594847