3~5年开发经验的 iOS工程师 应该知道的内容~本文总结以下内容
- 内存办理
- 野指针处理
- autoreleasePool
- weak
- 单例、告诉、block、承继和调集
导航
iOS 进阶常识总结(一)
- 目标
- 类目标
- 分类
- runtime
- 音讯与音讯转发
iOS 进阶常识总结(二)
- KVO
- KVC
- 多线程
- 锁
- runloop
- 计时器
iOS 进阶常识总结(三)
- 视图烘托和离屏烘托
- 事件传递和呼应链
- crash处理和性能优化
- 编译流程和发动流程
iOS 进阶常识总结(四)
- 内存办理
- 野指针处理
- autoreleasePool
- weak
- 单例、告诉、block、承继和调集
iOS 进阶常识总结(五)
- 网络根底
- 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
办法