iOSベース:Blockの最下位実装と理解

16363 ワード

本論文では,最近blockの下位層を学習した後の理解を記録するために用いた.本文の参考博文:Block技巧と底層の解析はObjective-C blockの実現を話します
一、Blockコンパイル変換OC->C++
コマンドclang -rewrite-objcを使用することによって実現される.1.まず、mainを新規作成します.mファイル.2.端末を開く、cdをmainに送る.mファイルが存在するディレクトリ.3.clang -rewrite-objc main.mコマンドを入力して変換します.4.最後のmain.mファイルが存在するディレクトリの下でmainを新たに生成する.cppファイル.
二、Blockタイプ
1. NSConcreteGlobalBlock
以下の2つの場合、blockがNSConcreteGlobalBlock a.でグローバル変数を記述する箇所にblock構文がある場合.b.block構文の式で外部変数が使用されない場合.
更新(11.03:00):
ここには迅速な判断方法があります.Blockのbodyに外部の非グローバル変数と非static静的変数が使用されている場合、このBlockはスタック上に即_を作成します.NSConcreteStackBlock.逆に、参照変数がないか、グローバル変数のみが参照されているか、static静的変数がグローバルBlock_NSConcreteGlobalBlock由来:漫談Block
例1:
#include 

void (^globalBlock)() = ^{
    
};

int main()
{
    globalBlock();
    return 0;
}

変換後のC++コード:
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __globalBlock_block_impl_0 {
  struct __block_impl impl;
  struct __globalBlock_block_desc_0* Desc;
  __globalBlock_block_impl_0(void *fp, struct __globalBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

構造体__block_implisaポインタはNSConcreteGlobalBlockを指す.
例2:main関数でblockを作成しようとしたが、blockは変数をキャプチャしなかったが、clang変換によりisaポインタがNSConcreteStackBlockを指していることが分かった.この点はおかしい.「ocプレミアムプログラミング」でこんな言葉を見ました.
関数内で広域変数が記述されていない場所でBlock構文を使用する場合でも、Blockが自動変数をキャプチャしない限り、Block用構造体インスタンスをプログラムのデータ領域に設定することができる.
2. NSConcreteStackBlock
スタックに保存されたblockは、関数が戻ると破棄されます.
#include 
int main() {
    int a = 100;
    void (^block2)(void) = ^{
        a;
    };
    return 0;
}

変換後のC++コード:
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

構造体__block_implisaポインタはNSConcreteStackBlockを指す.
3. NSConcreteMallocBlock
スタックに保存されたblockは、参照カウントが0の場合に破棄されます.しかしNSConcreteMallocBlockタイプのblockは通常ソースコードに直接現れず、[block copy]の場合、スタックにコピーされる.(以下のコードは、Objective-C blockの実装について)
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    // 1
    if (!arg) return NULL;
    // 2
    aBlock = (struct Block_layout *)arg;
    // 3
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 4
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    // 5
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    // 6
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    // 7
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;
    // 8
    result->isa = _NSConcreteMallocBlock;
    // 9
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }
    return result;
}

blockがcopyされると、_Block_copy_internalメソッドが呼び出され、内部result->isa = _NSConcreteMallocBlock;に呼び出される.
更新(11.03 9:45):
ありがとうございます.彼がくれたソースコードの中で以下のコードを見つけました.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

構造体中のflagsによってcopyが判断されることは明らかである.
次にcopyの例を挙げると,ARCではBlockが関数として値を返すときにNSConcreteMallocBlockタイプにコピーされ,本質はcopyメソッドを呼び出すことである.次のコードがあります.
typedef void (^Block)();

Block getBlock() {
  char c = 'YQ';
  void (^block)() = ^{
    printf("%c", e);
  };
  return block;
}

void main {
  Block block = getBlock();
  block();
}

ARC環境では、システムが作成されたときにobjc_retainBlockメソッドを呼び出すため、正常に動作することができますが、objc_retainBlockメソッドは実際にはBlock_copyメソッドです.(runtime/objc-arr.mmから)従って本質的には、以上のコードのシステム実装プロセスは、スタックにblock構造体オブジェクトを作成し、Block_copyを介してスタックにコピーし、その後、スタック上のオブジェクトを自動リリースプールに登録し、このスタック上のオブジェクトに戻るようになる.しかし、MRC環境では、システムが自動的にコピーしないため、クラッシュする.手動コピーが必要:[block copy]三、Blockのコピー
3種類のBlockコピー:NSConcreteGlobalBlockコピー後、何もしません.NSConcreteStackBlockコピー後、スタックからスタックにコピーします.NSConcreteMallocBlockコピー後、参照カウントを1つ加算します.
四、_block変数のコピー
int main()
{
    __block int i = 0;
    void (^block)(void) = ^{
        i = 1;
    };
    return 0;
}
struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        (i->__forwarding->i) = 1;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    return 0;
}

使用する場合ブロック修飾子の場合、基本データ型iは__Block_byref_i_0構造体に変換される.__Block_byref_i_0構造体にはisaポインタがあり、これもオブジェクトであることを示しています.blockが変数を変更すると、次のコードが呼び出されます.
 __Block_byref_i_0 *i = __cself->i; // bound by ref
(i->__forwarding->i) = 1;

