Preface · 序
DisguisedPtr<T>
是 Apple 开源代码中的一个结构体类型,其使用于诸多 Obj-C 运行时组件中,比如weak
、@synchronized
等。
Obj-C 中的 DisguisedPtr
2021.06.01 by kingcosWhat
正如 Apple 官方对于 DisguisedPtr<T>
的注释:「acts like pointer type T*
(行为类似于指针类型 T*
)」,即其本身的作用等同于指针引用对象,而不同之处则在于其将引用对象的内存地址隐藏了。我们可以在 Apple 开源的 objc4-818.2 中找到其具体实现(如下)。
DisguisedPtr<T>
的本质是 C++ 的模版类,其中只含有一个 uintptr_t
类型的成员变量 value
,uintptr_t
是无符号整型,64 位下的大小为 8 字节,等同于指针的大小,可以用来存储指针。因此 DisguisedPtr
类型的对象大小其实也是 8 字节。当我们将指针构造为该类型的对象时,将通过 disguise
静态函数首先将指针存储的内存地址本身强制转换为 uintptr_t
无符号整型的十进制数据,并取负达到隐藏的目的。当然,其也提供了 undisguise
静态函数将隐藏的数据转换回内存地址,以及一些操作符便于外界直接使用。
// objc-private.h
// DisguisedPtr<T> acts like pointer type T*, except the
// stored value is disguised to hide it from tools like `leaks`.
// nil is disguised as itself so zero-filled memory works as expected,
// which means 0x80..00 is also disguised as itself but we don't care.
// Note that weak_entry_t knows about this encoding.
//
// DisguisedPtr<T> 的行为类似于指针类型 T* 一样,唯一的不同在于存储的值是伪装的,以避免被 leaks 等工具发现。
// nil 被伪装成了自己,所以零填充的内存可以像预期的那样工作,这意味着 0x80...00 也被伪装成了自己,但我们并不在意。
// 注意,weak_entry_t 也使用了该编码。
template <typename T>
class DisguisedPtr {
// _uintptr_t.h:
// typedef unsigned long uintptr_t;
// uintptr_t 表示能够存储指针地址的无符号整数类型(不同操作系统可能存在不同)
uintptr_t value;
static uintptr_t disguise(T* ptr) {
// 将指针指向的地址转换为 unsigned long,并取负
return -(uintptr_t)ptr;
}
static T* undisguise(uintptr_t val) {
// 转换回指针指向的地址
return (T*)-val;
}
public:
// 无参构造方法
DisguisedPtr() { }
// 指针构造(value = disguise(ptr))
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
// 使用 DisguisedPtr 类型构造(value = ptr.value)
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }
// 实现 = 运算符,右操作数类型为 T*
DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
// 实现 = 运算符,右操作数类型为 DisguisedPtr<T>
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}
operator T* () const {
// Foo *someFoo = disguisedFooPtr;
return undisguise(value);
}
T* operator -> () const {
// disguisedFooPtr->bar
return undisguise(value);
}
T& operator * () const {
// *disguisedFooPtr
return *undisguise(value);
}
T& operator [] (size_t i) const {
// &disguisedFooPtr[0]
return undisguise(value)[i];
}
// pointer arithmetic operators omitted
// because we don't currently use them anywhere
// 省略了指针运算符,因为我们目前没有在任何地方使用它们
};
// fixme type id is weird and not identical to objc_object*
static inline bool operator == (DisguisedPtr<objc_object> lhs, id rhs) {
return lhs == (objc_object *)rhs;
}
static inline bool operator != (DisguisedPtr<objc_object> lhs, id rhs) {
return lhs != (objc_object *)rhs;
}
How
在平时的开发中,我们其实不会接触到这一「画蛇添足」的类型,但在 Apple 的许多官方组件中,却常常能见到其身影,大多数时候我们只要认为其等同于指针引用对象即可。下面是其 API 的部分用例,需要注意的是,正如上文提到的 DisguisedPtr<T>
属于 C++ 模版类,因此需要将引入其头文件的实现文件后缀改为 .mm
:
// main.mm
#import <Foundation/Foundation.h>
#import "objc-private.h"
class Foo {
public:
int bar;
double baz;
};
int main(int argc, const char * argv[]) {
// 创建对象
Foo foo;
foo.bar = 1;
foo.baz = 3.14;
// 指针 fooPtr 指向 foo(即 fooPtr 存储了 foo 的内存地址)
Foo *fooPtr = &foo;
// 构造 DisguisedPtr
DisguisedPtr<Foo> disguisedFooPtr = DisguisedPtr<Foo>(fooPtr);
Foo *someFoo = disguisedFooPtr; // => operator T* ()
NSLog(@"%d", someFoo->bar); // 1
disguisedFooPtr->bar = 10; // => T* operator -> ()
NSLog(@"%d", disguisedFooPtr->bar); // 10
NSLog(@"%d", (*disguisedFooPtr).bar); // 10 => T& operator * ()
NSLog(@"%p", &disguisedFooPtr[0]); // 0x16fdff320 => T& operator * ()
NSLog(@"%p", &disguisedFooPtr[1]); // 0x16fdff330 => T& operator * ()
return 0; // BREAKPOINT 🔴
}
// OUTPUT:
// 1
// 10
// 10
// 0x16fdff320
// 0x16fdff330
// LLDB:
// (lldb) memory read 0x16fdff320
// 0x16fdff320: 0a 00 00 00 00 00 00 00 1f 85 eb 51 b8 1e 09 40 ...........Q...@
// 0x16fdff330: 88 f3 df 6f 01 00 00 00 01 00 00 00 00 00 00 00 ...o............
// (lldb) memory read 0x16fdff330
// 0x16fdff330: 88 f3 df 6f 01 00 00 00 01 00 00 00 00 00 00 00 ...o............
// 0x16fdff340: 60 f3 df 6f 01 00 00 00 50 d4 26 89 01 00 00 00 `..o....P.&.....
Why
那么为什么 Apple 要如此「画蛇添足」地实现这一类型呢?根据官方注释:「to hide it from tools like leaks
(以避免被 leaks
等工具发现)」,由于可参考的信息太少,网络上大部分关于这一点的介绍也仅此而已,本文也仅只是「猜想」。
leaks
是 macOS 自带的一款内存泄漏检测工具,我们可以在命令行 man leaks
以及 Reference 中的参考链接了解更多关于 leaks
的细节。DisguisedPtr<T>
避免了被 leaks
工具的检测,我们猜想这也意味着在使用 leaks
确认问题时,避免了被运行时底层的信息干扰。