之前写过一篇文章 Objective-C对象内存分布是怎样确定的,作为姊妹篇,两者配合食用口味更佳。
0x00 API
在runtime.h
中可以找到如下接口:
OBJC_EXPORT id _Nullable
object_getIvar(id _Nullable obj, Ivar _Nonnull ivar)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
object_setIvarWithStrongDefault(id _Nullable obj, Ivar _Nonnull ivar,
id _Nullable value)
OBJC_AVAILABLE(10.12, 10.0, 10.0, 3.0, 2.0);
OBJC_EXPORT Ivar _Nullable
object_setInstanceVariable(id _Nullable obj, const char * _Nonnull name,
void * _Nullable value)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ARC_UNAVAILABLE;
OBJC_EXPORT Ivar _Nullable
object_setInstanceVariableWithStrongDefault(id _Nullable obj,
const char * _Nonnull name,
void * _Nullable value)
OBJC_AVAILABLE(10.12, 10.0, 10.0, 3.0, 2.0)
OBJC_ARC_UNAVAILABLE;
OBJC_EXPORT Ivar _Nullable
object_getInstanceVariable(id _Nullable obj, const char * _Nonnull name,
void * _Nullable * _Nullable outValue)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ARC_UNAVAILABLE;
这 6个函数是用来对成员变量进行存取操作的,其中后三个函数在ARC下不可用。从函数形参来看,MRC下的函数只需要传入成员变量的名字char *
即可对成员变量进行存取,而前三个函数则要传入Ivar
,显然MRC下的接口更易用。
0x01 set
void object_setIvar(id obj, Ivar ivar, id value);
void object_setIvarWithStrongDefault(id obj, Ivar ivar, id value);
Ivar object_setInstanceVariable(id obj, const char *name, void *value);
Ivar object_setInstanceVariableWithStrongDefault(id obj, const char *name, void *value);
查看源码可发现,这4个函数在最终都会调用_object_setIvar
static ALWAYS_INLINE
void _object_setIvar(id obj, Ivar ivar, id value, bool assumeStrong)
{
if (!obj || !ivar || obj->isTaggedPointer()) return;
ptrdiff_t offset;
objc_ivar_memory_management_t memoryManagement;
_class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);
if (memoryManagement == objc_ivar_memoryUnknown) {
if (assumeStrong) memoryManagement = objc_ivar_memoryStrong;
else memoryManagement = objc_ivar_memoryUnretained;
}
id *location = (id *)((char *)obj + offset);
switch (memoryManagement) {
case objc_ivar_memoryWeak: objc_storeWeak(location, value); break;
case objc_ivar_memoryStrong: objc_storeStrong(location, value); break;
case objc_ivar_memoryUnretained: *location = value; break;
case objc_ivar_memoryUnknown: _objc_fatal("impossible");
}
}
-
如果obj或ivar为空,或obj不为空但是个TaggedPointer,则直接返回。关于TaggedPointer可以查看 TaggedPointer的推理与验证
-
通过
_class_lookUpIvar
获取当前成员变量在obj中的偏移量offset,与当前成员变量的所有权memoryManagement -
如果所有权为unknown,则通过参数assumeStrong来对所有权赋值。assumeStrong为true,赋予__strong所有权,assumeStrong为false,赋予__unsafe_unretained所有权。
值得一提的是:object_setIvarWithStrongDefault
与object_setInstanceVariableWithStrongDefault
内部调用这个函数时,给assumeStrong 传递的参数都是true,这也是为什么我们在写诸如@property (nonatomic) id name
之类的代码,他的默认修饰符是strong
的原因。
可以就这个点简单的验证一下:
@interface Test : NSObject
@property (nonatomic, strong) id a;
@property (nonatomic, copy) id b;
@property (nonatomic) id c;
@property (nonatomic, weak) id d;
@property (nonatomic, assign) id e;
@property (nonatomic, unsafe_unretained) id f;
@end
@implementation Test
@end
xcrun -sdk macosx clang -arch x86_64 -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.15.1 -Wno-deprecated-declarations Test.m
通过以上命令得到:
可见,strong、copy或者默认修饰符对应着__strong
,weak对应__weak
,assign与unsafe_unretained对应着__unsafe_unretained
。
-
id *location = (id *)((char *)obj + offset)
这句代码是整个函数的核心所在,先获取obj地址,通过offset偏移量获得成员变量存储地址。location是指向成员变量地址的指针,当所有权是__weak与__strong时,分别通过objc_storeWeak
与objc_storeStrong
进行后续操作,当所有权是__unsafe_unretained时,直接向location(地址)写数据。
0x02 objc_storeStrong
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
obj所有权为__strong时会调用这个函数,函数本身没啥好说的,通过*location
取值,如果取到的值与要存的值相等则return,否则,先将要obj引用计数器加1,然后将向location(地址)中写入obj,再对开始通过*location
取出的prev执行release操作。
由于这里对obj执行了retain,所以obj不会释放,从而确保通过*location
取出的值就是obj。
0x03 objc_storeWeak
id objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};
所以可以简化为:
storeWeak<true, true, false>(location, (objc_object *)newObj);
接着来看storeWeak,这里的代码较多,但核心点就三个函数:
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id);
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating);
inline void objc_object::setWeaklyReferenced_nolock();
分别对应着:
- 将旧对象与location解绑
- 将新对象与location绑定
- 设置对象isa的weakly_referenced字段设置为true,用于标识有
弱引用
引用该对象
0x04 weak_table_t
在unregister于register函数中,可以看到weak_table形参的类型是weak_table_t *
,接着看weak_table_t 是什么:
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
weak_table_t是个典型的hash结构,具体成员含义如下:
-
weak_entries
弱引用对象的相关信息会被整合到weak_entry_t
类型的数据结构中,而weak_entries是个动态数组,用于存储这些weak_entry_t结构信息 -
num_entries
weak_entries动态数组中的元素个数 -
mask
hash掩码 -
max_hash_displacement
出现hash碰撞的最大可能次数
weak_table_t的取值操作如下:
static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
以要referent 为key,通过hash_pointer(referent) & weak_table->mask
计算得出索引值,如果从动态数组对应索引取出的weak_entry_t的成员referent与参数referent 不同(哈希碰撞),则index = (index+1) & weak_table->mask
如此循环直到两者相同。如果哈希碰撞次数超过最大可能次数,则通过bad_weak_table
报错。最后,将通过最终索引取到的weak_entry_t的地址返回。
可见,weak_table_t是以要存储对象为key,来存储weak_entry_t的,而要存储对象weak指针的信息存储在weak_entry_t中。
0x05 weak_entry_t
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent)
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
typedef DisguisedPtr<objc_object *> weak_referrer_t;
#if __LP64__
#define PTR_MINUS_2 62
#else
#define PTR_MINUS_2 30
#endif
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2
-
referent
要存储的对象 -
union
这个union分为两个struct,上半部分很明显又是个hash结构,用于动态存储弱引用指针的地址,下半部分是个静态数组,长度为2,用于静态存储弱引用指针的地址。当弱引用个数大于2时,会从静态存储转成动态存储。
OK,现在来重新捋一遍思路。弱引用的存储会调用objc_storeWeak
,而这个函数内部出现了weak_table_t
数据结构,weak_table_t 内部又有weak_entry_t
数据结构。现在回到objc_storeWeak
本身,weak_table_t类型的数据又是从哪来的?
0x06 SideTable与StripedMap
在objc_storeWeak 中有诸如&oldTable->weak_table
,&newTable->weak_table
的取值方式,而oldTable与newTable都是SideTable
类型
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
...
};
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];
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
...
}static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
enum { CacheLineSize = 64 };
因为CacheLineSize等于64,显然SideTable占64个字节。iPhone真机下,StripedMap
中的StripeCount
等于8,否则等于64。这也就意味着,在真机下最多只能存在8种不同对象的弱引用
最终可以总结如下:
- StripedMap中存储着多个SideTable(iPhone真机最多为8个,否则最多为64个),每个SideTable代表一种对象的弱引用
- SideTable中存储着weak_table_t,每个weak_table_t存储着weak_entry_t类型动态数组,动态数组的个数代表当前对象含有的弱引用的个数
- weak_entry_t用来存储具体弱引用指针的地址
到这里set部分就结束了,接着来看get部分
0x07 get
id object_getIvar(id obj, Ivar ivar);
Ivar object_getInstanceVariable(id obj, const char *name, void **value);
object_getInstanceVariable
中会调用object_getInstanceVariable
,因此直接来看这个函数:
id object_getIvar(id obj, Ivar ivar)
{
if (!obj || !ivar || obj->isTaggedPointer()) return nil;
ptrdiff_t offset;
objc_ivar_memory_management_t memoryManagement;
_class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);
id *location = (id *)((char *)obj + offset);
if (memoryManagement == objc_ivar_memoryWeak) {
return objc_loadWeak(location);
} else {
return *location;
}
}
这个逻辑很简单,如果对象不是TaggedPointer,则通过_class_lookUpIvar
取出偏移量与所有权,通过对象地址加上偏移量得到偏移量的地址,如果所有权是__weak,则通过objc_loadWeak
取值,否者直接通过*location
取值。
通过*location
取值又分两种情况,所有权为__strong与__unsafe_unretained:
- 所有权为__strong时,由于在set阶段已经对obj执行了retain 操作,所以通过
*location
总是可以取到正确的值 - 所有权为__unsafe_unretained,由于在set阶段直接
*location = value
赋值,value有可能已被释放,当通过*location
取值时,可能出现野指针导致crash
0x08 objc_loadWeak
id objc_loadWeak(id *location)
{
if (!*location) return nil;
return objc_autorelease(objc_loadWeakRetained(location));
}
objc_loadWeak
会调用objc_loadWeakRetained
,
关于objc_autorelease 与objc_loadWeakRetained,可以看我的另两篇文章 :
Have fun!
网友评论