1. 内存布局
2.ARC和MRC
MRC
系统是通过对象的引用计数来判断一个对象是否需要销毁,
1.对象被创建时引用计数为
1
2.当对象被其他指针引用时,需要手动调用
[objc retain]
,使对象的引用计数+1
3.当指针变量不再使用对象时,需要手动调用
[objc release]
来释放对象,使对象的引用计数-1
4.当对象的引用计数为
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… 感谢!