Peter's Den

悲观者只见到机会后面的问题,乐观者却看见问题后面的机会

Hello,在下2012年涉足Apple Developer,至今在iOS/OSX领域混迹多年,本职工作以iOS为主


精通Objective-c/Swift,对Python/Java/.Net/JavaScript也略懂一二,会与大家在这里记录分享

Objective-C中伪指针Tagged Pointer

Tagged Pointer

什么是TagPointer?

  • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free
  • 在内存读取上有着3倍的效率,创建时比以前快106倍

Apple为要引入Tagged Pointer呢?

我们知道NSInterge占用的字节在32位系统是4字节,到了64位系统就是8字节,当我们在存放小对象时导致了在64位系统可能出现成倍的浪费,从而导致内存翻倍,同时会对分配内存、维护引用计数、管理生命周期都增加了负担。

一般我们在存放NsNumber和NSDate这一类变量的时候本身占用的内存大小常常不需要8个字节。4字节带符号的整数可以达到2^31=2147483648,99.999%的情况都能满足了。

苹果预留了环境变量OBJC_DISABLE_TAGGED_POINTERS,通过设置该变量的布尔值,可以将Tagged Pointer技术的启用与关闭的决定权交给开发者!如果禁用Tagged Pointer,只需设置环境变量OBJC_DISABLE_TAGGED_POINTERS为YES即可!

在64位下面,Tagged Pointer的内存图是这样的:

Tagged Pointer源码

经过上面的解释想必对Tagged Pointer已经有了一定的了解,接下来我们来看下它源码,源码在objc/runtime/objc-internal.h中,下载地址

///生成TaggedPointer
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        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 {
        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);
    }
}
///TaggedPointer编码函数
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
///TaggedPointer解码函数
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

通过以上源码可以看到生成TaggedPointer时最终是通过_objc_encodeTaggedPointer 函数对真正的指针地址和objc_debug_taggedpointer_obfuscator 记性异或操作获得伪指针。

我们来打印下真伪指针:

NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberLager = @(555555555555555555);
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberLager pointer is %p", numberLager);

number1/number2/number3都是小数据,所以肯定会使用Tagged Pointer,但是numberLager因为是大数据,通过Tagger Pointer存放不下(而且也没有必要使用Tagger Pointer),所以numberLager是正在的指针地址

number1 pointer is 0x9237a2e097f0e96b  (伪指针)
number2 pointer is 0x9237a2e097f0e95b  (伪指针)
number3 pointer is 0x9237a2e097f0e94b  (伪指针)
numberLager pointer is 0x600001984200  (真指针,0x6堆区,0x7栈区)

为什么NSNumber、NSDate、NSString会自动转伪指针呢?其他的为什么不会呢?

这个就要扯到Tagged Pointer的注册了_objc_registerTaggedPointerClass

_objc_registerTaggedPointerClass

/***********************************************************************
* _objc_registerTaggedPointerClass
* Set the class to use for the given tagged pointer index.
* Aborts if the tag is out of range, or if the tag is already 
* used by some other class.
**********************************************************************/
void
_objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls)
{
	 //OBJC_DISABLE_TAGGED_POINTERS就是判断开发者有没有关闭Tagged Pointer
    if (objc_debug_taggedpointer_mask == 0) {
        _objc_fatal("tagged pointers are disabled");
    }
    Class *slot = classSlotForTagIndex(tag);
    if (!slot) {
        _objc_fatal("tag index %u is invalid", (unsigned int)tag);
    }
    Class oldCls = *slot;
    if (cls  &&  oldCls  &&  cls != oldCls) {
        _objc_fatal("tag index %u used for two different classes "
                    "(was %p %s, now %p %s)", tag, 
                    oldCls, oldCls->nameForLogging(), 
                    cls, cls->nameForLogging());
    }
    *slot = cls;
    // Store a placeholder class in the basic tag slot that is 
    // reserved for the extended tag space, if it isn't set already.
    // Do this lazily when the first extended tag is registered so 
    // that old debuggers characterize bogus pointers correctly more often.
    if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
        Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
        if (*extSlot == nil) {
            extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
            *extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
        }
    }
}
  • 数组objc_tag_classes:存储苹果定义的几个基础类;
  • 数组objc_tag_ext_classes:存储苹果预留的扩展类;
