1. 内存布局

2.ARC和MRC
MRC
系统是通过对象的引用计数来判断一个对象是否需要销毁,
1.对象被创建时引用计数为
12.当对象被其他指针引用时,需要手动调用
[objc retain],使对象的引用计数+13.当指针变量不再使用对象时,需要手动调用
[objc release]来释放对象,使对象的引用计数-14.当对象的引用计数为
0时,系统就会销毁这个对象
所以MRC的规则是:谁创建,谁释放,谁引用,谁管理
ARC
ARC是苹果引入的自动计数管理机制。是LLVM和Runtime配合的结果,ARC中禁止手动调用retain/release/retainCount/dealloc/autorelease,编译器会在适当的位置插入这些方法。
3.内存管理方案
除了上面提到的 MRC 和 ARC,还有三种内存管理方案,分别是:Tagged Pointer,Nonpointer_isa,SideTables.
NSString 内存
Tagged Pointer专⻔用来存储小的对象,例如NSNumber和NSDate,以及由数字、英文字母组合且长度小于等于9的NSString
下面通过NSString的内存管理来引出Tagged Pointer:
//初始化方式一:通过 WithString + @" "方式
NSString *str1 = @"123";
NSLog(@"%@ -- %p -- %@ -- %ld",str1,str1,[str1 class],(long)CFGetRetainCount((__bridge CFTypeRef)str1));
NSString *str2 = [[NSString alloc] initWithString:@"1234"];
NSLog(@"%@ -- %p -- %@ -- %ld",str2,str2,[str2 class],(long)CFGetRetainCount((__bridge CFTypeRef)str2));
NSString *str3 = [NSString stringWithString:@"12345"];
NSLog(@"%@ -- %p -- %@ -- %ld",str3,str3,[str3 class],(long)CFGetRetainCount((__bridge CFTypeRef)str3));
//初始化方式二:通过 WithFormat
//字符串长度在9以内
NSString *str4 = [NSString stringWithFormat:@"hello"];
NSLog(@"%@ -- %p -- %@ -- %ld",str4,str4,[str4 class],(long)CFGetRetainCount((__bridge CFTypeRef)str4));
NSString *str5 = [[NSString alloc] initWithFormat:@"hello"];
NSLog(@"%@ -- %p -- %@ -- %ld",str5,str5,[str5 class],(long)CFGetRetainCount((__bridge CFTypeRef)str5));
//字符串长度大于9
NSString *str6 = [NSString stringWithFormat:@"helloworld!!!"];
NSLog(@"%@ -- %p -- %@ -- %ld",str6,str6,[str6 class],(long)CFGetRetainCount((__bridge CFTypeRef)str6));
NSString *str7 = [[NSString alloc] initWithFormat:@"helloworld!!!!!!"];
NSLog(@"%@ -- %p -- %@ -- %ld",str7,str7,[str7 class],(long)CFGetRetainCount((__bridge CFTypeRef)str7));
}
复制代码
打印结果如下:
2021-03-24 14:41:23.439734+0800 TestDemo[28026:5804455] 123 -- 0x10a87e1b0 -- __NSCFConstantString -- 1152921504606846975
2021-03-24 14:41:23.439887+0800 TestDemo[28026:5804455] 1234 -- 0x10a87e1f0 -- __NSCFConstantString -- 1152921504606846975
2021-03-24 14:41:23.439978+0800 TestDemo[28026:5804455] 12345 -- 0x10a87e210 -- __NSCFConstantString -- 1152921504606846975
2021-03-24 14:41:23.440078+0800 TestDemo[28026:5804455] hello -- 0xc4c10d8f01d0ee6c -- NSTaggedPointerString -- 9223372036854775807
2021-03-24 14:41:23.440199+0800 TestDemo[28026:5804455] hello -- 0xc4c10d8f01d0ee6c -- NSTaggedPointerString -- 9223372036854775807
2021-03-24 14:41:23.440307+0800 TestDemo[28026:5804455] helloworld!!! -- 0x600003c8a8c0 -- __NSCFString -- 2
2021-03-24 14:41:23.440396+0800 TestDemo[28026:5804455] helloworld!!!!!! -- 0x600003290930 -- __NSCFString -- 1
复制代码
从结果可以看出:
__NSCFConstantString:字符串常量,是一种编译时的常量,存储在字符串常量区。其retainCount值很大,所以对其操作,不会引起引用计数变化.
NSTaggedPointerString:是苹果在64位环境下对NSString、NSNumber等对象做的优化。对于NSString对象来说当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区,其retainCount值很大。
__NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上,同样的当字符串的长度大于9时,也会创建成为__NSCFString类型。
Tagged Pointer 小对象类型
从上面的例子分析发现,Tagged Pointer的小对象的引用计数都比较大,那么对于Tagged Pointer来说,其引用计数的处理是怎样的呢?下面通过objc_retain、objc_release方法的源码来看一下:
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
复制代码
源码发现,如果对象是TaggedPointer,则直接返回,说明TaggedPointer类型的小对象不会retain和release,那么其引用计数也就不会改变。
对于一般的对象而言,其指针指向的是对象的地址,而Tagged Pointer指针的值不再是地址了,而且包含真正的值。
下面看一下Tagged Pointer的源码是怎么做的处理:
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
}
else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
复制代码
在iOS14之后,系统对Tagged Pointer小对象进行了混淆,通过与操作_OBJC_TAG_MASK混淆。下面通过objc_debug_taggedpointer_obfuscator来查找Tagged Pointer的编码和解码的过程。
//编码
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
//解码
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
复制代码
在编码和解码的源码中,看到Tagged Pointer小对象经过了两层异或(相同为0,不同为1),然后得到小对象自己。为了可以看到小对象的真实的地址,现在把解码的源码拷贝出来,通过上面的NSString小对象的例子,来看下:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str1 = [NSString stringWithFormat:@"a"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str1));
NSNumber *number1 = @1;
NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
}
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
复制代码
这里分别打印了对象的指针地址,值,经过解码后的真实的地址。打印结果

