どういうわけかアイデアが私の頭に浮かんだが、ブロックを取り、それをターゲットアクションのために与えることは可能か?
BlocksKitや他のライブラリなどの既製のソリューションがありますが、それらのソリューションはブロックを保存し、ターゲットを設定し、指定されたセレクターからブロックを呼び出すことです。
なぜこの記事が必要なのですか?
ブロックを呼び出すセレクターを生成する方法を作成したかったのです。 ここで複雑なことは何ですか? imp_implementationWithBlock + class_addMethodとケースが閉じられます。 しかし、このアプローチでは、1つの重大な要件があります。これは、ブロックの最初の引数-メソッドの所有者です。
この要件を回避してこれを行う方法は?
[button addTarget:self action:[self ax_lambda:^(UIButton *sender, UIEvent *event){ NSLog(@"click on button %@, event = %@", sender, event); }] forControlEvents:UIControlEventTouchUpInside]; [button addTarget:self action:[self ax_lambda:^{ NSLog(@"click"); }] forControlEvents:UIControlEventTouchUpInside];
またはそのように
__block NSInteger sum = 0; [self performSelector:[self ax_lambda:^(NSNumber *argA, NSNumber *argB) { sum = [argA integerValue] + [argB integerValue]; }] withObject:@(2) withObject:@(3)];
実装は非常に興味深いことが判明したため、私はそれについて書くことにしました。
実際、主なタスクは、ブロック呼び出しでselfの最初の引数を取り除くことです。 これは、ソリューション全体の根本的な問題です(これが唯一の問題ではないことは残念です)。
以前、私は
ブロックについて少し書きましたが、ブロックはオブジェクトであることに注意してください。つまり、呼び出しはNSInvocationを介して行われます。
ブロックが呼び出される瞬間を取得し、NSInvocationで(引数をシフトすることで)self引数を削除すると、目的の結果が得られます。
さらに、途中で理解する必要があります。
AXProxyBlock
問題は、ブロック呼び出しの瞬間にどのように侵入するのですか? ブロック呼び出しを取得する方法は?
私は非常に頻繁にこのフレーズを書きますが、ブロックはオブジェクトです。 最終形式のobjcのオブジェクトは構造体です。 idは構造体へのポインタであるため、逆も許可されます
(__bridge、hello) 。
偽のブロックを作成できることがわかりました。 まあ、またはブロックのプロキシ。
私のクラスのインターフェースは次のとおりです。 typedef void(^AXProxyBlockInterpose)(NSInvocation *invocation); @interface AXProxyBlock : NSProxy + (instancetype)initWithBlock:(id)block; - (void)setBeforeInvoke:(AXProxyBlockInterpose)beforeInvoke; - (NSString *)blockSignatureStringCTypes; @end
ご想像のとおり、setBeforeInvokeは、ブロック引数の「マジック」変換を実行できるブロックを受け入れます。
blockSignatureStringCTypesは、プロキシされたブロックの署名を返します。 ヘッダーファイルに含まれているのはなぜですか? それについては後で。
今から始まるドキュメントページへのリンクドキュメントを開始するために、ブロック構造と列挙型を作成します。 typedef struct AXBlockStruct_1 { unsigned long int reserved; unsigned long int size; void (*copy_helper)(void *dst, void *src); void (*dispose_helper)(void *src); const char *signature; } AXBlockStruct_1; typedef struct AXBlockStruct { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct AXBlockStruct_1 *descriptor; } AXBlockStruct; typedef NS_ENUM(NSUInteger, AXBlockFlag) { AXBlockFlag_HasCopyDispose = (1 << 25), AXBlockFlag_HasCtor = (1 << 26), AXBlockFlag_IsGlobal = (1 << 28), AXBlockFlag_HasStret = (1 << 29), AXBlockFlag_HasSignature = (1 << 30) };
それでは、クラスを見てみましょう。
対応する構造を作成する必要があります。 @interface AXProxyBlock () {
ここで、呼び出し時にクラスが受信ブロックを模倣する必要があります。
一致するフィールド値 - (instancetype)initWithBlock:(id)block { if (self != nil) { AXBlockStruct *blockRef = (__bridge AXBlockStruct *)block; _flags = blockRef->flags; _reserved = blockRef->reserved; _descriptor = calloc(1, sizeof(AXBlockStruct_1)); _descriptor->size = class_getInstanceSize([self class]); BOOL flag_stret = _flags & AXBlockFlag_HasStret; _invoke = (flag_stret ? (IMP)_objc_msgForward_stret : (IMP)_objc_msgForward); ...
これらのフィールドの目的の説明は、すべて同じ
clangドキュメントページで読むことができ
ます 。 これで、フィールドは呼び出し時のブロックに対応します。
しかし、2つの非常に重要なivarがあります。これらは、上記のスポイラーには含まれていません。これらは既にブロックコールを参照しているので、さらに詳しく説明したいからです。
_impBlockInvoke = (IMP)blockRef->invoke; _blockMethodSignature = [self blockMethodSignature];
_impBlockInvokeは、ブロック呼び出し関数の実装です。 これは通常の関数ポインターであり、手動で呼び出すことができます。
_blockMethodSignatureは、ブロック署名メソッドです。 それが何であるかは、以下で詳細に検討されます。
ブロックのNSMethodSignatureを取得する方法 - (NSMethodSignature *)blockMethodSignature { const char *signature = [[self blockSignatureStringCTypes] UTF8String]; return [NSMethodSignature signatureWithObjCTypes:signature]; } - (NSString *)blockSignatureStringCTypes { AXBlockStruct *blockRef = (__bridge AXBlockStruct *)_block; const int flags = blockRef->flags; void *signatureLocation = blockRef->descriptor; signatureLocation += sizeof(unsigned long int); signatureLocation += sizeof(unsigned long int); if (flags & AXBlockFlag_HasCopyDispose) { signatureLocation += sizeof(void(*)(void *dst, void *src)); signatureLocation += sizeof(void (*)(void *src)); } const char *signature = (*(const char **)signatureLocation); return [NSString stringWithUTF8String:signature]; }
ブロックを取得し、そこから記述子を取得し、目的の値にシフトしてブロックのシグネチャ(const char *)を取得し、それを使用してNSMethodSignatureを作成します。 NSMethodSignatureは、引数の数とタイプ、戻り値などを決定します。
複雑に見えませんが、フラグの操作は混乱を招きます。ブロックのタイプに応じて、その署名はさまざまな方法で見つけることができます。 たとえば、グローバルブロックはコピーおよび破棄機能を超えて移行する必要はありません。
私のクラスにはブロックを呼び出すメソッドがないため、forwardInvocationが呼び出されます。その前に、どのタイプのNSInvocationが形成されるかを調べる必要があるため、methodSignatureForSelectorが呼び出され、_blockMethodSignatureが返されます。
forwardInvocation - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setTarget:_block]; if (_beforeInvoke) { _beforeInvoke(anInvocation); } IMP imp = _impBlockInvoke; [anInvocation invokeUsingIMP:imp]; }
ここのコードは非常に明確でなければなりません(呼び出しの新しいターゲットを設定し、存在する場合はbeforeブロックを呼び出します)が、[anInvocation invoke]呼び出しはどこですか?!
これは黒魔術です。 invokeUsingIMPメソッドは、
ここで見つけることができるプライベートAPIです。続行する前にパズルを組み立てる
ブロックプロキシは特殊な素材であり、問題の後半の解決策にまっすぐ進むと、記事を読む人が少なくなると思います。 したがって、ラッパーは既製のソリューションのパズルを拾うと簡単に考えられ、最終的にタスクの後半がソートされます。 これにより、少しリラックスして、より体系的な方法で資料を収集できます。
記事の冒頭で呼び出されたメソッド、ax_lambdaについて話しましょう。 これはNSObjectの単なるカテゴリであり、次のように見えるメイン関数を呼び出すためのラッパーです。
SEL ax_lambda(id obj, id block, NSMutableArray *lambdas);
ラッパーが書かれている理由が明らかになっていると思います。 そして、1番目と2番目の引数で問題が発生しない場合、3番目の引数で考えさせられます。 最初に、3番目の引数の必要性について説明し、次にネタバレのカテゴリコードを示します。
SEL ax_lambda(id obj, id block, NSMutableArray *lambdas) { SEL selector = ax_generateFreeSelector(obj); AXProxyBlockWithSelf *proxyBlock = [AXProxyBlockWithSelf initWithBlock:block]; [proxyBlock setBeforeInvoke:^(NSInvocation *invocation){ ax_offsetArgInInvocation(invocation); }]; [lambdas addObject:proxyBlock]; IMP imp = imp_implementationWithBlock(proxyBlock); NSString *signatureString = [proxyBlock blockSignatureStringCTypes]; class_addMethod([obj class], selector, imp, [signatureString UTF8String]); return selector; }
これが主な機能であり、非常に組み立てられたパズルです。 AXProxyBlockWithSelfクラスについては後で検討しますが、現時点では、おそらくご想像のとおり、これはAXProxyBlockクラスの子孫であることに注意してください。
ブロックをメソッドにするには、セレクター、実装、および文字列署名が必要です。 実装はプロキシブロックから受信され、プロキシは文字列署名も提供します(AXProxyBlockではこれはプロキシされたブロックの署名ですが、AXProxyBlockWithSelfでは異なります。これについては後述します)。しかし、セレクタを生成するのは難しくありません。 では、なぜ3番目のパラメーターですか?
imp_implementationWithBlockが呼び出されると、ブロックコピー(Block_copy)が呼び出されます。 ブロック内のcopy_helperフィールドは、ブロックのコピー機能へのポインターです。 ただし、プロキシブロックにはこの機能はありません。 void(*)(void * dst、void * src)の形式のコピー関数を作成しても、目的の結果を得ることができません。 srcにコピーされるオブジェクトが来ますが、これは私のクラスのインスタンスではありません。 したがって、imp_implementationWithBlockを呼び出しても、proxyBlockオブジェクトの参照カウントは増加しません(関数の終了後にproxyBlockは破棄されます)。 これを防ぐために、内部参照カウントを増やすコレクションを使用します。 ブロックの寿命は、それを保存するコレクションの寿命に依存することがわかります。 カテゴリーの場合、ブロックの寿命は所有者の寿命によって制限されます。
AXLambda.h SEL ax_lambda(id obj, id block, NSMutableArray *lambdas); @interface NSObject (AX_Lambda) - (SEL)ax_lambda:(id)block; @end
AXLambda.m static char kAX_NSObjectAssociatedObjectKey; @interface NSObject (_AX_Lambda) @property (copy, nonatomic) NSMutableArray *ax_lambdas; @end @implementation NSObject (_AX_Lambda) @dynamic ax_lambdas; - (void)setAx_lambdas:(NSMutableArray *)lambdas { objc_setAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey, lambdas, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSMutableArray *)ax_lambdas { NSMutableArray *marrey = objc_getAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey); if (marrey == nil) { self.ax_lambdas = [NSMutableArray array]; } return objc_getAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey); } @end @implementation NSObject (AX_Lambda) - (SEL)ax_lambda:(id)block { return ax_lambda(self, block, self.ax_lambdas); } @end
さて、SEL ax_lambdaで使用される関数(id obj、idブロック、NSMutableArray * lambdas);
SEL ax_generateFreeSelector(id obj) SEL ax_generateFreeSelector(id obj) { SEL selector; NSMutableString *mstring = [NSMutableString string]; do { [mstring setString:@"ax_rundom_selector"]; u_int32_t rand = arc4random_uniform(UINT32_MAX); [mstring appendFormat:@"%zd", rand]; selector = NSSelectorFromString(mstring); } while ([obj respondsToSelector:selector]); return selector; }
void ax_offsetArgInInvocation(NSInvocation *呼び出し) void ax_offsetArgInInvocation(NSInvocation *invocation) { void *foo = malloc(sizeof(void*)); NSInteger arguments = [[invocation methodSignature] numberOfArguments]; for (NSInteger i = 1; i < arguments-1; i++) {
StringWithFormatとNSArrayを組み合わせてNSMethodSignatureを理解する
次のパートを始める前に、NSInvocationとNSMethodSignatureの基本的な理解が必要です。 私はこれを別の記事で分離することを考えましたが、実際に深く掘り下げなければ、記事は興味深くシンプルなものになります(具体的な例を分析すると)が、それほど大きくはないという結論に達しました。 そこで私はここでそれについて書くことにしました。
次のように、フォーマットと引数の配列から文字列を生成するメソッドが必要でした:
NSString *format = @"%@, foo:%@, hello%@"; NSArray *input = @[@(12), @(13), @" world"]; NSString *result = [NSString ax_stringWithFormat:format array:input];
残念ながら、SOで見つけたメソッドは機能しませんでした(
first 、
second )。 おそらくARCでそれらを正しく使用しようとしませんでした(成功した方は書き留めてください)が、作業バージョンが必要なため、実装を作成しました。
ポインターまたは変換でラウンドを行うことなく、ソリューションはメソッドの原理に完全に基づいています。
メソッドの最終形式は次のようになります。
+ (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arguments;
形式とパラメータによって文字列を作成する標準的な方法は次のとおりです
- (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0);
しかし、使用するには(および問題自体)、va_listを作成する必要があります(va_listと
は何ですか
?また、使用方法 )。
次の方法は素晴らしい
+(instancetype)ax_string:(NSString *)format、... + (instancetype)ax_string:(NSString *)format, ... { va_list list; va_start(list, format); NSString *str = [[NSString alloc] initWithFormat:format arguments:list]; va_end(list); return str; }
ここでの問題は、NSArrayからの引数を使用して呼び出す方法です。
NSInvocationは、オブジェクト間および/またはアプリケーション間でメッセージを保存および転送するために使用されるオブジェクトです。
ただし、NSInvocationを作成するときは、NSMethodSignatureが必要です。
NSMethodSignatureを使用
すると、メソッドが
受け取る引数の数、引数の型、オフセット、戻り値の型を決定でき
ます 。
ドキュメントからの
発言はこれについて非常に論理的に見えます
NSInvocationは、可変数の引数またはユニオン引数を使用したメソッドの呼び出しをサポートしていません。
結局のところ、引数の数が可変の関数/メソッドに渡される引数の数とタイプは不明です。
そして、あなたがまだ知っているなら? 電話する前に私自身がこの情報を知っていたら? そして、この場合、メソッドは例えば4つの引数を取り、関数は可変数の引数を取るかもしれませんが、これは動作します。
NSMethodSignatureは、上記のすべての情報を自分で指定した場合、生成された署名を通じて作成できます。 NSArrayには、ポインターと、ポインターのサイズによるすべてのパラメーターのオフセットのみが含まれているため、すべてが非常に単純です。 すでに
書いたように、メソッドにはselfと_cmdを使用できます。これらは暗黙的にメソッドに渡されるためです。
+(NSMethodSignature *)ax_generateSignatureForArguments:(NSArray *)引数 + (NSMethodSignature *)ax_generateSignatureForArguments:(NSArray *)arguments { NSInteger count = [arguments count]; NSInteger sizeptr = sizeof(void *); NSInteger sumArgInvoke = count + 3;
ここで何が起こっているかについて少し話す価値があります。 最初に
、ここでコーディング
のタイプを調べる必要があり
ます 。
そして今、順番に、あなたがテーブルを見たことを本当に願っています。
署名の最初の場所は戻り値の型とそのオフセットです(戻り値の型はすべての引数の後にあるため、最大のオフセットがありますが、最初に書き込まれます)。 sizeof(void *)が8であり、3つの引数の配列であるとします。 ただし、self + _cmd +渡される形式を含めて、6つの引数を取得します。 6x8 = 48
@ 48
その後、selfおよび_cmdに従います。 引数の中でselfが最初に来るので、
@ 48 @ 0:8
次にフォーマット
@ 48 @ 0:8 @ 16
と引数
@ 48 @ 0:8 @ 16 @ 24 @ 32 @ 40
署名ができたので、NSInvocationを使用できます
+(instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments + (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments { NSMethodSignature *methodSignature = [self ax_generateSignatureForArguments:arrayArguments]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setTarget:self]; [invocation setSelector:@selector(ax_string:)]; [invocation setArgument:&format atIndex:2]; for (NSInteger i = 0; i < [arrayArguments count]; i++) { id obj = arrayArguments[i]; [invocation setArgument:(&obj) atIndex:i+3]; } [invocation invoke]; __autoreleasing NSString *string; [invocation getReturnValue:&string]; return string; }
そして今、上記のメソッドをわずかに変更すると、+(instancetype)ax_stringメソッドを削除できます:(NSString *)format、...
ネタバレの下の完全なコード + (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments { NSMethodSignature *methodSignature = [self ax_generateSignatureForArguments:arrayArguments]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setTarget:self]; [invocation setSelector:@selector(stringWithFormat:)]; [invocation setArgument:&format atIndex:2]; for (NSInteger i = 0; i < [arrayArguments count]; i++) { id obj = arrayArguments[i]; [invocation setArgument:(&obj) atIndex:i+3]; } [invocation invoke]; __autoreleasing NSString *string; [invocation getReturnValue:&string]; return string; }
問題の後半の解決策-気付かれないブロックにさらに1つの引数を追加する方法は?
ブロックが呼び出された瞬間のインターセプトと引数のオフセットが考慮されました。 このアプリケーションのアイデアと小さなニュアンスを適用するためのコードが検討されました。 ただし、完了を妨げる問題が残っていました。
imp_implementationWithBlockで受け入れられるブロックは、最初に所有者の引数を取る必要があります。 ax_lambda関数の入力ブロックの署名は意図した署名とは異なり、引数はNSInvocationに完全に間違って転送されることがわかりました。
AXProxyBlockWithSelfクラスは、プロキシされたブロックの署名をやり直し、追加の最初の引数を追加します。 したがって、プロキシブロック呼び出しは正しい引数で完了し、最初の引数はブロック呼び出しの前にすでにシフトされています。
メソッドを書き換える必要があります-(NSString *)blockSignatureStringCTypes
-(NSString *)blockSignatureStringCTypes - (NSString *)blockSignatureStringCTypes { NSString *signature = [super blockSignatureStringCTypes]; NSString *unformatObject = [signature ax_unformatDec]; NSString *formatNewSignature = [self addSelfToFormat:unformatObject]; NSArray *byteSignature = [signature ax_numbers]; NSArray *byteNewSignature = [self changeByteSignature:byteSignature]; return [NSString ax_stringWithFormat:formatNewSignature array:byteNewSignature]; }
そのため、引数の型とオフセット、戻り値の型などを含むブロック署名があります。
追加の引数を署名に挿入し、引数をシフトする必要があります。
署名文字列形式の初期形式を取得します - (NSString *)ax_unformatDec { NSCharacterSet *characterSet = [NSCharacterSet decimalDigitCharacterSet]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"length > 0"]; NSArray *separated = [[self componentsSeparatedByCharactersInSet:characterSet] filteredArrayUsingPredicate:predicate]; NSString *format = [separated componentsJoinedByString:@"%@"]; if ([[self lastSubstring] isEqualToString:[format lastSubstring]] ) { return format; } else { return [format stringByAppendingString:@"%@"]; } } - (NSString *)lastSubstring { NSInteger lastIndex = [self length] - 1; return [self substringFromIndex:lastIndex]; }
次に
、ここでコーディング
のタイプを確認する必要があり
ます 。
引数\ "所有者\"を最初の場所に追加します - (NSString *)addSelfToFormat:(NSString *)format { NSMutableArray *marray = [[format componentsSeparatedByString:@"?"] mutableCopy]; [marray insertObject:@"?%@@" atIndex:1]; return [marray componentsJoinedByString:@""]; }
呼び出すNSArray引数オフセットを取得します - (NSArray *)ax_numbers { NSString *pattern = @"\\d+"; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; NSRange fullRange = NSMakeRange(0, [self length]); NSArray *matches = [regex matchesInString:self options:NSMatchingReportProgress range:fullRange]; NSMutableArray *numbers = [NSMutableArray array]; for (NSTextCheckingResult *checkingResult in matches) { NSRange range = [checkingResult range]; NSString *numberStr = [self substringWithRange:range]; NSNumber *number = @([numberStr integerValue]); [numbers addObject:number]; } return numbers; }
追加された引数を考慮して、引数のオフセットを新しい値に変更します - (NSArray *)changeByteSignature:(NSArray *)byteSignature { NSInteger value = sizeof(void *); NSMutableArray *marray = [NSMutableArray array]; for (NSNumber *number in byteSignature) { NSInteger offset = [number integerValue] + value; [marray addObject:@(offset)]; } [marray insertObject:@0 atIndex:1]; return marray; }
最後に、新しい文字列形式と新しいオフセットを持つNSArrayを使用して、新しい署名を作成します。 したがって、実装が呼び出されると、所有者は最初の引数としてドキュメントに従って転送され、インターセプトのためにシフトされ、元のブロックが呼び出されます。
完全なコードはこちら。 これは単なる実験であり、プロジェクトで使用するためにこのコードを記述することは望みませんでした。 しかし、私はこのビジネスを成功裏に完了できたことを嬉しく思います。 また、SOでNSArrayを使用して文字列を生成するための
ソリューションをレイアウトすることで、誰かを助けることができたことも嬉しいです。
理解しやすい形で資料を伝え、ブロックに分割できたことを願っています。