运行时动态创建类

Mike Ash Friday Q&A 中文译文:运行时动态创建类

作者 TommyWu
封面圖片: 运行时动态创建类

译文 · 原文: Friday Q&A 2010-11-6: Creating Classes at Runtime in Objective-C · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2010-11-6-creating-classes-at-runtime-in-objective-c.html 发布:2010-11-6 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


Friday Q & A 回来了! 过去几个月我有些非常重要的摸鱼事宜要处理,但现在我准备好恢复常态了。在这次周五 Q & A 的回归中,我将讨论如何在运行时(runtime)创建 Objective-C 类,这个主题由 Kevin Avila 建议提出。这个话题内容足够丰富,因此将分为两部分;今天的帖子将讨论在运行时创建类的基础知识,下一篇则会讨论这些类的用途以及如何利用它们。

MAObjCRuntime 在我的休息期间,我构建了 MAObjCRuntime,这是一个对许多常见运行时功能进行良好面向对象(OO)包装的库,包含了我今天要讨论的所有内容。为了今天的讨论,我将不会涉及 MAObjCRuntime,以便你能了解如何直接使用运行时。

如果你决定自己应用这些技术,我建议你使用 MAObjCRuntime,因为它会使工作变得相当轻松。

定义与目的 在运行时(runtime)创建类究竟意味着什么?如果你曾使用过 Objective-C,应该理解创建类的含义:编写一个 @interface 声明块、一个 @implementation 实现块,添加实例变量和方法,就能得到一个可使用的类。

在运行时创建类能达到相同效果。区别在于,你通过编写代码直接调用运行时库(Runtime)来在内存中构建类结构,而非编写由编译器解释的类定义。你同样可以添加方法和实例变量。

为何要这样做?在运行时创建新类常能方便地覆盖任意类的功能。例如,MAZeroingWeakRef 就通过此方式捕获内存管理事件,以实现零化弱引用(zeroing weak references)。

创建类 创建类的操作通过 objc/runtime.h 中的 objc_allocateClassPair 函数完成。你需要向其传入一个父类(superclass)、一个类名以及每个类的存储空间大小(通常设置为 0 即可),它会返回一个类给你:

Class mySubclass = objc_allocateClassPair([NSObject class], "MySubclass", 0);

附带说明:为何称之为「分配类对」?你可能已知,所有 Objective-C 类同时也是 Objective-C 对象。你可以像操作其他对象一样,将它们放入变量、向它们发送消息、添加到数组中等。所有对象都有一个类,而类的类(class of a class)被称为 metaclass(元类)。每个类都有一个唯一的元类,因此有了「对」(pair):objc_allocateClassPair 会同时分配类本身和它的元类。

关于元类是什么及其工作原理的完整讨论超出了本文的范围,但 Greg Parker 有一篇很好的元类讨论文章,如果你有兴趣深入了解,可以阅读。

添加方法
你知道如何创建一个类,但除非你实际向其中添加内容,否则它不会执行任何有趣的操作。

方法(Methods)是向新创建的类添加的最直观的内容。你可以使用 objc/runtime.h 中的 class_addMethod 函数来向类添加方法。该函数接受四个参数。

前两个参数是你想要操作的类,以及你想要添加的方法的 selector(选择子)。这两者都应该相当显而易见。

下一个参数是 IMP。这个类型是 Objective-C 中对函数指针的特殊 typedef(类型定义)。它的定义如下:

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

要创建传递给该函数的 IMP,需要实现一个以 id selfSEL _cmd 作为前两个参数的函数。后续参数即为该方法将接收的参数,而返回类型即为该方法的返回类型。

例如,假设你想要编写一个具有如下签名的 IMP:

- (NSUInteger)countOfObject: (id)obj;
static NSUInteger CountOfObject(id self, SEL _cmd, id obj)

最后一个参数是一个类型编码字符串(type encoding string),用于描述该方法的类型签名(type signature)。这个字符串会被运行时(runtime)用于生成通过 methodSignatureForSelector: 方法返回的 NSMethodSignature,此外还有其他用途。

生成这种类型编码字符串的最佳方式是:从一个已存在的类中获取拥有相同签名的方法对应的类型编码字符串。这样你只需信任编译器的正确性,而无需担心这些字符串的构造细节。例如,上述方法与 -[NSArray indexOfObject:] 拥有相同的签名,因此你可以获取到那个类型编码字符串:

