iOS开发进阶:Runtime

栏目: IOS · 发布时间: 5年前

内容简介:获取方法列表:获取属性列表:
typedef struct objc_selector *SEL;

IMP :指针,指向方法的具体实现地址

typedef void (*IMP)(void /* id, SEL, ... */ );

id :表示任意的OC类型

/// 代表OC类
typedef struct objc_class *Class;
/// 执行类实例的指针
typedef struct objc_object *id;

Classobjc_class 结构体类型的指针。 idobjc_object 结构体类型的指针。 id 表示对象, Class 表示类。

isa : 指向实例所属的类

struct objc_object {
    isa_t isa;
    //...
};
  1. 实例的 isa 指向 class ,当调用对象方法时,通过 isa 找到对应的类,找到对应的方法进行调用。如果没有通过 superclass 查找父类。
  2. 类的 isa 指向 meta-class ,当调用类方法时,通过 isa 找到对应的元类,找到对应的方法进行调用。如果没有通过 superclass 查找父类。

Method

typedef struct method_t *Method;
struct method_t {
    // 不同类的方法选择器可以是相同的
    // typedef struct objc_selector *SEL;
    // objc_selector 未开源,猜测应该与Char型相关。
    SEL name; // 函数名,类似 C语言 字符串,@selector()和 sel_registerName()获取字符串。
    const char *types; //编码(返回值类型、参数类型)使用Type Encode
    IMP imp;//指向函数的指针(函数地址)
};

获取方法列表:

// cls : 类,outCount: 方法数量
Method * class_copyMethodList(Class cls, unsigned int *outCount)

Ivar : 实例变量

typedef struct ivar_t *Ivar;
struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;
};

Category :分类

typedef struct category_t *Category;

// 分类结构体
struct category_t {
    const char *name; // 名称
    classref_t cls;
    struct method_list_t *instanceMethods; // 实例方法列表
    struct method_list_t *classMethods; // 类方法列表
    struct protocol_list_t *protocols; // 协议列表
    struct property_list_t *instanceProperties; // 属性列表
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; 
};

objc_property_t : 实例属性

typedef struct property_t *objc_property_t;

struct property_t {
    const char *name;
    const char *attributes;
};

获取属性列表:

Ivar * class_copyIvarList(Class cls, unsigned int *outCount)

Cache : 缓存

// 缓存曾经调用过的方法,提高查找速率
struct cache_t {
    struct bucket_t *_buckets; // 散列表, SLE :IMP
    mask_t _mask; //散列表的长度 - 1
    mask_t _occupied; // 已经缓存的方法数量,散列表的长度使大于已经缓存的数量的。
};
struct bucket_t {
    cache_key_t _key; //SEL作为Key
    IMP _imp; // 函数的内存地址
};
typedef uintptr_t cache_key_t; // ---> unsigned long 类型
typedef uint32_t mask_t;

objc_object : 对象

struct objc_object {
    isa_t isa;
};

objc_class : 类

struct objc_class : objc_object {
    // 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() { 
        return bits.data(); // &FAST_DATA_MASK 获取地址值
    }
    // ...
};

继承自 objc_object , 所以 objc_class 也是对象,有成员变量 isa

元类

元类是类对象的类。 objc_class 继承自 objc_object 它也包含 isa 指针,类自身也是对象,称为类对象。类对象对应的类就称为元类。实例对象的 isa 指针指向所对应的类,类的 isa 指针指向元类。

method_t

// method_t是对方法/函数的封装
struct method_t {
    // 不同类的方法选择器可以是相同的
    // typedef struct objc_selector *SEL;
    // objc_selector 未开源,猜测应该与Char型相关。
    SEL name; // 函数名,类似C语言字符串,@selector()和 sel_registerName()获取。
    const char *types; //编码(返回值类型、参数类型)使用Type Encode
    IMP imp;//指向函数的指针(函数地址)
    //...
};
  1. SEL :函数名,通过 @selector()sel_registerName() 获取。通过 sel_getName()NSStringFromSelector() 转成字符串。
  2. types : 表示返回值类型和参数类型,使用 Type Encodings - NSHipster ,另外,iOS提供了一个叫 @encode 的指令,可以将具体的类型表示成字符串编码。

category_t

