美文网首页
iOS Runtime二: Tagged Pointer, is

iOS Runtime二: Tagged Pointer, is

作者: Trigger_o | 来源:发表于2022-08-25 15:53 被阅读0次

    Tagged Pointer Object

    在objc中会有很多轻量的实例对象,比如NSNumber,NSDate,NSString等的实例,从64位开始,苹果使用了Tagged Pointer策略来优化这些轻量的对象的存储.
    这个对象直接存储在指针里,指针实际上没有指向任何对象,整体只有8字节.

    1.首先看看指向一个编译时常量的情况
    定义几个NSNumber和NSString常量

            NSNumber *num1 = @1;
            NSNumber *num2 = @2;
            NSNumber *num3 = @3;
            NSNumber *num4 = @4;
            NSString *str = @"aaa";
            NSLog(@"Hello, World!");
    

    用lld查看他们的地址,分别查看对象指针的地址,对象isa的地址,以及类的地址

    (lldb) p/x num1
    (NSConstantIntegerNumber *) $0 = 0x0000000100008110 (int)1
    (lldb) p/x num2
    (NSConstantIntegerNumber *) $1 = 0x0000000100008128 (int)2
    (lldb) p/x num3
    (NSConstantIntegerNumber *) $2 = 0x0000000100008140 (int)3
    
    (lldb) p/x num1->isa
    (Class) $4 = 0x00007ff85e83d228 NSConstantIntegerNumber
    (lldb) p/x num2->isa
    (Class) $5 = 0x00007ff85e83d228 NSConstantIntegerNumber
    (lldb) p/x num3->isa
    (Class) $6 = 0x00007ff85e83d228 NSConstantIntegerNumber
    
    (lldb) p/x num1.class
    (Class) $12 = 0x00007ff85e83d228 NSConstantIntegerNumber
    
    (lldb) p/x 0x00007ff85e83d228 & 0x00007ffffffffff8
    (long) $7 = 0x00007ff85e83d228
    
    (lldb) p/x str
    (__NSCFConstantString *) $8 = 0x0000000100004010 @"aaa"
    (lldb) p/x str2
    (__NSCFConstantString *) $9 = 0x0000000100004030 @"bbb"
    
    (lldb) p/x str->isa
    (Class) $10 = 0x00007ff85e8108d8 __NSCFConstantString
    (lldb) p/x str2->isa
    (Class) $11 = 0x00007ff85e8108d8 __NSCFConstantString
    
    (lldb) p/x str.class
    (Class) $14 = 0x00007ff85e8108d8 __NSCFConstantString
    

    1.首先NSNumber对象num1,num2,num3的地址都间隔了24个存储单元,也就是24字节(换成十进制计算).
    2.str和str2也是间隔24字节
    3.num1,num2,num3的isa都是0x00007ff85e83d228,这个值与上掩码还是这个值,并且NSConstantIntegerNumber这个类的地址也是0x00007ff85e83d228,说明这个isa除了shiftcls其他位置都是0,而且shiftcls就是NSConstantIntegerNumber的地址.
    实际上这时候不能说shiftcls了,因为它是一个Class指针形态,因为nonpointer是0,它的值就是class的地址.
    4.同样的str和str2的isa的shiftcls也都是__NSCFConstantString的地址.

    这说明常量的NSNumber和NSString是一个普通的对象,而且他们的isa的形态是Class指针,存在常量区.占用24个字节,

    然后再来看看内存里都存了什么

    (lldb) x/3gx num1
    0x100008110: 0x00007ff85e83d228 0x0000000100003f82
    0x100008120: 0x0000000000000001
    (lldb) x/3gx num2
    0x100008128: 0x00007ff85e83d228 0x0000000100003f82
    0x100008138: 0x0000000000000002
    

    注意现在输出的不是地址了,是内存单元里的内容,一个地址编号对应一个字节,也就是8位,

    GBD调试中的内存读取命令,可以输出存储单元里的二进制,格式是:
    x/<n/f/u><addr> n,f,u是可选的参数。
    第一个x是command.
    n表示输出几段.
    f 表示显示的进制格式.x 16进制,t二进制等.
    u是输出几个字节的内容,b一个字节,h两个字节,w四个字节,g八个字节.
    另外u,f分别对应不同的字母,因此不讲究顺序,x/3gx和x/3xg是一样的.
    addr表示开始的地址.
    x/3gx num1表示输出num1的地址开始的, 3*8个字节的内容,用十六进制输出,分为三段,每段8个字节.

    输出num1之后24个字节之后,第一段是NSConstantIntegerNumber的地址,也就是isa,第二段是对象的其他内容,第三段就是这个NSNumber的数值.

    2.然后看看编译时没有指向常量的情况
    定义一个类

    @interface MyObjc : NSObject
    @property (nonatomic, copy) NSString *text;
    @property (nonatomic, strong) NSNumber *number;
    @end
    
    my.text = [NSString stringWithFormat:@"123456789"];
    my.number = @(my.text.length);
    NSLog(@"%@",my.text);
    

    lldb查看

    (lldb) p/t my.text
    (NSTaggedPointerString *) $0 = 0b0001010011000110111001001011110000100100100101101001011101101111 @"123456789"
    (lldb) p/t my.number
    (__NSCFNumber *) $1 = 0b0000101001100111000100111001011110010111100111000010111111001101 (long)9
    
    (lldb) p my.text->isa
    error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
    (lldb) p my.number->isa
    error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory
    

    报错说无法读取这部分内存,此时是TaggedPointer.

    如何判断一个对象是否是tagged pointer

    static inline bool 
    _objc_isTaggedPointer(const void * _Nullable ptr)
    {
        return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    

    就是拿指针的值和_OBJC_TAG_MASK按位与算,这个方法只有一个地方调用,就是

    inline bool 
    objc_object::isTaggedPointer() 
    {
        return _objc_isTaggedPointer(this);
    }
    define _OBJC_TAG_MASK (1UL<<63)
    

    所以是那自己和_OBJC_TAG_MASK与运算,
    _OBJC_TAG_MASK的定义根据环境不同有的定义
    arm64和MSB的macos是1UL<<63,也就是1跟着63个0,其他情况都是1.
    当前环境是LBS的macos,x86_64,此时_OBJC_TAG_MASK=1,
    注意this是一个指针,所以如果对象的地址最低位是1,就是tagged pointer object.与nonpointer没有直接关系.

    Tagged Pointer的特性

    想知道Tagged Pointer的特性,只要搜索isTaggedPointer()在哪些地方调用了就行.

    inline Class
    objc_object::ISA(bool authenticated)
    {
        ASSERT(!isTaggedPointer());
        return isa.getDecodedClass(authenticated);
    }
    

    比如Tagged Pointer获取isa的时候会报错,也就是上面报错的原因,因为这里有个断言.
    这里其实有三个函数

     // ISA() assumes this is NOT a tagged pointer object
        Class ISA(bool authenticated = false);
    
        // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
        Class rawISA();
    
        // getIsa() allows this to be a tagged pointer object
        Class getIsa();
    

    这是objc_object的三个getIsa,第一个必须是非tagged pointer object才能调用,最终返回的是isa_t的getClass(),返回类的地址.
    第二个必须rawIsa能够调用,rawisa指的是纯指针,既不是tagged pointer也不是isa优化,isa的nonpointer位是0,返回isa_t的值.
    第三个才是tagged pointer能够调用的,它是这样实现的:

    inline Class
    objc_object::getIsa() 
    {
        if (fastpath(!isTaggedPointer())) return ISA(/*authenticated*/true);
    
        extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
        uintptr_t slot, ptr = (uintptr_t)this;
        Class cls;
    
        slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        cls = objc_tag_classes[slot];
        if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
            slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
            cls = objc_tag_ext_classes[slot];
        }
        return cls;
    }
    

    如果是非TaggedPointer,直接走第一个ISA()函数,fastpath是一个编译器优化指令,这里表示大概率不走后面的代码.
    后面就是按照预定的算法得到偏移,从一个数组中取出类返回.

    TaggedPointer不会增加引用计数

    inline id 
    objc_object::retain()
    {
        ASSERT(!isTaggedPointer());
    
        if (fastpath(!ISA()->hasCustomRR())) {
            return sidetable_retain();
        }
    
        return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
    }
    

    有一个常用的例子来观察tagged pointer object和NSObject

    
    @interface MyObjc : NSObject
    //这里用strong来修饰
    @property (nonatomic, strong) NSString *text;
    @end
    
    //字符串是10位
    for (int i = 0; i<10000; i++) {
                dispatch_async(dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT), ^{
                    my.text = [NSString stringWithFormat:@"1234567890"];
                });
     }
    
    /*
    //字符串是9位
    for (int i = 0; i<10000; i++) {
                dispatch_async(dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT), ^{
                    my.text = [NSString stringWithFormat:@"123456789"];
                });
     }
    */
    

    分别使用两端for循环,添加异步并发任务,上面for循环会crash,下面的不会
    strong修饰的set方法的实现大概是这样的

    void
    objc_storeStrong(id *location, id obj)
    {
        id prev = *location;
        if (obj == prev) {
            return;
        }
        objc_retain(obj);
        *location = obj;
        objc_release(prev);
    }
    
    

    并发去访问text,很可能会访问到坏地址.而tagged pointer不会,因为它不是一个对象,就是一个64位二进制.

    tagged pointer的结构

    image.png

    标志位的类型

        // 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,
    

    初始化tagged pointer 的函数是

    void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
    

    这里面有两句

     if (DisableTaggedPointers) {
                disableTaggedPointers();
            }
            initializeTaggedPointerObfuscator();
    

    DisableTaggedPointers是环境变量OBJC_DISABLE_TAGGED_POINTERS,环境变量在edit schema里添加


    image.png

    另外还有其他很多环境变量,定义在objc-env.h里

    initializeTaggedPointerObfuscator()就是初始化TaggedPointer的函数.

    static void
    initializeTaggedPointerObfuscator(void)
    {
        if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)) {
            // Pull random data into the variable, then shift away all non-payload bits.
            arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                           sizeof(objc_debug_taggedpointer_obfuscator));
            objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    
    #if OBJC_SPLIT_TAGGED_POINTERS
            // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
            objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
    
            // Shuffle the first seven entries of the tag permutator.
            int max = 7;
            for (int i = max - 1; i >= 0; i--) {
                int target = arc4random_uniform(i + 1);
                swap(objc_debug_tag60_permutations[i],
                     objc_debug_tag60_permutations[target]);
            }
    #endif
        } else {
            // Set the obfuscator to zero for apps linked against older SDKs,
            // in case they're relying on the tagged pointer representation.
            objc_debug_taggedpointer_obfuscator = 0;
        }
    }
    

    在这里做了混淆,获得 objc_debug_taggedpointer_obfuscator 将随机数据放入变量中,然后 ~_OBJC_TAG_MASK与运算移走所有非有效位.

    isa的走向

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
    

    类Class是objc_class的typedef,继承自objc_object,第一个成员是isa_t,第二个是Class指针,指向superclass,所以两个都是8字节.

    元类就是类对象的类,元类的设计并非objc的独创,也不是首创,相较于追究元类的优越性,更像是采取了一种常用设计.
    实例对象的成员属性方法添加在类对象上,向对象发送消息,对象会通过isa找到类,再去类的cache或者方法列表查找imp,同样的像类对象发送消息,就要去元类找.

    static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
    {
    /* ... */
    
      cls->initClassIsa(meta);
       if (superclass) {
            meta->initClassIsa(superclass->ISA()->ISA());
            cls->setSuperclass(superclass);
            meta->setSuperclass(superclass->ISA());
            addSubclass(superclass, cls);
            addSubclass(superclass->ISA(), meta);
        } else {
            meta->initClassIsa(meta);
            cls->setSuperclass(Nil);
            meta->setSuperclass(cls);
            addRootClass(cls);
            addSubclass(cls, meta);
        }
    

    这段代码是objc-runtime-new.mm中的,在动态创建一个类的时候,会执行这个函数,
    第一行,initClassIsa()内部是setClass,把类的信息放在shiftcls,所以这里是把元类放在类的isa中,因此类对象的isa指向它的元类.

    在看后面,如果传进来了superclass,也就是说这个类有父类,
    则meta用superClass.getClass().getClass()初始化isa,也就是父类的元类的getClass(),姑且叫做A,
    meta.initClassIsa()之后,meta.getClass()也就是这个A了,也就是元类的isa的shiftcls和父类的元类的isa的shiftcls是一样的,都是A,这个A就是根源类.
    然后接下来设置superclass,meta的superclass指向superclass的meta.
    以及设置subclass.

    如果superclass是nil,meta就用自己初始化isa,shiftcls是自己,superclass是Nil,
    并且这里有两个重要的事情,一是meta的父类是类对象本身,二是调用了一个addRootClass(cls).
    除了NSObjct和NSProxy,还可以动态创建rootClass.

    下面来验证一下:

    @interface MyObjc : NSObject
    
    @property (nonatomic, copy) NSString *text;
    @property (nonatomic, strong) NSNumber *number;
    @property (nonatomic, strong) NSObject *obj;
    
    @end
    
     MyObjc *my = [[MyObjc alloc]init];
    

    一个继承自NSObject的类MyObjc.

    运行,用lldb查看.

    (lldb) p/x my.class
    (Class) $0 = 0x0000000100008258 MyObjc
    (lldb) x/4gx $0
    0x100008258: 0x0000000100008230 0x00007ff85e983030
    0x100008268: 0x0000000108d13590 0x0001802c00000007
    

    0是类对象指针的值,就是MyObjc类的地址. 这里读取这个0的内存,读取32个字节,每段8个字节,第一段是类对象的isa_t,第二段是superclass.

    (lldb) p/x 0x0000000100008230
    (long) $1 = 0x0000000100008230
    (lldb) p/x $1 & 0x00007ffffffffff8
    (long) $2 = 0x0000000100008230
    (lldb) po $2
    MyObjc
    

    然后从第一段取出类的地址,输出,是MyObjc,这个MyObjc就是MyObjc的元类.

    (lldb) p/x 0x00007ff85e983030
    (long) $3 = 0x00007ff85e983030
    (lldb) po $3
    NSObject
    

    然后是$0的第二段,这段是Myobjc的superclass,输出是NSObject类对象.

    (lldb) x/4gx $3
    0x7ff85e983030: 0x00007ff85e982fe0 0x0000000000000000
    0x7ff85e983040: 0x0000000108f0a6c0 0x0001801000000003
    (lldb) p/x 0x00007ff85e982fe0 & 0x00007ffffffffff8
    (long) $4 = 0x00007ff85e982fe0
    (lldb) po $4
    NSObject
    

    读取NSObject类对象的内存,从第一段取出类的信息,这个NSObject是NSObject的元类.
    并且第二段是0x0,对应NSObject的父类是Nil.

    (lldb) x/4gx $4
    0x7ff85e982fe0: 0x00007ff85e982fe0 0x00007ff85e983030
    0x7ff85e982ff0: 0x000000010fc040e0 0x0003e03100000007
    (lldb) p/x 0x00007ff85e982fe0 & 0x00007ffffffffff8
    (long) $5 = 0x00007ff85e982fe0
    

    再读取NSObject的元类的内存,从第一段取出类的地址5,和4是一样的,所以NSObject的元类的isa指向自己.
    并且看到$4的第二段,0x00007ff85e983030是NSObject类对象,根元类的父类是根类.

    (lldb) x/4gx $2
    0x100008230: 0x00007ff85e982fe0 0x00007ff85e982fe0
    0x100008240: 0x00007ff81d112770 0x0000e03500000000
    

    读取MyObjc的元类的内存,第一段和$4相同,是根源类.所以任何一个元类的isa都指向根元类,包括根元类自己.

    运行objc4源码

    已经有很多人整理好了如何编译运行,比如这个项目GitHub
    直接使用最新的objc-841,选择编译的target是KCObjcBuild.
    m1可以选择Rosetta,运行起来可能会crash:

    objc[66360]: task_restartable_ranges_register failed (result 0x6: (os/kern) resource shortage)
    

    找到task_restartable_ranges_register这个函数调用的地方,这个暂时不重要,注释了就行


    解决crash

    然后就可以运行,可以试一下在+alloc的地方断点,能进断点就成功了.


    测试断点

    如果添加了新的文件,需要注意在build phases -> compile sources中让main.m保持在第一个的位置,否则不会进断点.


    可以拖动

    相关文章

      网友评论

          本文标题:iOS Runtime二: Tagged Pointer, is

          本文链接:https://www.haomeiwen.com/subject/cstzgrtx.html