web-dev-qa-db-ja.com

ARCでブロックを引き続きコピー/ Block_copyする必要がありますか?

私は次のようにつまずきましたSOトピック: なぜ保持するのではなくブロックをコピーする必要があるのですか? これには次の文があります:

ただし、iOS 6以降は通常のオブジェクトとして扱われるため、心配する必要はありません。

私はこの主張に本当に混乱していました。それが私が尋ねている理由です。この主張は本当にObjective-C開発者が

@property (copy) blockPropertiesまたは

[^(...){...) {} copy]

ブロックとその内容をスタックからヒープにコピーする方法は?

私が行った説明が明確であることを願っています。

冗長にしてください。


同様の質問

ARCの下で、ブロックがivarに直接割り当てられると、自動的にコピーされますか?

24

ARCは自動的にブロックをコピーします。 clangの Objective-C自動参照カウント ドキュメントから:

___strong_パラメーター変数の初期化または___weak_変数の読み取りの一部として行われる保持を除いて、これらのセマンティクスがブロックポインター型の値を保持するように呼び出すと、常に_Block_copy_。オプティマイザは、結果が呼び出しの引数としてのみ使用されていることを確認すると、そのようなコピーを削除する場合があります。

そのため、関数またはメソッド呼び出しの引数としてのみ使用されるブロックはスタックブロックのままでかまいませんが、それ以外の場合は、ARCがブロックを保持している任意の場所でブロックをコピーします。これは、objc_retainBlock()への呼び出しを発行するコンパイラーによって実装されます。 implementation は次のとおりです。

_id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}
_

強力なプロパティに割り当てられたブロックは実際にコピーされるため、ブロックプロパティをコピーセマンティクスを持つものとして宣言することは、依然として良い考えです。 Apple これもお勧めします

元のスコープ外のキャプチャされた状態を追跡するためにブロックをコピーする必要があるため、プロパティ属性としてcopyを指定する必要があります。これは自動参照カウントを使用するときに心配する必要はありません。これは自動的に行われるためですが、プロパティ属性が結果の​​動作を示すことがベストプラクティスです。

このコピーオン保持機能はARCによって提供されるため、ARCまたはARCLiteの可用性にのみ依存し、特定のOSバージョンまたは_OS_OBJECT_USE_OBJC_を必要としないことに注意してください。

42
Matt Stevens

編集:

「キャプチャされた」変数のアドレスを調べるのは解釈が難しく、ブロックがヒープにコピーされたか、まだヒープ上にあるかを把握するのに必ずしも適切ではないことがわかりました。ここで指定されたブロックの仕様 BLOCK IMPLEMENTATION SPECIFICATION は事実を十分に説明しますが、私は完全に異なるアプローチを試みます。

とにかくブロックとは何ですか?

これは公式仕様の要約です BLOCK IMPLEMENTATION SPECIFICATION

ブロックは、コード(関数のような)と、データのいくつかのピース、フラグ、関数ポインター、および「キャプチャされた変数」の可変長セクションを含む構造で構成されます。

この構造はプライベートであり、実装が定義されていることに注意してください。

ブロックは、この構造がスタックローカルメモリに作成される関数スコープで定義されるか、構造が静的ストレージに作成されるグローバルスコープまたは静的スコープで定義されます。

ブロックは、他のブロック参照、他の変数、および___block_変更された変数を「インポート」できます。

ブロックが他の変数を参照する場合、それらはインポートされます:

  • スタックローカル(自動)変数は、「constコピー」を作成することによって「インポート」されます。

  • ___block_変更された変数は、別の構造で囲まれた変数のアドレスにポインターを割り当てることによってインポートされます。

  • グローバル変数は単に参照されます(「インポート」されません)。

変数がインポートされる場合、「キャプチャされた変数」は、可変長セクションの前述の構造に存在します。つまり、自動変数(ブロックの外部にある)の「対応物」は、ブロックの構造内にストレージを持っています。

この「キャプチャされた変数」は読み取り専用であるため、コンパイラはいくつかの最適化を適用できます。たとえば、ブロックのコピーが必要な場合は、実際に必要なのはヒープ上のキャプチャされた変数のインスタンスoneだけです。 。

キャプチャされた変数の値は、ブロックリテラル式が評価されるときに設定されます。これは、キャプチャされた変数のストレージが書き込み可能でなければならない(つまり、コードセクションがない)ことも意味します。

キャプチャされた変数の寿命は関数の寿命です。そのブロックを呼び出すたびに、これらの変数の新しいコピーが必要になります。

スタック上にあるブロック内のキャプチャされた変数は、プログラムがブロックの複合ステートメントを離れると破棄されます。