#if SUPPORT_TAGGED_POINTERS //支持 Tagged Pointer
extern "C" {
    extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT*2];
    extern Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
#define objc_tag_classes objc_debug_taggedpointer_classes
#define objc_tag_ext_classes objc_debug_taggedpointer_ext_classes
#endif

classSlotForTagIndex

该函数的主要功能就是根据指定索引 tag 获取类指针:

  • 当索引tag为基础类的索引时,去数组objc_tag_classes中取数据;
  • 当索引tag为扩展类的索引时,去数组objc_tag_ext_classes中取数据;
  • 当索引tag无效时,返回一个 nil;
// Returns a pointer to the class's storage in the tagged class arrays, 
// or nil if the tag is out of range.
static Class *
classSlotForTagIndex(objc_tag_index_t tag)
{
    if (tag >= OBJC_TAG_First60BitPayload && tag <= OBJC_TAG_Last60BitPayload) {
        return classSlotForBasicTagIndex(tag);
    }
    if (tag >= OBJC_TAG_First52BitPayload && tag <= OBJC_TAG_Last52BitPayload) {
        int index = tag - OBJC_TAG_First52BitPayload;
        uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                    >> _OBJC_TAG_EXT_INDEX_SHIFT)
                                   & _OBJC_TAG_EXT_INDEX_MASK);
        return &objc_tag_ext_classes[index ^ tagObfuscator];
    }
    return nil;
}

classSlotForBasicTagIndex

classSlotForBasicTagIndex() 函数的主要功能就是根据指定索引 tag 从数组objc_tag_classes中获取类指针;该函数要求索引tag是个有效的索引!

// Returns a pointer to the class's storage in the tagged class arrays.
// Assumes the tag is a valid basic tag.
static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
    uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                >> _OBJC_TAG_INDEX_SHIFT)
                               & _OBJC_TAG_INDEX_MASK);
    uintptr_t obfuscatedTag = tag ^ tagObfuscator;
    // Array index in objc_tag_classes includes the tagged bit itself
#if SUPPORT_MSB_TAGGED_POINTERS //高位优先
    return &objc_tag_classes[0x8 | obfuscatedTag];
#else
    return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#endif
}

这边来通过文字描述下这个注册的流程

  • 1.判断objc_debug_taggedpointer_mask是否为0,判断是否开启Tagger Pointer,如果是禁用,直接回终止启动;
  • 2.根据tag去取出类指针,若传递的是一个无效的tag,没有取到正确的类指针,还是终止启动;
  • 3.尝试通过Class oldCls = *slot去获取该指针指向的类,若要注册的类与该处获取的类不是同一个,那么也是终止启动;
  • 4.通过*slot = cls将入参cls类赋值给指针slot指向的位置
  • 5.若注册的不是基础类型,则OBJC_TAG_RESERVED_7存储占位类OBJC_CLASS_$___NSUnrecognizedTaggedPointer

上面2个函数的参数都是objc_tag_index_t ,这个是何方神圣呢?

#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,
    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, //60-bit保留位
    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 
    OBJC_TAG_RESERVED_264      = 264 ////52-bit保留位
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

其实就是一个枚举,存储在Tagged Pointer的特殊标记的部位,比如NSNumber是3,NSString是2,NDate是6,等等

那么这个注册动作是在什么时候调用的呢?打个断点看下堆栈信息:

