内存布局
1. 内核区
- 内核占用的内存区域。
2. 栈区(Stack)
- 存放函数的参数值、局部变量的值、对象的指针地址;
- 编译器自动分配释放;
- 快速高效,但操作方式不够灵活(类似数据结构栈FIFO);
- 栈区地址分配方式:由高到低。
优化技巧
- 不要在一个函数内大量使用局部变量(例如循环创建),可以通过使用AutoReleasePool自动释放池及时释放来进行优化。
- 注意:局部变量的回收释放是会使用GC(垃圾回收)的,会消耗性能。尽量封装方法,使用方法嵌套,加快函数的读取效率,用空间换时间。
栈区地址分配由高到低,堆区地址分配由低到高,一旦碰头,会造成堆栈溢出。
3. 堆区(heap)
- iOS中创建的对象都是存储在堆区(alloc);
- 开发者管理(若开发者不回收,在程序结束时,由系统回收);
- 速度相对较慢,操作方式灵活(类似链表),会造成内存碎片化;
- 堆区地址分配方式:由低到高。
优化技巧
- 对象的创建和回收会消耗大量内存和性能,避免创建过多的对象,超出临界值会造成堆栈溢出。
- 方法和函数还是有细微区别的,两者之间尽量使用函数。
4. 全局(静态)区(.bbs/data)
- 存储全局变量或者静态变量(static修饰);
- 程序结束后由系统进行释放;
static int a;未初始化的全局静态变量存放在全局区的.bbs段;static int a = 10;已初始化的全局静态变量存放在全局区的data段。
优化技巧
- 注意静态变量的作用域(.h和.m的作用域不同,避免在头文件.h中定义全局变量)。
- 避免对已经定义的静态变量进行重新赋值。
5. 常量区
- 存储已使用的字符串常量(例如:const、extern修饰的字符串);
- 程序结束后由系统释放;
- 相同字符串地址一致(C语言的编译优化)。
6. 代码区(.text)
- 存储编译后的代码的区域;
- 包括操作码和要操作的对象或对象的地址引用。
7. 保留段
- 保留的一块内存区域。
数据结构
1. TaggedPointer
- Tagged Pointer是专⻔⽤来存储⼩的对象,例如NSNumber,NSDate等。
- Tagged Pointer指针的值不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也不需要malloc和free。
- 当指针不够存储数据时,就会使用动态分配内存的方式来存储数据。
2. NONPOINTER_ISA
- indexed(0):标记是否是纯的ISA指针,还是非指针型的NOPOINTER_ISA指针(0:纯isa指针;1:NOPOINTER_ISA指针);
- has_assoc(1):标记是否有关联对象(0没有,1存在);
- has_cxx_dtor(2):该对象是否有C++或者Objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象;
- shiftcls(3~35):标记当前对象的类对象的指针地址,占用33位;
- magic(36~41):用于调试器判断当前对象是真的对象还是没有初始化的空间;
- weakly_referenced(42):标记对象是否有弱引用指针,没有弱引用的对象可以更快释放;
- deallocating(43):标记对象是否正在进行dealloc操作;
- has_sidetable_rc(44):标记是否有sidetable结构用于存储引用计数;
- extra_rc(45~63):标记对象的引用计数(首先会存储在该字段中,当到达上限后,再存入对应的引用计数表中)。
3. SideTables
SideTables是一个64个元素长度的hash数组,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。 因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。
SideTable
spinlock_t slock:锁,用于上锁/解锁SideTable(自旋锁已被废弃,这里名字为spinlock,底层实现其实是os_unfair_lock);RefcountMap refcnts:以DisguisedPtr<objc_object>为key的hash表,用来存储OC对象的引用计数(仅在未开启isa优化,或在isa优化情况下,isa_t的引用计数溢出时才会用到);weak_table_t weak_table:存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。
RefcountMap 引用计数表中存储的值是size_t,实际上是一个无符号的长整型,前两个比特位分别为weakly_referenced和deallocating,用来表示是否有弱引用指针和是否正在进行dealloc,剩下的才是该对象的引用计数值,所以要计算某个对象具体的引用计数值时,要向右偏移两位。
weak_table_t 弱引用表中的Key为对象指针,Value为weak_entry_t,weak_entry_t是一个结构体数组,数组中的元素实际上就是OC对象的弱引用信息。
何谓自旋锁? 它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
哈希查找
- 散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
- 散列函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
- Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。
引用计数
1. ARC
在Objective-C中采用ARC机制,让编译器来进行内存管理。在新一代Apple LLVM 编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减小了开发程序的工作量。编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象。如此一来,应用程序将具有可预测性,且能流畅运行,速度也将大幅度提升。
- ARC是编译器和Runtime协作的结果。
- ARC中禁止手动调用
retain/release/retainCount/dealloc。 - ARC中新增了
weak、strong属性关键字。
例如非指针型isa中的nonpointer、weakly_referenced、has_sidetable_rc和extra_rc都和ARC有直接的关系。
2. alloc
- 经过一系列调用,最终调用了C函数calloc。
- 此时并没有设置引用计数为1。
alloc后通过retainCount获取引用计数为1。
3. retain
SideTable& table = SideTables()[this];通过当前对象的指针,到SideTables中,获取该对象所在的SideTable。size_t& refcntStorage = table.refcnts[this];通过当前对象的指针,在SideTable的引用计数表中,获取该对象的引用计数。refcntStorage += SIDE_TABLE_RC_ONE;因为size_t前两个比特位存储了特殊的值,所以这里的SIDE_TABLE_RC_ONE考虑到了两位二进制的偏移量,实际上加了4,反应在引用计数上还是加了1。
4. release
SideTable& table = SideTables()[this];RefcountMap::iterator it = table.refcnts.find(this);根据当前对象指针访问table中的引用计数表。it->second -= SIDE_TABLE_RC_ONE;将对应的值减去SIDE_TABLE_RC_ONE。
5. retainCount
SideTable& table = SideTables()[this];size_t refcnt_result = 1;声明一个局部变量,指定它的值为1。RefcountMap::iterator it = table.refcnts.find(this);通过当前对象,到引用计数表中查找。refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;将查找的结果向右偏移,与之前声明的局部变量相加,最后返回给调用方。
新alloc的对象,在引用计数表中其实没有相关联的key/value映射,但在获取retainCount时,会获取到值为1。
6. dealloc
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
_objc_rootDealloc实现:
void
_objc_rootDealloc(id obj)
{
ASSERT(obj); // 断言 验证对象
obj->rootDealloc();
}
rootDealloc()实现:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary? // 是否是标记指针
if (fastpath(isa.nonpointer && // 是否进行了isa优化
!isa.weakly_referenced && // 是否存在弱引用指向
!isa.has_assoc && // 是否设置了关联对象
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor && // 是否有C++的析构函数
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc)) // 引用计数是否过大无法存储在isa中
{
assert(!sidetable_present()); // 满足条件直接释放
free(this);
}
else {
object_dispose((id)this); // 不满足条件执行object_dispose()
}
}
object_dispose()实现:
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj); // 销毁实例
free(obj); // 释放对象
return nil;
}
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, /*deallocating*/true); // 移除当前对象的关联对象
obj->clearDeallocating();
}
return obj;
}
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());
}
clearDeallocating_slow()实现:
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this); // 将指向该对象的弱引用指针置为nil。
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this); // 从引用计数表中擦除该对象引用计数
}
table.unlock();
}
弱引用
1. weak实现原理
一个被声明为__weak的对象指针,经过编译器编译,会调用objc_initWeak()方法,然后经过一系列的函数调用栈,最终在weak_register_no_lock()函数中进行弱引用变量的添加。具体添加的位置是通过一个哈希算法来计算的:
- 如果查找的位置已经有了当前对象对应的弱引用数组
weak_entry_t,我们就将新的弱引用变量添加到该数组中; - 如果没有查找到,就创建一个新的弱引用数组,并将该指针添加到第0位,其他位置初始化为nil。
2. 当weak指向的对象被释放时,是如何让weak指针置为nil的?
当一个对象被释放时,在dealloc的内部实现中,会判断该对象是否存在弱引用指向,若存在则最终会调用弱引用清除的相关函数weak_clear_no_lock(),在该函数中会根据当前对象指针查找弱引用表,然后遍历当前对象所有的弱引用指针并置为nil。
自动释放池
1. 数据结构
- 是以栈为节点通过双向链表的形式组合而成。
- 是和线程一一对应的。
// class AutoreleasePoolPage;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
AutoreleasePoolPage::pop()
- 根据传入的哨兵对象找到对应位置。
- 给上次push操作之后添加的对象依次发送release消息。
- 回退next指针到正确位置。
2. 自动释放池何时释放?
在当次runloop将要结束的时候调用AutoreleasePoolPage::pop()。同时会push一个新的AutoreleasePool。
3. 自动释放池为何可以嵌套使用?
多层嵌套就是多次插入哨兵对象。
4. 自动释放池的使用场景。
在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool。
循环引用
1. 什么情况下需要注意循环引用?
- 代理
- Block
- NSTimer
- 大环引用
2. 如何破除循环引用?
- 避免产生循环引用
- 在合适的时机手动断环
3. 具体的解决方案都有哪些?
- __weak
- __block
- __unsafe_unretained
__block
- MRC下,__block修饰对象不会增加其引用计数,避免了循环引用。
- ARC下,__block修饰对象会被强引用,无法避免循环引用,需手动解环。
__unsafe_unretained
- 修饰对象不会增加其引用计数,避免了循环引用。
- 如果被修饰对象在某一时机被释放,会产生悬垂指针,导致内存泄漏。
4. 如何解决NSTimer造成的循环引用?
由于NSTimer被分派后,会被当前线程的Runloop强引用,所以无法通过对象弱引用NSTimer的方法解决循环引用问题。
通过创建一个中间对象,令中间对象持有两个弱引用变量,非别为原对象和NSTimer;在中间对象中实现NSTimer的回调,并在回调前增加一道判断,检查中间对象持有的原对象是否为nil;若原对象存在则执行回调,若原对象已经被释放则将NSTimer设为无效状态。
// NSTimer+WeakTimer.h
#import <Foundation/Foundation.h>
@interface NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo;
@end
// NSTimer+WeakTimer.m
#import "NSTimer+WeakTimer.h"
@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
- (void)fire:(NSTimer *)timer;
@end
@implementation TimerWeakObject
- (void)fire:(NSTimer *)timer {
if (self.target) {
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timer.userInfo];
}
} else {
[self.timer invalidate];
}
}
@end
@implementation NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)ti
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo {
TimerWeakObject *object = [[TimerWeakObject alloc] init];
object.target = aTarget;
object.selector = aSelector;
object.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:object selector:@selector(fire:) userInfo:userInfo repeats:yesOrNo];
return object.timer;
}
@end