ヒープ上にあるブロックでキャプチャされた変数は、ブロックが破棄されるときに破棄されます。

ブロックAPIによると、バージョン3.5:

最初に、ブロックリテラルが作成されると、この構造はスタックに存在します。

ブロックリテラル式が評価されると、スタックベース構造は次のように初期化されます続く:

  1. 静的記述子構造は、次のように宣言および初期化されます。

    a。関数呼び出しポインターは、ブロック構造を最初の引数として取り、残りの引数(存在する場合)をブロックに取り、ブロック複合ステートメントを実行する関数に設定されます。

    b。サイズフィールドは、次のブロックリテラル構造のサイズに設定されます。

    c。 copy_helperおよびdispose_helper関数ポインターは、ブロックリテラルで必要な場合、それぞれのヘルパー関数に設定されます。

  2. スタック(またはグローバル)ブロックリテラルデータ構造が作成され、次のように初期化されます。

    a。 isaフィールドは、libSystemで提供される初期化されていないメモリのブロックである外部_NSConcreteStackBlockのアドレスに設定されます。静的またはファイルレベルのブロックリテラルの場合は_NSConcreteGlobalBlockです。

    b。プログラムレベルのBlock_copy()およびBlock_release()操作でヘルパー関数を必要とするブロックにインポートされた変数がない限り、フラグフィールドはゼロに設定されます。この場合、(1 << 25)フラグビットが設定されます。

これはBlockリテラル用であることに注意してください。

ブロックに対するObjective-C拡張 によると、コンパイラはブロックをobjectsのように扱います。

観察

現在、これらのアサーションを証明するテストコードを作成することは困難です。したがって、デバッガを使用して、関連する関数にシンボリックブレークポイントを設定する方が良いようです。

  • __Block_copy_internal_および

  • malloc(最初のブレークポイントに到達した後でのみ有効にする必要があります)

次に、適切なテストコード(以下のスニペットなど)を実行して、何が起こるかを調べます。

次のコードスニペットでは、ブロックリテラルを作成し、それを呼び出す関数にパラメーターとして渡します。

_typedef void (^block_t)(void);

void func(block_t block) {
    if (block) {
        block();
    }
}

void foo(int param)
{
    int x0 = param;
    func(^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    });
}  
_

出力は次のとおりです。

_Hello block 1
Address of auto y0: 0x7fff5fbff8dc
Address of captured x0: 0x7fff5fbff940
_

「キャプチャされた」変数_x0_のアドレスは、それがスタック上に存在することを強く示しています。

また、__Block_copy_internal_にブレークポイントを設定しましたが、ヒットしません。これは、ブロックがヒープにコピーされていないことを示しています。関数fooでの割り当てを示さない別の証明をInstrumentsで作成できます。

ここで、ブロックvariableを作成して初期化すると、元のスタックに作成された元のBlock literal-のブロックデータ構造が次の場所にコピーされます。ヒープ:

_int capture_me = 1;
dispatch_block_t block = ^{ int y = capture_me; };
_

enter image description here

これは上記copiesスタック上に最初に作成されたブロックです。これは、ARCと右側にブロックリテラルがあり、左側のブロック変数blockにブロックリテラルが割り当てられるため、単純に発生する可能性があります。これにより、_Block_copy_操作。これにより、ブロックは通常のObjective-Cオブジェクトのように見えます。

以下の同様のコードで、インストゥルメントでトレースされた割り当て

_void foo(int param)
{
    dispatch_queue_t queue = dispatch_queue_create("queue", 0);

    int x0 = param;
    dispatch_block_t block = ^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    };

    block();
}    
_

ブロックが実際にコピーされることを示します:

enter image description here

注目すべき

  • ブロックが変数をキャプチャしない場合、それは通常の関数のようです。次に、コピーするものがないので、_Block_copy_操作が実際には何もしない最適化を適用することは理にかなっています。 clangはこれを実装し、そのようなブロックを「グローバルブロック」にします。

  • copyをブロック変数に送信し、その結果を別のブロック変数に割り当てる場合:

    _dispatch_block_t block = ^{
        int y0 = capture_me;
    };
    dispatch_block_t otherBlock = [block copy];
    _

    copyはかなりのcheap操作になります。これは、ブロックblockが、共有可能なブロックの構造にすでにストレージを割り当てているためです。したがって、copyはストレージを再度割り当てる必要はありません。

質問に戻る

たとえば、特定の状況でブロックを明示的にコピーする必要があるかどうかの質問に答えるには、次のようにします。

_@property (copy) block_t completion

[^{...} copy]
_

