6

ブロックが大好きで、ブロックが使えなくなると寂しいです。特に、これはデリゲートを使用するたびにほとんど発生します (例: UIKit クラス、主に事前ブロック機能)。

だから私は疑問に思います... ObjC のクレイジーなパワーを使って、このようなことをすることは可能でしょうか?

   // id _delegate; // Most likely declared as class variable or it will be released
   _delegate = [DelegateFactory delegateOfProtocol:@protocol(SomeProtocol)];
   _delegate performBlock:^{
       // Do something
   } onSelector:@selector(someProtocolMethod)]; // would execute the given block when the given selector is called on the dynamic delegate object.
   theObject.delegate = (id<SomeProtocol>)_delegate;
   // Profit!

performBlock:onSelector:

もしYES、どのように?そして、これを可能な限り行うべきではない理由はありますか?

編集

それは可能であるように見えます。現在の回答は、質問の最初の部分である方法に焦点を当てています。しかし、「やるべきか」という部分について議論をしたいと思います。

4

3 に答える 3

10

よし、やっとWoolDelegateを GitHub にアップすることができた。これで、適切な README を書くのにあと 1 か月しかかからないはずです (ただし、これは良い出発点だと思います)。

デリゲート クラス自体は非常に簡単です。SELs を Block にマッピングするディクショナリを維持するだけです。インスタンスが応答しないメッセージを受信すると、最終的にforwardInvocation:セレクターの辞書を調べます。

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    SEL sel = [anInvocation selector];
    GenericBlock handler = [self handlerForSelector:sel];

見つかった場合は、ブロックの呼び出し関数ポインターが引き出され、ジューシー ビットに渡されます。

    IMP handlerIMP = BlockIMP(handler);

    [anInvocation Wool_invokeUsingIMP:handlerIMP];
}

(この関数は、他のブロック プローブ コードと同様に、 Mike AshBlockIMP()のおかげです。実際、このプロジェクトの多くは、彼の金曜日の Q&A から学んだことに基づいています。これらのエッセイを読んでいない場合は、見逃していることになります。 .)

これは、特定のメッセージが送信されるたびに、完全なメソッド解決機構を通過することに注意してください。そこにスピードヒットがあります。別の方法は、Erik H. とEMKPantryがそれぞれたどった道であり、必要なデリゲート オブジェクトごとに新しいクラスを作成し、class_addMethod(). のすべてのインスタンスにWoolDelegateはハンドラーの独自の辞書があるため、これを行う必要はありませんが、ルックアップまたは呼び出しを「キャッシュ」する方法はありません。メソッドはクラスにのみ追加でき、インスタンスには追加できません。

このようにした理由は 2 つあります。これは、次に来る部分 (Block 呼び出しへのハンドオフ) を解決できるかどうかを確認するための演習であり、必要なインスタンスごとにNSInvocation新しいクラスを作成することは単純に思えました。私にはエレガントではありません。それが私のソリューションよりも洗練されていないかどうかは、各読者の判断に委ねます。

先に進むと、この手順の要点は、実際にはプロジェクトで見つかったNSInvocationカテゴリにあります。これはlibffiを使用して、実行時まで不明な関数 (ブロックの呼び出し) を、実行時まで不明な引数 ( 経由でアクセス可能NSInvocation) とともに呼び出します。通常、これは不可能です。これは、ava_listを渡すことができないのと同じ理由です。コンパイラは、引数がいくつあり、どれくらい大きいかを知る必要があります。libffi には、各プラットフォームの呼び出し規約を認識している/基づいている各プラットフォーム用のアセンブラーが含まれています。

ここには 3 つのステップがあります。libffi は、呼び出される関数への引数の型のリストを必要とします。引数の値自体を特定の形式にする必要があります。次に、関数 (ブロックの呼び出しポインター) を libffi を介して呼び出し、戻り値を に戻す必要がありますNSInvocation

最初の部分の実際の作業は、Mike Ash によって記述された から呼び出される関数によって主に処理されWool_buildFFIArgTypeListます。libffi にはstruct、関数の引数の型を記述するために使用する internal があります。関数の呼び出しを準備するとき、ライブラリはこれらの構造体へのポインターのリストを必要とします。NSMethodSignaturefor は、各引数のエンコーディング文字列へのNSInvocationアクセスを許可します。そこから正しいものへの翻訳はffi_type、一連のif/elseルックアップによって処理されます。

arg_types[i] = libffi_type_for_objc_encoding([sig getArgumentTypeAtIndex:actual_arg_idx]);

...

