阅读 139

iOS底层原理-类的探究分析(下)

我们在iOS底层原理-类的探究分析(上)对类底层从两个方向的进行了分析,得出了类的走位图。这篇我们从类的内部结构进行探索,看看类的结构到底是什么样的?

类的结构

Objective-C对象的本质是结构体,底层都是objc_class,从类的定义来看objc_class的结构(忽略类里面所有的方法),结构代码如下:

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;
    class_data_bits_t bits;
}
复制代码

从代码的结构可以看出,类主要有isasuperclasscachebits组成,isa在上一篇做过分析,superclass是父类,cache是缓存,bits是所有关于类的数据储存的地方,根据这些定义我们画个简单的结构图

类的结构图

001.png

本篇主要分析一下bits里存放的类的数据,要分析bits,还是可以通过读取内存地址的方式去获得bits的数据,通过获取类的首地址,经过平移得到bits的地址;我们知道isasuperclass都是结构体指针,所占内存都是8个字节,但cache所占内存大小是多少并不清楚,先看一下cache_t的结构

cache_t结构

objc4-818.2的源码中objc-runtime-new.h可以找到cache_t的定义,由于代码过多这里就不粘贴了,分析里面的结构可以看到包含大量的方法函数static修饰的变量,我们知道方法存储在方法区static修饰的变量存储在全局区,不占用结构体内存,所以最终可以得到下面cache_t结构:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4字节
#if __LP64__
            uint16_t                   _flags;  // 2字节
#endif
            uint16_t                   _occupied; // 2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
    };
}
复制代码

从上面的结构可以看到cache_t里uintptr_t类型的_bucketsAndMaybeMaskunion联合体,联合体内包含一个structpreopt_cache_t *类型,我们分析一下这个结构所占内存大小,_bucketsAndMaybeMaskuintptr_t修饰占用8字节,_maybeMaskmask_t修饰对应uint32_t,占用4字节,_occupied_flags个占用2字节,_originalPreoptCachepreopt_cache_t *结构体指针,占用8字节,由于union是互斥的,内存占用只取其一,所以cache_t总共占用8 + 8 = 16字节。

bits数据分析

根据class_data_bits_t定义,我们可以获取class_rw_t的data,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};
    }
}
复制代码

下面我们已实际代码lldb调试进行验证。

lldb调试

首先定义LGPerson类,包含属性、方法和协议。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol LGPersonDelegate <NSObject>

- (void)say666;

@end

@interface LGPerson : NSObject<LGPersonDelegate>

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;

- (void)saySomething;

- (void)say666;

@end

NS_ASSUME_NONNULL_END
复制代码

接一下来通过lldb调试查看属性、方法存储是类中的位置,在运行代码处加个断点。

  1. p/x LGPerson.class,获取LGPerson类的首地址0x00000001000087b8
  2. 根据之前的计算,获取bits地址,需要经过首地址平移8 + 8 + 16 = 32字节,也就是加上0x200x00000001000087b8 + 0x20 = 0x00000001000087d8
  3. p (class_data_bits_t *)0x00000001000087d8输出class_data_bits_t
  4. p $1->data()得到class_rw_tp *$2看到class_rw_t结果
  5. p $3.properties()获取类的属性,可以看到property_list_t结构的list
  6. p $4.list,p $5.ptr可以拿到property_list_t *结构的地址$6
  7. p *$6输出property_list_t类型的属性列表,可以count的数量
  8. 再可以一个个的数据输出,p $7.get(0)获取属性列表对应的值,可以看到我们定义的类中的属性都可以拿到。

下面的图例就是实际的输出结果 002.png 同样的方式也可以类中的方法列表协议列表 003.png 需要注意的问题是,获取methods直接通过get()方法返回的是个空,因为methods没有description方法,需要调用.big()才可以正常输出。

协议列表

关于协议列表和属性、方法之前逻辑一致,就是获取协议时需要特殊的调用,这个后续更新(待补充)...

总结

通过上面对类的结构分析,可以看到类的内部的组成,cache缓存类的方法,bits存储类的属性、方法和协议等,通过内存平移我们可以获取bits的数据,进而了解对应的内部结构,lldb调试对上面分析进行验证。

文章分类
iOS
文章标签