本篇主作总结笔记,参阅和摘抄了许多优质博客,在最底部。
内存布局
内存分五大区,App 发动时,体系会把程序拷贝到内存,在内存中履行代码。
首要说一下排在内存五大区之外的内核区和保存区:
- 内核区:首要处理内核模块,比方咱们的体系内存为 4GB,那么咱们实际上能运用 3GB,剩余的 1GB 便是给了内核区,指针地址 0xc0000000(3x1024x1024x1024)
- 保存区:用来给体系供给一些必要空间
当一个 app 发动后,代码区、常量区、大局区巨细就现已固定,因而指向这些区的指针不会产生溃散性的过错。而堆区和栈区是时时刻刻改变的(堆的创立毁掉,栈的弹入弹出),所以当运用一个指针指向这个区里边的内存时,必定要注意内存是否现已被开释,否则会产生程序溃散(也便是野指针报错)
1. 栈区(Stack)
栈区里边会放一些函数参数以及一些部分变量,逐渐增多并在内存地址中由高向低延伸,因为栈的数据结构的原因,它的内存地址是连续的,栈区的内存巨细在App发动时就确认下来的,若在压入时请求的内存空间大于栈的剩余空间,就会呈现栈溢出(内存泄露),打印的地址为0x7在栈区
- 栈区是一块
连续的
内存空间,内存巨细在 App 发动时就确认下来,若在压入时请求的内存空间大于栈的剩余空间,就会呈现栈溢出,即内存泄漏 - 存储结构
从高地址往低地址
延伸 - 栈区存储的是
部分变量
,函数
,办法
,参数
,指针
- 栈区的地址空间一般是以
0x7
最初
举例:
- (void)testStack{
// 栈区
int a = 10;
int b = 20;
NSObject *object = [NSObject new];
NSLog(@"a == %p",&a);
NSLog(@"b == %p",&b);
NSLog(@"object == %p",&object);
NSLog(@"%lu",sizeof(&object));
NSLog(@"%lu",sizeof(a));
}
打印成果如下:
举个栈溢出的比方:
while (10000) {
int a = 2;
}
上面这段代码会形成内存暴涨,因为栈区只要1M巨细,一个int类型是4字节,创立一个部分变量就会压入一个4字节到栈区,当超越栈区的上限时就会形成栈溢出。
解决办法能够给代码加一个主动开释池
栈区在内存中是由高向低存储,堆区是由低向高存储,当两者相遇时,就呈现了仓库溢出。
2. 堆区(heap)
- 堆内存巨细是
动态改变
的,取决于体系的虚拟内存 - 存储结构是
从低地址向高地址
扩展 - 体系是用链表来办理堆的内存的,所以它的内存地址是
不连续
的 - 堆区存储的是
方针
,alloc
或new
出来的变量。 - 堆区地址空间一般以
0x6
最初
举例:
- (void)testHeap{
// 堆区
NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSObject *object3 = [NSObject new];
NSObject *object4 = [NSObject new];
NSObject *object5 = [NSObject new];
NSObject *object6 = [NSObject new];
NSObject *object7 = [NSObject new];
NSObject *object8 = [NSObject new];
NSObject *object9 = [NSObject new];
NSLog(@"object1 = %@",object1);
NSLog(@"object2 = %@",object2);
NSLog(@"object3 = %@",object3);
NSLog(@"object4 = %@",object4);
NSLog(@"object5 = %@",object5);
NSLog(@"object6 = %@",object6);
NSLog(@"object7 = %@",object7);
NSLog(@"object8 = %@",object8);
NSLog(@"object9 = %@",object9);
}
成果如下:
在 OC,体系是经过引证计数判别是否开释方针,当引证计数为 0 就阐明没有任何变量运用该空间,体系将开释方针。
3. 大局区/静态区
大局变量和静态变量的存储是放在一块的,分为.bss段
和.data段
,内存地址一般由0x1
最初:
-
Bss
段:未初始化
的大局变量和静态变量在一块区域 -
Data
段:已初始化
的大局变量和静态变量在相邻的另一块区域
举例:
static int bss;
static int bssStr;
static int data = 10;
static NSString *dataStr = @"nihao";
- (void)globalTest {
// 大局区
NSLog(@"****bss****");
NSLog(@"bss == %p",&bss);
NSLog(@"bssStr == %p",&bssStr);
NSLog(@"****data****");
NSLog(@"data == %p",&data);
NSLog(@"dataStr == %p",&dataStr);
}
成果如下:
若在.h
中创立一个静态变量ws_number
,则运用这个静态变量的每个文件都会生成一个ws_number
的静态变量,且初始值都相同。也便是说在多个文件运用同一个静态变量,体系会各自生成一个相同初始值
且地址不同
的静态变量,这样在各自文件内运用就不会相互搅扰,数据比较安全。这也是为什么静态区也称作静态安全区
。
4. 常量区
- 常量字符串便是放在这儿的
- 程序完毕后由体系开释
5. 代码段(.text)
- 存储程序代码,在编译时加载到内存中,代码会被编译成
二进制的形式
进行存储
举个比方来理解一下:
1、方针查找进程: 在程序查找一个方针时,首要到栈区找到方针的指针,再经过这个方针指针到堆区找到这个方针。
内存办理
NONPOINTER_ISA
正如上面所说,防止地址空间糟蹋,isa
指针规划成了联合体,在isa
地址中存储了许多信息。
isa
的结构如下:
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19
};
#endif
};
OC方针的实质,每个OC方针都含有一个isa指针,__arm64__
之前,isa
仅仅是一个指针,保存着方针或类方针内存地址,在__arm64__
架构之后,apple对isa
进行了优化,变成了一个联合体union
结构,一起运用位域来存储更多的信息。 它是经过isa
的bits
进行位运算,取出呼应位置的值,runtime中的isa
是被联合体位域优化过的,它不单单是指向类方针了,而是把64位中的每一位都运用了起来,其间的shiftcls
为33位,代表了类方针的地址,其他的位都有各自的用处。
-
nonpointer
:表明是否对isa指针敞开指针优化 0:不敞开,表明纯isa指针。 1:敞开,不单单是类方针的地址,isa中包含了类信息和方针的引证计数等。 -
has_assoc
:相关方针标识位,0没有,1有,没有相关方针会开释的更快。 -
has_cxx_dtor
:该方针是否有 C++ 或许 Objc 的析构器,假如有析构函数,则需求做析构逻辑, 假如没有,则能够更快的开释方针。 -
shiftcls
:存储类指针class
的值。敞开指针优化的状况下,在 arm64 架构中有 33 位⽤用来存储类指针。 -
magic
:固定值为0xd2,用于在调试时分辨方针是否完结初始化。 -
weakly_referenced
:表明方针是否被指向或许曾经指向一个 ARC 的弱引证变量, 没有弱引⽤的方针能够更快开释。 -
deallocating
:标志方针是否正在开释内存。 -
has_sidetable_rc
:当方针的引证计数大于10,以至于无法存储在isa指针中时,用散列表sidetable去计数。 -
extra_rc
:表明该方针的引证计数,实际上是引证计数值减 1, 例如,假如方针的引证计数为 10,那么 extra_rc 为 9。假如引证计数⼤于 10, 则需求使⽤到has_sidetable_rc。
TaggedPointer
为了节约内存和提高履行功率,苹果提出了
Tagged Pointer
的概念。关于 64 位程序,引进Tagged Pointer
后,相关逻辑能削减一半的内存占用,以及 3 倍的拜访速度提高,100 倍的创立、毁掉速度提高。
NSTaggedPointer
类型的方针选用和isa
相同的联合体位域的办法,可直接从地址中读取出想要的值,一般当数据类型的”value”满足小时,体系会主动转换为NSTaggedPointer
类型,比方NSString
转换为NSTaggedPointerString
。
方针的值直接存储在了指针中,不用在堆上为其分配内存,节约了许多内存开支。
更具体的 TaggedPointer 解析能够看iOS – 陈词滥调内存办理(五):Tagged Pointer。
引证计数
首要摘自iOS办理方针内存的数据结构以及操作算法–SideTables、RefcountMap、weak_table_t-一
引证计数(Reference Count)是一个简单而有用的办理方针生命周期的办法。当咱们创立一个新方针的时分,它的引证计数为 1,当有一个新的指针指向这个方针时,咱们将其引证计数加 1,当某个指针不再指向这个方针时,咱们将其引证计数减 1,当方针的引证计数变为 0 时,阐明这个方针不再被任何指针指向了,这个时分咱们就能够将方针毁掉,收回内存。
SideTables
引证计数要么存放在 isa
的 extra_rc
中,要么存放在引证计数表中,而引证计数表包含在一个叫 SideTable
的结构中,它是一个散列表,也便是哈希表。而 SideTable
又包含在一个大局的 StripeMap
的哈希映射表中,这个表的名字叫 SideTables
。
NSObject.mm
中SideTables
对应的源码如下
// SideTables
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// SideTable
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引证计数表
weak_table_t weak_table; // 弱引证表
// other code ...
};
它们的联系如下图:
自旋锁spinlock_t
自旋锁适用于锁运用者坚持锁时刻比较短的状况。正是因为自旋锁运用者一般坚持锁时刻十分短,因而选择自旋而不是睡眠是十分必要的,自旋锁的功率远高于互斥锁。
spinlock_t slock
用于对sidetable
加锁,确保数据安全,运用自旋锁作为安全锁其实是因为引证计数的操作十分快且频繁。
引证计数器RefcountMap refcnts
具体的引证计数数量是记载在这儿的,refcnts
是C++
的Map
,在SideTable
中需求再次调用table.refcnts.find(0x0000)
或许table.refcnts.find(0x000f)
找到真实的引证计数器。
引证计数器的存储结构如下图所示
具体的,引证计数器RefcountMap refcnts
经过find
查找到的value
其实是个位域,类似NONPOINTER_ISA
指针:
- 1UL<<0:
WEAKLY_REFERENCED
表明是否有弱引证指向这个方针,假如有的话(值为1)在方针开释的时分需求把一切指向它的弱引证都变成nil(相当于其他言语的NULL),防止野指针过错。 - 1UL<<1:
DEALLOCATING
表明方针是否正在被开释。1正在开释,0没有。 -
REAL COUNT
图中REAL COUNT
的部分才是方针真实的引证计数存储区。所以咱们说的引证计数加一或许减一,实际上是对整个unsigned long
加四或许减四,因为真实的计数是从2^2位开端的。 - 1UL<<(WORD_BITS-1):
SIDE_TABLE_RC_PINNED
其间WORD_BITS在32位和64位体系的时分分别等于32和64。其实这一位没啥具体含义,便是随着方针的引证计数不断变大。假如这一位都变成1了,就表明引证计数现已最大了不能再增加了。
- (1UL<<0)的意思是将一个”1″放到最右侧的盒子里,然后将这个”1″向左移动0位(便是原地不动):0b0000 0000 0000 0000 0000 0000 0000 0001
- (1UL<<1)的意思是将一个”1″放到最右侧的盒子里,然后将这个”1″向左移动1位:0b0000 0000 0000 0000 0000 0000 0000 0010
弱引证列表weak_table_t weak_table
weak_table_t
结构如图所示
-
weak_entry_t *weak_entries
:是一个数组,上面的RefcountMap
是要经过find(key)
来找到准确的元素的。weak_entries
则是经过循环遍历来找到对应的entry
。-
referent
:被指方针的地址。前面循环遍历查找的时分便是判别方针地址是否和他持平。 -
referrers
:可变数组,里边保存着一切指向这个方针的弱引证的地址。当这个方针被开释的时分,referrers
里的一切指针都会被设置成nil。 -
inline_referrers
:只要4
个元素的数组,默许状况下用它来存储弱引证的指针。当大于4
个的时分运用referrers
来存储指针。
-
-
num_entries
:用来保护确保数组一直有一个适宜的size
。比方数组中元素的数量超越3/4
的时分将数组的巨细乘以2
。
Q:既然SideTables
是一个哈希映射的表,为什么不用 SideTables
直接包含自旋锁,引证计数表和弱引证表呢?
这是因为在众多线程一起拜访这个 SideTable
表的时分,为了确保数据安全,需求给其加上自旋锁,假如只要一张 SideTable
的表,那么一切数据拜访都会出一个进一个,单线程进行,十分影响功率,虽然自旋锁现已是功率十分高的锁,这会带来十分不好的用户体验。针对这种状况,将一张 SideTable
分为多张表的 SideTables
,再各自加锁确保数据的安全,这样就增加了并发量,提高了数据拜访的功率,这便是为什么一个 SideTables
下包括众多 SideTable
表的原因。
Q:为什么SideTables
现现已过Hash映射了,还需求RefcountMap
再映射一次
其实苹果选用的是分块化思维,内存中方针的数量实在是太巨大了咱们经过第一个Hash
表仅仅过滤了第一次,然后咱们还需求再经过这个Map
才能准确的定位到咱们要找的方针的引证计数器。
假定现在内存中有16个方针,0x0000、0x0001、...... 0x000e、0x000f
,咱们创立一个SideTables[8]
来存放这 16 个方针,那么查找的时分产生Hash
抵触的概率便是八分之一。假定SideTables[0x0000]
和SideTables[0x0x000f]
抵触,映射到相同的成果。
SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一个SideTable
苹果把两个方针的内存办理都放到同一个SideTable
中。你在这个SideTable
中需求再次调用table.refcnts.find(0x0000)
或许table.refcnts.find(0x000f)
来找到他们真实的引证计数器。
内存办理相关操作
alloc
从一个面试题开端探究alloc
流程:
问:alloc 之后,引证计数如何改变?
答:在初始化isa
的时分,并没有对extra_rc
进行操作。也便是说alloc
办法实际上并没有设置方针的引证计数值为 1。
验证如下:创立一个LGPerson
类,在main
办法中创立一个实例方针
#import <Foundation/Foundation.h>
#import "LGPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGPerson *objc2 = [LGPerson alloc];
NSLog(@"Hello, World! %@",objc2);
}
return 0;
}
从NSObject.mm
类的+ (id)alloc
办法开端探究,办法(函数)从上往下顺次履行,省掉了部分影响解读的代码
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// some code ...
id obj = class_createInstance(cls, 0);
return obj;
}
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
// 内部调用 calloc 办法分配内存。
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// some code ...
id obj;
obj = (id)calloc(1, size); // 此时分配内存
obj->initInstanceIsa(cls, hasCxxDtor);
return obj;
}
上面的代码首要目的是分配内存
,真实的初始化在initInstanceIsa
,内部会初始化isa
指针的内容
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
// 将cls右移3位赋值给shiftcls
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
initIsa
里边,将bits
赋值了ISA_MAGIC_VALUE
,ISA_MAGIC_VALUE
为宏界说0x001f800000000001ULL
,断点后能够看到nonpointer
为1
,magic
为59
,如下图
履行shiftcls
赋值办法后,cls
值现已变成了LGPerson
(自界说的类名),即现已赋值成功,如下图
此时能够注意到,extra_rc
的值是0
,表明alloc
这一步实际上分配了内存,初始化了方针,但引证计数实际上是 0(调用init
之后就变成 1 了)。
值得一提的是callAlloc
函数中的slowpath
和fastpath
,callAlloc
完好实现如下:
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
办法中运用到的 slowpath
和 fastpath
,其实这两个都是宏界说,与代码逻辑自身无关,界说如下:
// x 很可能为 true,期望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 false,期望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))
其实它们是所谓的快途径和慢途径,为了解释这个,咱们来看一段代码:
if (x)
return 1;
else
return 39;
因为计算机并非一次只读取一条指令,而是读取多条指令,所以在读到 if
语句时也会把 return 1
读取进来。假如 x
为 0,那么会从头读取 return 39
,重读指令相对来说比较耗时。
假如 x
有十分大的概率是 0,那么 return 1
这条指令每次不可防止的会被读取,而且实际上几乎没有机会履行,形成了不用要的指令重读。
因而,在苹果界说的两个宏中,fastpath(x)
依然回来 x
,仅仅告知编译器 x
的值一般不为 0,从而编译能够进行优化。同理,slowpath(x)
表明 x
的值很可能为 0,期望编译器进行优化。
// 以下代码表明,
// 很可能 checkNil && !cls 的成果是 false,
// 编译器能够不用每次都读取 return nil 指令
if (slowpath(checkNil && !cls)) return nil;
当然,当checkNil && !cls
判别建立的时分,return nil
指令还是会被读取,然后履行的。
fastpath(expression)
也是相同的机制,表明很可能 expression 成果是 true
。
init
// NSObject.mm
// Calls [[cls alloc] init].
id
objc_alloc_init(Class cls)
{
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
- (id)init {
return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
// In practice, it will be hard to rely on this function.
// Many classes do not properly chain -init calls.
return obj;
}
基类的init
办法啥都没干,仅仅将alloc
创立的方针回来。咱们能够重写init
办法来对alloc
创立的实例做一些初始化操作。
retainCount
retainCount
办法是取出方针的引证计数值。怎样取值的呢?信任你们现已想到了,isa
或Sidetable
,下面咱们进入源码看看它的取值进程。
retainCount
办法的函数调用栈如下
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
uintptr_t _objc_rootRetainCount(id obj)
{
return obj->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount()
{
// 假如是 tagged pointer,直接回来 this
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
// 获取 isa
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
// 假如 isa 是 nonpointer
if (bits.nonpointer) {
// isa指针里的引证计数字段extra_rc,再+1,为引证计数的值
// alloc没有让引证计数+1,而获取retainCount却是1原因就在这
uintptr_t rc = 1 + bits.extra_rc;
// 假如还额定运用 sidetable 存储引证计数
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock(); // 加上 sidetable 中引证计数的值
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
// 假如 isa 不是 nonpointer,回来 sidetable_retainCount() 的值
return sidetable_retainCount();
}
size_t
objc_object::sidetable_getExtraRC_nolock()
{
ASSERT(isa.nonpointer);
// 取得 SideTable
SideTable& table = SideTables()[this];
// 取得 refcnts
RefcountMap::iterator it = table.refcnts.find(this);
// 假如没找到,回来 0
if (it == table.refcnts.end()) return 0;
// 假如找到了,经过 SIDE_TABLE_RC_SHIFT 位掩码获取对应的引证计数
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
#define SIDE_TABLE_RC_SHIFT 2
小结
- 在
arm64
之前,isa
不是nonpointer
。方针的引证计数全都存储在SideTable
中,retainCount
办法回来的是方针自身的引证计数值 1,加上SideTable
中存储的值; - 从
arm64
开端,isa
是nonpointer
。方针的引证计数先存储到它的isa
中的extra_rc
中,假如 19 位的extra_rc
不行存储,那么溢出的部分再存储到SideTable
中,retainCount
办法回来的是方针自身的引证计数值 1,加上isa
中的extra_rc
存储的值,加上SideTable
中存储的值。 - 所以,其实咱们经过
retainCount
办法打印alloc
创立的方针的引证计数为 1,这是retainCount
办法的劳绩,alloc
办法并没有设置方针的引证计数。
alloc
办法没有设置方针的引证计数为 1,它内部也没有调用retainCount
办法,init
时也仅仅回来alloc
创立的方针,按照引证计数的界说,方针不会直接dealloc
吗?
dealloc
办法是在release
办法内部调用的。只要你直接调用了dealloc
,或许调用了release
且在release
办法中判别方针的引证计数为 0 的时分,才会调用dealloc
。概况请参阅release
源码剖析。
retain&release
- retain
- 将
isa
中的extra_rc
+1,假如溢出,就将extra_rc
的RC_HALF
搬运到sidetable
中存储,extra_rc
是19
位,而RC_HALF
宏是(1ULL<<18)
,实际上持平于进行了 +1 操作 - 关于
NSTaggedPointer
类型,直接回来,不参加引证计数计算,因为NSTaggedPointer
方针的值直接存储在了指针中,不用在堆上为其分配内存,这点在objc_msgSend
也有体现,首要会判别LNilOrTagged
。
- 将
- release
- 将
isa
中的extra_rc
-1,假如下溢,则判别has_sidetable_rc
是否为true
,便是否运用了sidetable
,假如有的话就从sidetable
中搬运RC_HALF
个引证计数给extra_rc
,若不行RC_HALF
个,就有多少搬运多少,假如extra_rc
中引证计数为 0 且has_sidetable_rc
为false
或许Sidetable
中的引证计数也为 0 了,那就dealloc
方针。
- 将
概况参阅 [iOS – 陈词滥调内存办理 – retain&release]
weak
假如用__weak 修饰一个变量,底层履行了什么呢?以下面代码为例
id obj = [[NSObject alloc] init];
id __weak obj1 = obj;
底层的操作其实是
objc_initWeak(&obj1, obj);
// NSObject.mm
① objc_initWeak
② storeWeak
// objc-weak.mm
③ weak_unregister_no_lock
④ weak_register_no_lock
接下来查看源码
objc_initWeak
// *location 为 __weak 指针地址(即obj1),newObj(即obj)为被弱引证方针地址
id objc_initWeak(id *location, id newObj)
{
// 假如方针为 nil,那就将 weak 指针置为 nil
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
storeWeak
// 更新 weak 变量
// 假如 HaveOld == true,表明方针有旧值,即旧地址,它需求被清理掉,这个旧值可能为 nil
// 假如 HaveNew == true,表明一个新值需求赋值给变量,这个新值可能为 nil
// 假如 CrashIfDeallocating == true,则假如方针正在毁掉或许方针不支持弱引证,则中止更新
// 假如 CrashIfDeallocating == false,则存储 nil
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry: // 分别获取新旧值相相关的弱引证表
// 假如变量有旧值,获取已有方针(该旧值方针)和旧表
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
// 假如有新值要赋值给变量,则创立新表
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
// 分别给 oldTable 和 newTable 加锁
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// 判别 oldObj 和 location 是否是同一方针,假如不是就从头获取旧值相相关的表
if (haveOld && *location != oldObj) {
// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
// 假如有新值,则判别新值所属的类是否现已初始化,没初始化的话在此初始化
// 这一步是防止 +initialize 内部调用 storeWeak 产生死锁
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
// 假如有旧值,则调用weak_unregister_no_lock履行清空操作
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
// 假如有新值,则调用weak_register_no_lock把一切 weak 指针从头指向新的方针
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
// 设置 weakly-referenced 标志位
// 假如方针是 Tagged Pointer,则不做操作
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
// 将 location 指向新的方针
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
// 解锁
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
store_weak
函数的履行进程如下:
- 分别获取新旧值相相关的弱引证表
- 假如有旧值,调用
weak_unregister_no_lock
函数铲除旧值,移除一切指向旧值的weak
引证,而不是赋值为nil
- 假如有新值,调用
weak_register_no_lock
函数分配新值,将一切weak
指针从头指向新的方针 - 设置
isa
的weakly_referenced
弱引证标志位
weak_unregister_no_lock
weak_unregister_no_lock
用来移除弱引证方针
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id)
{
// 被弱引证的方针
objc_object *referent = (objc_object *)referent_id;
// 弱引证变量的地址
objc_object **referrer = (objc_object **)referrer_id;
/**
* weak_entry_t 结构
* - referent 被弱引证的方针
* - referrers 当有超越4个弱引证方针时,则存储到 referrers 中
* - inline_referrers 存储小于4个的弱引证方针
*/
weak_entry_t *entry;
if (!referent) return;
// 调用 weak_entry_for_referent 找到 entry 弱引证指针 item
if ((entry = weak_entry_for_referent(weak_table, referent))) {
// 从内层 inline_referrers 中移除 entry
// inline_referrers 中只能存储 4 个弱引证指针,
// 多了就要存储到 referrers 中,所以要多一步 empty 判空操作
remove_referrer(entry, referrer);
bool empty = true;
if (entry->out_of_line() && entry->num_refs != 0) {
empty = false;
}
else {
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i]) {
empty = false;
break;
}
}
}
//从 weak_table 中移除 entry 弱引证条目
if (empty) {
weak_entry_remove(weak_table, entry);
}
}
// Do not set *referrer = nil. objc_storeWeak() requires that the
// value not change.
}
weak_unregister_no_lock
用来移除现已存在的弱引证表,一般用于弱引证方针
现已不再引证,但被弱引证方针
还没有死亡的状况,内部履行步骤为:
- 查询
weak_table
,假如有弱引证信息,则得到entry
。 -
remove_referrer(entry, referrer)
移除相相关的弱引证信息。
weak_register_no_lock
weak_register_no_lock
用来保存弱引证方针
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
//被弱引证的方针
objc_object *referent = (objc_object *)referent_id;
//弱引证变量的地址
objc_object **referrer = (objc_object **)referrer_id;
//假如该弱引证方针是taggedPointer方针,则不做处理直接回来该方针
//taggedPointer方针是为了苹果为了性能最大化做的处理,
//针对不需求到堆中寻觅的方针,能够直接从地址中经过必定的算法得到他们的值。
if (!referent || referent->isTaggedPointer()) return referent_id;
// ensure that the referenced object is viable
bool deallocating;
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}
else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
// now remember it and where it is being stored
weak_entry_t *entry;//弱引证指针的条目
//判别weak_table中是否有该条目
if ((entry = weak_entry_for_referent(weak_table, referent))) {
//假如有,则把弱引证方针追加进去
append_referrer(entry, referrer);
}
else {
//假如没有,则创立一个
weak_entry_t new_entry(referent, referrer);
//假如索引现已超越本来的3/4,则给weak_table扩容
weak_grow_maybe(weak_table);
//将新的entry插入weak_table
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
weak_register_no_lock
保存弱引证方针具体流程如下:
- 判别
TaggedPointer
类型则直接回来,不需求保存弱引证信息(TaggedPointer
不需求在堆中分配内存) - 假如正在开释且方针不支持弱引证,则中止更新(
crashIfDeallocating==true && deallocating == true
) - 判别
weak_table
中是否有该方针的弱引证表,- 有就追加上
append_referrer
- 没有则创立个
weak_table
在加入
- 有就追加上
小结
weak
方针在底层的存储流程如下图所示
dealloc
dealloc
办法的函数调用栈为:
// NSObject.mm
① dealloc
② _objc_rootDealloc
// objc-object.h
③ rootDealloc
// objc-runtime-new.mm
④ object_dispose
⑤ objc_destructInstance
// objc-object.h
⑥ clearDeallocating
// NSObject.mm
⑦ sidetable_clearDeallocating
clearDeallocating_slow
objc_rootDealloc&rootDealloc
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void objc_object::rootDealloc()
{
// 判别是否为 TaggerPointer 内存办理计划,是的话直接 return
if (isTaggedPointer()) return; // fixme necessary? *
if (fastpath(isa.nonpointer && // 假如 isa 为 nonpointer
!isa.weakly_referenced && // 没有弱引证
!isa.has_assoc && // 没有相关方针
!isa.has_cxx_dtor && // 没有 C++ 的析构函数
!isa.has_sidetable_rc)) // 没有额定选用 SideTabel 进行引证计数存储
{
assert(!sidetable_present());
free(this); // 假如以上条件建立,直接调用 free 函数毁掉方针
}
else {
object_dispose((id)this); // 假如以上条件不建立,调用 object_dispose 函数
}
}
fastpath(x)
表明内部的x
值很可能为true
,期望编译器进行优化,假如符合5
个条件,则直接free
。
object_dispose&objc_destructInstance
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
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 = obj->hasAssociatedObjects();
// This order is important.
// 假如有 C++ 的析构函数,调用 object_cxxDestruct 函数
if (cxx) object_cxxDestruct(obj);
// 假如有相关方针,调用 _object_remove_assocations 函数,移除相关方针
if (assoc) _object_remove_assocations(obj);
// 调用 clearDeallocating 函数
obj->clearDeallocating();
}
return obj;
}
clearDeallocating
inline void
objc_object::clearDeallocating()
{
// 假如 isa 不是 nonpointer
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
// 调用 sidetable_clearDeallocating 函数
sidetable_clearDeallocating();
}
// 假如 isa 是 nonpointer,且有弱引证或许有额定运用 SideTable 存储引证计数
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 函数
clearDeallocating_slow();
}
assert(!sidetable_present());
}
slowpath(x)
表明 x
的值很可能为false
,期望编译器进行优化。先不考虑isa
不是nonpointer
的状况,咱们持续看clearDeallocating_slow
铲除弱引证及引证计数的操作。
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
// 获取 SideTable
SideTable& table = SideTables()[this];
table.lock();
// 假如有弱引证
if (isa.weakly_referenced) {
// 调用 weak_clear_no_lock:将指向该方针的弱引证指针置为 nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
// 假如有运用 SideTable 存储引证计数
if (isa.has_sidetable_rc) {
// 调用 table.refcnts.erase:从引证计数表中擦除该方针的引证计数
table.refcnts.erase(this);
}
table.unlock();
}
weak_clear_no_lock
铲除弱引证的核心办法,在方针dealloc
的时分,会调用weak_clear_no_lock
函数将指向该方针的弱引证指针置为nil
,具体实现如下
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
//referent为被毁掉方针的指针
objc_object *referent = (objc_object *)referent_id;
//经过被毁掉方针的指针取得entry,这个entry里存着这个方针的弱引证指针数组
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
//printf("XXX no entry for clear deallocating %pn", referent);
return;
}
//弱引证指针数组
weak_referrer_t *referrers;
size_t count;
//判别弱引证指针数量,小于4个存在entry的inline_referrers中,大于4个存在entry的referrers中
if (entry->out_of_line()) {
//假如大于4个,则到referrers中取弱引证指针的数组
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
//循环把一切弱引证指针置为nil
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
// 一些 free和num_entries--操作
weak_entry_remove(weak_table, entry);
}
小结
dealloc
的调用流程如下:
-
dealloc
时首要履行dealloc
->_objc_rootDealloc()
,直接来到rootDealloc()
函数 - 在
rootDealloc()
中判别5个条件
决议是否能够直接开释free()
- NONPointer_ISA // 是否是非指针类型 isa
- weakly_reference // 是否有若引证
- has_assoc // 是否有相关方针
- has_cxx_dtor // 是否有 c++ 相关内容
- has_sidetable_rc // 是否运用到 sidetable
- 不符合
5个条件
则调用objc_dispose()
,进而履行objc_destructInstance()
函数逐渐开释- 先判别
hasCxxDtor
,毁掉 c++ 相关内容 - 再判别
hasAssociatedObjects
,毁掉相关方针 - 履行
clearDeallocating()
,用以毁掉弱引证和引证计数
- 先判别
-
clearDeallocating()
毁掉弱引证和引证计数- 履行
waek_clear_no_lock
毁掉弱引证 - 获取
SideTable
履行table.refcnts.eraser()
擦除该方针的引证计数
- 履行
主动开释池AutoReleasePool
这儿首要作总结笔记,更具体的内容参阅iOS – 聊聊 autorelease 和 @autoreleasepool
AutoreleasePool原理
- 主动开释池(即一切的
AutoreleasePoolPage
方针)是以栈
为结点经过双向链表
的形式组合而成,每当Page
满了的时分,就会创立一个新的Page
,并设置它为hotPage
,而首个Page
为coldPage
。 - 有个属性叫
POOL_BOUNDARY
,称为岗兵方针,用来解决主动开释池嵌套的问题- 每当创立一个主动开释池,就会调用
push()
办法将一个POOL_BOUNDARY
入栈 - 当毁掉一个主动开释池时,会调用
pop()
办法并传入一个POOL_BOUNDARY
,会从主动开释池中最终一个方针开端,顺次给它们发送release
音讯,直到遇到这个POOL_BOUNDARY
- 每当创立一个主动开释池,就会调用
- 主动开释池与线程一一对应,每个线程都会保护一个主动开释池仓库结构,新的pool在创立时会被压栈到栈顶,
pool
毁掉时,会被出栈,关于当前线程来说,开释方针会被压栈到栈顶,线程中止时,会主动开释与之相关的主动开释池 - 每个
AutoreleasePoolPage
方针占用4096
字节内存,其间56
个字节用来存放它内部的成员变量,剩余的空间(4040
个字节)用来存放autorelease
方针的地址。
嵌套@autoreleasepool
嵌套的@autoreleasepool
其实便是不断的push
岗兵方针(POOL_BOUNDARY
),在pop
时,会先开释里边的,在开释外面的。
举例如下
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool { //r1 = push()
_objc_autoreleasePoolPrint(); // print2
HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
@autoreleasepool { //r2 = push()
HTPerson *p3 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print4
@autoreleasepool { //r3 = push()
HTPerson *p4 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print5
} //pop(r3)
_objc_autoreleasePoolPrint(); // print6
} //pop(r2)
_objc_autoreleasePoolPrint(); // print7
} //pop(r1)
_objc_autoreleasePoolPrint(); // print8
return 0;
}
autoreleasePool
结构如图所示
Q:Runloop和@autoreleasePool联系
在iOS
中autorelease
方针的开释机遇是由RunLoop
控制的,会在RunLoop
每次循环完毕时开释。
阅读Runloop
源码其实没发现和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
,优先级最低,确保其开释池子产生在其他一切回调之后。
-
举例验证
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
// str是一个autorelease方针,设置一个weak的引证来调查它
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference); // Console: (null)
}
viewDidLoad
和viewWillAppear
是在同一个runloop
循环下调用的,因而在viewWillAppear
中,这个autorelease
的变量依然有值。
当然,咱们也能够手动干预autorelease
方针的开释机遇:
- (void)viewDidLoad
{
[super viewDidLoad];
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"sunnyxx"];
}
NSLog(@"%@", str); // Console: (null)
这样出了@autoreleasepool
效果域,内部的autorelease
方针就被开释了。
Q:main函数的@autoreleasepool和主线程runloop的@autoreleasepool联系?
Xcode
新旧版别main
函数里边的@autoreleasepool
办理的效果域?
Xcode 新版别
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Xcode 旧版别
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
假如你的程序运用了AppKit
或UIKit
框架,那么主线程的RunLoop
就会在每次事情循环迭代中创立并处理@autoreleasepool
。也便是说,应用程序一切autorelease
方针的都是由RunLoop
创立的@autoreleasepool
来办理。而main()
函数中的@autoreleasepool
仅仅负责办理它的效果域中的autorelease
方针。
旧版别 Xcode 中 main 函数的@autoreleasepool
Xcode 旧版别的main
函数中是将整个应用程序运转(UIApplicationMain
)放在@autoreleasepool
内,而主线程的RunLoop
便是在UIApplicationMain
中创立,所以RunLoop
创立的@autoreleasepool
是嵌套在main
函数的@autoreleasepool
内的。RunLoop
会在每次事情循环中对主动开释池进行pop
和push
,可是它的pop
只会开释掉它的POOL_BOUNDARY
之后的方针,它并不会影响到外层main
函数中@autoreleasepool
。
新版别 Xcode11 今后的 main 函数产生了哪些改变?
- 旧版别是将整个应用程序运转放在
@autoreleasepool
内,因为RunLoop
的存在,要return
即程序完毕后@autoreleasepool
效果域才会完毕,这意味着程序完毕后main
函数中的@autoreleasepool
中的autorelease
方针才会开释。 - 而在 Xcode 11 中,触发主线程
RunLoop
的UIApplicationMain
函数放在了@autoreleasepool
外面,这能够确保@autoreleasepool
中的autorelease
方针在程序发动后立即开释。正如新版别的@autoreleasepool
中的注释所写 “Setup code that might create autoreleased objects goes here.
”(如上代码),能够将autorelease
方针放在此处。
Q:什么时分需求手动增加@autoreleasepool?
AppKit 和 UIKit 框架会在RunLoop
每次事情循环迭代中创立并处理@autoreleasepool
,因而,你通常不用自己创立@autoreleasepool
,甚至不需求知道创立@autoreleasepool
的代码怎样写。可是,有些状况需求自己创立@autoreleasepool
。
例如,假如咱们需求在循环中创立了许多暂时的autorelease
方针,则手动增加@autoreleasepool
来办理这些方针能够很大程度地削减内存峰值。比方在for
循环中alloc
图片数据等内存消耗较大的场景,需求手动增加@autoreleasepool
。
苹果给出了三种需求手动增加
@autoreleasepool
的状况:
- ① 假如你编写的程序不是根据 UI 框架的,比方说命令行东西;
- ② 假如你编写的循环中创立了大量的暂时方针;
你能够在循环内运用@autoreleasepool
鄙人一次迭代之前处理这些方针。在循环中运用@autoreleasepool
有助于削减应用程序的最大内存占用。- ③ 假如你创立了辅佐线程。
一旦线程开端履行,就必须创立自己的@autoreleasepool
;否则,你的应用程序将存在内存泄漏。
参阅博客
iOS底层-内存分区与布局
iOS概念攻坚之路(三):内存办理
iOS办理方针内存的数据结构以及操作算法–SideTables、RefcountMap、weak_table_t-一
iOS – 陈词滥调内存办理(四):内存办理办法源码剖析
iOS – 陈词滥调内存办理(三):ARC 面世
iOS – 聊聊 autorelease 和 @autoreleasepool