Effective Objective-C 2.0: Method Swizzling to Debug Opaque Methods

来源:互联网 发布:男生油性皮肤知乎 编辑:程序博客网 时间:2024/05/30 22:42

运行时替换方法实现(通常是新增一个方法(category),然后swap); 这里推荐了一个,debug时候有点用


Item 13: Consider Method Swizzling to Debug Opaque Methods

The method to call when a message is sent to an object in Objective-C is resolved at runtime, as explained in Item 11. It might then occur to you that perhaps the method invoked for a given selector name could be changed at runtime. You’d be correct. This feature can be used to great advantage, as it can be used to change functionality in classes for which you don’t have the source code, without having to subclass and override methods. Thus, the new functionality can be used by all instances of the class rather than only instances of the subclass with overridden methods. Such an approach is often referred to as method swizzling.

A class’s method list contains a list of selector names to implementation mappings, telling the dynamic messaging system where to find the implementation of a given method. The implementations are stored as function pointers called IMPs and have the following prototype:

id (*IMP)(idSEL, ...)

The NSString class responds to selectors called lowercaseString,uppercaseString, and capitalizedString, among others. Each selector points to a different IMP, making up a table like the one shown in Figure 2.3.

Image

Figure 2.3 NSString’s selector table

This table can be manipulated by using a few different functions exposed by the Objective-C runtime. You can add selectors to the list, change the implementation pointed to for a given selector, or swap the implementation pointed to by two selectors. After performing a few of these operations, the class’s method table might look something like Figure 2.4.

Image

Figure 2.4 NSString’s selector table after performing a few operations on it

A new selector called newSelector has been added, the implementation ofcapitalizedString has been changed, and the implementations of lowercaseStringand uppercaseString have been swapped. All this can be done without writing a single subclass, and the new method table layout will be used for each instance ofNSString in the application. A very powerful feature, I’m sure you’ll agree!

The topic of this item refers to the process of exchanging implementations. In doing so, additional functionality can be added to a method. However, before explaining how it can be used to add functionality, I will explain how to simply swap two existing method implementations. To exchange implementations, you use the following function:

void method_exchangeImplementations(Method m1, Method m2)

This function takes as its arguments the two implementations to exchange, which can be obtained by using the following function:

Method class_getInstanceMethod(Class aClass, SEL aSelector)

This function retrieves a method from a class for the given selector. To swap the implementations of lowercaseString and uppercaseString as in the preceding example, you would perform the following:

Method originalMethod =
    class_getInstanceMethod([NSString class],
                            @selector(lowercaseString));
Method swappedMethod =
    class_getInstanceMethod([NSString class],
                            @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

From then on, whenever an NSString instance has lowercaseString called on it, the original implementation of uppercaseString will be invoked and vice versa:

NSString *string = @"ThIs iS tHe StRiNg";

NSString *lowercaseString = [string lowercaseString];
NSLog(@"lowercaseString = %@", lowercaseString);
// Output: lowercaseString = THIS IS THE STRING

NSString *uppercaseString = [string uppercaseString];
NSLog(@"uppercaseString = %@", uppercaseString);
// Output: uppercaseString = this is the string

That explains how to exchange method implementations, but in reality, simply swapping two implementations like that is not very useful. After all, there’s a good reason why the implementation for uppercaseString and lowercaseString do what they do! There’s no reason why you’d want to swap them. But the same approach can be used to add functionality to an existing method implementation. What if you wanted to log something every time lowercaseString was called? The same approach can be used to achieve just that. It involves adding another method that implements the additional functionality and then calls through to the original implementation.

The additional method can be added using a category, like so:

@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end

This method is going to be swapped with the original lowercaseString method, so the method table will end up looking like the one in Figure 2.5.

Image

Figure 2.5 Swapping the implementations of lowercaseString andeoc_myLowercaseString

The implementation of this new method would look like this:

@implementation NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString {
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@"self, lowercase);
    return lowercase;
}
@end

This might look like a recursive call, but remember that the implementations are going to be swapped. So at runtime, when the eoc_myLowercaseString selector is looked up, it’s the implementation of lowercaseString that gets called. Finally, to swap the method implementations, the following is used:

Method originalMethod =
    class_getInstanceMethod([NSString class],
                            @selector(lowercaseString));
Method swappedMethod =
    class_getInstanceMethod([NSString class],
                            @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

From then on, whenever any NSString has lowercaseString called on it, the log line will be printed out:

NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string

Being able to add in logging like this to methods that are completely opaque to you can be a very useful debugging feature. However, this is usually useful only for debugging. Rarely will you find a need other than debugging to perform method swizzling like this to alter functionality of a class globally. Don’t feel that you should use such a feature just because you can. Overuse can easily lead to code that is difficult to read and unmaintainable.

Things to Remember

Image Method implementations for a given selector of a class can be added and replaced at runtime.

Image Swizzling is the process of swapping one method implementation for another, usually to add functionality to the original implementation.

Image Meddling with methods through the runtime is usually good only for debugging and should not be used just because it can.

原创粉丝点击