Method indexOfObject = class_getInstanceMethod([NSArray class],
@selector(indexOfObject:));
const char *types = method_getTypeEncoding(indexOfObject);

如果你确实必须构建自己的类型编码字符串(type encoding string,不推荐),那么可以使用 @encode 指令为各个组成部分生成字符串,再将它们组合起来。编译器生成的字符串中还嵌入了数字栈偏移信息,这意味着你的字符串不会完全匹配其输出,但通常已足够接近。

一个方法的类型编码字符串由返回类型的 @encode 表示,后跟各参数类型构成,其中包括开头的两个隐含参数:

NSString *typesNS = [NSString stringWithFormat: @"%s%s%s%s",
@encode(NSUInteger),
@encode(id), @encode(SEL),
@encode(id)];
const char *types C = [typesNS UTF8String];

下面是一个为新建类添加 description 方法的完整示例:

static NSString *Description(id self, SEL _cmd)
{
return [NSString stringWithFormat: @"<%@ %p: foo=%@>", [self class], self, [self foo]];
}
// add Description to mySubclass
// grab NSObject's description signature so we can borrow it
Method description = class_getInstanceMethod([NSObject class],
@selector(description));
const char *types = method_getTypeEncoding(description);
// now add
class_addMethod(mySubclass, @selector(description), (IMP)Description, types);

添加实例变量
您可以使用 class_addIvar 方法为类添加实例变量(instance variable)。

该函数的前两个参数分别是要操作的类(class)和要添加的实例变量名称。这两者都很直观。

接下来的参数是实例变量的大小。如果您使用普通 C 类型作为实例变量,可以直接使用 sizeof 来获取大小。

之后是实例变量的对齐(alignment)。这指示实例变量的存储在内存中需要如何对齐,可能需要在其与前一个实例变量末尾之间填充空间。这里有一个技巧:该参数是对齐值的 log2,而非对齐值本身。传入 1 表示按 2 字节边界对齐,传入 4 表示按 16 字节对齐,以此类推。由于大多数类型希望按自身大小对齐,您可以直接使用 rint(log2(sizeof(type))) 来计算该参数的值。

最后一个参数是变量的类型编码字符串(type encoding string)。这可以通过 @encode 指令并传入您要添加的变量类型来生成。

以下是添加一个 id 实例变量的完整示例:

class_addIvar(mySubclass, "foo", sizeof(id), rint(log2(sizeof(id))), @encode(id));

运行时提供了两个用于访问实例变量的函数:object_setInstanceVariableobject_getInstanceVariable。它们接受一个对象和一个名称作为参数,前者还需要一个要设置的值,后者则需要一个用于存放当前值的位置。以下是获取和设置上文构建的 foo 变量的示例:

id currentValue;
object_getInstanceVariable(obj, "foo", &currentValue);
// it will be replaced, so autorelease
[currentValue autorelease];
id newValue = ...;
[newValue retain]; // runtime won't retain for us
object_setInstanceVariable(obj, "foo", newValue);

使用任何一种技术时,别忘了添加 dealloc 方法来释放你的对象实例变量(instance variables)。

如果你需要每个实例的存储,考虑使用关联对象 API(associated object API)(objc_setAssociatedObject 和 objc_getAssociatedObject)而不是实例变量。它会为你处理内存管理。

添加协议

你可以使用 class_addProtocol 向类添加协议(protocol)。这通常不是很常用,所以我不会详细介绍如何使用。记住,这个函数只是声明类遵循指定的协议,但实际上不会添加任何代码。如果你希望类实际实现协议中的方法,你必须自己实现并添加这些方法。

添加属性

尽管有很多函数用于查询类的属性(properties),但苹果显然忘记了提供任何方法来向类添加属性。幸运的是,就像协议一样,在运行时向类添加属性通常不是很常用,所以这不是一个大的损失。

注册类

完成类的设置后,你必须注册它才能使用。你可以使用 objc_registerClassPair 函数来做到这一点。

objc_registerClassPair(mySubclass);

使用类