可以看出,对于0xa000000000000611,61代表的就是a的ASCII码,也就是str1的值,对于0xb000000000000012,数字1代表的就是当前number1的值。这就说明了Tagged Pointer小对象指针地址确实存储了对象的值。
那么对于这两个地址前面的0xa和0xb代表什么呢?
来到_objc_isTaggedPointer的源码:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
复制代码
其中define _OBJC_TAG_MASK (1UL<<63),所以源码((uintptr_t)ptr & _OBJC_TAG_MASK)表示;ptr & 1左移63,即2^63,相当于除了64位,其他位都为0,只是保留了最高位的值,也即是判断最高位是否有值。所以判断是否为Tagged Pointer,就是判断第64位是否有值且值为1.
将0xa转换成二进制为 1010,第64位为1,后三位表示tagType类型为2,
将0xb转换为二进制为 1011,第64位为1,后三位表示tagType类型为3,
下面通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入tagType的枚举
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
复制代码
#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,
...部分代码省略
复制代码
此时可以看到,2表示NSString类型,3表示NSNumber类型。所以0xa表示NSString类型,0xb表示NSNumber类型。
然后通过一个NSDate类型的对象来验证一下,NSDate类型是否为6?

打印出来,date1的真实地址为0xe2d30c25fc861d86,其高位是0xe,转换为二进制为1110,第64位为1,其余三位为6,完全符合上述枚举值的情况。
总结:
Tagged Pointer小对象类型(存储NSNumber、NSDate、小的NSString(不超过9位)),小对象指针不再是简单的地址,而是指针地址加上值,即真正的值,所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以可以直接进行读取。优点是占用空间小节省内存。
Tagged Pointer小对象 不会进行retain和release操作,而是直接返回,意味着不需要ARC进行管理,所以可以直接被系统自主的释放和回收。
Tagged Pointer的内存并不存储在堆中,而是在常量区中,也不需要malloc和free,所以可以直接进行读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右。
所以,综合来说,
Tagged Pointer的内存管理方案,比常规的内存管理,要快很多Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值。
NONPOINTER_ISA
objc_objcet对象中isa指针分为指针型isa与非指针型isa(NONPOINTER_ISA),我们都知道isa是纯指针,直接指向objc_class,存储着Class、Meta-Class对象的内存地址。instance对象的isa指向class对象,class对象的isa指向meta-class对象;从 arm64架构开始,对isa进行了优化,用NONPOINTER_ISA表示
NONPOINTER_ISA不单单是指针,除了指向objc_class外,Objective-C 在运行时还会使用一些额外的数据位去存储引用计数,是否被弱引用等相关信息。将64位中的33位用来存储class、meta-class对象的内存地址信息。而且要通过位运算将isa的值& ISA_MASK掩码,才能得到class、meta-class对象的内存地址。下面通过源码来查看一下它的结构:
union isa_t
{
Class cls;
uintptr_t bits;
# if __arm64__ // arm64架构
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
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)
};
# elif __x86_64__ // arm86架构,模拟器是arm86
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
struct {
uintptr_t nonpointer : 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;
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
};
# else
# error unknown architecture for packed isa
# endif
}
复制代码
nonpointer:表示是否对isa指针开启指针优化。0表示纯isa指针,1表示不止是类对象地址,isa中包含了类信息、对象的引用计数等。has_assoc:关联对象标志位,0没有,1存在。has_cxx_dtor:该对象是否有c++或者objc的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象。shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针。magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。weakly_referenced:志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。deallocating:标志对象是否正在释放内存。has_sidetable_rc:当对象引用技术大于10时,则需要借用该变量存储进位。extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1, 例如,如果对象的引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用到has_sidetable_rc。
SideTables 散列表
通过上面的内容,我们知道当引用计数存储到一定值时,将不会再存储到Nonpointer_isa的位域的extra_rc中,而是会存储到SideTables 散列表中。
void
objc_object::sidetable_lock()
{
SideTable& table = SideTables()[this];
table.lock();
}
void
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
复制代码
SideTables 散列表的使用是全局的,通过上面的源码可以看出SideTables并不只有一张表,而是多张表。事实上SideTables是能够存储一系列SideTable的hash数组。SideTables的hash键值是通过要存储对象的地址计算而来。
一个对象对应一个SideTable。但是一个SideTable,会存储多个对象的引用计数。因为SideTable的数量有限,所以会有很多对象共用同一个SideTable的情况。
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
复制代码
可以看到,SideTables的实质类型为模板类型StripedMap。下面来看一下StripedMap的定义:
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
复制代码
可以看到当为真机类型时,SideTables最多有8个SideTable。
接着来看一下SideTable的定义:
struct SideTable {
spinlock_t slock;//自旋锁
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);
};
复制代码
可以看到包含三个成员:
spinlock_t slock: 自旋锁,用于上锁和解锁SideTable。RefcountMap refcnts:引用计数表,用来存储OC对象的引用计数weak_table_t weak_table: 存储对象弱引用指针的hash表。weak功能实现的核心数据结构。
下面就引出了一个问题:为什么SideTable在内存中会存在多张?
通过上面SideTable的定义,我们知道,SideTable里面有一个自旋锁,现在假如当前的SideTable在内存中只存在一张,意味着全局所有的对象都会存在当前这一张SideTable里,当每一次对对象的操作,都会对当前SideTable进行解锁,那么这一张表就不安全。那么假如给每一个对象都开一张SideTable,彼此分割独立,这样会导致操作效率和查询效率都很低。所以SideTable在内存中存在多张,而且最多8张,同时一个对象对应一个SideTable。但是一个SideTable,会存储多个对象。

注解:图片来源于github.com/tbfungeek/i… 感谢!






















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)