さて、いったんブロックがコピーされると、いつでも、そして次にターゲット変数(Block変数)が割り当てられます-ブロックは既にヒープにあるので、明示的にコピーしなくても常に安全でなければなりません。

ブロックpropertyの場合、単純に書き込みを行うだけで安全です。

_@property dispatch_block_t completion;
_

その後:

_foo.completion = ^{ x = capture_me; ... };
_

基になるブロック変数__completion_が(スタックに存在する)ブロックリテラルを割り当てると、ブロックがヒープにコピーされるため、これは安全です。

それでも、属性copyを使用することをお勧めします。これは、公式ドキュメントによってベストプラクティスとして提案されており、ブロックが通常のObjective-Cオブジェクトのように動作せず、 ARCなし。

既存のシステムAPIも、必要に応じてブロックをコピーします。

_dispatch_async(queue, ^{int x = capture_me;});
_

dispatch_async()がコピーを作成します。したがって、心配する必要はありません。

他のシナリオはより微妙です:

_dispatch_block_t block;
if (condition) {
    block = ^{ ... };
}
else {
    block = ^{ ... };
}
dispatch_sync(queue, block);
_

しかし、実際には安全です。ブロックリテラルがコピーされ、ブロック変数blockが割り当てられます。

この例は恐ろしいように見えるかもしれません:

_int x0 = param;
NSArray* array = [NSArray arrayWithObject:^{
    int y0 = x0;
    printf("Hello block 1\n");
}];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
    block_t block = array[0];
    block();
});
_

しかし、ブロックリテラルはヒープにブロックリテラルをコピーするメソッド_arrayWithObject:_のパラメーター(id)に引数(ブロック)を割り当てる効果として正しくコピーされるようです。

enter image description here

さらに、NSArrayretainメソッド_arrayWithObject:_のパラメーターとして渡されるオブジェクトです。これにより、Block_copy() howhowへの別の呼び出しが発生します。ただし、ブロックはすでにヒープ上にあるため、この呼び出しはストレージを割り当てません。

これは、_arrayWithObject:_の実装のどこかでブロックリテラルに送信された「保持」メッセージも実際にブロックをコピーすることを示しています。

では、実際にいつブロックを明示的にコピーする必要があるのでしょうか?

A ブロックリテラル-例表現:

_^{...}
_

スタック上にブロック構造を作成します。

したがって、実際にはneedを使用して、ヒープ上でBlockが必要な場合にのみコピーを作成します。ただし、これが事実であるシナリオはほとんどありません。 Blockリテラルであることがわからず、最初にcopyを必要とするメソッドにパラメーターとしてBlockを渡す場合でも(例:_arrayWithObject:_)、受信者はおそらくパラメータで指定されたオブジェクトへのretainメッセージ。ブロックが最初にヒープにコピーされます。

copyを明示的に使用する例は、Blockリテラルにブロック変数またはidを割り当てないため、ARCはブロックオブジェクトのコピーを作成する必要があることを理解できません。または「保持」を送信する必要があります。

この場合、式

_[^{...} copy];  
_

構造がヒープ上にある自動解放ブロックを生成します。

6
CouchDeveloper

私の元の答えは間違っていました。編集された答えは答えではありませんが、「それは確かに良い質問です」ということです。

どうしてかというものがどういうものなのか、実際の参考文献についてはマットの回答を参照してください。

IOS7で次のコードをテストした後:

int stackVar;
void (^someBlock)() = ^(){};
NSLog(@"int: %x\nblock: %x,\ncopied: %x", (unsigned int)&stackVar, (unsigned int)someBlock, (unsigned int)[someBlock copy]);

私はこれを得た:

int: bfffda70
block: 9d9948
copied: 9d9948

つまり、スタック変数に作成および割り当てられたブロックは、実際にはすでにヒープ上にあり、それらをコピーしても何も影響を受けません。

ただし、これは公式のソースによってバックアップされていません。ブロックがスタック上に作成されると、「受け渡し時」にコピーする必要があるとまだ述べられているためです。


私がテストする前の回答の一部で、どのドキュメントが例と矛盾しているかを述べています。

ARCへの移行に関するドキュメント の状態:

リターンなどのARCモードでスタックをブロックに渡すと、ブロックは「そのまま動作」します。 Block_copyに電話する必要はありません。スタックを「ダウン」して[^{} copy]および保持を行う他のメソッドに渡すときは、引き続きarrayWithObjects:を使用する必要があります。

およびブロックとプロパティに関するドキュメント は言う:

元のスコープ外のキャプチャされた状態を追跡するためにブロックをコピーする必要があるため、copyをプロパティ属性として指定する必要があります。

1
coverback