Objective-C 运行时简介

Mike Ash Friday Q&A 中文译文:Objective-C 运行时简介

作者 TommyWu
封面圖片: Objective-C 运行时简介

译文 · 原文: Friday Q&A 2009-03-13: Intro to the Objective-C Runtime · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2009-03-13-intro-to-the-objective-c-runtime.html 发布:2009-03-13 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


欢迎回到又一个周五问答,又是一个 13 号的星期五。本周我将采纳 Oliver Mooney 的建议,谈谈 Objective-C 的运行时(runtime),它是如何工作的,以及它能为你做些什么。

许多 Cocoa 程序员对 Objective-C 运行时只有一个模糊的认识。他们知道它存在(有些人甚至不知道这点!),知道它很重要,没有它就无法运行 Objective-C,但也就仅限于此了。

今天,我想详细讲解一下 Objective-C 在运行时层面究竟是如何工作的,以及你能用它做些什么。

(注意:我只会讨论苹果在 10.5 及之后版本上的运行时。10.4 及更早版本的运行时缺少许多 API,转而强制进行直接的结构体访问,而用于 GNU 和 Cocotron 的运行时则完全是另一回事。)

对象 在 Objective-C 中我们始终在使用对象,但对象是什么?好吧,让我们来看一看,并构造一些东西来告诉我们关于它们的信息。

首先,我们知道「object(对象)」是通过「pointer(指针)」来引用的,比如 NSObject *。而且我们知道我们使用 +alloc「method(方法)」来创建它们。它的文档只是说它调用了 +allocWithZone:。进一步跟踪文档链,我们发现了 NSDefaultMallocZone,并看到它们只是使用 malloc 来分配。简单!

但是当它们被分配时,它们是什么样子的呢?让我们一探究竟:

#import <Foundation/Foundation.h>
@interface A : NSObject { @public int a; } @end
@implementation A @end
@interface B : A { @public int b; } @end
@implementation B @end
@interface C : B { @public int c; } @end
@implementation C @end
int main(int argc, char **argv)
{
[NSAutoreleasePool new];
C *obj = [[C alloc] init];
obj->a = 0xaaaaaaaa;
obj->b = 0xbbbbbbbb;
obj->c = 0xcccccccc;
NSData *objData = [NSData dataWithBytes:obj length:malloc_size(obj)];
NSLog(@"Object contains %@", objData);
return 0;
}
2009-01-27 15:58:04.904 a.out[22090:10b] Object contains <20300000 aaaaaaaa bbbbbbbb cccccccc>

但开头这个 20300000 是什么东西?嗯,它出现在 A 的实例变量之前,那一定是属于 NSObject 的。我们来看看 NSObject 的定义:

/*********** Base class ***********/
@interface NSObject {
Class isa;
}
typedef struct objc_class *Class;
struct objc_class {
Class isa;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

让我们来看看另一个根类,NSProxy

@interface NSProxy {
Class isa;
}
typedef struct objc_object {
Class isa;
} *id;

如名称和类型所示,isa 实例变量(isa ivar)指明特定对象属于哪个类。每个 Objective-C 对象必须以 isa 指针开头,否则运行时系统将无法处理它。关于对象类型的所有信息都封装在这个小小的指针中。对象的剩余部分基本上只是一大块内存,就运行时系统而言,它并不重要。由各个类来赋予这块内存以意义。

那么,类究竟包含什么呢?那些” 不可用” 的结构体成员给出了很好的线索。(它们是为了兼容 Leopard 之前的运行时系统而存在的,如果您的目标系统是 Leopard,则不应使用,但它仍然告诉我们其中存在哪些类型的信息。)首先是 isa,它允许类也能像对象一样工作。接下来是指向超类(superclass)的指针,以此提供正确的类层次结构(class hierarchy)。随后是一些关于该类的其他基本信息。最后则是真正有趣的部分:一个实例变量列表、一个方法列表以及一个协议列表。所有这些内容都可以在运行时访问,并且也可以在运行时进行修改。(译注:此处描述的 ObjC 2.0 之前的类结构布局,现代 Runtime 可能已重构)

查看 runtime.h 的其余部分,你会发现大量用于访问和操作这些属性的函数。每个函数都以其操作对象为前缀:通用 runtime 函数以 objc_ 开头,操作类的函数以 class_ 开头,依此类举。例如,你可以调用 class_getInstanceMethod 来获取特定方法的信息(如参数 / 返回值类型);也可以调用 class_addMethod 在运行时向现有类添加新方法;甚至可以通过 objc_allocateClassPair 在运行时创建一个全新的类。

实际应用
这类运行时元信息可以实现许多实用功能,以下是一些思路:

  • 自动实例变量 / 方法搜索:Apple 的 Key-Value Coding(键值编码)已经实现了这类功能:你提供一个名称,它便会根据该名称查找对应的实例变量(ivar)或方法并进行操作。你也可以自行实现类似逻辑,例如需要根据名称查找实例变量等情况。

  • 自动注册 / 调用子类(subclass)。通过 objc_getClassList 可获取当前运行时(runtime)已知的所有类,通过追踪类层级(class hierarchy),即可识别出哪些类是指定类的子类。这让你能够编写处理特定数据格式等场景的子类,而无需繁琐地手动注册每个子类,父类便能自行查找它们。

  • 自动调用每个类的特定方法。这在自定义单元测试框架之类的场景中非常有用。与 #2 类似,但关注的是方法是否被实现,而非特定的类层级。

  • 在运行时覆盖方法(method)。运行时提供了一整套工具,能够将方法重新指向自定义的方法实现(method implementation),从而在不修改源代码的情况下改变类的行为。

  • 自动释放综合属性。@synthesize 关键字虽然便于让编译器自动生成 setter / getter 方法,但仍需要在 -dealloc 释放方法中手动编写清理代码。通过读取类属性的元信息,你可以编写能自动遍历并清理所有综合属性的代码,而无需为每个属性单独编写清理逻辑。

  • 语言桥接。通过在运行时动态生成类,并按需查找必要的属性,你可以在 Objective-C 和另一门(足够动态的)语言之间创建桥梁。