注意,你必须在使用一个类之前注册它,并且注册之后你不能再向该类添加任何实例变量。但是,你可以在注册之后向该类添加方法。

一旦你注册了该类,就可以像操作任何其他类一样向它发送消息:

id myInstance = [[mySubclass alloc] init];
NSLog(@"%@", myInstance);

结论

现在你已经了解了如何在运行时(runtime)创建新类、如何为其添加方法和实例变量,以及如何在代码中使用它。两周后,我将探讨如何利用上述技术实现真正有用且有趣的事情,而不是仅仅用四倍的代码量来模拟编译器的工作。在此之前,请继续发送你的主题建议;下一篇文章已安排妥当,但我对后续内容持开放态度,欢迎提供想法。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2010-11-6-creating-classes-at-runtime-in-objective-c.html

Friday Q&A is back! I had some very important slacking to take care of for the past couple of months, but now I’m ready to resume business as usual. For this return to Friday Q&A, I’m going to talk about how to create Objective-C classes at runtime, a topic suggested by Kevin Avila. This topic is meaty enough that this will be a two-parter; today’s post will talk about the basics of how to create classes at runtime, and then the next one will discuss uses for such classes and how to take advantage of them.

MAObjCRuntime One of the things I did during my off time was build MAObjCRuntime, a nice OO wrapper around a lot of common runtime functionality, including everything that I’m going to talk about today. For my discussion today I will not involve MAObjCRuntime, so that you can see how to use the runtime directly.

If you decide to use these techniques on your own, I’d recommend using MAObjCRuntime instead, as it makes life considerably easier.

What and Why What exactly does it mean to create a class at runtime? If you’ve done any Objective-C at all, you know what it means to create a class. You create an @interface block, an @implementation block, add instance variables and methods, and you have a class that you can use.

Creating a class at runtime gives you the same result. The difference is that you write code which calls into the runtime to create class structures in memory directly, rather than writing classes to be interpreted by the compiler. You can add methods and instance variables just as you would normally.

Why would you do such a thing? It’s often handy to create new classes at runtime to override functionality in arbitrary classes. For example, MAZeroingWeakRef does this in order to catch memory management events in order to implement zeroing weak references.

Creating a Class The act of creating a class is accomplished using the objc_allocateClassPair function in objc/runtime.h. You pass it a superclass, a name, and a size for per-class storage (generally best left at 0), and it returns a class to you:

Class mySubclass = objc_allocateClassPair([NSObject class], "MySubclass", 0);

An aside: why is it called “allocate class pair”? As you probably already know, all Objective-C classes are also Objective-C objects. You can put them in variables, send them messages, add them to arrays, etc. just like you would with any other object. All objects have a class, and the class of a class is called the metaclass. Each class has a unique metaclass, and thus the pair: objc_allocateClassPair allocates both the class and the metaclass together.

A full discussion of what the metaclass is and how it works is beyond the scope of this post, but Greg Parker has a good discussion of metaclasses if you’re interested in reading more.

Adding Methods You know how to create a class, but it won’t do anything interesting unless you actually put things in it.

Methods are the most obvious things to add to a newly created class. You add methods to a class using the class_addMethod function in objc/runtime.h. This function takes four parameters.

The first two parameters are the class you want to manipulate, and the selector of the method that you want to add. Both of these should be pretty obvious.

The next parameter is an IMP. This type is a special Objective-C typedef for a function pointer. It’s defined as:

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

To create the IMP that you pass to this function, implement a function that takes id self and SEL _cmd as its first two parameters. The rest of the parameters are the parameters that the method will take, and the return type is the method return type.

For example, let’s say you wanted to write an IMP with this signature:

- (NSUInteger)countOfObject: (id)obj;
static NSUInteger CountOfObject(id self, SEL _cmd, id obj)

The last parameter is a type encoding string which describes the type signature of the method. This is the string that the runtime uses to generate the NSMethodSignature that’s returned from methodSignatureForSelector:, among other uses.

The best way to generate this type encoding string is to retrieve it from an existing class which has a method with the same signature. This way you can just trust the compiler to get it right and don’t have to worry about the details of how these strings are put together. For example, the method above has the same signature as -[NSArray indexOfObject:], so you can retrieve that type encoding string:

