美文网首页
Objc4-818底层探索(四):isa与类

Objc4-818底层探索(四):isa与类

作者: ShawnAlex | 来源:发表于2021-06-18 18:39 被阅读0次

    建议先看下Objc4-818底层探索(三):isa

    先补充一些之前的知识点:

    知识点1:关于掩码

    isa 掩码以x86_64环境下为例

    #   define ISA_MASK        0x00007ffffffffff8ULL (ULL: unsigned long long无符号长整型 C++语法)
    
    掩码转二进制

    0x00007ffffffffff8 转二进制为
    0001 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
    其实就是
    000...000...中间44个1..000
    其他位抹零, 只保留中间44位, 即取到shiftcls类信息

    知识点2:关于 __has_feature(ptrauth_calls)

    有些时候__has_feature(ptrauth_calls)TARGET_OS_SIMULATOR一起使用, 需要先普及ARM64e概念

    // ARM64 simulators have a larger address space, so use the ARM64e
    // scheme even when simulators build for ARM64-not-e.

    //ARM64模拟器有更大的地址空间,所以使用ARM64e
    //即使在为ARM64-not-e构建模拟器时也是如此。

    ARM64earm64e架构,用于Apple A12及更高版本A系列处理器 或 新型OS_SIMULATOR 模拟器的设备。详细可参考官方提供的文档, 如下

    ARM64e官方文档
    • __has_feature(ptrauth_calls): 是判断编译器是否支持指针身份验证功能
    • ptrauth_calls 指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构

    先熟悉下几个lldb命令, 方便之后探索

    • x/4gx: 读内存段, 以16进制读取4个内存片段
    • p/x: 以16进制形式打印, 可用作打印地址
    • po: 读出/输出值, 也可以用作打印对象

    我们还是先看个例子


    例子

    首先

    • x/4gx test: 以16进制4个片段读取test内存信息, 其中0x1007040c0为首地址, 0x011d8001000081adisa

    • 0x0000000100008180 & 0x00007ffffffffff8ULL获取类信息, 返回(unsigned long long) $1 = 0x00000001000081a8, 我们 po读取一下, po 0x00000001000081a8返回SATest

    • 接下来, 我们读取下类的片段x/4gx 0x00000001000081a8, 竟然发现首地址0x1000081a8和isa0x0000000100008180都发生了改变

    • 再做一次& 掩码得到类信息0x011d8001000081ad & 0x00007ffffffffff8获取类信息, 返回(unsigned long long) $3 = 0x0000000100008180, 我们 po读取一下, po 0x0000000100008180竟然还是返回SATest, 难道说明一个类在内存当中会有多个地址 ???

    • 我们再重复之前操作, 发现第三次po得到的是NSObject(再往后操作结果一样都是0x000000010036a140, po都是NSObject), 这究竟是什么原因 ???

    操作例子

    为了探索类是否在底层存多份, 或者系统会创建多个地址我们做下这个操作

            SATest *test = [SATest alloc];
           
            Class cls1 = [SATest class];
            Class cls2 = [SATest alloc].class;
            Class cls3 = object_getClass([SATest alloc]);
            
            NSLog(@"cls1: %p", cls1);
            NSLog(@"cls2: %p", cls2);
            NSLog(@"cls3: %p", cls3);
    
    探索类例子

    使用系统方法获取类, 对比下, 系统方法得到的类信息为0x1000081b8为类信息

    • 第一次得到的类信息0x00000001000081b8 VS 0x1000081b8, 相同没问题
    • 第二次得到的类信息0x0000000100008190 VS 0x1000081b8, 不同有问题

    很显然第二次的不是类, 那么它是什么? 并且第三次为NSObject又是为什么?

    MachOView

    这里我们要用反编译看一下究竟发生了什么

    MachOView加入反编译程序

    Symbol TableSymbols中可看到, 实际在底层多了个_OBJC_METACLASS(meta class : 元类)

    元类

    这里要涉及一个新的知识点元类


    元类

    元类苹果系统定义的, 其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类

    • 对象的isa, 但类其实也是一个对象,可以称为类对象,而这个对象苹果系统就定义为元类

    • 元类本身是没有名称的,由于与类相关联,所以苹果系统给与了同类名一样的名称

    • 对象类的 isa指向元类, 元类的isa指向根元类NSObject

    isa走位图

    验证NSObject

    之前自定义类的可看到满足isa走位图, 这里再验证下NSObjectisa走向, 看下是否满足

    NSObject

    可看到NSObjectisa走势为: NSObject根元类根元类自身

    NSObject isa 走位验证

    验证继承关系

    isa走位图没问题满足, 这里再验证下继承关系/父类链是否满足

    继承关系例子
    自定义类的父类链继承
    • SATest继承于NSObject, 类→根类 (因为没有父类, 直接指向根类)
    • NSObjectnull, NSObject没有父类, 指向nil
    元类的父类链继承

    0x1000080e0元类
    0x7fff80815fe0根元类
    0x7fff80816008根类(根类是NSObject)

    • 元类 继承于 根元类
    • 根元类继承于 根类
    • 根类NSObject继承于 nil

    当然我们也可以加个子类打印下继承链关系, 如图


    加子类继承关系例子

    objct_class

    因为所有的类都是继承于objct_class, 那么我们接下来看下objct_class底层的实现

    全局搜索objct_class, 在objc-runtime-new.h可以找到struct objc_class : objc_object

    objct_class

    首先objct_class结构体类型, 继承于objc_object, 同时类结构里面默认一个Class ISA同时包含Class superclass, cache_t cache, class_data_bits_t bits;等等。

    objct_class

    类结构分析

    首先还是先看几个例子

    普通指针
    类结构分析例子1

    定义2个变量a, b = 10, 打印两个变量值以及内存地址

    类结构分析例子1
    普通指针
    普通指针例子

    定义2个变量a, b = 10, 打印两个变量值以及内存地址

    普通指针例子分析
    • a 和 b 为变量都指向10, 10是系统开辟的固定内存空间, 其他需要10的值的变量都可以指向内存固定生成的10

    • a 和 b 地址不一样, 这是一种拷贝, 属于值拷贝, 也成深拷贝, 可发现a, b地址相差 4 个字节,这取决于a、b的类型

    对象指针
    对象指针例子
    • &p1/&p2 是二级指针, 指向对象的指针地址(0x7ffeefbff478, 0x7ffeefbff480 为对象指针)

    • p1/p2 是一级指针, 指向的 [SATest alloc] 开辟空间的内存地址

    • SATest为 [SATest alloc]创建内存空间, [SATest alloc]开辟空间的 isa指向SATest

    对象指针例子分析
    数组指针
    image.png
    • &arr == &arr[0] == 首地址, 其实他都是取的首地址, 数组地址其实就是数组第一个元素地址即数组名为首地址

    • &arr[0]与%arr[1]相差4字节, 取决于数据类型

    • 数组类型指针可以通过首地址+偏移量得到其他元素(偏移量为数组下标)

    • 移动的字节数 等于 偏移量 * 数据类型字节数, 这个根据&arr[0], &arr[1]看出, 两者相差4


    bits探索

    有了上面的概念, 便于我们理解之后的探索objc_class中的类信息

    objc-runtime-new.h
    
    struct objc_class : objc_object {
      objc_class(const objc_class&) = delete;
      objc_class(objc_class&&) = delete;
      void operator=(const objc_class&) = delete;
      void operator=(objc_class&&) = delete;
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    ...
        class_rw_t *data() const {
            return bits.data();
        }
    
    ...
    }
    

    因为我们之前在Clang看到bits里面存放着类信息, 所以我们先探索下bits。因为我们之前知道, 已知首地址, 可以通过平移方法, 得到我们

    • 64位结构体指针类型8字节, 即isa8字节
    • superclass同理也是结构体指针类型8字节, 即superclass8字节

    接下来我们看下cache, 大小

    方法一:

    因为cachecache_t类型, 最简单的方法lldb命令 po读一下cache_t

    cache

    可看到cache_t16字节

    方法二:

    进入cache_t 底层

    struct cache_t {
    private:
        explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
        union {
            struct {
                explicit_atomic<mask_t>    _maybeMask;
    #if __LP64__
                uint16_t                   _flags;
    #endif
                uint16_t                   _occupied;
            };
            explicit_atomic<preopt_cache_t *> _originalPreoptCache;
        };
    
    ...
    后面一些函数方法, 而方法不占用内存可以不看
    还有一些static属性的内容, 在全局区也可以不看
    }
    
    

    先看下这个源码explicit_atomic

    // Version of std::atomic that does not allow implicit conversions
    // to/from the wrapped type, and requires an explicit memory order
    // be passed to load() and store().
    template <typename T>
    struct explicit_atomic : public std::atomic<T> {
        explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {}
        operator T() const = delete;
        
        T load(std::memory_order order) const noexcept {
            return std::atomic<T>::load(order);
        }
        void store(T desired, std::memory_order order) noexcept {
            std::atomic<T>::store(desired, order);
        }
        
        // Convert a normal pointer to an atomic pointer. This is a
        // somewhat dodgy thing to do, but if the atomic type is lock
        // free and the same size as the non-atomic type, we know the
        // representations are the same, and the compiler generates good
        // code.
        static explicit_atomic<T> *from_pointer(T *ptr) {
            static_assert(sizeof(explicit_atomic<T> *) == sizeof(T *),
                          "Size of atomic must match size of original");
            explicit_atomic<T> *atomic = (explicit_atomic<T> *)ptr;
            ASSERT(atomic->is_lock_free());
            return atomic;
        }
    };
    

    可看到explicit_atomic的大小取决于传入的T的大小

    • uintptr_t定义 typedef unsigned long uintptr_t;, long8字节
    • 联合体大小为内部成员最大的大小, 成员有2个一个是结构体struct, 一个是explicit_atomic<preopt_cache_t *> _originalPreoptCache;

    我们先看下结构体

    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    typedef unsigned int uint32_t;
    
    • uint32_tint类型占4字节
    • typedef unsigned short uint16_t; short类型占2字节
      所以结构体大小为4 + 2 + 2 = 8字节

    而后面的explicit_atomic<preopt_cache_t *> _originalPreoptCache;指针类型占8字节

    cache8 + 8 = 16字节

    已知首地址以及 ISA8字节, superclass8字节, cache16字节, 固bits前面总共8+8+16 = 32字节, 可通过首地址平移32字节获取bits信息。

    class

    我们检查下是否可以真正读出来, 检测钱先看下class_data_bits_t方便我们下面探索

    读取bits
    • x/4gx test其中首地址 0x1018c0170
    • 0x1018c0170平移32字节为0x1018c01a0
    • 因为bitsclass_data_bits_t类型, 我们要取地址所以class_data_bits_t *类型转一下, 变成指针地址
    • p $1->data()这里要再看下struct objc_class : objc_object源码(->是因为当前的是指针, 结构体的话用·)
    struct objc_class : objc_object {
    ...
    class_rw_t *data() const {
            return bits.data();
        }
    }
    

    可看到bit里面有data()函数方法(获取数据方法, class_rw_t有多数据我们之后再讨论)。p $1->data()读取下bits里面数据, 看见返回(class_rw_t *) $2 = 0x00007fff3e24b6e0

    • p *$2取一下$2里面的内容, 可看到返回一些类信息(class_rw_t)
      `
    class_rw_t

    我们接下来看下class_rw_t `, 源码比较多, 我们挑重点的看

    struct class_rw_t {
    ...
        const method_array_t methods() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
            } else {
                return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
            }
        }
    
        const property_array_t properties() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
            } else {
                return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
            }
        }
    
        const protocol_array_t protocols() const {
            auto v = get_ro_or_rwe();
            if (v.is<class_rw_ext_t *>()) {
                return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
            } else {
                return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
            }
        }
    };
    

    可看到class_rw_t(结构体)里面提供一些属性properties, 方法列表methods, 协议列表protocols的方法。

    那么我们再SATest.h中定义一些成员变量, 属性, 方法打印看一下

    @interface SATest : NSObject{
        NSString *SAHobby;
    }
    
    
    @property (nonatomic, strong) NSString *SAName;
    @property (nonatomic, assign) int SAAge;
    
    - (void)sayHello;
    - (void)sayNB;
    +(void)sayGunDan;
    
    @end
    

    属性列表打印

    先看属性列表打印情况


    属性列表

    bits 数据信息在之前的例子我上面已经讲过了, 我们从读bits数据信息$11之后开始

    • p $12.properties()获得的属性列表的list结构, 其中list中的ptr就是属性数组的参数指针地址。(p $12.properties()命令中的propertoes方法是由class_rw_t提供的, 方法中返回的实际类型为property_array_t)

    -p *$13.list.ptr读一下指针地址指向内容, 可看到获得属性list信息, count = 2, 也符合我们建的2个属性

    • p $14.get(0)可获取到SAName对应属性(property_t) $15 = (name = "SAName", attributes = "T@"NSString",&,N,V_SAName")

    • p $14.get(1)可获取到SAAge属性(property_t) $16 = (name = "SAAge", attributes = "Ti,N,V_SAAge")

    • p $14.get(3)数组越界, 因为我们只建立了2个属性

    方法列表打印

    先了解个知识点

    struct class_rw_ext_t {
        DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
        class_ro_t_authed_ptr<const class_ro_t> ro;
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
        char *demangledName;
        uint32_t version;
    };
    
    struct property_t {
        const char *name;
        const char *attributes;
    };
    
    struct method_t {
        static const uint32_t smallMethodListFlag = 0x80000000;
    
        method_t(const method_t &other) = delete;
    
        // The representation of a "big" method. This is the traditional
        // representation of three pointers storing the selector, types
        // and implementation.
        struct big {
            SEL name;
            const char *types;
            MethodListIMP imp;
        };
    ...
    }
    

    想必与属性列表, 方法列表的相关内容name, types, imp储存在struct big里面(818新改动), 所以获取方法列表里面的信息也要稍微变一下

    方法列表 方法列表
    • p $3.methods()获得的方法列表的list结构, 接下来仿照属性类型, 依次读取指针地址, 读取列表对应项

    • 方法列表留意下得到list之后, 不能直接读取, 新版818的 name, types, imp存在big中固p $5.get(0).big()这样读取

    • .cxx_destruct由于底层是C++, 系统默认添加的方法

    • 有自定义的方法sayNB, sayHello, 同时也有属性自动生成set, get方法("setSAName:", "SAName")

    • 方法列表超过范围也会报错

    当然你要读协议列表的list结构, 那里就p $3.protocols()即可


    探索成员变量以及类方法存放位置

    探索成员变量

    打印过程中我们会发现, 成员变量以及类方法并没有在属性列表, 方法列表里面, 那它究竟在哪里存放的呢? 回过头我们再看struct class_rw_t方法

    struct class_rw_t

    其实我们发现, 在方法列表上面还有一个ro方法, const class_ro_t *ro(), 看下ro底层

    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        union {
            const uint8_t * ivarLayout;
            Class nonMetaclass;
        };
    
        explicit_atomic<const char *> name;
        // With ptrauth, this is signed if it points to a small list, but
        // may be unsigned if it points to a big list.
        void *baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
      ...
    };
    

    可看到const ivar_list_t * ivars;, 有一个ivars属性(ivars: 实例变量), 我们仿照下上面也读一下ro

    读取ro
    • p $3.ro()获得的ro的里面的信息
    • p $14.ivars获得的ivars_list_t即成员变量的列表里面

    接下来我们仿照属性列表去读取, 发现实例变量储存在ivars_list_t里面, 同时也会发现还有属性的成员变量。这一点之前在Clang时候我们看过, 属性在底层是以成员变量+set/get方法 形式存放的。

    • 通过XXXX {}定义的成员变量,会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性的成员变量

    • 通过@property定义的属性,存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含property属性

    探索类方法

    所谓的对象/实例方法, 类方法其实是OC上层或者说苹果官方人为加入的概念, 其底层是都是函数, 不区分+, -。但实例方法与类方法还是有必要区分的, 则苹果将实例方法存在里面, 而类方法存在元类里面。一方面避免对象存储太大会发生混乱, 一方面也是为了有个调用区分。

    所以类方法要在元类中查找。

    类方法查找
    • x/4gx SATest.class以4片段16进账形式打印SATest类的内存段, 这里留意, 我们要取的是元类中的类信息, 所以要用类去打印。得到0x0000000100008238isa

    • 0x0000000100008238 & 0x00007ffffffffff8ULL即: isa & 掩码 得到类信息0x0000000100008238

    • p (class_data_bits_t *)0x0000000100008258, 字节平移32位, 得到bits, 并转换class_data_bits_t *, 这里要留意下千万别忘平移, 不然获取的是系统给定isa类方法, 几十W条。

    • p $2->data()读取bits里面的data

    • p $4.methods()读取方法列表

    • p *$5.list.ptr获取方法列表里面的信息

    • p $6.get(0).big()获取方法列表里面第一条数据, 可看到有

    (lldb) p $6.get(0).big()
    (method_t::big) $7 = {
      name = "sayGunDan"
      types = 0x0000000100003f6e "v16@0:8"
      imp = 0x0000000100003d70 (SAObjcBuild`+[SATest sayGunDan])
    }
    

    综上也可看出

    • 实例方法: 存在对应bits

    • 类方法: 存在对应元类bits

    相关文章

      网友评论

          本文标题:Objc4-818底层探索(四):isa与类

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