if(str[0] == @encode(type)[0]) \
{ \
    if(sizeof(type) == 1) \
        return &ffi_type_sint8; \
    else if(sizeof(type) == 2) \
        return &ffi_type_sint16; \

次に、libffi は引数値自体へのポインターを必要とします。これは で行われWool_buildArgValListます: 各引数のサイズを再び から取得し、NSMethodSignatureそのサイズのメモリのチャンクを割り当ててから、リストを返します。

NSUInteger arg_size;
NSGetSizeAndAlignment([sig getArgumentTypeAtIndex:actual_arg_idx], 
                      &arg_size, 
                      NULL);
/* Get a piece of memory that size and put its address in the list. */
arg_list[i] = [self Wool_allocate:arg_size];
/* Put the value into the allocated spot. */
[self getArgument:arg_list[i] atIndex:actual_arg_idx];

(余談ですが、コードには、任意のメソッド呼び出しに (隠された) 2 番目に渡される引数である をスキップすることに関するいくつかの注意事項がありSELます。ブロックの呼び出しポインターには、 を保持するためのスロットがありませんSEL。引数であり、残りは「通常の」引数です。ブロックは、クライアント コードで記述されているように、とにかくその引数にアクセスできなかったので (その時点では存在しませんでした)、無視することにしました。)

libffi は、「準備」を行う必要があります。それが成功する限り (そして戻り値用のスペースを割り当てることができる限り)、呼び出し関数ポインターを「呼び出す」ことができ、戻り値を設定できます。

ffi_call(&inv_cif, (genericfunc)theIMP, ret_val, arg_vals);
if( ret_val ){
    [self setReturnValue:ret_val];
    free(ret_val);
}

プロジェクトの main.m に機能のデモがいくつかあります。

最後に、「これを行うべきですか?」という質問については、「生産性が向上する限り、はい」と答えると思います。WoolDelegateは完全にジェネリックであり、インスタンスは完全に書き出されたクラスのように振る舞うことができます。ただし、私の意図は、単純な 1 回限りのデリゲートを作成することでした。これは、1 つまたは 2 つのメソッドしか必要とせず、デリゲーターを過ぎて生活する必要はありません。まったく新しいクラスを作成するよりも作業が少なく、読みやすくなります。 /maintainable いくつかのデリゲート メソッドをビュー コントローラーに貼り付けるよりも、それらを配置するのが最も簡単な場所であるためです。このようにランタイムと言語のダイナミズムを利用することで、ブロックベースのNSNotificationハンドラーと同じように、コードの可読性が向上することが期待されます。

于 2013-04-19T20:32:46.313 に答える
5

私はちょうどあなたがこれを行うことができる小さなプロジェクトをまとめました...

@interface EJHDelegateObject : NSObject

+ (id)delegateObjectForProtocol:(Protocol*) protocol;

@property (nonatomic, strong) Protocol *protocol;
- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector;

@end


@implementation EJHDelegateObject
static NSInteger counter;

+ (id)delegateObjectForProtocol:(Protocol *)protocol 
{
    NSString *className = [NSString stringWithFormat:@"%s%@%i",protocol_getName(protocol),@"_EJH_implementation_", counter++];
    Class protocolClass = objc_allocateClassPair([EJHDelegateObject class], [className cStringUsingEncoding:NSUTF8StringEncoding], 0);
    class_addProtocol(protocolClass, protocol);
    objc_registerClassPair(protocolClass);
    EJHDelegateObject *object = [[protocolClass alloc] init];
    object.protocol = protocol;
    return object;
}


- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector
{
    unsigned int outCount;
    struct objc_method_description *methodDescriptions = protocol_copyMethodDescriptionList(self.protocol, NO, YES, &outCount);
    struct objc_method_description description;
    BOOL descriptionFound = NO;
    for (int i = 0; i < outCount; i++){
        description = methodDescriptions[i];
        if (description.name == selector){
            descriptionFound = YES;
            break;
        }
    }
    if (descriptionFound){
        class_addMethod([self class], selector, imp_implementationWithBlock(blockImplementation), description.types);
    }
}

@end

EJHDelegateObject を使用して:

self.alertViewDelegate = [EJHDelegateObject delegateObjectForProtocol:@protocol(UIAlertViewDelegate)];
[self.alertViewDelegate addImplementation:^(id _self, UIAlertView* alertView, NSInteger buttonIndex){
    NSLog(@"%@ dismissed with index %i", alertView, buttonIndex);
} forSelector:@selector(alertView:didDismissWithButtonIndex:)];

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Example" message:@"My delegate is an EJHDelegateObject" delegate:self.alertViewDelegate cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
于 2013-03-15T19:33:47.797 に答える
2

編集:これは、あなたの要件を理解した後に思いついたものです。これは簡単なハックであり、開始するためのアイデアであり、適切に実装されておらず、テストもされていません。送信者を唯一の引数として取るデリゲート メソッドで機能するはずです。動作する 通常のデリゲート メソッドと構造体を返すデリゲート メソッドで動作するはずです。

typedef void *(^UBDCallback)(id);
typedef void(^UBDCallbackStret)(void *, id);

void *UBDDelegateMethod(UniversalBlockDelegate *self, SEL _cmd, id sender)
{   
    UBDCallback cb = [self blockForSelector:_cmd];
    return cb(sender);
}

void UBDelegateMethodStret(void *retadrr, UniversalBlockDelegate *self, SEL _cmd, id sender)
{
    UBDCallbackStret cb = [self blockForSelector:_cmd];
    cb(retaddr, sender);
}

@interface UniversalBlockDelegate: NSObject

- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block;

@end

@implementation UniversalBlockDelegate {
    SEL selectors[128];
    id blocks[128];
    int count;
}

- (id)blockForSelector:(SEL)sel
{
    int idx = -1;
    for (int i = 0; i < count; i++) {
        if (selectors[i] == sel) {
            return blocks[i];
        }
    }

    return nil; 
}

- (void)dealloc
{
    for (int i = 0; i < count; i++) {
        [blocks[i] release];
    }
    [super dealloc];
}

- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block
{
    if (count >= 128) return NO;

    selectors[count] = sel;
    blocks[count++] = [block copy];

    class_addMethod(self.class, sel, (IMP)(stret ? UBDDelegateMethodStret : UBDDelegateMethod), mSig);

    return YES;
}

@end

使用法:

UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
UniversalBlockDelegate *d = [[UniversalBlockDelegate alloc] init];
webView.delegate = d;
[d addDelegateSelector:@selector(webViewDidFinishLoading:) isStret:NO methodSignature:"v@:@" block:^(id webView) {
    NSLog(@"Web View '%@' finished loading!", webView);
}];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://google.com"]]];
于 2013-03-15T17:22:38.407 に答える