(lldb)  thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100382213 libobjc.A.dylib`::_objc_registerTaggedPointerClass(tag=OBJC_TAG_NSNumber, cls=__NSCFNumber) at objc-runtime-new.mm:6668
    frame #1: 0x00007fff433a0b5b CoreFoundation`__CFNumberGetTypeID_block_invoke + 65
    frame #2: 0x0000000100d087c3 libdispatch.dylib`_dispatch_client_callout + 8
    frame #3: 0x0000000100d0a48b libdispatch.dylib`_dispatch_once_callout + 87
    frame #4: 0x00007fff433a0b17 CoreFoundation`CFNumberGetTypeID + 39
    frame #5: 0x00007fff433a0103 CoreFoundation`__CFInitialize + 715
    frame #6: 0x0000000100020a68 dyld`ImageLoaderMachO::doImageInit(ImageLoader::LinkContext const&) + 316
    frame #7: 0x0000000100020ebb dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 29
    frame #8: 0x000000010001c0da dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 358
    frame #9: 0x000000010001c06d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 249
    frame #10: 0x000000010001c06d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 249
    frame #11: 0x000000010001c06d dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 249
    frame #12: 0x000000010001b254 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 134
    frame #13: 0x000000010001b2e8 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 74
    frame #14: 0x000000010000a756 dyld`dyld::initializeMainExecutable() + 169
    frame #15: 0x000000010000f78f dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6237
    frame #16: 0x00000001000094f6 dyld`dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) + 1154
    frame #17: 0x0000000100009036 dyld`_dyld_start + 54

可以分析看到,是在_dyld_start阶段注册的

那么它是如何判断一个指针是否是伪指针的呢?

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 做运算的。简单的说就是,将一个指针地址和_OBJC_TAG_MASK量做&运算:判断该指针的最高位(iOS)或者最低位(MacOS)为 1,那么这个指针就是 Tagged Pointer。

在iOS系统中,是遵循MSB规则(高位优先),所以_OBJC_TAG_MASK 值就是(1UL<<63) = 0x8000000000000000,它的二进制就是1000000000000000000000000000000000000000000000000000000000000000,看上面的_objc_isTaggedPointer,只要指针的最高位是1,结果肯定是True,简单说就是只要大于等于0x8开头就都是指针。

我们反过来再看下上面之前输出的指针地址:0x9237a2e097f0e96b,0x9 = 1001,最左边是1,所以它就是一个伪指针。

那么当它是一个伪指针时,如何找出它的tag类型呢?

static inline objc_tag_index_t 
_objc_getTaggedPointerTag(const void * _Nullable ptr) 
{
    // assert(_objc_isTaggedPointer(ptr));
    uintptr_t value = _objc_decodeTaggedPointer(ptr); //解码
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    uintptr_t extTag =   (value >> _OBJC_TAG_EXT_INDEX_SHIFT) & _OBJC_TAG_EXT_INDEX_MASK;
    if (basicTag == _OBJC_TAG_INDEX_MASK) {
        return (objc_tag_index_t)(extTag + OBJC_TAG_First52BitPayload);
    } else {
        return (objc_tag_index_t)basicTag;
    }
}
最近的文章

AutoReleasePool实现原理探究

什么是AutoreleasePool 一听到AutoreleasePool,有些同学可能会与MRC联系起来,其实它并不是属于MRC时代的,当你创建一个项目的时候,可以去main.m看下,是用AutoreleasePool包起来了;还有当我们写一个for循环的时候,考虑到内存可以及时释放的时候我们也会主动加上AutoreleasePool,所以ARC也是可以用AutoreleasePool的。Main函数int main(int argc, char * argv[]) { @aut...…

iOS继续阅读
更早的文章

iOS Runtime深度解析

什么是Runtime? Objective-C是基础C语言扩展出来的一门语言,并且是加入了面向对象特性和Smalltalk式的消息发送机制;然后这个扩展的核心,就是Runtime,它是Objective-C面向对象和动态机制的基石。 Runtime源码地址为什么说Objective-C是一门动态语言? 在说为什么之前首先来看下什么是静态语言,什么是动态语言? 静态语言:强类型语言,静态语言是在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据...…

iOS继续阅读