内存管理
内存分配方式
Objective-C内存管理方式
1、TaggedPointer
iPhone5s开始采用64bit cpu架构,编译器通常分配给一个指针的大小就是64bit。一个NSNumber通常不需要8个字节,4个字节就够了。4个字节有符号的整数最大值2^31为20多亿。所以把指针拆成2个部分,一部分保存数据值,一部分做特殊标记,指明这是一个特别的指针,不指向任何一个地址。这样就省去了对象的内存分配,引用计数维护,管理生命周期等操作
TaggedPointer
1 专门用于存储小对象,例如NSNumber NSDate
2 TaggedPointer指针值不再是地址,而是真正的值。
3 节省内存,提高执行效率。
判断对象是否在使用TaggedPointer,是看标志位是否为1
inline bool
objc_object::isTaggedPointer()
{
return ((uintptr_t)this & TAG_MASK);
}
2、isa指针 (NONPOINTER_ISA)
非指针型isa : 值的部分代表class地址
指针型isa:值代表class地址
64 bit存储一个内存地址显然是种浪费。于是可以优化存储方案,用一部分额外的存储空间存储其他内容。isa是objc_object的一个私有成员,它的结构如下:
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 : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
……
};
变量名 | 含义 |
---|---|
indexed | 0表示普通的isa,1表示使用优化的存储引用计数 |
has_assoc | 对象是否包含associated object |
has_cxx_dtor | 该对象是否有 C++ 或 ARC 的析构函数 |
shiftcls | 类的指针 |
magic | 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化 |
weakly_referenced | 该对象是否有过 weak 对象 |
deallocating | 该对象是否正在析构 |
has_sidetable_rc | 是否使用了引用计数表sideTable |
extra_rc | 存储引用计数值减一后的结 |
猜测:TaggedPointer是把值存在指针当中,不给指针所指的对象分配内存。NONPOINTER_ISA是给对象分配了内存空间,但是不使用sideTable管理引用计数,而是把引用计数存在isa当中。
3、散列表
根据key查找内存存储位置的数据结构。通过key得到存储位置的函数是散列函数,存放记录的数组称为散列表。具体实现原理:数组长度固定比如是arr[20],key=abc通过hash函数后得到一个整数n,n%20 == 14. 此时通过key=abc就得到了arr[14]. arr[14]中存放了一个链表的头指针,通过遍历链表获取key相等的就是要找的值。
SideTable包含了引用计数表,弱引用计数表,以及一个自旋锁。结构如下
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
……
}
自旋锁原理:如果锁被其他线程获取,当前线程会不断去探测锁是否被释放。若有释放,会第一时间去获取这个锁。普通的锁线程没有获取到会从用户态切换为内核态进行休眠,而使用自旋锁的线程不会休眠,只会忙等。适用于轻量访问(加一、减一)。
SideTable &table = SideTables()[this];
size_t &refcntStorage = table.refcnts[this];
//refcntStorage 就是引用计数 两次hash查找
引用计数表:实际是用hash表实现的。应用计数会存在多张sideTable中。修改引用计数,需要经过两次hash算法,第一次是从sideTables中找到具体的sideTable。第二次是从sideTable中找到对应的引用计数。之所以设计成多张sideTable而不是一张sideTables,是因为每次操作都需要加锁,减锁操作。多张可以分离锁,加快操作速度。
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
弱引用计数表:苹果使用sideTables保存所有的weak引用。key就是对象,weak_entry_t作为值。weak_entry_t中保存了所有指向该对象的弱引用。
引用计数管理
dealloc的操作
调用dealloc函数,实际会调用到rootDealloc()。从函数中可以看出,如果使用了TaggedPointer就直接返回,交给栈自己处理。若果是普通的isa,没有弱引用对象,没有关联对象,没有使用c++和ARC的析构函数,没有使用引用计数表,那么直接调用free释放。 否则调用object_dispose,先销毁c++对象,然后移除关联对象,最后清除弱引用表和引用计数表。
inline void
objc_object::rootDealloc()
{
assert(!UseGC);
if (isTaggedPointer()) return;
if (isa.indexed &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc)
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
#if SUPPORT_GC
if (UseGC) {
auto_zone_retain(gc_zone, obj); // gc free expects rc==1
}
#endif
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}
return obj;
}
obj->clearDeallocating(); //中包含了下面2行代码
weak_clear_no_lock(&table.weak_table, (id)this);
table.refcnts.erase(this);
weak引用
id __weak obj1 = obj; 经过编译器会调用
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<false/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
对象被销毁时,调用dealloc中清除弱引用方法。
第一次hash(obj)得到sideTables中具体的sideTable
第二次hash(obj)从sideTable中的weak_table获取具体的weak_entry_t。
The global weak references table. Stores object ids as keys,and weak_entry_t structs as their values. 找到对象的弱引用数据,遍历置为nil.
自动释放池
@autoreleasepool{} 实际是:
void *ctx = objc_autoreleasePoolPush();
//code
objc_autoreleasePoolPop(ctx);
objc_autoreleasePoolPush具体操作:
1 在AutoreleasePoolPage的next位置插入哨兵nil
2 在哨兵后面位置add(obj)
3 位置不够,创建新的AutoreleasePoolPage,然后add(obj)
objc_autoreleasePoolPop具体操作:
1 根据传入的哨兵对象找到对应位置
2 给上次push操作后的对象依次发送release消息
3 回退next指针到正确的位置
AutoreleasePoolPage数据结构
id *next; //栈的下个位置
pthread_t const thread; //当前线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
本质是:以栈为节点(AutoreleasePoolPage),通过双向链表的形式组合而成 。和线程一一对应。
问题1: viewDidLoad中 NSMutableArray *array = [NSMutableArray array];何时释放?
答:在当次RunLoop循环结束后调用AutoreleasePoolPage:pop()时。
问题二:autoreleasepool多层嵌套?
多层嵌套就是多次插入哨兵对象。
问题三:手动插入autorealeasepool
for循环中alloc图片数据等内存消耗大的场景插入
问题四:实现原理
已AutoreleasePoolPage栈为节点的,双向链表的数据结构组合而成
NSTimer循环引用
VC +++++++++> banner--------->NSTimer
NSRunLoop+++++++++>NSTimer+++++++++>banner 导致了VC dealloc后,banner任然没被释放。
解决方案:引入中间层
VC +++++++++>banner-------> 中间层 ------->NSTimer
NSRunLoop+++++++++>NSTime------------>中间层
ARC
是由LLVM编译器和Runtime共同协作来为我们实现自动引用计数的管理。
MRC :手动引用计数
alloc retain release retainCount autorelease dealloc
ARC: 自动引用计数
ARC是LLVM和Runtime协作结果
ARC禁止手动调用retain/release/retainCount/dealloc
ARC中新增weak、strong属性关键字
__weak __block __unsafe_unreatined
__block 在MRC下,不会增加引用计算,避免循环引用
在ARC下,会被强引用,无法表面循环引用
__unsafe_unreatined 不会增加引用计算,如果被修饰的对象在某一时刻被释放,会产生悬垂指针导致内存泄露