Method indexOfObject = class_getInstanceMethod([NSArray class],
@selector(indexOfObject:));
const char *types = method_getTypeEncoding(indexOfObject);

If you absolutely must build your own type encoding string (not recommended), then you can do it using the @encode directive to generate strings for the individual components, then combine them. Compiler-generated strings also have numeric stack offset information embedded in them, which means that your string won’t completely match its output, but it’s often good enough.

The components of a method’s type encoding string are simply the @encode representation of the return type, followed by the argument types, including the two implicit parameters at the beginning:

NSString *typesNS = [NSString stringWithFormat: @"%s%s%s%s",
@encode(NSUInteger),
@encode(id), @encode(SEL),
@encode(id)];
const char *types C = [typesNS UTF8String];

Here’s a full example of adding a description method to a newly created class:

static NSString *Description(id self, SEL _cmd)
{
return [NSString stringWithFormat: @"<%@ %p: foo=%@>", [self class], self, [self foo]];
}
// add Description to mySubclass
// grab NSObject's description signature so we can borrow it
Method description = class_getInstanceMethod([NSObject class],
@selector(description));
const char *types = method_getTypeEncoding(description);
// now add
class_addMethod(mySubclass, @selector(description), (IMP)Description, types);

Adding Instance Variables You can add instance variables to a class using the class_addIvar method.

The first two parameters to this function are the class to manipulate and the name of the instance variable you want to add. Both are straightforward.

The next parameter is the size of the instance variable. If you’re using a plain C type as the instance variable, then you can simply use sizeof to get the size.

Next is the alignment of the instance variable. This indicates how the instance variable’s storage needs to be aligned in memory, potentially with padding in between it and the end of the previous instance variable. A trick to this parameter is that it’s the log2 of the alignment rather than the alignment itself. Passing 1 means aligning it to a 2-byte boundary, passing 4 means 16-byte alignment, etc. Since most types want to be aligned to their size, you can simply use rint(log2(sizeof(type))) to generate the value of this parameter.

The last parameter is a type encoding string for the parameter. This can be generated using the @encode directive and giving it the type of the variable that you’re adding.

Here’s a full example of adding an id instance variable:

class_addIvar(mySubclass, "foo", sizeof(id), rint(log2(sizeof(id))), @encode(id));

The runtime provides two functions for accessing instance variables: object_setInstanceVariable and object_getInstanceVariable. They take an object and a name, and either a value to set, or a place to put the current value. Here’s an example of getting and setting the foo variable constructed above:

id currentValue;
object_getInstanceVariable(obj, "foo", &currentValue);
// it will be replaced, so autorelease
[currentValue autorelease];
id newValue = ...;
[newValue retain]; // runtime won't retain for us
object_setInstanceVariable(obj, "foo", newValue);

With either technique, don’t forget to add a dealloc method to release your object instance variables.

If you need per-instance storage, consider using the associated object API (objc_setAssociatedObject and objc_getAssociatedObject) instead of instance variables. It takes care of memory management for you.

Adding Protocols You can add a protocol to a class using class_addProtocol. This is not usually very useful, so I won’t go into how to use it. Keep in mind that this function only declares the class as conforming to the protocol in question, but it doesn’t actually add any code. If you want the class to actually implement the methods in a protocol, you have to implement and add those methods yourself.

Adding Properties Although there are plenty of functions for querying the properties of a class, Apple apparently forgot to provide any way to add a property to a class. Fortunately, like protocols, it’s not usually very useful to add a property to a class at runtime, so this is not a big loss.

Registering the Class After you’re done setting up the class, you have to register it before you can use it. You do this with the objc_registerClassPair function:

objc_registerClassPair(mySubclass);

Note that you must register a class before you use it, and you can’t add any instance variables to a class after you register it. You can add methods to a class after registration, however.

Using the Class Once you’ve registered the class, you can message it just like you would any other class:

id myInstance = [[mySubclass alloc] init];
NSLog(@"%@", myInstance);

Conclusion Now you know how to create a new class at runtime, how to add methods and instance variables to it, and then use it from code. In two weeks, I’ll cover how to actually do useful and interesting things with the above, instead of just using four times the code to imitate what the compiler does. Until then, keep sending in your suggestions for topics; the next article is already booked, but I’m open for ideas after that.