Runtime底层学习

Objective-C作为一门高级编程语言,想要成为可执行文件需要先编译成汇编语言,在汇编成机器语言,机器语言也是计算机能识别的唯一语言。但是Objective-C并不能直接编译成汇编语言,需要先转写为C语言在进行编译和汇编的操作。从Objective-C到C语言的过渡就是由Runtime来实现的。
Objective-C的语言特性是动态性比较强,这种动态性就是由Runtime API来支撑的。想要了解Runtime的原理,首先需要知道OC语言的isaCache缓存class_rw_t

isa详解

要想学习Runtime,首先要了解它底层的一些常用的数据结构,比如isa指针。
在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。
从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。共用体如下

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };

我们来看一段代码

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,  assign, getter=isTall) BOOL tall;
@property (nonatomic,  assign, getter=isRich) BOOL rich;
@property (nonatomic,  assign, getter=isHandsome) BOOL handsome;
@end

#import "Person.h"

@implementation Person

@end

#import <Foundation/Foundation.h>
#import "Person.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.rich = NO;
        person.tall = YES;
        person.handsome = NO;
        NSLog(@"%zd",class_getInstanceSize([Person class]));
    }
    return 0;
}

在main.m函数中,系统给Person对象分配了16个字节的内存,其中Person的isa指针占用8个字节,三个属性占用3个字节。
既然Person的属性是BOOL类型,可以考虑使用共用体。

共用体:共用体内的成员变量共用一块内存。

Person类的.m文件内,可以修改成如下:

#import "Person.h"

@interface Person()
{
    union{
        char bits;
        struct{
            char tall : 1;
            char rich : 1;
            char handsome  : 1;
        };
    }_tallRichHandsome;
}
@end

@implementation Person

- (void)setTall:(BOOL)tall{
    _tallRichHandsome.bits = tall;
}
- (BOOL)isTall{
    return !!(_tallRichHandsome.bits );
}

- (void)setRich:(BOOL)rich{
    _tallRichHandsome.bits = rich;
}
- (BOOL)isRich{
    return !!(_tallRichHandsome.bits);
}

- (void)setHandsome:(BOOL)handsome{
    _tallRichHandsome.bits = handsome;
}
- (BOOL)isHandsome{
    return !!( _tallRichHandsome.bits);
}
@end

在结构体union中

       struct{
            char tall : 1;
            char rich : 1;
            char handsome  : 1;
        };

只是为了方便阅读代码,struct内的成员变量,都存储在unionchar中。所以回到开头的union isa_t中,struct是为了阅读方便,共用体union中的信息都存储在uintptr_t bits;中。

Class的结构

objc_class的结构
  • class_rw_t里面的methodspropertiesprotocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容。
    class_rw_t方法结构图

    method_array_t是一个二维数组,每一个元素是一个分类或者类的方法数组method_list_t。在method_list_t数组中包含的才是分类或者类的方法信息。
  • class_ro_t里面的baseMethodListbaseProtocolsivarsbaseProperties是一维数组,是只读的,包含了类的初始内容。
    class_ro_t结构图
  • method_t是对方法\函数的封装
    method_t
  • IMP代表函数的具体实现
    IMP
  • SEL代表方法\函数名,一般叫做选择器。底层结构跟char *类似
    可以通过@selector()sel_registerName()获得。

方法缓存

Class内部结构中有个方法缓存(cache_t),用散列表来缓存曾经调用的方法,可以提高方法的查找速度。

cache_t

buckets是散列表,是数组。
bucket_t

  • 散列表的存储方式
    散列表的存储方式

    在上图中,当Person类中有方法- (void)personTest;
  • 首先,通过@selector(personTest)&_Mask,得出一个值,比如得到2,则将这个方法personTest存储到下表为2的数组中。一般存储的位置从高到低。如果当前被存储内容,这得到数值减1,往下存储。
  • 当调用这个方法时,通过@selector(personTest)&_Mask,得到同样的值,然后直接找到这个方法的地址值,直接调用。如果取值时,当前的cache_key_t不是所需要的,这得到数值减1,往下取值。
  • 缺点:用空间换时间。

objc_msgSend

先来看段代码

#import <Foundation/Foundation.h>

@interface Person : NSObject
- (void)personTest;
@end

#import "Person.h"

@implementation Person
- (void)personTest{

}
@end

#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
       [person personTest];
    }
    return 0;
}

当调用- (void)personTest;方法时,是在底层转化为C语言调用

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("personTest"));
    }
    return 0;
}
  • OC中的方法调用,其实都是转化为objc_msgSend函数的调用,给receiver(方法接收者)发送了一条消息(selector方法名);
  • objc_msgSend的执行流程可以分为3大阶段:

✔️ 消息发送

  • 1、判断消息接收者receiver是否为nil。如果是nil,这直接退出;
  • 2、如果receiver不为nil,receiver通过isa指针找到receiverClass(接收者类对象)。从receiverClasscache中查找方法。如果找到方法,调用方法,结束查找;
  • 3、如果没有找到方法,则从reveiverClassclass_rw_t中查找方法。如果找到了方法,调用方法,结束查找,并将方法缓存到receiverClasscache中;
  • 4、如果没找到方法,从superClass的cache中查找方法,如果找到方法,调用方法,结束查找,并将方法缓存到receiverClasscache中;
  • 5、如果没有找到方法,从superClass的class_rw_t中查找方法。如果找到方法,调用方法,结束查找,并将方法缓存到receiverClasscache中;
  • 6、如果没找到方法,则判断是否还有superclass。如果有,则执行第4步。如果没找到方法,则动态方法解析。