struct category_t {
    const char *name; // 名称
    classref_t cls; // 所属类
    struct method_list_t *instanceMethods; // 实例方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; 

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

分类的底层结构 category_t 结构体,里面存储着分类的对象方法、类方法、属性、协议信息等。

在程序运行时,运行时系统会将分类的数据,合并到类信息中(类对象或者元类对象中)。

category 不同于 extension ,前者运行时合并信息,后者编译时就已经合并。

分类中有 load 方法吗? load 方法的调用顺序?

分类中存在 load 方法,在运行时加载类、分类的时候调用。

另外, load 方法可以继承,通常情况下系统会自动调用,无需手动。

先调用类的,按照编译顺序调用。调用子类的之前先调用父类的。在调用分类的 load 方法,按照编译顺序(先编译先调用)。

提到 load 方法,还有一个叫 initialize 方法。

initialize 方法会在类第一接收到消息时调用。先调用父类,在调用子类。

initialize 是通过 objc_msgSend() 方法实现的。

不一样的 isa

网上很多关于 isa 的资料,发现大部分都是旧版的 isa 结构。所以学习新的运行时源码整理下面的笔记。

旧版 runtimeisa ,在苹果提供的运行时源码 runtime.h 文件中, isa 定义如下:

typedef struct objc_class *Class;
Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

isaobjc_class 结构体类型的指针。

源码文件 objc_runtime_new.h/mm 。 新版 isa 此时不再是 objc_class 结构体类型,修改成了 isa_t 类型。 这段代码是运行时系统定义的对象结构体。

struct objc_object {
    isa_t isa;    
    // ...
};

另外,上面也提及到类的结构体 objc_class 继承自 objc_object 结构体,那么 objc_class 也包含 isa_t 类型的变量。

struct objc_class : objc_object {
    // 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() { 
        return bits.data(); // &FAST_DATA_MASK 获取地址值
    }
    // ...
};

接下来我们看看 isa_t 究竟是什么?不过在此之前先复习一下共用体。

共用体

首先回顾一下 共用体 特点,通过对比结构体来理解共用体。

  1. 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙)。
  2. 共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

上面提到共用体的特性: 共用体占用的内存等于最大的成员占用的内存 。这就说明共用体中的成员是共用一段内存,修改其中的值势必会修改其他的值。

下面通过一段代码理解共用体。

union data {
    int n; // 4
    char ch; // 1
    short f; // 2
};

int main(int argc, const char * argv[]) {
    union data a;
    printf("%lu, %lu\n", sizeof(a), sizeof(union data)); // 大小为 4
    
    a.n = 0x40;
    // %X 16进制输出; %hX 16进制输出short int(2字节)
    printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 40, @, 40
    
    a.ch = '9';
    printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 39, 9, 39
    
    a.f = 0x2059;
    printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 2059, Y, 2059
    
    a.n = 0x3E25AD54;
    printf("%X, %c, %hX\n", a.n, a.ch, a.f); // 3E25AD54, T, AD54
    return 0;
}

输出结果:

4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

共用体的内存结构。

iOS开发进阶:Runtime

在每次存入值时,共用体中成员的值都会发生改变。

isa_t 共用体

首先,运行时源代码 isa_t 的定义如下:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits; 
# if __arm64__ // arm64位系统
#   define ISA_MASK        0x0000000ffffffff8ULL //用来取出33位内存地址使用(&)操作
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; //0:代表普通指针,1:表示优化过的,可以存储更多信息。
        uintptr_t has_assoc         : 1; //是否设置过关联对象。如果没设置过,释放会更快
        uintptr_t has_cxx_dtor      : 1; //是否有C++的析构函数
        uintptr_t shiftcls          : 33; // 存储着Class、Meta-Class对象的内存地址信息
        uintptr_t magic             : 6; //用于在调试时分辨对象是否未完成初始化
        uintptr_t weakly_referenced : 1; //是否有被弱引用指向过
        uintptr_t deallocating      : 1; //是否正在释放
        uintptr_t has_sidetable_rc  : 1; //引用计数器是否过大无法存储在ISA中。如果为1,那么引用计数会存储在一个叫做SideTable的类的属性中
        uintptr_t extra_rc          : 19; //里面存储的值是引用计数器减1

#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
    // ....
};

参考上面的注释。

