19·iOS 面试题·什么是 ARC ?(ARC 是为了解决什么
前言
对于 iOS 内存分区有:栈区、堆区、静态区、全局区、常量区、代码区。对于静态区、全局区、常量区、代码区,对象的生命周期是伴随着应用的生命周期,是常驻内存的,所以不需要我们去管理对象内存。对于栈区的对象,遵循着栈先进后出的原则,由系统自动帮我们管理,也是不需要我们去管理对象的内存。对于堆区的对象,则需要我们自己去开辟内存和释放内存,在程序开发中内存管理最主要的就是堆区的内存管理。
在 iOS 中,使用 引用计数机制 来进行内存管理,最初是用 MRC(手动引用计数),然后逐渐转成 ARC(自动引用计数),我们这里介绍下引用计数机制的原理,MRC 和 ARC 的一些特性,最后我们再看看 Java 是如何管理对象内存的。
引用计数机制
在 Objective-C 中,利用引用计数来进行内存管理:每个对象都有一个对应的引用计数,当这个对象被持有的时,其引用计数就会递增,当这个对象的某个持有被释放时,对象的引用计数就会递减,当这个对象的引用计数为 0 的时候,这个对象就会被释放。
每个对象都对应着一个引用计数,在内存中,通过一个 SideTable RefcountMap 来存储这个对应关系:对象的地址作为 Key,引用计数的值作为 Value。
增加和减少对象的引用计数核心代码如下:
// 增加对象的引用计数
id objc_object::sidetable_retain()
{
//获取table
SideTable& table = SideTables()[this];
//加锁
table.lock();
//获取引用计数
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//增加引用计数
refcntStorage += SIDE_TABLE_RC_ONE;
}
//解锁
table.unlock();
return (id)this;
}
//减少对象的引用计数
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
//找到对应地址的
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) { //找不到的话,执行dellloc
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用计数小于阈值,dealloc
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减去1
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
//执行dealloc
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
MRC 手动引用计数
对于 MRC 手动引用计数,需要程序员手动调用 retain
、release
、autorelease
这些方法来控制对象的引用计数,这种情况下开发者需要花费大量的精力来进行内存管理,并且很容易出现 release
一个已经释放的对象而导致程序崩溃。
对于上面三个方法分别的作用介绍如下:
- retain 增加对象的引用计数。
- release 降低对象引用计数,引用计数为0的时候,释放对象。
- autorelease 在当前的auto release pool结束后,降低对象引用计数。
ARC 自动引用计数
对于 MRC 环境出现的问题,ARC 自动引用计数,则是由编译器智能的帮我们在合理的地方隐式地添加 retain
、release
、autorelease
方法(PS:这里不是简单插入这几个方法,而是插入它们对应的底层方法,一定程度上的优化执行效率)。ARC 使得内存管理更加轻松,但是 ARC 还有以下不足:
- 不能管理 CoreFoundation 中的对象,所以对于 CoreFoundation 对象,需要我们手动来管理内存
- 不能释放循环引用的对象(这里也不算是 ARC 不足,这个是引用计数机制本身的缺点)
Java 使用 GC 来管理内存
我们已经知道在 Objective-C 中,利用引用计数机制来管理内存。但是在 Java 开发中,是使用 Garbage Collection 垃圾回收机制来进行内存管理(其实 Objective-C 在最早期也使用过该模式,但是后面转换成引用计数机制了)。
垃圾回收(Garbage Collection)是 Java 虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
对于垃圾回收机制,需要处理两个逻辑:
- 找到所有存活的对象。
- 回收被无用对象占用的内存空间,使该空间可被程序再次使用。
找到存活对象
利用根搜索算法来查找存活的对象,并且标记存活对象,对于没有标记的对象就会再接下来的阶段被清除。(PS:进行标记前,会暂停程序导致卡顿)
回收垃圾对象内存
对于回收垃圾对象内存,对于不同的情况会采用不同的算法来进行回收,如何选择可以参阅:浅析JAVA的垃圾回收机制(GC)
- 标记—清除算法
- 标记—整理算法
- Copying算法
- Adaptive算法
引用计数机制与 GC 的区别:
- 引用计数是在编译时期插入代码,对象不被使用就会立即回收;GC 则是一段时间一段时间进行遍历来回收对象,会导致卡顿。
- 引用计数不能很好的处理循环引用问题,GC 可以处理。
- 引用计数增加程序执行的开销,GC 相对比较简单。
总结
通过这篇文章,我们了解到了 Objective-C 选择使用引用技术来管理内存,MRC 转换到 ARC 有什么好处,还简单的了解到了 Java 利用 GC 来进行内存管理(更加详细的 GC 操作可以查看参考链接)。
因为后面还有类似的面试题,我们暂时忽略了自动释放池的底层实现、weak 修饰的对象内存释放的时候会置为 nil,这些在后续的面试题中会一一讲到。