  • 更多可能。不要局限于以上应用,请发挥你自己的创意!

总结 Objective-C 是一门强大的语言,而全面的运行时 API 是其极其实用的组成部分。虽然在大量 C 代码中摸爬滚打可能略显丑陋,但实际上使用起来并不困难,它所提供的强大能力完全值得这份投入。

以上就是本周的 Friday Q & A 专栏内容。欢迎提出你的建议,可以在下方留言或通过电子邮件发送(请说明是否需要匿名)。Friday Q & A 依靠各位的建议运营,期待你的来信!

你是否对 Objective-C 运行时有特别喜欢的用途?或是有什么不满意的地方?有什么技巧想要分享?请在下方评论区分享。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2009-03-13-intro-to-the-objective-c-runtime.html

Welcome back to another Friday Q&A, on another Friday the 13th. This week I’m going to take Oliver Mooney’s suggestion and talk about the Objective-C runtime, how it works, and what it can do for you.

Many Cocoa programmers are only vaguely aware of the Objective-C runtime. They know it’s there (and some don’t even know this!), that it’s important, and you can’t run Objective-C without it, but that’s about where it stops.

Today I want to run through exactly how Objective-C works at the runtime level and what kinds of things you can do with it.

(Note: I’ll be talking only about Apple’s runtime on 10.5 and later. The runtime on 10.4 and earlier is missing many APIs, instead forcing direct structure access, and the runtimes for GNU and Cocotron are different beasts entirely.)

Objects In Objective-C we work with objects all the time, but what is an object? Well, let’s take a look and construct something that will tell us about them.

First, we know that objects are referred to using pointers, like NSObject *. And we know that we create them using the +alloc method. The documentation for that just says that it calls +allocWithZone:. Following the chain of documentation a bit further, we discover NSDefaultMallocZone and see that they’re just allocated using malloc. Easy!

But what do they look like when they’re allocated? Let’s find out:

#import <Foundation/Foundation.h>
@interface A : NSObject { @public int a; } @end
@implementation A @end
@interface B : A { @public int b; } @end
@implementation B @end
@interface C : B { @public int c; } @end
@implementation C @end
int main(int argc, char **argv)
{
[NSAutoreleasePool new];
C *obj = [[C alloc] init];
obj->a = 0xaaaaaaaa;
obj->b = 0xbbbbbbbb;
obj->c = 0xcccccccc;
NSData *objData = [NSData dataWithBytes:obj length:malloc_size(obj)];
NSLog(@"Object contains %@", objData);
return 0;
}
2009-01-27 15:58:04.904 a.out[22090:10b] Object contains <20300000 aaaaaaaa bbbbbbbb cccccccc>

But what’s this 20300000 thing at the beginning? Well, it comes before A’s ivar, so it must be NSObject’s. Let’s look at NSObject’s definition:

/*********** Base class ***********/
@interface NSObject {
Class isa;
}
typedef struct objc_class *Class;
struct objc_class {
Class isa;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

Let’s look at another root class, NSProxy:

@interface NSProxy {
Class isa;
}
typedef struct objc_object {
Class isa;
} *id;

As the name and type imply, the isa ivar indicates what class a particular object is. Every Objective-C object must begin with an isa pointer, otherwise the runtime won’t know how to work with it. Everything about a particular object’s type is wrapped up in that one little pointer. The remainder of an object is basically just a big blob and as far as the runtime is concerned, it is irrelevant. It’s up to the individual classes to give that blob meaning.

Classes What exactly do classes contain, then? The “unavailable” structure members give a good clue. (They’re there for compatibility with the pre-Leopard runtime, and you shouldn’t use them if you’re targeting Leopard, but it still tells us what kind of information is there.) First comes the isa, which allows a class to act like an object as well. There’s a pointer to the superclass, giving the proper class hierarchy. Some other basic information about the class follows. At the end is the really interesting stuff. There’s a list of instance variables, a list of methods, and a list of protocols. All of this stuff is accessible at runtime, and can be modified at runtime too.

Looking at the rest of runtime.h you’ll see a lot of functions for accessing and manipulating these properties. Each function is prefixed with what it operates on. General runtime functions start with objc_, functions that operate on a class start with class_, and so forth. For example, you can call class_getInstanceMethod to get information about a particular method, like the argument/return types. Or you can call class_addMethod to add a new method to an existing class at runtime. You can even create a whole new class at runtime by using objc_allocateClassPair.

Practical Applications There are tons of useful things that can be done with this kind of runtime meta-information, but here are some ideas.

  • Automatic ivar/method searches. Apple’s Key-Value Coding does this kind of thing already: you give it a name, and it looks up a method or ivar based on that name and does some stuff with it. You can do that kind of thing yourself, in case you need to look up an ivar based on a name or something of the sort.

  • Automatically register/invoke subclasses. Using objc_getClassList you can get a list of all classes currently known to the runtime, and by tracing out the class hierarchy, you can identify which ones subclass a given class. This can let you write subclasses to handle specialized data formats or other such situations and let the superclass look them up without having to tediously register every subclass manually.

  • Automatically call a method on every class. This can be useful for custom unit testing frameworks and the like. Similar to #2, but look for a method being implemented rather than a particular class hierarchy.

  • Override methods at runtime. The runtime provides a complete set of tools for re-pointing methods to custom implementations so that you can change what classes do without touching their source code.

  • Automatically deallocate synthesized properties. The @synthesize keyword is handy for making the compiler generate setters/getters but it still forces you to write cleanup code in -dealloc. By reading meta-information about the class’s properties, you can write code that will go through and clean up all synthesized properties automatically instead of having to write code for each case.

  • Bridging. By dynamically generating classes at runtime, and by looking up the necessary properties on demand, you can create a bridge between Objective-C and another (sufficiently dynamic) language.

  • Much more. Don’t feel limited to the above, come up with your own ideas!

Wrapping Up Objective-C is a powerful language and the comprehensive runtime API is an extremely useful part of it. While it may be a bit ugly groveling around in all that C code, it’s really not that difficult to work with, and it’s well worth the power it provides.

That’s it for this week’s Friday Q&A. Please send in your suggestions, either by posting them below or by e-mail (tell me if you don’t want me to use your name). Friday Q&A runs on your suggestions so please write in!

Have a favorite use of the ObjC runtime? Something you dislike about it? Have a tip to share? Post it all below.