内存分配区域
iOS程序内存分为5个区域
栈区,堆区,BSS,全局变量,代码区
五个区域有两种分配时间
运行时分配:栈区,堆区
栈区:局部变量,函数参数(形式参数),自动分配内存,当局部变量的作用域执行完毕之后就会被系统立即回收,由编译器分配和自动释放,函数执行完后,局部变量和形参占用的空间会自动被释放。效率比较高,但是分配的容量很有限。
堆区:程序代码new出的对象,动态分配内存,alloc出来的对象需要程序员自己进行内存管理
编译时分配:BSS,数据段,代码段
BSS:静态变量,用来存放未初始化的全局变量,静态变量,一旦初始化成功则回收,并转存到数据段中
数据段:已经初始化的全局变量,常量,静态变量,直到程序结束的时候才会被回收
代码段:程序二进制代码,程序结束的时候系统会自动回收储存在代码段中的数据,内存区域较小
栈区的地址一般0x7开头,堆区0x6开头,这两个区域时函数调用的时候分配,函数执行结束后一般会释放。
BSS区域,常量区域内存再运行期间一直存在,直到程序运行结束。
函数的调用过程:
执行某个函数时,如果有参数,则在栈上为形式参数分配空间(如果是引用类型的参数则除外),继续进入到函数体内部,如果遇到变量,则按情况,为变量在不同的存储区域分配空间(如果是static类型的变量,则是在进行编译的过程中已经就分配了空间),函数内的语句执行完后,如果函数没有返回值,则 直接返回调用该函数的地方(即执行原点),如果存在返回值,则先将返回值进行拷贝传回,再返回执行原点,函数全部执行完毕后,进行退栈操作,将刚才函数内 部在栈上申请的内存空间释放掉。
#import "ViewController.h"
**@interface** ViewController ()
**@end**
**static** NSInteger test = 1;
**const** NSString *str = @"hello";
**@implementation** ViewController
- (**void**)viewDidLoad {
[**super** viewDidLoad];
[**self** testMethod:@"method"]; // Do any additional setup after loading the view.
}
- (**void**)testMethod:(NSString *)param
{
NSString *local = [[NSString alloc] initWithFormat:@"%@", @"hello world!"];
NSString *localstr = [[NSString alloc] initWithFormat:@"%@", param];
//局部变量
NSInteger tmpInt = 2;
//堆变量
NSLog(@"%@", local);
//taggedPointer
NSLog(@"%@", localstr);
//全局变量
NSLog(@"%@", str);
//静态变量
NSLog(@"%ld", test);//断点加在这里,在 右击debug区域的变量,选择View Memory of "xxxx"可以看到这个变量的内存地址。这里有个变量比较特殊,局部变量localstr在debug 区域可以看到它是NSTaggedPointerString,也没有放在堆区。对于这种较短的字符串,苹果做了优化,没有将他们放到堆区,而是直接把值写入到指针中,以便加快访问速度,并减少内存。
}
**@end**
栈区:
特点:
1、栈是系统数据结构,其对应的进程或者线程是唯一的
2、栈是向低地址扩展的数据结构
3、栈是一块连续的内存区域,遵循先进后出(FILO)原则
4、栈的地址空间在iOS中是以0X7开头
5、栈区一般在运行时分配
存储内容
1、栈区是由编译器自动分配并释放的,主要用来存储局部变量
2、函数的参数,例如函数的隐藏参数(id self,SEL _cmd)
优缺点
优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效
缺点:栈的内存大小有限制(其实绝大部分情况都不会达到最大值)
iOS主线程栈大小是1MB,其他主线程是512KB,MAC只有8M
注意:传入函数的参数值、函数体内声明的局部变量等,由编译器自动分配释放,通常在函数执行结束后就释放了(不包括static修饰的变量,static意味该变量存放在全局/静态区)。
堆区–heep
特点
1、堆是向高地址扩展的数据结构
2、堆是不连续的内存区域,类似于链表结构(便于增删,不便于查询),
3、遵循先进先出(FIFO)原则
4、堆的地址空间在iOS中是以0x6开头,其空间的分配总是动态的
5、堆区的分配一般是在运行时分配
存储内容
1、堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收
2、OC中使用alloc或者使用new开辟空间创建对象
3、C语言中使用malloc、calloc、realloc分配的空间,需要free释放
优缺点
优点:灵活方便,数据适应面广泛
缺点:需手动管理,速度慢、容易产生内存碎片
注意:当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区。因为现在iOS默认使用ARC来进行内存管理,所以也不需要手动释放。
全局区(静态区)(BSS段)
**BSS段(bss segment)**通常是指用来存放程序中未初始化的或者初始值为0的全局变量的一块内存区域。BSS是Block Started by Symbol的简称。BSS段属于静态内存分配。
**数据段(data segment)**通常是指用来存放程序中已初始化的全局变量的一块内存区域,数据段属于静态内存分配。
全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放,主要存放
1、未初始化的全局变量和静态变量,即BSS区(.bss)
2、已初始化的全局变量和静态变量,即数据区(.data)
由static修饰的变量会成为静态变量,该变量的内存由全局/静态区在编译阶段完成分配,且仅分配一次。
static可以修饰局部变量也可以修饰全局变量。
常量区(数据段)
常量区是编译时分配的内存空间,在iOS中的地址一般以0x1开头,比如:0x100000000011038a,在程序结束后由系统释放
通常是指用来存放程序中已经初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。字符串常量等是放在只读数据段中,结束程序时才会被收回。
常量区是编译时分配的内存空间
,在程序结束后由系统释放,主要存放:
■ 已经使用了的,且没有指向的字符串常量
■ 字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存
代码区(代码段)
代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存
代码区需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
注:除了以上内存区域外,系统还会保留一些内存区域。
TaggedPointer
简单来说 TaggedPointer 是会将某些类型的短小数据直接存储到 对象指针中 ,而不是存储到指针指向的地址中,也就是说 如果这个对象是 TaggedPointer 对象,则该对象的地址里存储的内容就包含了该对象引用的值。
Tagged Pointer 专门用来存储小对象,例如NSNumber,NSDate等,Tagged Pointer指针的值不再是单纯的地址了,而是真正的值,所以,实际上它也不再是一个对象了,它只是一个披着对象皮的普通变量而已,所以它的内存并不存储在堆区,也不需要malloc和free。这样在读取上有着3倍的效率,创建时候比以前快106倍。
那么哪些类型支持 TaggedPointer 呢?
在 objc-internal.h 文件中有如下定义 ,
__NSCFConstantString
Constant->常量
通俗理解其就是常量字符串,是一种编译时常量
这种对象存储在字符串常量区。
通过打印起retainCount的值,发现很大,2^64 - 1,测试证明对其进行release操作时,retainCount不会产生任何变化是创建之后便无法释放掉的对象。
当我们使用不同的字符串对象进行创建时当内容相同,其对象的地址也相同,这也就证明了常量字符串是一种单例。
这种对象一般通过字面值 @“…”、CFSTR(“…”) (一种宏定义创建字符串的方法)或者 stringWithString: 方法(现在会报警告⚠️这个方法等同于字面值创建的方法)。
__NSCFString
和 __NSCFConstantString 不同, __NSCFString 对象是在运行时创建的一种 NSString 子类,他并不是一种字符串常量。所以和其他的对象一样在被创建时获得了 1 的引用计数。
这种对象被存储在堆上。
通过 NSString 的 stringWithFormat 等方法创建的 NSString 对象一般都是这种类型。
如果字符串长度大于9或者如果有中文或其他特殊符号(可能是非 ASCII 字符)存在的话则会直接成为 __NSCFString 类型
NSTaggedPointerString
标签指针的概念
理解这个类型,需要明白什么是标签指针,这是苹果在 64 位环境下对 NSString,NSNumber 等对象做的一些优化。
简单来讲可以理解为把指针指向的内容直接放在了指针变量的内存地址中,因为在 64 位环境下指针变量的大小达到了 8 字节足以容纳一些长度较小的内容。于是使用了标签指针这种方式来优化数据的存储方式。
从其的引用计数可以看出,这也是一个释放不掉的单例常量对象。当我们使用不同的字符串对象进行创建时当内容相同,其对象的地址也相同。在运行时根据实际情况创建。
对于 NSString 对象来讲,当非字面值常量的数字,英文字母字符串的长度小于等于 9 的时候会自动成为 NSTaggedPointerString 类型。(小于等于9这个数据也是原博主进行猜测,经过测试在字符串含有q的时候是小于等于7)
在源码中,苹果是怎么判断是否是Tagged Point 呢?是将第63位读取出来判断01吗?其实不是的
很多函数内部都有做判断,笔者整理如下:
1、判断class
2、获取 isa指针的时候
3、设置关联对象的时候会判断
4、自动释放池在释放对象的时候会判断
5、引用计数加减的时候会进行判断
6、对象释放的时候会进行判断(好像有个经典崩溃面试题用到了这个知识点)
对于一个对象的存储,苹果做了优化,对于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
};
#endif
};
从图中可以看出,我们所谓的isa指针,最后实际上落脚于isa_t的联合类型。那么何为联合类型呢? 联合类型是C语言中的一种类型,是一种n选1的关系,联合的作用在于,用更少的空间,表示了更多的可能的类型,虽然这些类型是不能够共存的。比如isa_t 中包含有cls,bits, struct三个变量,它们的内存空间是重叠的。在实际使用时,仅能够使用它们中的一种,你把它当做cls,就不能当bits访问,你把它当bits,就不能用cls来访问。
对于isa_t联合类型,主要包含了两个构造函数isa_t(),isa_t(uintptr_t value)和三个变量cls,bits,struct,而uintptr_t的定义为typedef unsigned long。
当isa_t作为Class cls使用时,这符合了我们之前一贯的认知:isa是一个指向对象所属Class类型的指针。然而,仅让一个64位的指针表示一个类型,显然不划算。
因此,绝大多数情况下,苹果采用了优化的isa策略,即,isa_t类型并不等同而Class cls, 而是struct。
// ISA_BITFIELD定义如下
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
具体的内容参考这篇文章,此处不过多赘述[[isa指针]]
散列表、引用计数表
Sidetable主要包含spinlock,引用计数(存放extra_rc接收的另一半引用计数),弱引用表。
truct SideTable {
spinlock_t slock;
// 存放从extra_rc接收的那一半引用计数
RefcountMap refcnts;
// 弱引用表
weak_table_t weak_table;
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
1.spinlock_t 加锁
spinlock_t 不是自旋锁,在底层代码查找的过程中,我们可以发现他是一把os_unfair_lock锁,在使用sidetable的时候,频繁的读取需要加锁,一张表无疑影响了效率,因此,我们采用stripedMap来分散压力,且stripedMap的数量是根据系统来确定的(真机模式下sidetable最多为8张,虚拟机等为64张).
// 上面 SideTables 的实现
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
2.RefcountMap(引用计数表)
- RefcountMap本身从DenseMap得来
- `typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
存放从extra_rc接收的那一半引用计数
if (variant == RRVariant::Full) {
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
// 将引用计数一半存在散列表中的方法
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
} else {
ASSERT(!transcribeToSideTable);
ASSERT(!sideTableLocked);
}
接着,来看一下 sidetable_addExtraRC_nolock 方法:
bool
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
ASSERT(isa.nonpointer);
// 获取SideTables,也就是StripeMap
SideTable& table = SideTables()[this];
size_t& refcntStorage = table.refcnts[this];
size_t oldRefcnt = refcntStorage;
// isa-side bits should not be set here
ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}
}
WeakTable(弱引用表)
弱引用底层调用objc_initWeak:
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
storeWeak:
添加引用的时候调用storeWeak一共有五个参数,其中3个参数定义在了template模版参数中(HaveOld:weak指针是否指向一个弱引用;HavNew:weak指针是否需要指向一个新的引用;crashIfDeallocating表示被弱引用的对象是否正在析构)。
weak_unregister_no_lock:清除原来弱引用表中的数据
weak_register_no_lock:将weak的指针地址添加到对象的弱引用表
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
// HaveOld:weak指针是否指向一个弱引用
// HavNew:weak指针是否需要指向一个新的引用
template <HaveOld haveOld, HaveNew haveNew,
enum 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;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {//如果旧表不存在对应的obj
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.
if (haveNew && newObj) {//有新表和新对象
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized()) //如果类没有初始化就重新初始化
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
class_initialize(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.
if (haveOld) {//如果指针曾经指向别的对象,就清除
// 清除原来弱引用表中数据
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
// 将weak的指针地址添加到对象的弱引用表
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (!newObj->isTaggedPointerOrNil()) {
// 将对象曾经指向过弱引用的标识置为true,没有弱引用的释放更快
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
// This must be called without the locks held, as it can invoke
// arbitrary code. In particular, even if _setWeaklyReferenced
// is not implemented, resolveInstanceMethod: may be, and may
// call back into the weak reference machinery.
callSetWeaklyReferenced((id)newObj);
return (id)newObj;
}
weak_entry_t:
struct weak_table_t {
// 弱引用数组
weak_entry_t *weak_entries;
// 数组个数
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
weak_entries是一个哈希数组,一个对象可以被多个弱引用指针引用,因此,这里用数组的形式表示一个对象的多个弱引用;数组中存储的内容就是弱引用对象的指针地址。当对象的弱引用个数小于等于4时走静态存储(在weak_entry_t初始化的时候一并分配好),大于4走动态存储。
sidetables总结:
sidetables可以理解为一个全局的hash数组,里面存储了sidetables类型的数据,其中长度为8或者64
一个obj(oc对象)对应了一个sideTable,但是一个SideTable,会对应多个obj,因为sidetabels的数量只有8或者64个,所以有很多obj会共用一个sidetable
在弱引用表中,key是对象的地址,value是weak指针地址的数组(weak_entry_t)
weak_unregister_no_lock 和 weak_register_no_lock 中都是对 weak_entry_t 类型的数组进行操作
_weak修饰对象(不会放入自动释放池),会调用objc_loadWeakRetained;使得引用计数加一,但仅是临时变量;被引用的对象不会增加他的引用计数。