获取ISA

新版的 isa 获取真实的内存地址需要进行一次位运算。

# if __arm64__ // arm64位系统
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__ // arm32为系统
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

获取真实内存地址 (Class)(isa.bits & ISA_MASK);

验证上面结论:

NSObject *objc = [[NSObject alloc] init];
    
Class objcClass = [NSObject class];

设置断点,通过 LLDB 命令:

(lldb) p/x objc->isa
(Class) $1 = 0x001dffff8da8a141 NSObject
(lldb) p/x objcClass
(Class) $2 = 0x00007fff8da8a140 NSObject

objc实例对象的isa指向的地址: 0x001dffff8da8a141 ;objcClass的指向的地址值: 0x00007fff8da8a140 。 通过一次位运算 0x001dffff8da8a141 & ISA_MASK 等到 0x00007fff8da8a140

窥探 objc_class 结构

上面基础术语中给出了 objc_class 结构体的定义:

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct objc_class : objc_object {
    // Class ISA;
    Class superclass; //父类指针
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 用于获取地址

    class_rw_t *data() { 
        return bits.data(); // &FAST_DATA_MASK 获取地址值
    }
    // ...
};
  1. bits 获取地址,通过 &FAST_DATA_MASK 运算获取。
  2. superclass : 指向父类的指针。
  3. cache : 方法缓存,后面详细讲解。
  4. isa : 指向元类。
  5. data : 指向 class_rw_t 结构体类型。

class_rw_t 结构体:

struct class_rw_t {

    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro; // 指向只读的结构体,存放类初始信息

    /*
     这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
     methods中,存储 method_list_t ----> method_t
     二维数组,method_list_t --> method_t
     这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
     */
    method_array_t methods; // 方法列表(类对象存放对象方法,元类对象存放类方法)
    property_array_t properties; // 属性列表
    protocol_array_t protocols; //协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};

methodspropertiesprotocols 都是二维数组,包含类的初始内容、分类的内容和协议等。

class_ro_t 结构体:

struct class_ro_t {
    uint32_t flags; // 判断是否为元类
    uint32_t instanceStart;
    uint32_t instanceSize; // 实例对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name; //类名
    /*
        一维数组,只读的,包含的是类的初始信息
     */
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

进阶部分

KVO

KVO实现原理

iOS 是如何实现对一个对象的 KVO

通过运行时系统动态生成一个子类,并且让实例对象的 isa 指向这个全新的子类。当实例对象的属性被修改时,通过调用 FoundationNSSetXXXValueAndNotify 函数。实现如下:

willChangeValueForKey:
didChangeValueForKey:

内部会调用监听器的监听方法: observeValueForKeyPath:ofObject:change:context:

如果想要手动触发 KVO 就需要手动调用, willChangeValueForKey:didChangeValueForKey: 方法。 直接修改成员变量不会触发 KVO ,但是通过属性可以触发 KVO

关联对象

有时需要给分类添加成员变量,通常使用 关联对象 间接实现。

objc_setAssociatedObject
objc_getAssociatedObject
objc_removeAssociatedObjects

推荐用法: 使用get方法的@selector作为key。

objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))

实现原理

iOS开发进阶:Runtime

参考文章: OS底层原理总结 - 关联对象实现原理

objc_msgSend、动态方法解析、消息转发

OC 中调用方法,其实都是转成 objc_msgSend 方法。它的执行流程分为三步:

  1. 消息发送
  2. 动态方法解析
  3. 消息转发

以上所述就是小编给大家介绍的《iOS开发进阶:Runtime》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序员面试笔试宝典

程序员面试笔试宝典

何昊、叶向阳、窦浩 / 2012-10 / 59.80元

《程序员面试笔试宝典》除了对传统的计算机相关知识(C/C++、数据结构与算法、操作系统、计算机网络与通信、软件工程、数据库、智力题、英语面试等)进行介绍外,还根据当前计算机技术的发展潮流,对面试笔试中常见的海量数据处理进行了详细的分析。同时,为了更具说服力,《程序员面试笔试宝典》特邀多位IT名企面试官现身说法,对面试过程中求职者存在的问题进行了深度剖析,同时《程序员面试笔试宝典》引入了一批来自于名......一起来看看 《程序员面试笔试宝典》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具