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

建议先看下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 _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic _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 
struct explicit_atomic : public std::atomic {
    explicit explicit_atomic(T initial) noexcept : std::atomic(std::move(initial)) {}
    operator T() const = delete;
    
    T load(std::memory_order order) const noexcept {
        return std::atomic::load(order);
    }
    void store(T desired, std::memory_order order) noexcept {
        std::atomic::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 *from_pointer(T *ptr) {
        static_assert(sizeof(explicit_atomic *) == sizeof(T *),
                      "Size of atomic must match size of original");
        explicit_atomic *atomic = (explicit_atomic *)ptr;
        ASSERT(atomic->is_lock_free());
        return atomic;
    }
};

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

  • uintptr_t定义 typedef unsigned long uintptr_t;, long8字节
  • 联合体大小为内部成员最大的大小, 成员有2个一个是结构体struct, 一个是explicit_atomic _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 _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()) {
            return v.get(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get(&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 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 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与类)