iOS 内存管理
-
iOS 内存管理的理解?
实际上是三种方案的结合
1.1TaggedPointer
(针对类似于NSNumber
的小对象类型)
1.2NONPOINTER_ISA
(64位系统下)
- 第一位的 0 或 1 代表是纯地址型
isa
指针,还是NONPOINTER_ISA
指针。 - 第二位,代表是否有关联对象
- 第三位代表是否有
C++
代码。 - 接下来33位代表指向的内存地址
- 接下来有 弱引用 的标记
- 接下来有是否
delloc
的标记....等等
1.3 散列表(引用计数表、weak
表) -
SideTables
表在 非嵌入式的64位系统中,有 64张SideTable
表 - 每一张
SideTable
主要是由三部分组成。自旋锁、引用计数表、弱引用表。 - 全局的 引用计数 之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
- 引用计数表 中引入了 分离锁的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率
在 isa_t 联合体中不仅仅表明了指向对象的地址信息,而且这个 64 位数据还记录了其 bits 情况以及该实例每一位保存的对象信息
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
- 使用自动引用计数应遵循的原则?
- 不能使用
retain
、release
、retainCount
、autorelease
。 - 不可以使用
NSAllocateObject
、NSDeallocateObject
。 - 必须遵守内存管理方法的命名规则。
- 不需要显示的调用
Dealloc
。 - 使用
@autoreleasePool
来代替NSAutoreleasePool
。 - 不可以使用区域
NSZone
。 - 对象性变量不可以作为
C
语言的结构体成员。 - 显示转换
id
和void*
。
ARC
自动内存管理的原则?
- 自己生成的对象,自己持有
- 非自己生成的对象,自己可以持有
- 自己持有的对象不再需要时,需要对其进行释放
- 非自己持有的对象无法释放
- 访问
__weak
修饰的变量,是否已经被注册在了@autoreleasePool
中?为什么?
- 答案是肯定的,
_weak
修饰的变量属于弱引用,如果没有被注册到@autoreleasePool
中,创建之后也就会随之销毁,为了延长它的生命周期,必须注册到@autoreleasePool
中,以延缓释放。
ARC
的retainCount
怎么存储的?
- 存在64张哈希表中,根据哈希算法去查找所在的位置,无需遍历,十分快捷
- 散列表(引用计数表、weak表) -
SideTables
表在 非嵌入式的64位系统中,有 64张SideTable
表 - 每一张SideTable
主要是由三部分组成。自旋锁
、引用计数表
、弱引用表
。 - 全局的引用计数
之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。 -引用计数表
中引入了分离锁
的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率
引用计数表(哈希表) - 通过指针的地址,查找到引用计数的地址,大大提升查找效率
- 通过
DisguisedPtr(objc_object)
函数存储,同时也通过这个函数查找,这样就避免了循环遍历。
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
…..
}
}
@autoreleasePool
的数据结构?
- 简单说是双向链表,每张链表头尾相接,有
parent
、child
指针 - 每创建一个池子,会在首部创建一个 哨兵 对象,作为标记
- 最外层池子的顶端会有一个
next
指针。当链表容量满了,就会在链表的顶端,并指向下一张表
__weak
和_Unsafe_Unretain
的区别?
-
weak
修饰的指针变量,在指向的内存地址销毁后,会在Runtime
的机制下,自动置为nil
。 -
_Unsafe_Unretain
不会置为nil
,容易出现悬垂指针
,发生崩溃。但是_Unsafe_Unretain
比__weak
效率高。
- 为什么已经有了
ARC
,但还是需要@AutoreleasePool
的存在?
- 避免内存峰值,及时释放不需要的内存空间
__weak
属性修饰的变量,如何实现在变量没有强引用后自动置为nil
?
- 用的弱引用 -
weak
表。也是一张 哈希表。 - 被
weak
修饰的指针变量所指向的地址是key
,所有指向这块内存地址的指针会被添加在一个数组里,这个数组是Value
。当内存地址销毁,数组里的所有对象被置为nil
。
- 对
retain
,copy
,assign
,weak
,_Unsafe_Unretain
关键字的理解?
Strong
Strong
修饰符表示指向并持有该对象,其修饰对象的引用计数会加1。该对象只要引用计数不为0就不会被销毁。当然可以通过将变量强制赋值nil
来进行销毁。
Weak
weak
修饰符指向但是并不持有该对象,引用计数也不会加1。在Runtime
中对该属性进行了相关操作,无需处理,可以自动销毁。weak
用来修饰对象,多用于避免循环引用的地方。weak
不可以修饰基本数据类型。
assign
assign
主要用于修饰基本数据类型, 例如NSInteger
,CGFloat
,存储在栈中,内存不用程序员管理。assign
是可以修饰对象的,但是会出现问题。
copy
copy
关键字和strong
类似,copy
多用于修饰有可变类型的不可变对象上NSString
,NSArray
,NSDictionary
上。
__unsafe_unretain
__unsafe_unretain
类似于weak
,但是当对象被释放后,指针已然保存着之前的地址,被释放后的地址变为僵尸对象
,访问被释放的地址就会出问题,所以说他是不安全的。
__autoreleasing
将对象赋值给附有
__autoreleasing
修饰的变量等同于ARC
无效时调用对象的autorelease
方法,实质就是扔进了自动释放池。
ARC
在编译时做了哪些工作?
- 根据代码执行的上下文语境,在适当的位置插入
retain
,release
ARC
在运行时做了哪些工作?
- 主要是指
weak
关键字。weak
修饰的变量能够在引用计数为0 时被自动设置成 nil,显然是有运行时逻辑在工作的。 - 为了保证向后兼容性,
ARC
在运行时检测到类函数中的autorelease
后紧跟其后retain
,此时不直接调用对象的autorelease
方法,而是改为调用objc_autoreleaseReturnValue
。objc_autoreleaseReturnValue
会检视当前方法返回之后即将要执行的那段代码,若那段代码要在返回对象上执行retain
操作,则设置全局数据结构中的一个标志位,而不执行autorelease
操作,与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行retain
,而是改为执行objc_retainAoutoreleasedReturnValue
函数。此函数要检测刚才提到的标志位,若已经置位,则不执行retain
操作,设置并检测标志位,要比调用autorelease
和retain
更快。
- 函数返回一个对象时,会对对象
autorelease 么
?为什么?
- 会 ,为了延长返回对象的生命周期,给其他使用者留足调用的时间
- 说一下什么是
悬垂指针
?什么是野指针
?
悬垂指针
指针指向的内存已经被释放了,但是指针还存在,这就是一个
悬垂指针
或者说迷途指针
野指针
没有进行初始化的指针,其实都是
野指针
- 内存管理默认的
关键字
是什么?
MRC
@property (atomic,readWrite,retain) UIView *view;
ARC
@property (atomic,readWrite,strong) UIView *view;
如果改为基本数据类型,那就是 assign
。
- 内存中的5大区分别是什么?
- 栈区(
stack
):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。 - 堆区(
heap
):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 - 全局区(静态区)(
static
):全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后由系统释放。 - 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放。
- 程序代码区:存放函数体的二进制代码。
- 是否了解
深拷贝
和浅拷贝
的概念,集合类深拷贝如何实现
-
深拷贝
内存拷贝浅拷贝
指针拷贝 - 集合类深拷贝通过归档、解档实现。
BAD_ACCESS
在什么情况下出现?
- 访问了已经被销毁的内存空间,就会报出这个错误。 根本原因是有
悬垂指针
没有被释放
- 讲一下
@dynamic
关键字?
-
@dynamic
意味着编译器不会帮助我们自动合成setter
和getter
方法。我们需要手动实现、这里就涉及到Runtime
的动态添加方法的知识点。
autoReleasePool
什么时候释放?
App
启动后,苹果在主线程RunLoop
里注册了两个Observer
,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。第一个
Observer
监视的事件是Entry
(即将进入Loop
),其回调内会调用_objc_autoreleasePoolPush()
创建自动释放池。其order
是-2147483647
,优先级最高,保证创建释放池发生在其他所有回调之前。第二个
Observer
监视了两个事件:BeforeWaiting
(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit
(即将退出Loop) 时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个Observer
的order
是2147483647
,优先级最低,保证其释放池子发生在其他所有回调之后。在主线程执行的代码,通常是写在诸如事件回调、
Timer
回调内的。这些回调会被RunLoop
创建好的AutoreleasePool
环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool
了
retain
、release
的实现机制?
Retain
的实现机制。
SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage += SIZE_TABLE_RC_ONE;
Release
的实现机制。
SideTable& table = SideTables()[This];
size_t& refcntStorage = table.refcnts[This];
refcntStorage -= SIZE_TABLE_RC_ONE;
二者的实现机制类似,概括讲就是通过第一层 hash
算法,找到 指针变量
所对应的 sideTable
。然后再通过一层 hash
算法,找到存储 引用计数
的 size_t
,然后对其进行增减操作。retainCount
不是固定的 1,SIZE_TABLE_RC_ONE
是一个宏定义,实际上是一个值为 4 的偏移量。
-
能不能简述一下
Dealloc
的实现机制?
Dealloc
的实现机制是内容管理部分的重点,把这个知识点弄明白,对于全方位的理解内存管理的只是很有必要。
Dealloc
调用流程
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
NONPointer_ISA
weakly_reference
has_assoc
has_cxx_dtor
has_sidetable_rc
- 如果有以上五中任意一种,将会调用
object_dispose()
方法,做下一步的处理。 - 如果没有之前五种情况的任意一种,则可以执行释放操作,C函数的
free()
。
object_dispose()
调用流程。
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
- 直接调用
objc_destructInstance()
。 - 之后调用
C
函数的free()
。 - 执行完毕
objc_destructInstance()
调用流程
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
- 先判断
hasCxxDtor
,如果有C++
的相关内容,要调用object_cxxDestruct()
,销毁C++
相关的内容。 - 再判断
hasAssocitatedObjects
,如果有的话,要调用object_remove_associations()
,销毁关联对象的一系列操作。 - 然后调用
clearDeallocating()
。 - 执行完毕。
clearDeallocating()
调用流程。
inline void objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
void objc_object::sidetable_clearDeallocating()
{
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
table.unlock();
}
- 先执行
sideTable_clearDellocating()
。 - 再执行
weak_clear_no_lock
,在这一步骤中,会将指向该对象的弱引用指针置为nil
。 - 接下来执行
table.refcnts.eraser()
,从引用计数表中擦除该对象的引用计数。 - 至此为止,
Dealloc
的执行流程结束。
- 在
MRC
下如何重写属性的Setter
和Getter
?
重写setter
-(void)setBrand:(NSString *)brand{
//如果实例变量指向的地址和参数指向的地址不同
if (_brand != brand)
{
//将实例变量的引用计数减一
[_brand release];
//将参数变量的引用计数加一,并赋值给实例变量
_brand = [brand retain];
}
}
重写getter
-(NSString *)brand{
//将实例变量的引用计数加1后,添加自动减1
//作用,保证调用getter方法取值时可以取到值的同时在完全不需要使用后释放
return [[_brand retain] autorelease];
}
重写dealloc
//MRC下 手动释放内存 可重写dealloc但不要调用dealloc 会崩溃
-(void)dealloc{
[_string release];
//必须最后调用super dealloc
[super dealloc];
}
- 在 Obj-C 中,如何检测内存泄漏?你知道哪些方式?
Memory Leaks
Allocations
Analyse
Debug Memory Graph
MLeaksFinder
泄露的内存主要有以下两种:
-
Leak Memory
这种是忘记 Release 操作所泄露的内存。 -
Abandon Memory
这种是循环引用,无法释放掉的内存。
前四种都比较麻烦,需要不断地调试运行,第五种是腾讯阅读团队出品,效果好一些
- MLeaksFinder:精准 iOS 内存泄露检测工具
- MLeaksFinder 新特性