专注、坚持

Obj-C 中的 DisguisedPtr

2021.06.01 by kingcos
Preface · 序
DisguisedPtr<T> 是 Apple 开源代码中的一个结构体类型,其使用于诸多 Obj-C 运行时组件中,比如 weak@synchronized 等。

What

正如 Apple 官方对于 DisguisedPtr<T> 的注释:「acts like pointer type T*(行为类似于指针类型 T*)」,即其本身的作用等同于指针引用对象,而不同之处则在于其将引用对象的内存地址隐藏了。我们可以在 Apple 开源的 objc4-818.2 中找到其具体实现(如下)。

DisguisedPtr<T> 的本质是 C++ 的模版类,其中只含有一个 uintptr_t 类型的成员变量 valueuintptr_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 确认问题时,避免了被运行时底层的信息干扰。

Reference