ぐるぐる回っているのを見つけて、下に整理します.最も理解できないのは__forwardingポインタです.__forwardingポインタは常に自分を指しています.__block int iがスタック内にあるとき、__forwardingはスタック内の自分を指す.iがスタックにコピーされると、__forwardingは、スタック内の自分を指す.
『ocプレミアムプログラミング』で述べたように、
__ブロック修飾変数は構造体メンバー変数__forwardingで実現できるblock変数はスタック上でもスタック上でも正しくアクセスできます_block変数.
五、Blockコピーペア_block変数の影響
に影響
Blockが使用していたら_ブロック変数、ブロックがスタックからスタックにコピーされると、a.スタックの_block変数はスタックにコピーされ、Blockによって保持されます.b.山の中の_block変数はBlockによって保持される.
これはOCの参照カウントメモリ管理と同じです.下にBlockA、BlockB、_block a、 __block b. BlockAがスタック上のaとbを使用している場合、[BlockA copy]がスタック上にコピーされると、a,bも同時にスタック上にコピーされ、スタック上のBlockAはスタック上のa,bを保持する.同様に、BlockA、BlockBはスタック上のaを使用し、[BlockA copy]、[BlockB copy]がスタック上にコピーされると、BlockAとBlockBはスタック上のaを同時に保持する.
4時の__block int iを振り返ると、なぜ__forwarding針があるのか理解できます.次のコードがあります.
int main()
{
    __block int i = 0;
    void (^block)(void) = [^{
        ++i;
    } copy];
    ++i;
    block();
    printf("%d", i);
    return 0;
}

上のコードをC++に変換すると、
// block  ++i  
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        ++(i->__forwarding->i);
    }
// main    ++i
++(i.__forwarding->i);

いずれも++(i.__forwarding->i);であり,すなわち,いずれもスタック中のiを指していることがわかる.したがって出力は2です.
六、いくつかのテーマ、ARCとMRC環境の下で運行できるかどうかを判断する
1すべて実行可能
void exampleA() {
  char a = 'A';
  ^{
    printf("%cn", a);
  }();
}

2 ARC運転可能
void exampleB_addBlockToArray(NSMutableArray *array) {
  char b = 'B';
  [array addObject:^{
    printf("%cn", b);
  }];
}

void exampleB() {
  NSMutableArray *array = [NSMutableArray array];
  exampleB_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}

3すべて実行可能
void exampleC_addBlockToArray(NSMutableArray *array) {
  [array addObject:^{
    printf("Cn");
  }];
}

void exampleC() {
  NSMutableArray *array = [NSMutableArray array];
  exampleC_addBlockToArray(array);
  void (^block)() = [array objectAtIndex:0];
  block();
}

4 ARC運転可能
typedef void (^dBlock)();

dBlock exampleD_getBlock() {
  char d = 'D';
  return ^{
    printf("%cn", d);
  };
}

void exampleD() {
  exampleD_getBlock()();
}

5 ARC運転可能
typedef void (^eBlock)();

eBlock exampleE_getBlock() {
  char e = 'E';
  void (^block)() = ^{
    printf("%cn", e);
  };
  return block;
}

void exampleE() {
  eBlock block = exampleE_getBlock();
  block();
}

七、更新(11.03 12:00)blockによる各種変数の処理
漫談Block後半を見て、その部分を整理したいと思いました.まずは!取得した変数がid, NSObject, __attribute__((NSObject)), block型の場合block修飾変数は_Block_object_assignメソッドを呼び出す._Block_object_assignの方法を一番上に書きます!
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;
      
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
      default:
        break;
    }
}

1.変数が基本データ型無_block
#include 
int main() {
    int a = 100;
    void (^block2)(void) = ^{
        a;
    };
    return 0;
}

blockの構造体内部にint a変数を追加するだけです.修正できません.
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

2.変数が基本データ型であり_block
int main()
{
    __block int i = 0;
    void (^block)(void) = ^{
        i = 1;
    };
    return 0;
}

あります_block修飾の基本データ型は__Block_byref_i_0構造体に変換される.
struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

そして彼がコピーされると_Block_object_assignメソッドのBLOCK_FIELD_IS_BYREF caseに入ります
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            ...

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    ...
    return src->forwarding;
}

コード発見により、copyオブジェクトがスタック上に作成された.そして
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy

常に山の上の自分を指さすようにします.
3.変数がオブジェクトなし_block BLOCK_FIELD_IS_OBJECTに入ります
case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;
static void _Block_retain_object_default(const void *ptr __unused) { }
//   _Block_retain_object    _Block_retain_object_default,      
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

// Called from CF to indicate MRR. Newer version uses a versioned structure, so we can add more functions
// without defining a new entry point.
void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}


デフォルトの_Block_retain_objectは、_Block_retain_object_defaultに割り当てられています.つまり、何もしません.すなわち,ARC環境ではBlockがここでオブジェクトを持つことはない.(ARC環境ではより完全なメモリ管理が行われており、外部変数が__strong、copy、strongで修飾されると、Blockはキャプチャされた変数を__strongで修飾して保持する目的を達成する.)MRR環境では、_Block_retain_objectid, NSObject, __attribute__((NSObject))型変数をBlockが保持する.
4.変数がオブジェクトにあります_block BLOCK_FIELD_IS_OBJECTに入ります
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;

直接付与.だからblock修飾は_Block_retain_objectメソッドへの呼び出しを避けることができ,すなわちMRR環境下で_blockは、Blockが変数を強く持つことを回避し、循環参照を回避する.