内存一般分为五大区:栈区、堆区、常量区、全局区、代码区。如图
是由编译器自动分配并释放的,主要用来存储局部变量、函数的参数等,是一块连续的内存区域,遵循先进后出(FILO)原则。一般在运行时分配。它的分配由高地址空间向低地址空间分配。
优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效。
缺点:栈的内存大小有限制,数据不灵活。
例如:下图,创建两个变量,存放在栈区,地址是递减4。
堆区是由程序员手动管理。 主要用来存放动态分配的对象类型数据。是不连续的内存区域。在MRC环境下,由程序员手动释放,在ARC环境下,由编译器自动释放。一般在运行时分配。它的分配是从低地址空间向高地址空间分配。
优点:灵活方便,数据适应面广泛。
缺点:需手动管理,速度慢、容易产生内存碎片。
例如:下图,创建两个对象,存放在堆区,地址递增。
常量区存放的就是字符串常量和基本类型常量。在编译时分配,程序结束后回收
全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在.data
段,未初始化的全局变量和静态变量在相邻的.bss
区域,在编译时分配,程序结束后由系统释放。
代码区是在编译时分配,主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的
2013 年 9 月苹果推出了首个采用 64 位架构的 A7 双核处理器的手机 iPhone5s,为了改进从 32 位 CPU
迁移到 64 位 的内存浪费和效率问题,在 64 位 环境下,苹果工程师提出了 Tagged Pointer
的概念。采用这一机制,系统会对 NSString
、NSNumber
和 NSDate
等对象进行优化。建议大家看看 WWDC2020 这个视频的介绍。
Tagged Pointer 专门用来存储小对象,例如NSNumber,NSDate等,Tagged Pointer指针的值不再是单纯的地址了,而是真正的值,所以,实际上它也不再是一个对象了,它只是一个披着对象皮的普通变量而已,所以它的内存并不存储在堆区,也不需要malloc和free。这样在读取上有着3倍的效率,创建时候比以前快106倍。
由上图可以看出NSdate是NSTaggedPointer,
此外当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString
类型,当超过10个时,字符串的类型才是__NSCFString
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
上面这个方法我们看到,判断一个对象类型是否为NSTaggedPointerString
类型实际上是讲对象的地址与_OBJC_TAG_MASK
进行按位与操作,结果在跟_OBJC_TAG_MASK
进行对比,我们在看下_OBJC_TAG_MASK
的定义:
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
我们都知道一个对象地址为64位二进制,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer
类型。
那么我们在看下上面打印出的地址,所有NSTaggedPointerString
地址都是0xd
开头,d转换为二进制1110
,根据上面的结论,我们看到首位为1表示为NSTaggedPointerString
类型。在这里得到验证。
注意
:TaggedPointer
类型在iOS和MacOS中标志位是不同的iOS为最高位而MacOS为最低位
正常情况下一个对象的类型,是通过这个对象的ISA指针来判断的,那么对于NSTaggedPointer
类型我们如何通过地址判断对应数据是什么类型的呢?
在objc4-723之前,我们可以通过与判断TaggedPointer
标志位一样根据地址来判断,而类型的标志位就是对象地址的61-63位,比如对象地址为0xa
开头,那么转换成二进制位1010
,那么去掉最高位标志位后,剩余为010
,即10进制中的2。
接着我们看下runtime源码objc-internal.h中有关于标志位的定义如下:
#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
objc4-750之后
// Returns a pointer to the class's storage in the tagged class arrays.
// Assumes the tag is a valid basic tag.
static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
>> _OBJC_TAG_INDEX_SHIFT)
& _OBJC_TAG_INDEX_MASK);
uintptr_t obfuscatedTag = tag ^ tagObfuscator;
// Array index in objc_tag_classes includes the tagged bit itself
#if SUPPORT_MSB_TAGGED_POINTERS 高位优先
return &objc_tag_classes[0x8 | obfuscatedTag];
#else
return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}
classSlotForBasicTagIndex() 函数的主要功能就是根据指定索引 tag 从数组objc_tag_classes中获取类指针,而下标的计算方法发是根据外部传递的索引tag。比如字符串 tag = 2。当然这并不是简单的从数组中获取某条数据。
objc4-750之后源码:
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr)
{
// assert(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
static inline intptr_t
_objc_getTaggedPointerSignedValue(const void * _Nullable ptr)
{
// assert(_objc_isTaggedPointer(ptr));
uintptr_t value = _objc_decodeTaggedPointer(ptr);
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
if (basicTag == _OBJC_TAG_INDEX_MASK) {
return ((intptr_t)value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
return ((intptr_t)value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
NSString *str1 = [NSString stringWithFormat:@"1"];
NSString *str11 = [NSString stringWithFormat:@"11"];
NSString *str2 = [NSString stringWithFormat:@"2"];
NSString *str22 = [NSString stringWithFormat:@"22"];
// 0x31 1 0x32 1
uintptr_t value1 = objc_getTaggedPointerValue((__bridge void *)str1);
uintptr_t value2 = objc_getTaggedPointerValue((__bridge void *)str2);
uintptr_t value11 = objc_getTaggedPointerValue((__bridge void *)str11);
uintptr_t value22 = objc_getTaggedPointerValue((__bridge void *)str22);
// 以16进制形式输出
NSLog(@"%lx", value1);
NSLog(@"%lx", value11);
NSLog(@"%lx", value2);
NSLog(@"%lx", value22);
TaggedPointer[89535:3033433] 311 TaggedPointer[89535:3033433] 31312 TaggedPointer[89535:3033433] 321 TaggedPointer[89535:3033433] 32322
TaggedPoint对象是一个特殊的对象,不会涉及到引用计数retain
、release
等内存操作。对象的值就存在指针中,不过值通过了一份加密。
上面我们说了,对于一个对象的存储,苹果做了优化,那么对于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
。
下面我们先来看下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)
注意:
成员后面的:表明了该成员占用几个bit
而每个成员的意义如下表
nonopointer:表示是否对isa指针开启了指针优化,
0:纯isa指针,1:不止是类对象地址,isa中包含了类信息,对象的引用计数等。
has_assoc:关联对象标志位,0:没有,1:存在
has_cxx_dtor:该对象是否有c++或者Objc的析构器,如果有析构器函数,则需要做析构逻辑,如果没有,则可以更快的释放。
shiftcls:存储指针的值,开启指针优化的情况下,再arm64架构中有33位用来存储类指针,
magic:用于调试器判断当前对象是真的对象还是未初始化的空间。
weakly_referenced:标致对象是否指向或者曾经指向一个ARC的若变量,没有弱引用的对象可以更快的释放。
deallcating:标志对象是否正在释放内存
has_sidetable_rc:当前对象引用计数大于10的时,则需要借用变量存储进位
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么extra_rc为9,如果用于计数大于10则需要使用下面的has_sidetable_rc。
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
static void lockTwo(SideTable *lock1, SideTable *lock2);
template
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
spinlock_t 不是自旋锁,在底层代码查找的过程中,我们可以发现他是一把os_unfair_lock锁,在使用sidetable的时候,频繁的读取需要加锁,一张表无疑影响了效率,因此,我们采用stripedMap来分散压力,且stripedMap的数量是根据系统来确定的(真机模式下sidetable最多为8张,虚拟机等为64张).
// 上面 SideTables 的实现
static StripedMap& SideTables() {
return SideTablesMap.get();
}
typedef objc::DenseMap,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;
}
}
弱引用底层调用objc_initWeak:
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak
(location, (objc_object*)newObj);
}
storeWeak:
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
// HaveOld:weak指针是否指向一个弱引用
// HavNew:weak指针是否需要指向一个新的引用
template
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(oldTable, newTable);
if (haveOld && *location != oldObj) {//如果旧表不存在对应的obj
SideTable::unlockTwo(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(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(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走动态存储。
Object-C提供了两种内存管理机制MRC(Mannul Reference Counting)和ARC(Automatic Reference Counting),为Objective-C提供了内存的手动和自动管理。
基本思想:通过手动引用计数来进行对象的内存管理
涉及方法
autorelease即“自动释放”,是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放。自动释放池销毁的时候,池子里面所有的对象都会做一次release操作
那么,autorelease释放与简单的release释放有什么区别呢?
调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中),调用方拿到了对象,但这个对象还不被调用方所持有。当自动释放池销毁时,其中的所有的对象都会调用一次release操作。
本质上,区别在于autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理 。
autorelease 方法相当于把调用者注册到 autoreleasepool 中,ARC环境下不能显式地调用 autorelease 方法和显式地创建 NSAutoreleasePool 对象,但可以使用@autoreleasepool { }块代替(并不代表块中所有内容都被注册到了自动释放池中)。
对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。
RunLoop和AutoReleasePool是通过线程的方式一一对应的
在非手动添加Autorelease pool下,Autorelease对象是在当前runloop进入休眠等待前被释放的
当一个runloop在不停的循环工作,那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠):而去BeforeWaiting(准备进入休眠) 时会调用_objc_autoreleasePoolPop()和 _objc_autoreleasePoolPush()释放旧的池并创建新池,那么这两个方法来销毁要释放的对象,所以我们根本不需要担心Autorelease的内存管理问题。
内存管理方案
iOS内存管理方案有:
当ARC有效时,id类型和对象类型必须附加所有权修饰符,一共有如下四种。
__strong修饰符是id类型和对象类型默认的所有权修饰符。
弱引用表示并不持有对象,当所引用的对象销毁了,这个变量就自动设为nil。
可以利用__weak修饰符来解决循环引用问题。
__unsafe_unretained和__weak很像,唯一区别就是,__unsafe_unretained变量引用的对象再被销毁以后,不会被自动设置为nil,仍然指向对象销毁前的内存地址。所以它的名字叫做unsafe,此时你再尝试通过变量访问这个对象的属性或方法就会crash。一旦对象释放,则会成为悬垂指针,程序崩溃,因此__unnsafe_unretained修饰符的变量一定要在赋值的对象存在的情况下使用。
ARC无效时使用autorelease,在ARC下__autoreleasing的使用:
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}