✔️动态方法解析

  • 是否曾经存在过动态方法解析,如果是,就进入消息转发阶段。
  • 如果没有动态方法解析过,则调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法(实例方法调用)或者+(BOOL)resolveClassMethod:(SEL)sel(类方法调用)判断是否有动态绑定方法,并标记为已经动态方法解析,然后进入消息发送阶段。

✔️消息转发

  • 通过动态方法解析不成功,则调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,把这条消息转给其他接收者来处理,如果有其他接收者来处理,返回不为nil,则调用objc_msgSend(返回值,SEL)
  • 如果返回值为nil,则调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法,生成方法签名,然后系统用这个方法签名生成NSInvocation对象,如果这个方法返回的nil,则崩溃报错unrecognized selector sent to instance
  • 如果- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法返回值不为nil,则调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法。

注意:
1、如果调用的是类方法,上述流程中,方法换成类方法(+开头)。
2、dynamic告诉编译器不要自动生成setter方法和getter方法的实现,等到运行时在添加方法的实现。
3、@synthesize age = _age;是为age属性生成_age,并且自动生成setter方法和getter方法,并赋值。

有关super

[super message]的底层实现

objc_msgSendSuper(self,[Person class],@selector(message));

[super class]返回的是当前类

- (class)class{
     return object_getClass(self);
}

[super superClass]返回的是父类

- (Class)superclass{
     return class_getSuperclass(object_getClass(self));
}
  • 1、消息接收者仍然是子类对象;
  • 2、从父类开始查找方法的实现;

Runtime的应用API

类相关
  • 1、动态创建一个类(参数:父类,类名,额外的内存空间)创建之后要注册这个类
    Class objc_allocateClassPair(Class superClass,const char *name,size_t extraBytes)
  • 2、注册一个类(要在类注册之前添加成员变量)
    Void objc_registerClassPair(Class cls)
  • 3、销毁一个类
    Void objc_disposeClassPair(Class cls)
  • 4、获取isa指向的Class,或者类对象(元类对象)
    Class object_getClass(id obj)
  • 5、设置isa指向的class,此isa指向其他的类
    Class object_setClass(id obj,class cls)
  • 6、判断一个OC对象是否为class,传入实例对象,BOOL为0,传入类对象,BOOL为1
    BOOL object_isClass(id obj)
  • 7、判断一个Class是否为元素
    BOOL class_isMetaClass(Class cls)
  • 8、获取父类
    Class class_getSuperclass(class cls)
成员变量相关
  • 9、获取成员变量信息(获取不到值)
    Ivar class_getInstanceVariable(Class cls,const char *name)
  • 10、拷贝实例变量列表(最后需要调用free释放)
    Ivar *class_copyIvarList(Class cls,unsigned int *outCount)
  • 11、设置和获取成员变量的值
    Void object_setIvar(id obj,Ivar ivar,id value)
    Id object_getIvar(id obj,Ivar ivar)
  • 12、动态添加成员变量
    BOOL class_addIvar(Class cls,const char *name,size_t size,unit8_t alignment,
    const char *types)
  • 13、获取成员变量的相关信息
    const char *ivar_getName(Ivar v)
    const char *ivar_getTypeEncoding(Ivar v)
属性相关
  • 14、获取一个属性
  • 15、拷贝属性列表(最后需要调用free释放)
    objc_property_t *class_copyPropertyList(Class cls,unsigned int *outCount)
  • 16、动态添加属性
    BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t
    *attributes, unsigned int attributeCount)
  • 17、动态替换属性
    void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t
    *attributes,unsigned int attributeCount)
  • 18、获取属性的一些信息
    const char *property_getName(objc_property_t property)
    const char *property_getAttributes(objc_property_t property)
方法相关
  • 19、获得一个实例方法、类方法
    Method class_getInstanceMethod(Class cls, SEL name)
    Method class_getClassMethod(Class cls, SEL name)
  • 20、方法实现相关操作
    IMP class_getMethodImplementation(Class cls, SEL name)
    IMP method_setImplementation(Method m, IMP imp)
    void method_exchangeImplementations(Method m1, Method m2)
  • 21、拷贝方法列表(最后需要调用free释放)
    Method *class_copyMethodList(Class cls, unsigned int *outCount)
  • 22、动态添加方法
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  • 23、动态替换方法
    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
  • 24、获取方法的相关信息(带有copy的需要调用free去释放)
    SEL method_getName(Method m)
    IMP method_getImplementation(Method m)
    const char *method_getTypeEncoding(Method m)
    unsigned int method_getNumberOfArguments(Method m)
    char *method_copyReturnType(Method m)
    char *method_copyArgumentType(Method m, unsigned int index)
  • 25、选择器相关
    const char *sel_getName(SEL sel)
    SEL sel_registerName(const char *str)
  • 26、用block作为方法实现
    IMP imp_implementationWithBlock(id block)
    id imp_getBlock(IMP anImp)
    BOOL imp_removeBlock(IMP anImp)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,460评论 6 538
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,067评论 3 423
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 177,467评论 0 382
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,468评论 1 316
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,184评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,582评论 1 325
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,616评论 3 444
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,794评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,343评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,096评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,291评论 1 371
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,863评论 5 362
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,513评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,941评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,190评论 1 291
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,026评论 3 396
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,253评论 2 375

推荐阅读更多精彩内容