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函数,把开释的空间填入0x55XCode的僵尸目标填充的便是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形式引证计数交由体系办理
  • 自动开释池AutoReleasePoolOC的一种内存自动收回机制,收回一致开释声明为autorelease的目标;体系中有多个内存池,体系内存不足时,取出栈顶的池子把引证计数为0的目标开释掉,收回内存給当时应用程序运用。 自动开释池本身毁掉的时分,里边所有的目标都会做一次release

autoreleasepool的运用场景

  • 创立了大量目标的时分,例如循环的时分

autoreleasePool的数据结构

  • autoreleasePool底层是AutoreleasePoolPage
  • 能够理解为双向链表,每张链表头尾相接,有 parentchild指针
  • 每次初始化调用objc_autoreleasePoolPush,会在首部创立一个岗兵目标作为符号,开释的时分就以岗兵为止
  • 最外层池子的顶端会有一个next指针。当链表容量满了(4096字节,一页虚拟内存的巨细),就会在链表的顶端,并指向下一张表。

autoreleasePool 什么时分被开释?

  • ARC中所有的重生目标都是自动增加autorelese
  • @atuorelesepool处理了大部分内存暴增的问题。
  • autoreleasepool中的目标在当时runloop循环完毕的时分自动开释。

子线程中的autorelease变量什么时分开释?

  • 子线程中会默许生成一个autoreleasepool, 当线程退出的时分开释。

autoreleasepool是怎么完结的?

  • @autoreleasepool{} 本质是一个结构体
  • autoreleasepool会被转化成__AtAutoreleasePool
  • __AtAutoreleasePool 里边有objc_autoreleasePoolPushobjc_autoreleasePoolPop两个要害函数
  • 终究调用的是AutoreleasePoolPagepushpop 办法
  • push是压栈,pop是出栈,pop的时分以岗兵作为参数,对所有晚于岗兵刺进的目标发送release音讯进行开释

放入@autuReleasePool的目标,当自动开释池调用drain办法时,一定会开释吗

  • drainrelease都会促进自动开释池目标向池内的每一个目标发送release音讯来开释池内目标的引证计数
  • release触发的操作,不会考虑目标是否需求release
  • drain会在自动开释池向池内目标发送release音讯的时分,考虑目标是否需求release
  • 目标是否开释取决于引证计数是否为0,池子是否开释仍是取决于里边的所有目标是否引证计数都为0。

@aotuReleasePool的嵌套运用,目标内存是怎么被开释的

  • 每次初始化调用objc_autoreleasePoolPush,会在首部创立一个岗兵目标作为符号
  • 开释的时分就会依次对每个pool里晚于岗兵的目标都进行release
  • 从内到外的顺序开释

ARC环境下有内存走漏吗?举例说明

  • 有。例如两个strong润饰的目标彼此引证。
  • block中的循环引证
  • NSTimer的循环引证
  • delegate的强引证
  • OC目标的内存处理(需手动开释)

呈现内存走漏,该怎么处理?

  • 运用Instrument傍边的Leak检测工具
  • 运用僵尸变量,依据打印日志,然后分析原因,找出内存走漏的地方

ARCreatain & release优化了什么

  • 依据上下文及暗影联系,削减了不必要的retainrelease
  • 例如MRC环境下引证一个autorelease目标,目标会经历new -> autorelease -> retain -> release,可是仅仅仅仅引证罢了,中间的autoreleaseretain操作其实能够去除,所以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
  • 优化算法
  • 防止循环引证
  • 定时运用InstrumentLeak检测内存走漏

结构体对齐办法

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

deletefree的差异

  • 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_tweak_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_onceonceToken为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内部能够修正堆中的内容, 不能够直接修正栈中的内容。

ARCMRCblock拜访目标类型的变量时,有什么差异

  • ARC环境会依据外部变量是__strong仍是__weak润饰进行引证计数办理,到达强引证或弱引证的效果
  • MRC环境下,block属于栈区,外部变量是auto润饰的,不手动copy的话变量就不会被block强引证。

