一、Tagged Pointer细节探究
苹果为了提高执行效率和节省内存,引入了
Tagged Pointer
的概念,对于64位程序来说可以达到3倍的访问速度和100多倍的创建销毁的速度。支持Tagged Pointer
的类型以某种方式创建后便是Tagged Pointer
指针,这种特殊的指针包括了数据内容和附加信息,访问的时候可以通过指针地址解码获得。
在objc
源码中定义了全部的支持Tagged Pointer
的类型,常用的类型摘录如下,如NSString
、NSNumber
、NSIndexPath
、NSDate
、UIColor
、NSIndexSet
等:
...// 60-bit payloads
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
...// 52-bit payloads
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_NSMethodSignature = 20,
先来看如下代码的打印结果:
NSString *str1 = @"abcd";
NSString *str2 = [[NSString alloc] initWithString:str1];
NSString *str3 = [[NSString alloc] initWithFormat:@"%@", str1];
NSString *str4 = [[NSString alloc] initWithFormat:@"%@-%@-%@", str1,str1,str1];
NSLog(@"str1=%@, ptr=%p, class=%@;", str1, str1, [str1 class]);
NSLog(@"str2=%@, ptr=%p, class=%@;", str2, str2, [str2 class]);
NSLog(@"str3=%@, ptr=%p, class=%@;", str3, str3, [str3 class]);
NSLog(@"str4=%@, ptr=%p, class=%@;", str4, str4, [str4 class]);
打印结果:
str1=abcd, ptr=0x1020fc9d0, class=__NSCFConstantString;
str2=abcd, ptr=0x1020fc9d0, class=__NSCFConstantString;
str3=abcd, ptr=0xa1e53d6849de69de, class=NSTaggedPointerString;
str4=abcd-abcd-abcd, ptr=0x283195de0, class=__NSCFString;
- 打印结果中
str3
的真实类型为NSTaggedPointerString
。str1
、str2
的真实类型为__NSCFConstantString
。str4
的真实类型为__NSCFString
。通过打印superclass
找到了他们之间的继承关系,其中NSTaggedPointerString
是NSString
的子类。
- 打印结果可以看出,是否支持
Tagged Pointer
跟创建的方式和初始化的内容长度等也有关系。
先从这里入口,如何判断一个对象是不是支持Tagged Pointer
?
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr){
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
//而_OBJC_TAG_MASK的定义与架构平台相关,真机的_OBJC_TAG_MASK = (1UL<<63)
,也就是高1位是1就是Tagged Pointer指针。
#if __arm64__
# define OBJC_SPLIT_TAGGED_POINTERS 1 //64位真机
#else
# define OBJC_SPLIT_TAGGED_POINTERS 0
#endif
#if OBJC_SPLIT_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63) //真机:指针最高位为1
#elif OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
# endif
为了避免开发人员直接从指针地址上直接获取到内容,我们直接通过指针取地址获取到的都是encode
之后的,要想拿到真实的信息需要decode
:
static inline void * _Nonnull _objc_encodeTaggedPointer(uintptr_t ptr){
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return (void *)ptr;
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
return (void *)value;
}
static inline uintptr_t_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr){
uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return value;
#endif
return value ^ objc_debug_taggedpointer_obfuscator;
}
encode
的时候通过objc_debug_taggedpointer_obfuscator
与指针地址按位异或,decode
是objc_debug_taggedpointer_obfuscator
按位异或encode
之后的值。而objc_debug_taggedpointer_obfuscator
则是在应用启动时_read_images()
->initializeTaggedPointerObfuscator()
初始化的。在按位异或中相同为0,不同为1,如果objc_debug_taggedpointer_obfuscator
的值为0
则encode/decode
前后的值应该相同。DisableTaggedPointerObfuscation==YES
的时候初始化的objc_debug_taggedpointer_obfuscator=0
,可以在scheme
的环境变量中配置OBJC_DISABLE_TAG_OBFUSCATION==YES
之后在调试更方便。
设置环境变量操作如下:
设置之后再次打印如上数据,其中str3的打印结果发生变化:
str3=abcd, ptr=0x8000003231b130a2, class=NSTaggedPointerString;
接下来按照官方说的方式去还原一下数据:
常用类型总结:
变量 | 地址 | 二进制地址 | 说明 |
---|---|---|---|
[NSString stringWithFormat:@"abcd"] | 0x8000003231b130a2 | 0B1-0000000000000000000000000110 0100011000110110001001100001-0100-010 |
最高位为1,最低3位2代表NSString类型,4-7位为4表示字符串长度 |
[NSString stringWithFormat:@"abcdefg"] | 0xb3b332b231b130ba | 0B1-0110011101100110011001010110 0100011000110110001001100001-0111-010 |
最高位为1,最低3位2代表NSString类型,4-7位为7表示字符串长度 |
[NSNumber numberWithChar:1] | 0x8000000000000083 | 0B1-0000000000000000000000000000 0000000000000000000000000001-0000-011 |
最高位为1,最低3位3代表NSNumber类型,4-7位为0表示char型 |
[NSNumber numberWithShort:3]] | 0x800000000000018b | 0B1-0000000000000000000000000000 0000000000000000000000000011-0001-011 |
最高位为1,最低3位3代表NSNumber类型,4-7位为1表示short型 |
[NSNumber numberWithInt:7] | 0x8000000000000393 | 0B1-0000000000000000000000000000 0000000000000000000000000111-0010-011 |
最高位为1,最低3位3代表NSNumber类型,4-7位为2表示int型 |
[NSNumber numberWithLong:52] | 0x8000000000001a1b | 0B1-0000000000000000000000000000 0000000000000000000000110100-0011-011 |
最高位为1,最低3位3代表NSNumber类型,4-7位为3表示long型 |
[NSIndexPath indexPathWithIndex:5] | 0x8000000000002874 | 0B1-0000000000000000000000000000 0000000000000000000001010000-1110-100 |
最高位为1,最低3位4代表NSIndexPath类型 |
[NSDate date] | 0x969db1df206a00a6 | 0B1-0010110100111011011000111011 1110010000001101010000000001-0100-110 |
最高位为1,最低3位6代表NSDate类型 |
采用Tagged Pointer存储的小对象,需要在类型、创建方式、内容长度等方面满足要求,简单老说就是数据内容、标识位和扩展信息需要在2^64位中能存储完整完整。我们在实际开发中不应该依赖这些细节,这些内容不同平台不一样,而且可能会经常改变。
二、retain/release的流程梳理
先看retain:
retain核心流程梳理如下:
- 1.首次进入
rootRetain(tryRetain, variant)
:参数tryRetain=false
,variant=FastOrMsgSend
。 - 2.如果是
isTaggedPointer
,则直接return this
;反之继续3. - 3.
variant=FastOrMsgSend
,执行objc_msgSend(this, @selector(retain))
,继续4。 - 4.二次进入
rootRetain(tryRetain, variant)
:参数tryRetain=false
,variant=Fast
。 - 5.如果
isa.nonpointer==0
,执行sidetable_retain()
:引用计数全部全部存储在sidetable
中;直接根据当前对象找到存储该对象的table
,然后找到原有的refcntStorage+=SIDE_TABLE_RC_ONE
即可。 - 6.如果
isa.nonpointer==1
,应用计数存储在isa.extra_rc
和sidetable
中。引用计数+1
会优先添加到isa.extra_rc
上。如果存满了,则先保存一半RC_HALF
在extra_rc
中,并标记has_sidetable_rc=true
已使用引用计数表,处理完isa后更新isa的数据。再将另一半RC_HALF
追加到sidetable
中,保存到side_table
的流程与2
同。 - 7.
retain
的最后返回this
指针。
源码中release的核心流程objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
中,源码较多,这里梳理核心流程如下:
release核心流程梳理如下:
- 1.首次进入
rootRelease(performDealloc, variant)
:参数performDealloc=true
,variant= FastOrMsgSend
。 - 2.如果是
isTaggedPointer
,则直接return false
;反之继续3
。 - 3.
variant=FastOrMsgSend
,执行objc_msgSend)(this, @selector(release))
,继续4。 - 4.二次进入
rootRelease(performDealloc, variant)
:参数performDealloc=true
,variant= Fast
。 - 5.如果
isa.nonpointer==0
,执行sidetable_release()
:引用计数全部存在sidetable
中;根据当前的对象找到存储该对象引用计数的table,然后找到原有的refcnt -= SIDE_TABLE_RC_ONE
;即可。满足dealloc
条件的,继续执行dealloc
流程。 - 6.如果
isa.nonpointer==1
,应用计数存储在isa.extra_rc
和sidetable
中。引用继续-1会先从isa.extra_rc
上减。如果不够减了,会进入underflow流程7. - 7.如果该对象有
has_sidetable_rc
,执行rootRelease_underflow
流程,三次进入rootRelease(performDealloc, variant)
:参数performDealloc=true
,variant= Full
。 - 8.执行
auto borrow = sidetable_subExtraRC_nolock(RC_HALF)
;也就是问sidetable
借RC_HALF
,返回借到的数量和剩余的数量。 - 9.如果借到了则将借到的数量-1保存到
isa.extrac_rc
中。如果sidetable
中剩余为0则标记isa.has_sidetable_rc=0
,再存储新的isa.bits
的数据。处理存储失败的情况。 - 10.如果没有借到或者根本就没有再sidetable中存储则执行
dealloc
相关流程。
小结:如果是
Tagged Pointer
小对象,没有占用堆空间分配内存,无需引用计数的管理,小对象的释放随着栈空间的回收而释放。常规对象,先判断有没开启isa
优化(isa.nonpointer==0
),没有开启则对象的引用计数都存储在sidetable
中,无论retain
还是release
都操作的是sidetable
中的计数+1
,-1
,如果是release
,则当计数为0的时候执行dealloc
操作。如果开启了isa
优化(isa.nonpointer==1
),则对象的引用计数存储在isa.extra_rc和sidetable
中,优先操作isa.extra_rc
。retain
操作isa.extra_rc++
,当isa.extra_rc
中存满255个后,就会分一半(1<<7)到sidetable
中,并在isa中标记isa.has_sidetable_rc
=1。而release
操作isa.extra_rc--
,当isa.extra_rc
中不够减了,则会从sidetable
中尝试借1<<7个,如果sidetable
中被借了之后没有了会设置isa.has_sidetable_rc=0
,借到的数据会加到isa.extra_rc
中,方便后续使用。如果没有借到则执行dealloc
操作。
三、dealloc流程梳理
先看看底层objc_object::rootDealloc
函数:
inline void objc_object::rootDealloc(){
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc)){
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
从这里可以看出如果一个对象是isTaggedPointer
,那么这里什么事情都不做。然后再判断是否有开启isa
优化、是否没有被弱引用、是否没有关联对象、是否没有C++
构造/析构函数、是否在sidetable
中没有引用计数了,如果这些都是是,则直接调用底层free(this)
回收内存;反之则调用object_dispose(this)
。
接着进入object_dispose
函数:
id object_dispose(id obj){
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
先执行objc_destructInstance(obj)
,再执行free(obj)
。结合上下文推测objc_destructInstance()
应该是处理弱引用表、关联对象表、引用计数表相关的问题的。
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.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
在objc_destructInstance
函数中,做了三个事情:
- 1、如果有
c++
析构方法,则调用object_cxxDestruct(obj)
,内部继续调用object_cxxDestructFromClass()
方法,最终会从子类到父类依次调用析构函数(如果存在的话),具体调用析构函数做什么,还需要进一步探究。 - 2.如果有关联对象,则调用
_object_remove_assocations(obj, true)
,将该对象相关的关联记录擦除,同时如果关联对象存在且引用计数策略是OBJC_ASSOCIATION_SETTER_RETAIN
,则像改对象发送objc_release(_value)
消息。 - 3.调用
obj->clearDeallocating()
函数,进行弱引用对象、引用计数相关的处理:如果没有开启isa.nonpointer
,则调用sidetable_clearDeallocating()
。如果该对象被弱引用或者再sidetable
中存储了引用计数则调用clearDeallocating_slow()
。
sidetable_clearDeallocating()和clearDeallocating_slow()做的事情是一致的,核心代码如下:
//3.1
void objc_object::sidetable_clearDeallocating(){
SideTable& table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
table.refcnts.erase(it);
}
}
//3.2
NEVER_INLINE void objc_object::clearDeallocating_slow(){
SideTable& table = SideTables()[this];
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
}
- 4.weak_clear_no_lock进行弱引用关系的处理:相关细节标记在如下代码中
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) {
objc_object *referent = (objc_object *)referent_id; //当前对象referent
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);//从weak_table取出entry
if (entry == nil) {
return;//如果weak_table中没有,则直接return
}
weak_referrer_t *referrers;//weak指针列表,List结构
size_t count;//weak指针的数量
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
//一次遍历weak指针
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
//如果weak指针指向的是自己,则将weak指针置空,这也是为啥weak指针在对象释放后被自动置空的真正原因。
*referrer = nil;
}
else if (*referrer) {
//weak指针存在entry. referrers中,但是weak指针并不指向自己:异常情况!!!处理
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);//从weak_table表中移除该entry。
}
- 5.table.refcnts.erase()进行引用计数的处理。