Reading the Mac OS X 10.6 Reference Library, I experienced mixed emotions: so many new features, but if you use them, the programs will not run on PowerPC poppies, and besides, not everyone will want to install the Snow Leopard if Leo quite suits them. The simplest solution seems to not use these features, but it means to limit yourself. I don’t know about you, but I don’t like to be limited. I want the program to use all the advantages of the Snow Leopard, but at the same time it could work on the previous version of Mac OS X. Is it possible?
Maybe! And with Objective-C you make it as transparent as possible. Imagine (and this is only a small part) that using functions from the Objective-C Runtime Library (objc / runtime.h) you can add methods and variables to an object, replace implementations of methods, get and set the value of object variables from functions, and that's it. during the execution of the program! With other (especially compiled) languages, you will not be able to achieve the same flexibility.
Version detection
Cocoa changes only with a change in the minor version of Mac OS X, and new versions of Cocoa are not backported and cannot be installed on older versions of Mac OS X. Given these facts, it is very easy to determine the functionality of Cocoa: you just need to know the version of the system. Gestalt Manager lets you know almost everything about the system, but now we only need its version.
SInt32 version;
Gestalt(gestaltSystemVersionMinor, &version);
if (version <= 5) {
// 10.5
}
else {
// 10.6 and later
}
Register a new method
You can simply declare a method in a protocol or interface and refuse to implement it altogether! Thus, neither this method nor its implementation exists at the time of program launch, and you can (and are obliged not to raise an exception when you receive the corresponding message) add it to the class during program execution.
')
Use the
class_addMethod function
(Class cls, SEL name, IMP imp, const char * types) to register a new method and implement the
resolveInstanceMethod: or
resolveClassMethod: methods for class or instance methods. These messages are sent to Cocoa before launching the message forwarding mechanism.
+ (BOOL) resolveInstanceMethod: (SEL) aSEL {
if (aSEL == @selector(null)) {
class_addMethod([self class], aSEL, (IMP) _null, "v@:");
return YES;
}
else if (aSEL == @selector(isHidden)) {
class_addMethod([self class], aSEL, (IMP) _isHidden, "c@:");
return YES;
}
else if (aSEL == @selector(setHidden:)) {
class_addMethod([self class], aSEL, (IMP) _setHidden, "v@:c");
return YES;
}
return [super resolveInstanceMethod: aSEL];
}
The most interesting thing about
class_addMethod is the third argument - a pointer to a string with the type schema of the return value and the arguments of the function that implements the method. Each base type corresponds to a string of characters, first the type of the return value is written, then the types of two required arguments: the
self object and the selector
_cmd , after the types of explicit parameters.
Character code table:
: :
c char
i int
s short
l long, l 32 64-
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B C++ bool C99 _Bool
v void
* (char *)
@
# (Class)
: (SEL)
[array type]
{name=type...} (struct)
(name=type...) (union)
b num num
^ type type
? ( )
Much better, especially with redefined and complex types, use the
@encode () directive to create a string with a schema, for example:
const char *types = [[NSString stringWithFormat: @"%s%s%s%s", @encode(void), @encode(id), @encode(SEL), @encode(BOOL)] UTF8String];
How it works? If you send a message to the object with a missing method, its selector is passed to
resolveInstanceMethod: or
resolveClassMethod: for the purpose of dynamic resolution. If you want the object to be able to send this message, you need to return for its selector
NO :
if (aSEL == @selector(setHidden:)) {
class_addMethod([self class], aSEL, (IMP) _setHidden, "v@:c");
return NO;
}
This method is great if you just need to add a method, for example, if you want to write an existing method from Cocoa yourself. In this case, you do not need to check the system version: just if the program was launched at 10.5, the method is missing on it, it will be registered, if it is launched at 10.6 and the method exists, then its selector will not be the argument of the
resolveInstanceMethod: message and nothing will change.
Everything would be fine, but, unfortunately, the implementation of all the features of system methods can be very difficult, and secondly, and most importantly, we simply may not know the private classes and structures used, or the methods and functions, respectively, in some cases this option.
Moreover, if you need to use several implementations of one (not Cocoa) method depending on the conditions, then this is not very convenient: the message is sent with each selector, you need to create a string with a schema, independent implementations for the methods of the class and the instance. Because of all this, the code is repeatedly duplicated and replete with if `s, which does not look very good and is even worse debugged. In general, avoid obscure architecture.
We use several implementations of the method
To get away from these problems, we implement the
+ initialize method, this message is sent to Cocoa only once to each class, and it is guaranteed to receive it before any other message, which is what we need. We also use the
class_replaceMethod function
(Class cls, SEL name, IMP imp, const char * types) , it is somewhat safer, because, for example, if the method does not exist, then it will simply add it as
class_addMethod , if the class has a method, it will replace its implementation as
method_setImplementation and ignore the
types string. This means that we can write an empty method and use the following code:
+ initialize {
SInt32 version;
Gestalt(gestaltSystemVersionMinor, &version);
const char *types = [[NSString stringWithFormat: @"%s%s%s", , @encode(BOOL), @encode(id), @encode(SEL)] UTF8String];
if (version < 6) {
class_replaceMethod([self class], @selector(isHidden), (IMP) Legacy_HSFileSystemItem_IsHidden, types);
}
else {
class_replaceMethod([self class], @selector(isHidden), (IMP) HSFileSystemItem_isHidden, types);
}
}
- (BOOL) isHidden {
return NO;
}
Change the implementation of the method
Still, this is not the best way, because you need to write several functions and monitor their proper operation. If you want, for example, to abandon support for Mac OS X 10.5, you will need to rewrite methods and do other non-creative crap. In order not to do this, it is better to write methods with a calculation on the latest version of Cocoa, then write only one function used by the legacy system, and, if necessary, replace the implementation with it!
In addition, in order not to write a type string, use the
class_getClassMethod (Class cls, SEL name) ,
class_getInstanceMethod (Class cls, SEL name) and
method_setImplementation (Method m, IMP imp) functions. Method is a pointer to the method structure.
The final option:
// HSFileSystemItem.m
#import "HSFileSystemItem.h"
#ifdef __HS_Legacy__
#import "HSFileSystemItem_Legacy.h"
#import <CoreServices/CoreServices.h>
#endif // __HS_Legacy__
@implementation HSFileSystemItem
#ifdef __HS_Legacy__
+ initialize {
SInt32 version;
Gestalt(gestaltSystemVersionMinor, &version);
if (version < 6) {
Method _isHidden_method = class_getInstanceMethod([self class], @selector(isHidden));
method_setImplementation(_isHidden_method, (IMP) HSFileSystemItem_isHidden);
}
}
#endif // __HS_Legacy__
- (BOOL) isHidden {
id value = nil;
[_url getResourceValue: &value forKey: NSURLIsHiddenKey error: nil];
return [value boolValue];
}
// Other
@end
But how to change the implementation in the category, if you do not know whether the class has
initialize ?
+ Load methods come to the rescue, the class and each category can have its own implementation of this method and they will not be redefined or competed, great, isn't it?
Instance variables
Finally, everything, or not? The unanswered question is how to get the instance variables in the function. You can simply use setter, getter or KVC, or, if you are not looking for easy ways,
object_getInstanceVariable (id obj, const char * name, void ** outValue) ,
object_setInstanceVariable (id obj, const char * name, void * value), class_getInstanceVariable (Class cls, const char * name) .
The first two functions can get and set the value of an instance variable. In addition, all three functions return
Ivar , a pointer to a structure with information about an instance variable that can be cached. If you need to get or set the value of variables several times, use
Ivar with the
object_getIvar functions
(id obj, Ivar ivar) ,
object_setIvar (id ob, Ivar ivar, id value) .
// HSFileSystemItem_Legacy.m
#import <ApplicationServices/ApplicationServices.h>
#import <CoreServices/CoreServices.h>
#import <objc/runtime.h>
#import "HSFileSystemItem.h"
static BOOL HSFileSystemItem_isHidden(id self, SEL _cmd)
{
NSURL *_url = nil;
object_getInstanceVariable(self, "_url", &_url);
LSItemInfoRecord itemInfo;
// Get file`s item info
OSStatus err = LSCopyItemInfoForURL((CFURLRef) _url, kLSRequestAllFlags, &itemInfo);
if (err != noErr) {
NSLog(@"LSCopyItemInfoForURL: error getting item info for %@. The error returned was: %d", _url, err);
}
return itemInfo.flags & kLSItemInfoIsInvisible;
}
or
static BOOL HSFileSystemItem_isHidden(id self, SEL _cmd)
{
static Ivar _url_ivar = class_getInstanceVariable([self class], "_url");
NSURL *_url = object_getIvar(self, _url_ivar);
// Get file`s item info
OSStatus err = LSCopyItemInfoForURL((CFURLRef) _url, kLSRequestAllFlags, &itemInfo);
if (err != noErr) {
NSLog(@"LSCopyItemInfoForURL: error getting item info for %@. The error returned was: %d", _url, err);
}
return itemInfo.flags & kLSItemInfoIsInvisible;
}
Do not use
object_getInstanceVariable ,
object_setInstanceVariable ,
object_getIvar ,
object_setIvar , if you have instance variables - ordinary C types! They assume that instance variables are pointers to objects. The point is that the pointers, no matter what they indicate, have the same size: 32 or 64 bits. If the size of the variable is different from the size of the pointer, then what you want will be copied at all. Instead, you need to play around with the pointers:
static Ivar _int_ivar = class_getInstanceVariable([self class], "_num");
int *_num = (int *) ((uint8_t *) self + ivar_getOffset(ivar));
Or you can use the
NSObject category written by John Calsbeek, with it you can get an instance variable from any object (regardless of the programmer’s desire ;-)):
@implementation NSObject (InstanceVariableForKey)
- (void *) instanceVariableForKey: (NSString *) aKey {
if (aKey) {
Ivar ivar = object_getInstanceVariable(self, [aKey UTF8String], NULL);
if (ivar) {
return (void *)((char *)self + ivar_getOffset(ivar));
}
}
return NULL;
}
@end
int _num = *(int *) [self instanceVariableForKey: "_num"];
Epilogue
Bonus: a variant allowing the method to determine its implementation in the first call (you have to be a pervert ;-))
- (BOOL) isHidden {
SInt32 version;
Gestalt(gestaltSystemVersionMinor, &version);
const char *types = [[NSString stringWithFormat: @"%s%s%s", , @encode(BOOL), @encode(id), @encode(SEL)] UTF8String];
if (version < 6) {
class_replaceMethod([self class], _cmd, (IMP) HSFileSystemItem_Legacy_IsHidden, types);
}
else {
class_replaceMethod([self class], _cmd, (IMP) HSFileSystemItem_isHidden, types);
}
return [self isHidden];
}
Links
Key links:
Gestalt_Manager / Reference / reference.htmlObjCRuntimeGuide / Introduction / Introduction.htmlObjCRuntimeRef / Reference / reference.htmlNSObject_Class / Reference / Reference.htmlReferences:
http://cocoasamurai.blogspot.com/2010/01/understanding-objective-c-runtime.htmlhttp://mikeash.com/pyblog/friday-qa-2009-03-13-intro-to-the-objective-c-runtime.htmlhttp://stackoverflow.com/questions/1219081/object-getinstancevariable-works-for-float-int-bool-but-not-for-double