iOS 进阶知识总结(四)

10,008 阅读18分钟

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方法