Objective-CiOS开发经验总结前端精选

Runtime基础元素解析

2015-07-11  本文已影响934人  tripleCC

Objective-C的runtime语言使它具备了动态语言的特性,也就是平时所说的“运行时”。在runtime的基础上,可以做很多平时难以想到事,或者化简原先 较为繁杂的解决方案。

相对于静态语言,比如C的以下程序

#include 
void run()
{}
int main()
{
    return 0;
}

执行clang -c进行编译后,获取符号表nm run.o,可以得到全局唯一的符号_run,对函数run的调用直接参考链接后_run符号在代码段的地址

0000000000000010 T _main
0000000000000000 T _run

对比Objective-C的以下函数

@implementation Dog : NSObject
- (void)run
{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];
        [dog run];
    }
    return 0;
}

执行clang -rewrite-objc main.m将其转换成底层C++文件后可以得到

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}

可以看到,对Objective-C编译前期,会将内部的方法调用,转换成调用objc_msgSend。也就是说,编译完成后,方法地址是不能确定的,需要在运行时,通过Selector进行查找,而这正是runtime的关键,也就是发送消息机制。

runtime的基本要素

如上面例子所示,在编译后[dog run]被编译器转化成了

((void (*)(id, SEL))(void *)objc_msgSend)((id)dog, sel_registerName("run"));

// 假设能省略(void (*)(id, SEL))(void *)和id指针强转[实际上还是需要的]
// sel_registerName表示注册一个selector
objc_msgSend(dog, sel_registerName("run"));

将上面的情况抽取成统一的说法就是,在编译器编译后[receiver message]会被转化成以下形式

objc_msgSend(receiver, selector)

objc_msgSend是一个消息发送函数,它以消息接收者和方法名作为基础参数。

在有参数的情况下,则会被转换为

objc_msgSend(receiver, selector, arg1, arg2, ...)

消息的接收者receiver在接受到消息后,查找对应selector的实现,根据查找的结果可以进行若干种种不同的处理。

更深层的了解,需要了解下对应的数据结构

id

上文中objc_msgSend的第一个参数有个强转类型,即id。id是可以指向对象的万能指针,查看runtime源码,得知其定义如下:

typedef struct objc_object *id;

// objc_object
struct objc_object {
private:
    isa_t isa;
}

// isa_t
union isa_t
{
    Class cls;
    uintptr_t bits;
}

根据union联合的存储空间以大成员的存储空间计算性质,可以猜测isa_t的作用只是真不同位数处理器的优化,我们可以直接这样表示:

struct objc_object {
private:
    Class isa;
}

可以看出,id是一个指向objc_object结构体的指针(注意,在runtime中对象可以用结构体进行表示)。

objc_object结构体包含了Class isa成员,而isa就是我们常说的创建一个对象时,用来指向所属类的指针。因此根据isa就可以获取对应的类。

Class

上文中,isaClass类型,而Class则是objc_class指针类型的别名:

typedef struct objc_class *Class;

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_data_bits_t
struct class_data_bits_t {
    ...
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    ...
}

// class_rw_t
struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    union {
        method_list_t **method_lists;  // RW_METHOD_ARRAY == 1
        method_list_t *method_list;    // RW_METHOD_ARRAY == 0
    };
    struct chained_property_list *properties;
    const protocol_list_t ** protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

// 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;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    const property_list_t *baseProperties;
};

在上文中已经介绍过objc_object结构体,objc_class继承自结构体objc_object。可以看出objc_objectisaprivate类型成员变量,objc_class继承后无法访问,所以objc_object提供了以下两个成员函数:

Class ISA();

// getIsa内部调用ISA返回isa_t联合中cls成员
Class getIsa();

所以,对objc_class重要的成员变量进行下解释:

IMP objc_class::getLoadMethod()
{
rwlock_assert_locked(&runtimeLock);

const method_list_t *mlist;
uint32_t i;

assert(isRealized());
assert(ISA()->isRealized());
assert(!isMetaClass());
assert(ISA()->isMetaClass());

mlist = ISA()->data()->ro->baseMethods;
if (mlist) {
    for (i = 0; i < mlist->count; i++) {
        method_t *m = method_list_nth(mlist, i);
        const char *name = sel_cname(m->name);
        if (0 == strcmp(name, "load")) {
            return m->imp;
        }
    }
}

return nil;

}


其中先了解下`ivar_list_t`、`method_list_t`、`cache_t`的结构定义:


`ivar_list_t`的结构为:

  - `ivar_t`就是对应的成员变量

