2011年11月22日火曜日

Method Swizzling

Mac OSX で NSColor -> CGColor の変換をよく使いますが NSColor は CGColor を返すメソッドがありません(*1)せめて NSColor のメソッドが CGColor を返してくれるようになったら嬉しい。そんなわけで NSColor が CGColor を返してくれるようにカテゴリで拡張します。まずはソースコードから。

*1 NSColor のメソッドに - (CGColorRef)CGColor が追加されました。10.8 以降 使用できます。

NSColor+CGColor.h
#import <Foundation/Foundation.h>

@interface NSColor (CGColor)
- (CGColorRef)CGColor;
@end


NSColor+CGColor.m
#import "NSColor+CGColor.h"
#import <objc/runtime.h>

@interface NSColor (Swizzling)

- (void)methodToExchangeImplementation;
- (void)preDealloc;

@end

@interface NSColor (Accessor)

- (void)initializeCGColorCollection;
- (void)releaseColorCollection;
- (void)setCGColorRef:(CGColorRef)colorRef;

@end

@implementation NSColor (Swizzling)

static IMP PRE_DEALLOC_IMPLEMENTATION = NULL;
static IMP DEALLOC_IMPLEMENTATION = NULL;

- (void)methodToGetImplementation {
    
    // メソッド構造体を取得する
    Method preDealloc = class_getInstanceMethod([self class], @selector(preDealloc));
    Method dealloc = class_getInstanceMethod([self class], @selector(dealloc));
    
    // メソッド構造体からメソッド実装を取得する
    PRE_DEALLOC_IMPLEMENTATION = method_getImplementation(preDealloc);
    DEALLOC_IMPLEMENTATION = method_getImplementation(dealloc);
    
}

- (void)methodToExchangeImplementation {
    
    if (PRE_DEALLOC_IMPLEMENTATION == NULL || DEALLOC_IMPLEMENTATION == NULL) {
        
        [self methodToGetImplementation];
        
    }
    
    // メソッド構造体を取得する
    Method preDealloc = class_getInstanceMethod([self class], @selector(preDealloc));
    Method dealloc = class_getInstanceMethod([self class], @selector(dealloc));
    
    // メソッドの実装を互いに入れかえる
    method_exchangeImplementations(preDealloc, dealloc);
    
}

- (void)preDealloc {
    
    // 辞書に CGColorRef を加えたオブジェクトは dealloc の前に解放する。
    [self setCGColorRef:NULL];
    [self releaseColorCollection];
    
    // dealloc を実装から呼ぶ。
    DEALLOC_IMPLEMENTATION(self, _cmd);

}
@end

@implementation NSColor (Accessor)

static NSMutableDictionary *CGColorCollection = nil;

- (void)initializeCGColorCollection {
    
    @synchronized (self) {
        
        if (!CGColorCollection) {
            
            CGColorCollection = [[NSMutableDictionary dictionary] retain];
            [self methodToExchangeImplementation];
        }
    }
}

- (void)releaseColorCollection {
    
    @synchronized (self) {
        
        if ([CGColorCollection count] == 0) {
            
            [CGColorCollection release];
            CGColorCollection = nil;
            
            [self methodToExchangeImplementation];
        }
    }
}

- (void)setCGColorRef:(CGColorRef)newColorRef {
    
    NSString *aKey = [NSString stringWithFormat:@"%p",self];
    
    @synchronized (self) {
        
        NSValue *value = [CGColorCollection valueForKey:aKey];
        
        if (value) {
            
            CGColorRef colorRef = (CGColorRef )[value pointerValue];
            [CGColorCollection removeObjectForKey:aKey];
            CGColorRelease(colorRef);
            
        }
        
        if (newColorRef != NULL) {
            
            [CGColorCollection setValue:[NSValue valueWithPointer:newColorRef]
                                 forKey:aKey];
        } 
    }
}
@end

@implementation NSColor (CGColor)
- (CGColorRef)CGColor {
    
    [self initializeCGColorCollection];
    
    // 現在のカラーの情報を取得する。
    NSColorSpace *colorSpace = [self colorSpace];
    size_t numberOfComponents = (size_t)[self numberOfComponents];
    CGFloat *components = (CGFloat *)calloc(numberOfComponents, sizeof(CGFloat));
    [self getComponents:components];
    
    // CGColorRef を生成する。
    CGColorRef colorRef = CGColorCreate([colorSpace CGColorSpace], components);
    free(components);
    
    // 自身をキーに生成した CGColorRef を辞書に加える。
    [self setCGColorRef:colorRef];
    
    return colorRef;
}
@end

こう考えてみた
  1. 単に NSColor からCGColor オブジェクトのインスタンスを生成するのでなく、NSColor の寿命に応じて 生成した CGColor の寿命もシンクロさせる。
  2. カテゴリはインスタンス変数の追加が出来ないので CGColor オブジェクトを適当なスコープ内に定義したグローバル変数で管理する。
生成した CGColor を個別に解放してやるのはスマートではないと思ったので、NSColor の寿命に応じて生成した CGColor の寿命もシンクロさたいと思いました。そうすると、どのタイミングで CGColor オブジェクトを解放するかですが、インスタンスの参照カウンタが 0 になると -(void) dealloc が自動で呼ばれることを利用して、ランタイム API で CGColor が生成されているあいだ -(void) dealloc の実装を -(void) preDealloc の実装で入れかえることで解決します。

preDealloc で擬似的なクラス変数に見立てた CGColorCollection を管理してやります。CGColorCollection が CGColor を持たなければメソッドの実装を元に戻してやります。ランタイムは NSColor の dealloc だと思って preDealloc を呼出しているので、最終的に本来呼ばれるべき dealloc を実装から直接呼んでやります。

0 件のコメント:

コメントを投稿