Basically, I want to print each time a class object is instantiated. The following code shows the intent.
@interface NSObject (ILogger)
+ (void)initialize;
@end
@implementation NSObject (ILogger)
+ (void)initialize
{
NSLog(@"Initializing %s", class_getName([self class]));
}
@end
This does not work because NSObject
already has a +initialize
method so this approach results in undefined behavior. The compiler also warns about the issue: warning: category is implementing a method which will also be implemented by its primary class
One idea would be to somehow swizzle +[NSObject initialize]
and do the logging. How do I do that safely?
EDIT:
Maybe I'm using the wrong terminology but the goal is to know if a class is used at all in the app. If many objects of a class are created, there is no need to log every time, once is sufficient.
You are correct about use of +[NSObject initialize]
method for tracking the first use of a class. I don't know anything more appropriate for that. The swizzling would look like this:
#import <objc/runtime.h>
@implementation NSObject (InitializeLog)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getClassMethod(self, @selector(initialize));
Method swizzledMethod = class_getClassMethod(self, @selector(tdw_initializeLog));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
+ (void)tdw_initializeLog {
const char *className = class_getName(self);
printf("Initializing %s\n", className);
[self tdw_initializeLog];
}
@end
There are a few things to be advised about:
initialize
doesn't fallback to the NSObject
implementation (which is swizzled above) if derived classes have this method implemented AND don't have [super initialize];
called. Thus for any custom class inherited from Cocoa classes either don't implement this method OR call [super initialize];
somewhere in your implementation:+ (void)initialize {
[super initialize];
...
}
Cocoa classes are rarely as straightforward as they look like. Quite a lot of interfaces and classes are hidden under the same name and sometimes the logs will be somewhat misleading (e.g. in place of NSNumber
you will get NSValue
class reported). Thus, take any logging out of Foundation classes with a grain of salt and always double-check where it comes from (also be ready that those classes won't be reported at all).
First use of NSLog
also triggers some classes to initialise themselves and it make them to call +[NSObject initialize]
. In order to avoid an infinite loop or bad_access errors I decided to use printf
to log the fact of initialisation in my implementation.
The + (void)initialize
method has very little to do with objects instantiation, since it gets called for each Objective-C class shortly before it's first time used in your client code. It might be called multiple times if subclasses of a given class don't have this method implemented and never gets called afterward. Thus it's just a bad choice if you want to track objects instantiation.
However there are still a few options you may want to employ to track occasions of objects instantiation.
-[NSObject init]
First, I would consider init
method of NSObject
:
#import <objc/runtime.h>
@implementation NSObject (InitLog)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(init));
Method swizzledMethod = class_getInstanceMethod(self, @selector(initLog_tdw));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (instancetype)initLog_tdw {
self = [self initLog_tdw];
if (self) {
const char *className = class_getName([self class]);
NSLog(@"Instantiating %s", className);
}
return self;
}
@end
It will work fine as long as instances falls back to the -[NSObject init]
method. Unfortunately quite a lot of Cocoa classes don't do that. Consider the following scenario:
NSObject *obj = [NSObject new]; // NSLog prints "Instantiating NSObject"
NSString *hiddenStr = [[NSMutableString alloc] initWithString:@"Test"]; // NSLog is silent
NSURL *url = [[NSURL alloc] initWithString:@"http://www.google.com"]; // NSLog is silent
-[NSURL initWithString:]
and -[NSMutableString initWithString:]
somehow avoids NSObject
's default constructor being called. It will still work for any custom classes which don't have any fancy initialisation:
@implementation TDWObject
- (instancetype)initWithNum:(int)num {
self = [super init];
if (self) {
_myNum = num;
}
return self;
}
@end
TDWObject *customObj = [TDWObject new]; // NSLog prints "Instantiating TDWObject"
TDWObject *customObjWithNum = [[TDWObject alloc] initWithNum:2]; // NSLog prints "Instantiating TDWObject"
+[NSObject alloc]
Alternatively you can swizzle the alloc
method:
#import <objc/runtime.h>
@implementation NSObject (AllocLog)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getClassMethod(self, @selector(alloc));
Method swizzledMethod = class_getClassMethod(self, @selector(tdw_allocLog));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
+ (instancetype)tdw_allocLog {
id allocatedObject = [self tdw_allocLog];
if (allocatedObject) {
const char *className = class_getName([allocatedObject class]);
NSLog(@"Allocating %s", className);
}
return allocatedObject;
}
@end
It will intercept almost all Cocoa classes instantiation (the exception must be some of the fabric methods, where class-specific optimisation takes place, e.g. +[NSNumber numberWith..]
family of methods), but there are other problems to be aware of. The allocated instances returned from alloc
method are not always that straightforward. E.g. for NSMutableString
example above NSLog
will print NSPlaceholderMutableString
:
TDWObject *customObj = [TDWObject new]; // NSLog prints "Allocating TDWObject"
TDWObject *customObjWithNum = [[TDWObject alloc] initWithNum:2]; // NSLog prints "Allocating TDWObject"
NSObject *obj = [NSObject new]; // NSLog prints "Allocating NSObject"
NSString *hiddenStr = [[NSMutableString alloc] initWithString:@"Test"]; // NSLog prints "Allocating NSPlaceholderMutableString"
NSURL *url = [[NSURL alloc] initWithString:@"http://www.google.com"]; // NSLog prints "Allocating NSURL"
That's is because Foundation framework uses Class Cluster design pattern heavily and instances returned by alloc
are often some kind of abstract factories, which are later leveraged by Cocoa classes to make a concrete instance of the class requested.
Both approaches have their own downsides, but I struggle to come up with anything more concise and reliable.