```objc
struct ivar_list_t {
    uint32_t entsize;
    uint32_t count;
    ivar_t first;
};

method_list_t为:

struct method_list_t {
    uint32_t entsize_NEVER_USE;  // high bits used for fixup markers
    uint32_t count;
    method_t first;

    // iterate methods, taking entsize into account
    // fixme need a proper const_iterator
    struct method_iterator {
        uint32_t entsize;
        uint32_t index;  // keeping track of this saves a divide in operator-
        method_t* method;
    ...
    }

cache_t为:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
...
}

// bucket_t
struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
...
}

上文还涉及到了一个概念metaClass元类,元类为类对象所属的类,以实例解释:

当我们调用类方法时,消息的接收者即为类,如文中一开始的代码:

Dog *dog = [[Dog alloc] init];

这里的alloc消息即发送给了Dog类,编译转换后的代码为:

Dog *dog = ((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)((Dog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));

我们只需要关注这一行:

// objc_getClass表示根据对象名获取对应的类
objc_getClass("Dog")

// 获取元类
objc_getClass(objc_getClass("Dog"))

关于元类,苹果提供了这么一张表:

对象-类-元类-超类
图中的实线是superclass指针,虚线是isa指针。可以看到,根元类的超类NSObject(Root class)并没有对应的超类,并且,它的isa指针指向了自己。
总结一下:

Ivar

Ivar,我把它理解成instance variable,也就是实例变量,可以观察它的定义:

typedef struct ivar_t *Ivar;

// ivar_t
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;
    // 内存中数据对齐(如字对齐、半字对齐等)
    uint32_t alignment() {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

Ivar其实是指向ivar_t结构体的指针,它包含了实例变量名(name)、类型(type)、相对对象地址偏移(offset)以及内存数据对齐等信息。

跟多关于实例变量的剖析可以查看Objective-C类成员变量深度剖析

Method

从以下定义的结构体可以看出,Method主要住用为关联了方法名SEL和方法的实现IMP,当遍通过Method自己的定义的迭代器查找方法名SEL时,就可以找到对应的方法实现IMP,从而调用方法的实现执行相关的操作。types表示方法实现的参数以及返回值类型。

typedef struct method_t *Method;

// method_t
struct method_t {
    SEL name;
    const char *types;
    IMP imp;
    ...
}

SEL

SEL为方法选择器,观察下它的定义:

typedef struct objc_selector *SEL;

可以看出SEL实际是objc_selector指针类型的别名,它用于表示运行时方法的名字,以便进行方法实现的查找。因为要对应方法实现,所以每一个方法对应的SEL都是唯一的。因此它不具备C++可以进行函数重载的特性,当两个方法名一样时,会发生编译错误,即使参数不一样。

IMP

IMP的定义如下:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

可以看出IMP其实就是一个函数指针的别名,也可以把它理解为函数名。它有两个必须的参数:

在某些情况下,通过获取IMP而直接调用方法实现,可以直接跳过消息传递机制,像C语言调用函数那样,在一定程度上,可以提供程序的性能。

消息传递

了解完runtime中一些必要的元素,继续回到文章开头的代码:

@implementation Dog : NSObject
- (void)run
{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];
        [dog run];
    }
    return 0;
}

编译器将其转换成了:

typedef (Dog *(*)(id, SEL))(void *) MyImp;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        Dog *dog = ((MyImp)objc_msgSend)((id)((MyImp)objc_msgSend)((id)objc_getClass("Dog"), sel_registerName("alloc")), sel_registerName("init"));
        ((MyImp)objc_msgSend)((id)dog, sel_registerName("run"));
    }
    return 0;
}

从上面的代码可以看出,第二个objc_msgSend返回值是作为第一个objc_msgSend的首个参数的。

上文已经说过,[receiver message]会被转化成以下形式

objc_msgSend(receiver, selector, ...)

接下来看看它主要做了哪几件事情:

方法列表查找路径
除此之外,objc_msgSend还会传递两个隐藏参数:

objc_msgSend找到方法实现后,会在调用该实现时,传入这两个隐藏参数,这样就能够在方法实现里面里面获取消息接受对象,即方法调用者了。

隐藏参数表示这两个参数在源代码方法的定义中并没有声明这两个参数,这两个参数是在代码编译期间,被插入到实现中的。

self和super的联系

根据上文对objc_msgSend的了解,可以解决以下代码输出一致问题

@implementation Dog : NSObject

- (void)run
{
    NSLog(@"%@", [self class]);
    NSLog(@"%@", [super class]);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];

        [dog run];
    }
    return 0;
}

输出为:

[5491:173185] Dog
[5491:173185] Dog

这是为什么呢?先来看看编译后的-run方法的情况:

static void _I_Dog_run(Dog * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_3f5nr6h10h1csn8byghy30q80000gn_T_main_d06ff4_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class")));
}

这里面只要关注两句:

// [self class]
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))

// [super class]
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }, sel_registerName("class"))

首先我们需要了解selfsuper的差异:

这里可以看出,编译后,经过super标识符修饰的方法调用,会调用objc_msgSendSuper函数来进行消息的发送,而不是objc_msgSend。先来了解下objc_msgSendSuper的声明:

id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

其中objc_super的定义为:

// receiver   消息实际接收者
// class      指向当前类的父类
struct objc_super { id receiver; Class class; };

结合以上信息,我们可以知道:

(__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Dog")) }

就是对结构体objc_super的赋值,也就是说objc_super->receiver=self。到这里可能就有点明了了,super只是告诉编译器,去查找父类中的class方法,当找到之后,使用objc_super->receiverself进行调用。用流程表示就是:

[super class]->objc_msgSendSuper(objc_super{self, superclass)}, sel_registerName("class"))->objc_msgSend(objc_super->self, sel_registerName("class"))=[self class]

可以看出两者输出结果一致的关键就是,[self class]的消息接收者和[super class]的消息接收者一样,都是调用方法的实例对象。

方法解析和消息转发

当上文objc_msgSend处理流程中,selector没有找到时,会触发三个阶段,在这三个阶段都可以进行相关处理使程序不抛出异常:

由于实际代码中很少有看到这种操作,所以这里不做详细解释,参考这个资料即可Objective-C Runtime 运行时之三:方法与消息

参考

1.Objective-C Runtime 运行时之一:类与对象

2.Objective-C Runtime

上一篇 下一篇

猜你喜欢

热点阅读