block能够用strong润饰吗

  • MRC环境下,不能够。strong只会把block进行一次retain操作,栈上的block不会被仿制到堆区,仍旧无法共享
  • ARC环境下,能够。block在堆区,而且blockretain操作也是经过copy完结

block为什么用copy润饰?

  • MRC环境,block创立在栈区,只需函数效果域消失就会被开释,外部再去调用就会溃散。经过copy润饰能够把它仿制到堆区,外部调用也没问题,然后处理了这个问题。
  • ARC环境,block创立在堆区,用strongcopy都相同。blockretain操作也是经过copy完结。曾经用copy就一直连续了。

block在什么情况下会被copy

  • 自动调用copy办法
  • block作为回来值时
  • block赋值给__strong指针时
  • block作为GCD API的办法参数时
  • block作为Cocoa API办法名含有usingBlock的办法参数时

block的内存办理

  • block经过block_copyblock_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强持有外部变量,能够防止外部变量被提前开释

Masonryblock中,运用self,会形成循环引证吗?假如是在一般的block中呢?

  • 不会,由于这是个栈block,没有推迟运用,运用后马上开释
  • 一般的block会,一般会运用强引证持有,就会触发copy操作

在一般的block中只运用下划线特点去拜访,会形成循环引证吗

  • 会,和调用self.是相同的

NSNotification

音讯告诉的理解

  • 告诉(NSNotification)支撑一对多的信息传递办法
  • 运用时先注册绑定接纳告诉的办法,然后告诉中心创立并发送告诉
  • 不再监听时需求移除告诉

完结原理(结构设计、告诉怎么存储的、name & observer & SEL之间的联系等)

  • Observation是告诉调查目标,存储告诉名、objectSEL的结构体
  • NSNotificationCenter持有一个根容器NCTable,根容器里边包括三个张链表

    • wildCards,寄存没有name & object的告诉调查目标(Observation
    • nameless,寄存没有name可是有object的告诉调查目标(Observation
    • named,寄存有name & object的告诉调查目标(Observation
  • 当增加告诉调查的时分,NSNotificationCenter依据传入参数是否齐全,创立Observation并增加到不同链表
    • 创立一个新的告诉调查目标(Observation
    • 假如传入参数包括称号,在named表里查询对应称号,假如现已存在同名的告诉调查目标,将新的告诉调查目标刺进这以后,假如不存在则增加到表尾。存储结构为链表,节点内先以name作为key,一个字典作为value。假如告诉参数带有object,字典内以objectkey,以Observation作为value
    • 假如传入的参数假如只包括object,在nameless表查询对应称号,将新的告诉调查目标刺进这以后,假如不存在则增加到表尾。存储结构为链表,节点内以objectkey,以Observation作为value
    • 假如传入参数没有name也没有object,直接增加到wildCards表尾。结构为链表,节点内存储Observation

告诉的发送是同步的,仍是异步的

  • 告诉的接纳和发送是在一个线程里,实践上发送告诉都是同步的,不存在异步操作
  • 告诉供给了枚举设置发送时机
  • NSPostWhenIdlerunloop空闲的时分发送
  • NSPostASAP,赶快发送,会穿插在事件完结的空隙中发送
  • NSPostNow,马上发送或兼并完结后发送

NSNotificationCenter 接受音讯和发送音讯是在一个线程里吗?怎么异步发送音讯

  • 是的
  • 异步发送,也便是推迟发送,能够运用addObserverForName:object: queue: usingBlock:

NSNotificationQueue是异步仍是同步发送?在哪个线程呼应

  • 异步发送,也便是推迟发送
  • 在同一个线程发送和呼应

NSNotificationQueuerunloop的联系

  • NSNotificationQueue仅仅把告诉增加到告诉行列,并不会自动发送
  • NSNotificationQueue依靠runloop,假如线程runloop没开启就不生效。
  • NSNotificationQueue发送告诉需求runloop循环中会触发NotifyASAPNotifyIdle然后调用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办法