精华内容
下载资源
问答
  • 一处或多处循环引用
    万次阅读
    2022-04-01 18:25:57

    一、前言

    • Objective-C 使用引用计数作为 iPhone 应用的内存管理方案,引用计数相比 GC 更适用于内存不太充裕的场景,只需要收集与对象关联的局部信息来决定是否回收对象,而 GC 为了明确可达性,需要全局的对象信息。引用计数固然有其优越性,但也正是因为缺乏对全局对象信息的把控,导致 Objective-C 无法自动销毁陷入循环引用的对象。虽然 Objective-C 通过引入弱引用技术,让开发者可以尽可能地规避这个问题,但在引用层级过深,引用路径不那么直观的情况下,即使是经验丰富的工程师,也无法百分百保证产出的代码不存在循环引用。
    • 这时候就需要有一种检测方案,可以实时检测对象之间是否发生了循环引用,来辅助开发者及时地修正代码中存在的内存泄漏问题。要想检测出循环引用,最直观的方式是递归地获取对象强引用的其他对象,并判断检测对象是否被其路径上的对象强引用了,也就是在有向图中去找环。明确检测方式之后,接下来需要解决的是如何获取强引用链,也就是获取对象的强引用,尤其是最容易造成循环引用的 block。

    二、Block 捕获实体引用

    ① 捕获区域布局

    • 根据 block 的定义结构,可以简单地将其视为:
    struct sr_block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct sr_block_descriptor *descriptor;
        /* Imported variables. */
    };
    
    // 标志位不一样,这个结构的实际布局也会有差别,这里简单地放在一起好阅读
    struct sr_block_descriptor {
        unsigned long reserved; 		 // Block_descriptor_1
        unsigned long size; 			 // Block_descriptor_1
        void (*)(void *dst, void *src);  // Block_descriptor_2 BLOCK_HAS_COPY_DISPOSE
        void (*dispose)(void *); 		 // Block_descriptor_2
        const char *signature; 		     // Block_descriptor_3 BLOCK_HAS_SIGNATURE
        const char *layout; 			 // Block_descriptor_3 contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
    
    • 可以看到 block 捕获的变量都会存储在 sr_block_layout 结构体 descriptor 字段之后的内存空间中,通过 clang -rewrite-objc 重写如下代码语句:
    int i = 2;
    ^{
        i;
    };
    
    • 可以得到 :
    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int i;
      ...
    };
    
    • __main_block_impl_0 结构中新增了捕获的 i 字段,即 sr_block_layout 结构体的 imported variables 部分,这种操作可以看作在 sr_block_layout 尾部定义了一个 0 长数组,可以根据实际捕获变量的大小,给捕获区域申请对应的内存空间,只不过这一操作由编译器完成:
    struct sr_block_layout {
        void *isa;
        int flags;
        int reserved;
        void (*invoke)(void *, ...);
        struct sr_block_descriptor *descriptor;
        char captured[0];
    };
    
    • 既然已经知道捕获变量 i 的存放地址,那么就可以通过 *(int *)layout->captured 在运行时获取 i 的值,得到捕获区域的起始地址之后,再来看捕获区域的布局问题,考虑以下代码块:
    int i = 2;
    NSObject *o = [NSObject new];
    void (^blk)(void) = ^{
        i;
        o;
    };
    
    • 捕获区域的布局分两部分看:顺序和大小,先使用老方法重写代码块:
    struct __main_block_impl_0 {
      struct __block_impl impl;           // 24
      struct __main_block_desc_0* Desc;   // 8 指针占用内存大小和寻址长度相关,在 64 位机环境下,编译器分配空间大小为 8 字节
      int i;                              // 8
      NSObject *o;                        // 8
      ...
    };
    
    • 按照目前 clang 针对 64 位机的默认对齐方式(下文的字节对齐计算都基于此前提条件),可以计算出这个结构体占用的内存空间大小为 24 + 8 + 8 + 8 = 48字节,并且按照上方代码块先 i 后 o 的捕获排序方式,如果要访问捕获的 o 对象指针变量,只需要在捕获区域起始地址上偏移 8 字节即可,可以借助 lldb 的 memory read (x) 命令查看这部分内存空间:
    (lldb) po *(NSObject **)(layout->captured + 8)
    0x0000000000000002
    (lldb) po *(NSObject **)layout->captured
    <NSObject: 0x10073f290>
    (lldb) p *(int *)(layout->captured + 8)
    (int) $6 = 2
    (lldb) p (int *)(layout->captured + 8)
    (int *) $9 = 0x0000000100740d18
    (lldb) p layout->descriptor->size
    (unsigned long) $11 = 44
    (lldb) x/44bx layout
    0x100740cf0: 0x70 0x21 0x7b 0xa6 0xff 0x7f 0x00 0x00
    0x100740cf8: 0x02 0x00 0x00 0xc3 0x00 0x00 0x00 0x00
    0x100740d00: 0x40 0x1d 0x00 0x00 0x01 0x00 0x00 0x00
    0x100740d08: 0xb0 0x20 0x00 0x00 0x01 0x00 0x00 0x00
    0x100740d10: 0x90 0xf2 0x73 0x00 0x01 0x00 0x00 0x00
    0x100740d18: 0x02 0x00 0x00 0x00
    
    • 和使用 clang -rewrite-objc 重写时的猜想不一样,可以从以上终端日志中看出以下两点:
      • 捕获变量 i、o 在捕获区域的排序方式为 o、i,o 变量地址与捕获起始地址一致,i 变量地址为捕获起始地址加上 8 字节;
      • 捕获整形变量 i 在内存中实际占用空间大小为 4 字节;
    • 那么 block 到底是怎么对捕获变量进行排序,并且为其分配内存空间的呢?这就需要看 clang 是如何处理 block 捕获的外部变量。

    ② 捕获区域布局分析

    • 首先解决捕获变量排序的问题,根据 clang 针对这部分的排序代码,可以知道,在对齐字节数 (alignment) 不相等时,捕获的实体按照 alignment 降序排序 (C 结构体比较特殊,即使整体占用空间比指针变量大,也排在对象指针后面),否则按照以下类型进行排序:
      • __strong 修饰对象指针变量;
      • __block 修饰对象指针变量;
      • __weak 修饰对象指针变量;
      • 其他变量;
    • 再结合 clang 对捕获变量对齐子节数计算方式 ,可以知道,block 捕获区域变量的对齐结果趋向于被 attribute ((packed)) 修饰的结构体,举个例子:
    struct foo {
        void *p;    // 8
        int i;      // 4
        char c;     // 4 实际用到的内存大小为 1
    };
    
    • 创建 foo 结构体需要分配的空间大小为 8 + 4 + 4 = 16,关于结构体的内存对齐方式,编译器会按照成员列表的顺序一个接一个地给每个成员分配内存,只有当存储成员需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间,以提升计算机的访问速度(对齐标准一般和寻址长度一致),在声明结构体时,让那些对齐边界要求最严格的成员最先出现,对边界要求最弱的成员最后出现,可以最大限度地减少因边界对齐而带来的空间损失。再看以下代码块:
    struct foo {
        void *p;    // 8
        int i;      // 4
        char c;     // 1
    } __attribute__ ((__packed__));
    
    • attribute ((packed)) 编译属性会告诉编译器,按照字段的实际占用子节数进行对齐,所以创建 foo 结构体需要分配的空间大小为 8 + 4 + 1 = 13。
    • 结合以上两点,可以尝试分析以下 block 捕获区域的变量布局情况:
    NSObject *o1 = [NSObject new];
    __weak NSObject *o2 = o1;
    __block NSObject *o3 = o1;
    unsigned long long j = 4;
    int i = 3;
    char c = 'a';
    void (^blk)(void) = ^{
        i;
        c;
        o1;
        o2;
        o3;
        j;
    };
    
    • 按照 aligment 排序,可以得到排序顺序为 [o1 o2 o3] j i c,再根据 __strong、__block、__weak 修饰符对 o1 o2 o3 进行排序,可得到最终结果 o1[8] o3[8] o2[8] j[8] i[4] c[1]。同样的,我们使用 lldb 的 x 命令验证分析结果是否正确:
    (lldb) x/69bx layout
    0x10200d940: 0x70 0x21 0x7b 0xa6 0xff 0x7f 0x00 0x00
    0x10200d948: 0x02 0x00 0x00 0xc3 0x00 0x00 0x00 0x00
    0x10200d950: 0xf0 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x10200d958: 0xf8 0x20 0x00 0x00 0x01 0x00 0x00 0x00
    0x10200d960: 0xa0 0xf6 0x00 0x02 0x01 0x00 0x00 0x00  // o1
    0x10200d968: 0x90 0xd9 0x00 0x02 0x01 0x00 0x00 0x00  // o3
    0x10200d970: 0xa0 0xf6 0x00 0x02 0x01 0x00 0x00 0x00  // o2
    0x10200d978: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00  // j
    0x10200d980: 0x03 0x00 0x00 0x00 0x61                 // i c
    (lldb) p o1
    (NSObject *) $1 = 0x000000010200f6a0
    
    • 可以看到,小端模式下,捕获的 o1 和 o2 指针变量值为 0x10200f6a0,对应内存地址为 0x10200d960 和 0x10200d970,而 o3 因为被 __block 修饰,编译器为 o3 捕获变量包装了一层 byref 结构,所以其值为 byref 结构的地址 0x102000d990,而不是 0x10200f6a0,捕获的 j 变量地址为 0x10200d978,i 变量地址为 0x10200d980,c 字符变量紧随其后。

    ③ Descriptor 的 Layout 信息

    • 经过上述的一系列分析,捕获区域变量的布局方式已经大致清楚,接下来回过头看下 sr_block_descriptor 结构的 layout 字段是用来干什么的?从字面上理解,这个字段很可能保存了 block 某一部分的内存布局信息,比如捕获区域的布局信息,依然使用上文的最后一个例子,看看 layout 的值:
    (lldb) p layout->descriptor->layout
    (const char *) $2 = 0x0000000000000111 ""
    
    • 可以看到 layout 值为空字符串,并没有展示出任何直观的布局信息,看来要想知道 layout 是怎么运作的,可以阅读 block 代码clang 代码,继续一步步地分析这两段代码里面隐藏的信息,这里贴出其中的部分代码和注释:
    // block
    // Extended layout encoding.
    
    // Values for Block_descriptor_3->layout with BLOCK_HAS_EXTENDED_LAYOUT
    // and for Block_byref_3->layout with BLOCK_BYREF_LAYOUT_EXTENDED
    
    // If the layout field is less than 0x1000, then it is a compact encoding 
    // of the form 0xXYZ: X strong pointers, then Y byref pointers, 
    // then Z weak pointers.
    
    // If the layout field is 0x1000 or greater, it points to a 
    // string of layout bytes. Each byte is of the form 0xPN.
    // Operator P is from the list below. Value N is a parameter for the operator.
    
    enum {
        ...
        BLOCK_LAYOUT_NON_OBJECT_BYTES = 1,    // N bytes non-objects
        BLOCK_LAYOUT_NON_OBJECT_WORDS = 2,    // N words non-objects
        BLOCK_LAYOUT_STRONG           = 3,    // N words strong pointers
        BLOCK_LAYOUT_BYREF            = 4,    // N words byref pointers
        BLOCK_LAYOUT_WEAK             = 5,    // N words weak pointers
        ...
    };
    
    // clang 
    /// InlineLayoutInstruction - This routine produce an inline instruction for the
    /// block variable layout if it can. If not, it returns 0. Rules are as follow:
    /// If ((uintptr_t) layout) < (1 << 12), the layout is inline. In the 64bit world,
    /// an inline layout of value 0x0000000000000xyz is interpreted as follows:
    /// x captured object pointers of BLOCK_LAYOUT_STRONG. Followed by
    /// y captured object of BLOCK_LAYOUT_BYREF. Followed by
    /// z captured object of BLOCK_LAYOUT_WEAK. If any of the above is missing, zero
    /// replaces it. For example, 0x00000x00 means x BLOCK_LAYOUT_STRONG and no
    /// BLOCK_LAYOUT_BYREF and no BLOCK_LAYOUT_WEAK objects are captured.
    
    • 首先要解释的是 inline 这个词,Objective-C 中有一种叫做 Tagged Pointer 的技术,它让指针保存实际值,而不是保存实际值的地址,这里的 inline 也是相同的效果,即让 layout 指针保存实际的编码信息。在 inline 状态下,使用十六进制中的一位表示捕获变量的数量,所以每种类型的变量最多只能有 15 个,此时的 layout 的值以 0xXYZ 形式呈现,其中 X、Y、Z 分别表示捕获 __strong、__block、__weak 修饰指针变量的个数,如果其中某个类型的数量超过 15 或者捕获变量的修饰类型不为这三种任何一个时,比如捕获的变量由 __unsafe_unretained 修饰,则采用另一种编码方式,这种方式下,layout 会指向一个字符串,这个字符串的每个字节以 0xPN 的形式呈现,并以 0x00 结束,P 表示变量类型,N 表示变量个数,需要注意的是,N 为 0 表示 P 类型有一个,而不是 0 个,也就是说实际的变量个数比 N 大 1。
    • 需要注意的是,捕获 int 等基础类型,不影响 layout 的呈现方式,layout 编码中也不会有关于基础类型的信息,除非需要基础类型的编码来辅助定位对象指针类型的位置,比如捕获含有对象指针字段的结构体。
    • 如下所示:代码块没有捕获任何对象指针,所以实际的 descriptor 不包含 copy 和 dispose 字段:
    unsigned long long j = 4;
    int i = 3;
    char c = 'a';
    void (^blk)(void) = ^{
        i;
        c;
        j;
    };
    
    • 去除这两个字段后,再输出实际的布局信息,结果为空(0x00 表示结束),说明捕获一般基础类型变量不会计入实际的 layout 编码:
    (lldb) p/x (long)layout->descriptor->layout
    (long) $0 = 0x0000000100001f67
    (lldb) x/8bx layout->descriptor->layout
    0x100001f67: 0x00 0x76 0x31 0x36 0x40 0x30 0x3a 0x38
    
    • 接着尝试第一种 layout 方式:
    NSObject *o1 = [NSObject new];
    __block NSObject *o3 = o1;
    __weak NSObject *o2 = o1;
    void (^blk)(void) = ^{
        o1;
        o2;
        o3;
    };
    
    • 以上代码块对应的 layout 值为 0x111,表示三种类型变量每种一个:
    (lldb) p/x (long)layout->descriptor->layout
    (long) $0 = 0x0000000000000111
    
    • 再尝试第二种 layout 编码方式:
    NSObject *o1 = [NSObject new];
    __block NSObject *o3 = o1;
    __weak NSObject *o2 = o1;
    NSObject *o4 = o1;
    ... // 5 - 18
    NSObject *o19 = o1;
    void (^blk)(void) = ^{
        o1;
        o2;
        o3;
        o4;
        ... // 5 - 18
        o19;
    };
    
    • 以上代码块对应的 layout 值是一个地址 0x0000000100002f44 ,这个地址为编码字符串的起始地址,转换成十六进制后为 0x3f 0x30 0x40 0x50 0x00,其中 P 为 3 表示 __strong 修饰的变量,数量为 15(f) + 1 + 0 + 1 = 17 个,P 为 4 表示 __block 修饰的变量,数量为 0 + 1 = 1 个, P 为 5 表示 __weak 修饰的变量,数量为 0 + 1 = 1 个:
    (lldb) p/x (long)layout->descriptor->layout
    (long) $0 = 0x0000000100002f44
    (lldb) x/8bx layout->descriptor->layout
    0x100002f44: 0x3f 0x30 0x40 0x50 0x00 0x76 0x31 0x36
    

    ④ 结构体对捕获布局的影响

    • 由于结构体字段的布局顺序在声明时就已经确定,无法像 block 构造捕获区域一样,按照变量类型、修饰符进行调整,所以如果结构体中有类型为对象指针的字段,就需要一些额外信息来计算这些对象指针字段的偏移量,需要注意的是,被捕获结构体的内存对齐信息和未捕获时一致,以寻址长度作为对齐基准,捕获操作并不会变更对齐信息。
    • 同样地,先尝试捕获只有基本类型字段的结构体:
    struct S {
        char c;
        int i;
        long j;
    } foo;
    void (^blk)(void) = ^{
      foo;
    };
    
    • 然后调整 descriptor 结构,输出 layout :
    (lldb) x/8bx layout->descriptor->layout
    0x100001f67: 0x00 0x76 0x31 0x36 0x40 0x30 0x3a 0x38
    
    • 可以看到,只有含有基本类型的结构体,同样不会影响 block 的 layout 编码信息。给结构体新增 __strong 和 __weak 修饰的对象指针字段:
    struct S {
        char c;
        int i;
        __strong NSObject *o1;
        long j;
        __weak NSObject *o2;
    } foo;
    void (^blk)(void) = ^{
      foo;
    };
    
    • 同样分析输出 layout :
    (lldb) x/8bx layout->descriptor->layout
    0x100002f47: 0x20 0x30 0x20 0x50 0x00 0x76 0x31 0x36
    
    • layout 编码为0x20 0x30 0x20 0x50 0x00,其中 P 为 2 表示 word 字类型(非对象),由于字大小一般和指针一致,所以表示占用 8 * (N + 1) 个字节,第一个 0x20 表示非对象指针类型占用了 8 个字节,也就是 char 类型和 int 类型字段对齐之后所占用的空间,接着 0x30 表示有一个 __strong 修饰的对象指针字段,第二个 0x20 表示非对象指针 long 类型占用 8 个字节,最后的 0x50 表示有一个 __weak 修饰的对象指针字段。由于编码中包含每个字段的排序和大小,就可以通过解析 layout 编码后的偏移量,拿到想要的对象指针值。 P 还有个 byte 类型,值为 1,和 word 类型有相似的功能,只是表示的空间大小不同。

    ⑤ Byref 结构的布局

    • 由 __block 修饰的捕获变量,会先转换成 byref 结构,再由这个结构去持有实际的捕获变量,block 只负责管理 byref 结构:
    // 标志位不一样,这个结构的实际布局也会有差别,简单地放在一起好阅读
    struct sr_block_byref {
        void *isa;
        struct sr_block_byref *forwarding;
        // contains ref count
        volatile int32_t flags; 
        uint32_t size;
        // requires BLOCK_BYREF_HAS_COPY_DISPOSE
        void (*byref_keep)(struct sr_block_byref *dst, struct sr_block_byref *src);
        void (*byref_destroy)(struct sr_block_byref *);
        // requires BLOCK_BYREF_LAYOUT_EXTENDED
        const char *layout;
    };
    
    • 以上代码块就是 byref 对应的结构体,第一眼看上去,比较困惑为什么还要有 layout 字段,虽然 block 源码注释说明 byref 和 block 结构一样,都具备两种不同的布局编码方式,但是 byref 不是只针对一个变量吗,难道和 block 捕获区域一样也可以携带多个捕获变量?带着这个困惑,先看下以下表达式 :
    __block  NSObject *o1 = [NSObject new];
    
    • 使用 clang 重写之后:
    struct __Block_byref_o1_0 {
        void *__isa;
        __Block_byref_o1_0 *__forwarding;
        int __flags;
        int __size;
        void (*__Block_byref_id_object_copy)(void*, void*);
        void (*__Block_byre/* @autoreleasepool */o{ __AtAutoreleasePool __autoreleasepool; e)(void*);
        NSObject *o1;
    };
    
    • 和 block 捕获变量一样,byref 携带的变量也是保存在结构体尾部的内存空间里,当前上下文中,可以直接通过 sr_block_byref 的 layout 字段获取 o1 对象指针值。可以看到,在包装如对象指针这类常规变量时,layout 字段并没有起到实质性的作用,那什么条件下的 layout 才表示布局编码信息呢?如果使用 layout 字段表示编码信息,那么携带的变量又是何处安放的呢?
    • 针对第一个问题,先看以下代码块 :
    __block struct S {
        NSObject *o1;
    } foo;
    foo.o1 = [NSObject new];
    void (^blk)(void) = ^{
      foo;
    };
    
    • 使用 clang 重写之后:
    struct __Block_byref_foo_0 {
      void *__isa;
      __Block_byref_foo_0 *__forwarding;
      int __flags;
      int __size;
      void (*__Block_byref_id_object_copy)(void*, void*);
      void (*__Block_byref_id_object_dispose)(void*);
      struct S foo;
    };
    
    • 和常规类型一样,foo 结构体保存在结构体尾部,也就是原本 layout 所在的字段,重写的代码中依然看不到 layout 的踪影,接着输出 foo :
    (lldb) po foo.o1
    <NSObject: 0x10061f130>
    (lldb) p (struct S)a_byref->layout
    error: Multiple internal symbols found for 'S'
    (lldb) p/x (long)a_byref->layout
    (long) $3 = 0x0000000000000100
    (lldb) x/56bx a_byref
    0x100627c20: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x100627c28: 0x20 0x7c 0x62 0x00 0x01 0x00 0x00 0x00
    0x100627c30: 0x04 0x00 0x00 0x13 0x38 0x00 0x00 0x00
    0x100627c38: 0x90 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x100627c40: 0x00 0x1c 0x00 0x00 0x01 0x00 0x00 0x00
    0x100627c48: 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00
    0x100627c50: 0x30 0xf1 0x61 0x00 0x01 0x00 0x00 0x00
    
    • 看来事情并没有看上去的那么简单,首先重写代码中 foo 字段所在内存保存的并不是结构体,而是 0x0000000000000100,这个 100 是不是看着有点眼熟?没错,这就是 byref 的 layout 信息,根据 0xXYZ 编码规则,这个值表示有 1 个 __strong 修饰的对象指针。
    • 接着针对第二个问题,携带的对象指针变量存在哪?往下移动 8 个字节,这不就是 foo.o1 对象指针的值么?总结下,在存在 layout 的情况下,byref 使用 8 个字节保存 layout 编码信息,并紧跟着在 layout 字段后存储捕获的变量。
    • 以上是 byref 的第一种 layout 编码方式,再尝试第二种:
    __block struct S {
        char c;
        NSObject *o1;
        __weak NSObject *o3;
    } foo;
    foo.o1 = [NSObject new];
    void (^blk)(void) = ^{
      foo;
    };
    
    • 使用 clang 重写代码之后 :
    struct __Block_byref_foo_0 {
      void *__isa;
    __Block_byref_foo_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*/* @autoreleasepool */c{ __AtAutoreleasePool __autoreleasepool; _byref
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    • 上面代码并不是粘贴错误,貌似 Rewriter 并不能很好地处理这种情况,看来又需要直接去看对应内存地址中的值:
    (lldb) x/72bx a_byref
    0x100755140: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x100755148: 0x40 0x51 0x75 0x00 0x01 0x00 0x00 0x00
    0x100755150: 0x04 0x00 0x00 0x13 0x48 0x00 0x00 0x00
    0x100755158: 0x10 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x100755160: 0xa0 0x1b 0x00 0x00 0x01 0x00 0x00 0x00
    0x100755168: 0x8d 0x3e 0x00 0x00 0x01 0x00 0x00 0x00
    0x100755170: 0x00 0x5f 0x6b 0x65 0x79 0x00 0x00 0x00
    0x100755178: 0xd0 0x6e 0x75 0x00 0x01 0x00 0x00 0x00
    0x100755180: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    (lldb) x/8bx a_byref->layout
    0x100003e8d: 0x20 0x30 0x50 0x00 0x53 0x52 0x4c 0x61
    
    • 地址 0x100755168 中保存 layout 编码字符串的地址 0x0000000100003e8d ,将此字符串转换成十六进制后为 0x20 0x30 0x50 0x00。

    ⑥ 强引用对象的获取

    • 已经知道 block / byref 如何布局捕获区域内存,以及如何获取关键的布局信息,接下来就可以尝试获取 block 强引用的对象,强引用的对象可以分成两部分:
      • 被 block 强引用;
      • 被 byref 结构强引用。
    • 只要获取这两部分强引用的对象就可以了,由于上文已经将整个原理脉络理清,所以编写出可用的代码并不困难。这两部分都涉及到布局编码,先根据 layout 的编码方式,解析出捕获变量的类型和数量:
    SRCapturedLayoutInfo *info = [SRCapturedLayoutInfo new];
        
    if ((uintptr_t)layout < (1 << 12)) {
        uintptr_t inlineLayout = (uintptr_t)layout;
        [info addItemWithType:SR_BLOCK_LAYOUT_STRONG count:(inlineLayout & 0xf00) >> 8];
        [info addItemWithType:SR_BLOCK_LAYOUT_BYREF count:(inlineLayout & 0xf0) >> 4];
        [info addItemWithType:SR_BLOCK_LAYOUT_WEAK count:inlineLayout & 0xf];
    } else {
        while (layout && *layout != '\x00') {
            unsigned int type = (*layout & 0xf0) >> 4;
            unsigned int count = (*layout & 0xf) + 1;
            
            [info addItemWithType:type count:count];
            layout++;
        }
    }
    
    • 然后遍历 block 的布局编码信息,根据变量类型和数量,计算出对象指针地址偏移,获取对应的对象指针值:
    - (NSHashTable *)strongReferencesForBlockLayout:(void *)iLayout {
        if (!iLayout) return nil;
        
        struct sr_block_layout *aLayout = (struct sr_block_layout *)iLayout;
        const char *extenedLayout = sr_block_extended_layout(aLayout);
        _blockLayoutInfo = [SRCapturedLayoutInfo infoForLayoutEncode:extenedLayout];
        
        NSHashTable *references = [NSHashTable weakObjectsHashTable];
        uintptr_t *begin = (uintptr_t *)aLayout->captured;
        for (SRLayoutItem *item in _blockLayoutInfo.layoutItems) {
            switch (item.type) {
                case SR_BLOCK_LAYOUT_STRONG: {
                    NSHashTable *objects = [item objectsForBeginAddress:begin];
                    SRAddObjectsFromHashTable(references, objects);
                    begin += item.count;
                } break;
                case SR_BLOCK_LAYOUT_BYREF: {
                    for (int i = 0; i < item.count; i++, begin++) {
                        struct sr_block_byref *aByref = *(struct sr_block_byref **)begin;
                        NSHashTable *objects = [self strongReferenceForBlockByref:aByref];
                        SRAddObjectsFromHashTable(references, objects);
                    }
                } break;
                case SR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {
                    begin = (uintptr_t *)((uintptr_t)begin + item.count);
                } break;
                default: {
                    begin += item.count;
                } break;
            }
        }
        return references;
    }
    
    • block 布局区域中的 byref 结构需要进行额外的处理,如果 byref 直接携带 __strong 修饰的变量,则不需要关心 layout 编码,直接从结构尾部获取指针变量值即可,否则需要和处理 block 布局区域一样,先得到布局信息,然后遍历这些布局信息,计算偏移量,获取强引用对象地址:
    - (NSHashTable *)strongReferenceForBlockByref:(void *)iByref {
        if (!iByref) return nil;
        
        struct sr_block_byref *aByref = (struct sr_block_byref *)iByref;
        NSHashTable *references = [NSHashTable weakObjectsHashTable];
        int32_t flag = aByref->flags & SR_BLOCK_BYREF_LAYOUT_MASK;
        
        switch (flag) {
            case SR_BLOCK_BYREF_LAYOUT_STRONG: {
                void **begin = sr_block_byref_captured(aByref);
                id object = (__bridge id _Nonnull)(*(void **)begin);
                if (object) [references addObject:object];
            } break;
            case SR_BLOCK_BYREF_LAYOUT_EXTENDED: {
                const char *layout = sr_block_byref_extended_layout(aByref);
                SRCapturedLayoutInfo *info = [SRCapturedLayoutInfo infoForLayoutEncode:layout];
                [_blockByrefLayoutInfos addObject:info];
                
                uintptr_t *begin = (uintptr_t *)sr_block_byref_captured(aByref) + 1;
                for (SRLayoutItem *item in info.layoutItems) {
                    switch (item.type) {
                        case SR_BLOCK_LAYOUT_NON_OBJECT_BYTES: {
                            begin = (uintptr_t *)((uintptr_t)begin + item.count);
                        } break;
                        case SR_BLOCK_LAYOUT_STRONG: {
                            NSHashTable *objects = [item objectsForBeginAddress:begin];
                            SRAddObjectsFromHashTable(references, objects);
                            begin += item.count;
                        } break;
                        default: {
                            begin += item.count;
                        } break;
                    }
                }
            } break;
            default: break;
        }
        return references;
    }
    

    ⑦ 另一种强引用对象获取方式

    • 上文通过将 block 的布局编码信息转化为对应字段的偏移量来获取强引用对象,还有另外一种比较取巧的方式,也是目前检测循环引用工具获取 block 强引用对象的常用方式,比如 facebook 的 FBRetainCycleDetector
    • 根据 FBRetainCycleDetector 对应的源码,此方式大致原理如下:
      • 获取 block 的 dispose 函数 (如果捕获了强引用对象,需要利用这个函数解引用);
      • 构造一个 fake 对象,此对象由若干个扩展的 byref 结构 (对象) 组成,其个数由 block size 决定,即把 block 划分为若干个 8 字节内存区域,就像以下代码块一样 :
    struct S {
        NSObject *o1;
        NSObject *o2;
    };
    struct S s = {
        .o2 = [NSObject new]
    };
    void **fake = (void **)&s;
    // fake[1] 和 s.o2 是一样的
    
      • 扩展的 byref 结构会重写 release 方法,只在此方法中设置强引用标识位,不执行原释放逻辑;
      • 将 fake 对象作为参数,调用 dispose 函数,dispose 函数会去 release 每个 block 强引用的对象,这些强引用对象被替换成 byref 结构,所以可以通过它的强引用标识位判断 block 的哪块区域保存了强引用对象地址;
      • 遍历 fake 对象,保存所有强引用标志位被设置的 byref 结构对应索引,通过这个索引可以去 block 中找强引用指针地址;
      • 释放所有的 byref 结构;
      • 根据上面得到的索引,获取捕获变量偏移量,偏移量为索引值 * 8 字节 (指针大小) ,再根据偏移量去 block 内存块中拿强引用对象地址。
    • 关于这种方案,需要明确:
      • 首先这种方案也需要在明确 block 内存布局的情况下才能够实施,因为 block ,或者说 block 结构体,实际执行内存对齐时,并没有按照寻址大小也就是 8 字节对齐,假设 block 捕获区域的对齐方式变成如下的这样 :
    struct __main_block_impl_0 {
      struct __block_impl impl;           // 24
      struct __main_block_desc_0* Desc;   // 8 指针占用内存大小和寻址长度相关,在 64 位机环境下,编译器分配空间大小为 8 字节
      int i;                              // 4    FakedByref 8
      NSObject *o1;                       // 8    FakedByref 8 [这里上个 FakedByref 后 4 个子节和当前 FakedByref 前 4 字节覆盖 o1 对象指针的 8 字节,导致 miss ]
      char c;                             // 1
      NSObject *o2;                       // 8
    }
    
      • 那么使用 fake 的方案就会失效,因为这种方案的前提是 block 内存对齐基准基于寻址长度,即指针大小。不过 block 对捕获的变量按照类型和尺寸进行了排序,__strong 修饰的对象指针都在前面,本来只需要这种类型的变量,并不关心其它类型,所以即使后面的对齐方式不满足 fake 条件也没关系,另外捕获结构体的对齐基准是基于寻址长度的,即使结构体有其他类型,也满足 fake 条件 :
    struct __main_block_impl_0 {
      struct __block_impl impl;           // 24
      struct __main_block_desc_0* Desc;   // 8 指针占用内存大小和寻址长度相关,在 64 位机环境下,编译器分配空间大小为 8 字节
      NSObject *o1;                       // 8    FakedByref 8
      NSObject *o2;                       // 8    FakedByref 8
      int i;                              // 4    FakedByref 8
      char c;                             // 1        
    }
    
      • 可以看到,通过以上代码块的排序,让 o1 和 o2 都被 FakedByref 结构覆盖,而 i、c 变量本身就不会在 dispose 函数中访问,因此怎么设置都不会影响到策略的生效;
      • 第二点是为什么要用扩展的 byref 结构,而不是随便整个重写 release 的类,这是因为当 block 捕获了 __block 修饰的指针变量时,会将这个指针变量包装成 byref 结构,而 dispose 函数会对这个 byref 结构执行 _Block_object_dispose 操作,这个函数有两个形参,一个是对象指针,一个是 flag,当 flag 指明对象指针为 byref 类型,而实际传入的实参不是,就会出现问题,所以必须用扩展的 byref 结构;
      • 第三点是这种方式无法处理 __block 修饰对象指针的情况。
    • 不过这种方式贵在简洁,无需考虑内部每种变量类型具体的布局方式,就可以满足大部分需要获取 block 强引用对象的场景。

    三、对象成员变量强引用

    • 对象强引用成员变量的获取相对来说直接些,因为每个对象对应的类中都有其成员变量的布局信息,并且 runtime 有现成的接口,只需要分析出编码格式,然后按顺序和成员变量匹配即可。获取编码信息的接口有两个, class_getIvarLayout 函数返回描述 strong ivar 数量和索引信的编码信息,相对的 class_getWeakIvarLayout 函数返回描述 weak ivar 的编码信息。
    • class_getIvarLayout 返回值是一个 uint8 指针,指向一个字符串,uint8 在 16 进制下占用 2 位,所以编码以 2 位为一组,组内首位描述非 strong ivar 个数,次位为 strong ivar 个数,最后一组如果 strong ivar 个数为 0,则忽略,且 layout 以 0x00 结尾。
    • 如下所示:
    // 0x0100
    @interface A : NSObject {
        __strong NSObject *s1;
    }
    @end
    
    • 起始非 strong ivar 个数为 0,并且接着一个 strong ivar ,得出编码为 0x01 。
    // 0x0100
    @interface A : NSObject {
        __strong NSObject *s1;
        __weak NSObject *w1;
    }
    @end
    
    • 起始非 strong ivar 个数为 0,并且接着一个 strong ivar ,得出编码为 0x01,接着有个 weak ivar,但是后面没有 strong ivar,所以忽略。
    // 0x011100
    @interface A : NSObject {
        __strong NSObject *s1;
        __weak NSObject *w1;
        __strong NSObject *s2;
    }
    @end
    
    • 起始非 strong ivar 个数为 0,并且接着一个 strong ivar ,得出编码为 0x01,接着有个 weak ivar,并且后面紧接着一个 strong ivar ,得出编码 0x11 ,合并得到 0x0111。
    // 0x211100
    @interface A : NSObject {
        int i1;
        void *p1;
        __strong NSObject *s1;
        __weak NSObject *w1;
        __strong NSObject *s2;
    }
    @end
    
    • 起始非 strong ivar 个数为 2,并且紧接着一个 strong ivar,得出编码 0x21,接着有个 weak ivar,后面紧接着一个 strong ivar ,得出编码 0x11 ,合并得到 0x2111。
    • 了解了成员变量的编码格式,剩下的就是如何解码并依次和成员变量进行匹配, FBRetainCycleDetector 已经实现了这部分功能 ,主要原理如下:
      • 获取所有的成员变量以及 ivar 编码;
      • 解析 ivar 编码,跳过非 strong ivar ,获得 strong ivar 所在索引值 (把对象分成若干个 8 字节内存片段);
      • 利用 ivar_getOffset 函数获取 ivar 的偏移量,除以指针大小就是自身的索引值 (对象布局对齐基准为寻址长度,这里为 8 字节);
      • 匹配 2、3 步获得的索引值,得到 strong ivar;
      • 实现了对结构体的处理。

    四、总结

    • “Block 捕获实体引用”和“对象成员变量强引用”是检测循环引用两个比较关键的点,特别是获取 block 捕获的强引用对象环节,block ABI 中并没有详细说明捕获区域布局信息,需要自己结合 block 源码以及 clang 生成 block 的 CodeGen 逻辑去推测实际的布局信息。
    更多相关内容
  • 如果Excel内容比较,那么可以通过复制多一份,然后在复制的那份进行删除行列来排查 比如:删除第整行,然后保存,关闭Excel,然后重新打开,如果还提示说明不是当前删除行问题,以此类推,直到重新打开没有...

    【Excel文档提示】

    【排查方式】

    如果Excel内容比较多,那么可以通过复制多一份,然后在复制的那份进行删除行或列来排查

    比如:删除第一整行,然后保存,关闭Excel,然后重新打开,如果还提示说明不是当前删除行问题,以此类推,直到重新打开没有提示,那么就说明肯定是当前行有问题

    【当前存在问题的行】

     

    展开全文
  • 循环引用,指的是个对象相互引用时,使得引用形成个环形,导致外部无法真正是否掉这块环形内存。其实有点类似死锁。 举个例子:A->B->C->….->X->B ->表示强引用,这样的B的引用计数就是2,假如A被系统释放了,...
  • block的循环引用

    千次阅读 2021-12-03 19:33:05
    相互循环引用:如果当前block对当前对象的某成员变量进行捕获的话,可能会对它产生强引用。根据block的变量捕获机制,如果block被拷贝到堆上,且捕获的是对象类型的auto变量,则会连同其所有权修饰符一起捕获,...

    前言

    前面我们了解了block的捕获变量机制 block的copy实现,本文通过底层代码说一下block的循环引用

    为什么 block 会产生循环引用

    Q: 为什么 block 会产生循环引用?
    相互循环引用:
    如果当前block访问当前对象的某一成员变量,会讲当前对象进行捕获,可能会对它产生强引用。根据block的变量捕获机制,如果block被拷贝到堆上,且捕获的是对象类型的auto变量,则会连同其所有权修饰符一起捕获,所以如果对象是__strong修饰,则block会对它产生强引用(如果block在栈上就不会强引用)。而当前block可能又由于当前对象对其有一个强引用,就产生了相互循环引用的问题
    多方循环引用:
    如果使用__block的话,在ARC下可能会产生循环引用,由于__block修饰符会将变量包装成一个对象,如果block被拷贝到堆上,则会直接对__block变量产生强引用,而__block如果修饰的是对象的话,会根据对象的所有权修饰符做出相应的操作,形成强引用或者弱引用,如果对象是__strong修饰,则__block变量对它产生强引用,如果这时候该对象是对block持有强引用的话,就产生了三方循环引用的问题。

    相互循环引用
    请添加图片描述
    多方循环引用
    请添加图片描述

    为什么我们要处理循环引用?

    循环引用会导致实例对象不能释放 也就是实例对象所占用的内存不能及时被系统回收,会造成内存泄漏。

    内存泄漏又是什么?危害是什么?
    已经动态分配的堆内存由于某种原因程序未释放或无法释放造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

    常见的内存泄漏有哪些
    1、对象之间的循环引用问题
    2、block的循环引用
    3、delegate 的循环引用
    4、通知的循环引用
    5、NSTimer和target的循环引用
    6、WKWebView 和self 之间的循环引用
    了解更多看我这篇文章

    所以我们在ARC环境下 特别容易block循环引用。

    ARC

    定义一个 Person类 代码如下

    typedef void(^MyBlock)(void);
    @interface Person : NSObject {
        @public NSString *name;
    }
    
    @property (nonatomic, assign) int age;
    @property (nonatomic, strong) MyBlock myblock;
    @property (nonatomic, strong) void(^block2)(void);
    @end
    
    @implementation Person
    
    - (void)dealloc {
        NSLog(@"%s", __func__);
    }
    @end
    

    VC类中调用

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        Person *person = [[Person alloc] init];
        person.age = 30;
        person.myblock = ^{
            NSLog(@"-------30");
        };
        NSLog(@"%@",[person.myblock class]);
        NSLog(@"%ld",CFGetRetainCount((CFTypeRef)(person)));
    }
    
    - (void)dealloc {
        NSLog(@"%s",__func__);
    }
    
    

    输出

    __NSGlobalBlock__
    1
    -[Person dealloc]
    -[VC dealloc]
    

    分析

    Person实例对象销毁是正常
    VC实例对象销毁是正常

    看一个不正常的情况 代码如下

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        Person *person = [[Person alloc] init];
        person.age = 30;
        person.myblock = ^{
            NSLog(@"-------%d", person.age);
        };
        NSLog(@"%ld",CFGetRetainCount((CFTypeRef)(person)));
    }
    - (void)dealloc {
        NSLog(@"%s",__func__);
    }
    

    输出

    __NSMallocBlock__
    3
    -[VC dealloc]
    

    发生循环引用导致Person实例对象没有被释放,会引起内存泄漏

    Clang
    Person.cpp

    重要代码注解

    // 所有对象的结构体的类型都是objc_object
    typedef struct objc_object Person;
    
    //objc_object是只包含一个成员变量isa的结构体
    struct objc_object {
        Class _Nonnull isa __attribute__((deprecated));
    };
    
    
    struct NSObject_IMPL {
        __unsafe_unretained Class isa;
    };
    
    // 对象的本质就是结构体  Person实例对象 被转化为Person_IMPL结构体
    struct Person_IMPL {
        // 对象的继承就是父类作为成员变量
        // isa指向NSobject
        struct NSObject_IMPL NSObject_IVARS;
        // _name 没有声明成属性 没有帮我们生成 set get方法
        NSString *__strong _name;
        int _age;
        __strong MyBlock _myblock;
        void (*_block2)();
    };
    
    // @property (nonatomic, assign) int age;
    // @property (nonatomic, strong) MyBlock myblock;
    // @property (nonatomic, strong) void(^block2)(void);
    /* @end */
    
    
    // @implementation Person
    
    
    // 根据属性修饰符 生成age属性get set方法
    static int _I_Person_age(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_age)); }
    static void _I_Person_setAge_(Person * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }
    
    // 生成 myblock属性 get set方法
    static void(* _I_Person_myblock(Person * self, SEL _cmd) )(){ return (*(__strong MyBlock *)((char *)self + OBJC_IVAR_$_Person$_myblock)); }
    static void _I_Person_setMyblock_(Person * self, SEL _cmd, MyBlock myblock) { (*(__strong MyBlock *)((char *)self + OBJC_IVAR_$_Person$_myblock)) = myblock; }
    
    // 生成 block2属性 get set方法
    static void(* _I_Person_block2(Person * self, SEL _cmd) )(){ return (*(void (**)())((char *)self + OBJC_IVAR_$_Person$_block2)); }
    static void _I_Person_setBlock2_(Person * self, SEL _cmd, void (*block2)()) { (*(void (**)())((char *)self + OBJC_IVAR_$_Person$_block2)) = block2; }
    
    static void _I_Person_dealloc(Person * self, SEL _cmd) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_zm_558cwfjs099fbm2r8kxg8wt00000gt_T_VC_60be72_mi_0, __func__);
    }
    // @end
    

    vc.cpp

    // VC实例对象
    struct VC_IMPL {
    	struct UIViewController_IMPL UIViewController_IVARS;
    };
    
    struct __VC__viewDidLoad_block_impl_0 {
      struct __block_impl impl;
      struct __VC__viewDidLoad_block_desc_0* Desc;
      //  person指针变量 指向Person实例对象  __strong 表示强引用Person实例对象
      Person *__strong person;
      __VC__viewDidLoad_block_impl_0(void *fp, struct __VC__viewDidLoad_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    分析

    • oc对象编译成C++后的本质就是一个结构体Person_IMPL,该结构体的第一个成员变量就是isa指针,指向类对象本身,@property修饰的属性,系统会自动生成一个结构体成员变量,还为之生成getter和setter方法 这些可以看我之前的文章

    • person实例对象结构体Person_IMPL中含有_MyBlock变量,通过setter方法_I_Person_myblock将_MyBlock对象(等号右边)赋值给_MyBlock变量,因此_MyBlock指向_MyBlock对象(强引用)

    • 在__VC__viewDidLoad_block_impl_0结构体中,我们看到Person *__strong person,所以,block对象本身对Person实例对象也是强引用

    • block对象结构体__main_block_impl_0通过其内部成员指针变量Person *__strong per持有Person实例对象(强引用),而Person实例对象结构体Person_IMPL通过其内部成员指针变量_MyBlock持有block对象(强引用)因此二者构成循环引用。

    图示,我们仔细扣一下这里面的细节

    请添加图片描述

    对象持有block的过程

    // 实现block
    person.myblock = ^{
        NSLog(@"30");
    };
    
    // 根据上面的实现 生成block结构体 
    __VC__viewDidLoad_block_impl_0 {
        .....
    }
    
    // 判断结构体地址 是堆block还是栈block还是全局block 
    
    // 编译器转化为objc_msgSend  把判断的结果 也就是结构体地址扔给 setMyblock 方法 
    ((void (*)(id, SEL, MyBlock))(void *)objc_msgSend)((id)person, sel_registerName("setMyblock:"), ((void (*)())&__VC__viewDidLoad_block_impl_0((void *)__VC__viewDidLoad_block_func_0, &__VC__viewDidLoad_block_desc_0_DATA)));
    
    // 参数A 就是 结构体地址 
    参数A: (void (*)())&__VC__viewDidLoad_block_impl_0((void *)__VC__viewDidLoad_block_func_0, &__VC__viewDidLoad_block_desc_0_DATA))
    
    // setMyblock 也就是Myblock的set方法 被上面的objc_msgSend 调用
    [person setMyblock: 参数A];
    
    //  将参数A传递进来 赋给OBJC_IVAR_$_Person$_myblock成员变量   
    static void _I_Person_setMyblock_(Person * self, SEL _cmd, MyBlock myblock) {
        (*(__strong MyBlock *)((char *)self + OBJC_IVAR_$_Person$_myblock)) = myblock;
    }
    
    person对象通过block成员变量就持有block
    

    block 持有person对象的过程

    Person *person = [[Person alloc] init];
    // 其实就是指针变量person  强引用[Person alloc]对象
    Person *__strong person = [[Person alloc] init];   // 思考下这里 弄成 __weak 会怎样   
    
    person.age = 30;
    person.myblock = ^{
        NSLog(@"%@", person.age);
    };
    
    struct __VC__viewDidLoad_block_impl_0 {
      struct __block_impl impl;
      struct __VC__viewDidLoad_block_desc_0* Desc;
      // block捕获机制 根据变量强弱 内部保持一致
      Person *__strong person;
      __VC__viewDidLoad_block_impl_0(void *fp, struct __VC__viewDidLoad_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    

    解决思路

    打破循环引用,只需要将其中一个强引用变成弱引用即可,那么要改变哪一个弱引用呢?
    block从堆上被移除时候 会调用_Block_object_dispose 函数,该函数会自动释放引用的auto变量 也就是说
    Person实例对象内部拥有block属性,当该实例对象销毁时,其block属性也会随之销毁,所以我们只需要将block对象中的Person类型指针变成弱指针即可

    请添加图片描述
    解决方法

    ARC

    使用__weak
    使用__unsafe_unretained
    使用__block解决(必须要调用block)

    MRC

    使用__unsafe_unretained
    使用__block解决

    ARC环境

    例子1 __weak

    使用 weak 修饰

    Person *person = [[Person alloc] init];
    person.age = 30;
    __weak Person *weakPer = person;
    person.myblock = ^{
        NSLog(@"%d", weakPer.age);
    };
    NSLog(@"%@",[person.myblock class]);
    

    输出

    __NSMallocBlock__
    -[Person dealloc]
    -[VC dealloc]
    

    clang

    struct __VC__viewDidLoad_block_impl_0 {
      struct __block_impl impl;
      struct __VC__viewDidLoad_block_desc_0* Desc;
      Person *__weak weakPer;
      __VC__viewDidLoad_block_impl_0(void *fp, struct __VC__viewDidLoad_block_desc_0 *desc, Person *__weak _weakPer, int flags=0) : weakPer(_weakPer) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    分析

    block对象中,Person指针变量类型变成了__weak类型 Person 实例对象释放正常 VC释放正常

    还有一种写法

    Person *person = [[Person alloc] init];
    person.age = 30;
    //__weak Person *weakPer = person;
    __weak typeof(person) weakPer = person;
    person.myblock = ^{
        NSLog(@"%d", weakPer.age);
    };
    person.myblock();
    NSLog(@"%@",[person.myblock class]);
    

    例子2 __unsafe_unretained

    使用 __unsafe_unretained

    Person *person = [[Person alloc] init];
    person.age = 30;
    __unsafe_unretained Person *weakPer = person;
    person.myblock = ^{
        NSLog(@"%d", weakPer.age);
    };
    person.myblock();
    NSLog(@"%@",[person.myblock class]);
    

    输出

    30
    __NSMallocBlock__
    -[Person dealloc]
    -[VC dealloc]
    

    注:__weak修饰和__unsafe_unretained的区别 看我这篇文章

    例子3 __block

    使用 __block

    __block Person *person = [[Person alloc] init];
    person.age = 30;
    person.myblock = ^{
        NSLog(@"%d", person.age);
        person = nil;
    };
    NSLog(@"%@",[person.myblock class]);
    person.myblock();
    

    输出

    __NSMallocBlock__
    30
    -[Person dealloc]
    -[VC dealloc]
    

    clang

    struct __Block_byref_person_0 {
      void *__isa;
    __Block_byref_person_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__strong person;
    };
    
    struct __VC__viewDidLoad_block_impl_0 {
      struct __block_impl impl;
      struct __VC__viewDidLoad_block_desc_0* Desc;
      __Block_byref_person_0 *person; // by ref   这默认是强持有
      __VC__viewDidLoad_block_impl_0(void *fp, struct __VC__viewDidLoad_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    分析

    • block对象结构体 __VC__viewDidLoad_block_impl_0

    • __block变量对象结构体 __Block_byref_person_0

    • block对象持有__block对象 __block对象又持有Person实例对象,Person实例对象又持有block对象 构成3角循环

    请添加图片描述

    • 通过block回调 ,Person实例对象指针被置为nil,而该指针本质是__block对象中的Person *__strong per指针,因此该指针不可能再指向Person实例对象了,所以,第2条持有就断开了,打破了三角循环
    • 需要加 __block 需要调用block 还需要把指针制为nil 一旦忘记将指针置为nil,就会造成内存泄露,所以改方法比较麻烦 推荐使用 weak

    MRC

    该环境下不支持__weak修饰

    例子4 __unsafe_unretained

    使用 __unsafe_unretained

    - (void)test {
        Person *person = [[Person alloc] init];
        person.age = 30;
        NSLog(@"block前%ld",person.retainCount);
        __unsafe_unretained Person *uup = person;
        NSLog(@"__unsafe_unretainedh后%ld",person.retainCount);
        person.myblock = ^{
            NSLog(@"%d", uup.age);
        };
        NSLog(@"block后%ld",person.retainCount);
        NSLog(@"%@",[person.myblock class]);
        person.myblock();
        [person release];
        person = nil;
    }
    

    输出

    block前1
    __unsafe_unretainedh后1
    block后1
    __NSMallocBlock__
    30
    -[Person dealloc]
    -[VC dealloc]
    

    执行 copy

    person.myblock = [^{
        NSLog(@"%d", uup.age);
    } copy];
    

    输出

    block前1
    __unsafe_unretainedh后1
    block后1
    __NSMallocBlock__
    30
    -[Person dealloc]
    -[VC dealloc]
    

    分析

    • 创建并持有1次 block copy到堆区 对象retainCount+1 变成2 对该对象发送release消息时,其retainCount自动减1,所以当程序结束时,Person实例对象retainCount为1,其内存并不会被系统回收从而导致内存泄露。
    • 那么加上 __unsafe_unretained修饰后,无论后面有多少次retain或者copy操作,Person实例对象的retainCount始终为1 ,对该对象发送release消息,其retainCount值变为0,此时内存被回收,而不会导致内存泄露的问题

    例子5 __block

    __block修饰

    - (void)test {
        Person *person = [[Person alloc] init];
        person.age = 30;
        NSLog(@"block前%ld",person.retainCount);
        __block Person *bp = person;
        NSLog(@"__block后%ld",person.retainCount);
        person.myblock = [^{
            NSLog(@"%d", bp.age);
        } copy];
        NSLog(@"block后%ld",person.retainCount);
        NSLog(@"%@",[person.myblock class]);
        person.myblock();
        [person release];
        person = nil;
    }
    

    输出

    block前1
    __block后1
    block后1
    __NSMallocBlock__
    30
    -[Person dealloc]
    

    分析

    MRC下,__block修饰对象类型的auto对象类型,系统生成的__block对象并不会根据test()方法中的person指针是强指针类型而对Person实例对象[[Person alloc进行retain操作
    所以此时,__block的作用相当于__unsafe_unretained的作用,原理一样

    ARC

    例子6 __strong

    ARC环境下,弱指针不能通过"->"形式来访问对象的成员变量

    请添加图片描述

    weakPer很可能为为空(即有可能提前被释放了),所以必须使用强指针来访问

    - (void)test {
        Person *person = [[Person alloc] init];
        person.age = 30;
        person->_name = @"yang";
        __weak typeof(person) weakPer = person;
        person.myblock = ^{
            //  其实就是骗编译器  block 内部引用的还是 weakPer
            __strong typeof(weakPer) strongPer = weakPer;
            NSLog(@"%d", weakPer.age);
            NSLog(@"%@", strongPer->_name);
        };
        NSLog(@"%@",[person.myblock class]);
        person.myblock();
    }
    

    输出

    __NSMallocBlock__
    30
    yang
    -[Person dealloc]
    -[VC dealloc]
    

    clang

    struct __VC__test_block_impl_0 {
      struct __block_impl impl;
      struct __VC__test_block_desc_0* Desc;
      Person *__weak weakPer;
      __VC__test_block_impl_0(void *fp, struct __VC__test_block_desc_0 *desc, Person *__weak _weakPer, int flags=0) : weakPer(_weakPer) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    block 内部成员变量weakPer还是弱引用Person实例对象 是weak类型,并不受block代码块内部的__strong转化,该转化只是为了骗取编译器通过编译而已

    例子7 没有循环引用

    @interface VC ()
    @property (nonatomic, copy) NSString *name;
    @end
    
    @implementation VC
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        self.name = @"yang";
        void(^block)(void) = ^{
            NSLog(@"%@",self.name);
        };
        NSLog(@"block类型%@",[block class]);
        block();
    }
    
    - (void)dealloc {
        NSLog(@"vc dealloc");
    }
    

    输出

    block类型__NSMallocBlock__
    yang
    vc dealloc
    

    clang

    // vc 不持有 block
    struct VC_IMPL {
    	struct UIViewController_IMPL UIViewController_IVARS;
    	NSString *_name;
    };
    
    
    static void _I_VC_viewDidLoad(VC * __strong self, SEL _cmd) {
        void(*block)(void) = ((void (*)())&__VC__viewDidLoad_block_impl_0((void *)__VC__viewDidLoad_block_func_0, &__VC__viewDidLoad_block_desc_0_DATA, self, 570425344));
    }
    
    struct __VC__viewDidLoad_block_impl_0 {
      struct __block_impl impl;
      struct __VC__viewDidLoad_block_desc_0* Desc;
      VC *const __strong self;
      __VC__viewDidLoad_block_impl_0(void *fp, struct __VC__viewDidLoad_block_desc_0 *desc, VC *const __strong _self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    

    分析

    block持有self 强引用self
    self不持有block 不构成循环引用

    总结

    • block对象与OC对象相互持有(强引用) 才会造成相互循环引用

    • block对象持有__block变量对象 __block变量对象持有oc对象 oc对象持有block对象 构成3角循环引用

    • 循环引用会导致实例对象不能释放 也就是实例对象所占用的内存不能及时被系统回收,会造成内存泄漏。

    展开全文
  • 主要给大家介绍了iOS面试中关于如何优雅回答Block导致循环引用的问题的相关资料,文中通过图文介绍的非常相信,相信对大家具有一定的参考价值,需要的朋友们下面来一起看看吧。
  • 最近在使用SpringBoot配置AOP动态数据源,通过继承AbstractRoutingDataSource这个类来实现的,在使用的时候报了个错误,如下 大概就是dynamicDataSource依赖dataSourceScxys,dataSourceScxys依赖...

    最近在使用SpringBoot配置AOP动态数据源,通过继承AbstractRoutingDataSource这个类来实现的,在使用的时候报了一个错误,如下

    大概就是dynamicDataSource依赖dataSourceScxys, dataSourceScxys依赖DataSourceInitializerInvoker,而DataSourceInitializerInvoker又依赖dynamicDataSource,造成一个循环引用,在Spring初始化bean的时候就不知道该先初始化哪一个。

    解决办法:

    在springboot启动的时候排除其自动配置类,如下

    展开全文
  • 1. 走向毁灭的函数循环调用如果个函数相互调用,构成闭环,就形成了函数的循环调用。下面的例子中,函数a在其函数体中调用了函数b,而函数b在其函数体中又调用了函数a,这就是典型的函数循环调用。此种情况下,...
  • 小记 TypeScript 中的循环引用问题

    千次阅读 2020-10-09 19:43:24
    随着项目规模的不断增长,循环引用问题似乎总是不可避免,本文就 TypeScript 中可能出现的循环引用问题做了一些简单记录~ 平时编写 TypeScript 代码时,一般都倾向于使用模块(Module),通过结合使用 import 和 export ...
  • python解决循环引用问题

    千次阅读 2021-02-10 11:43:22
    当项目中的模块过多,功能划分不够清晰时会出现循环引用的问题,如下有两个模块moduleA 和 moduleB:#moduleAfrom moduleB import bdef a():print 'aaaaaaaa'b()def c():print 'cccc'if __name__ == '__main__':a...
  • 小时候,常被一些可笑的问题困扰——尽管成年以后面临的疑惑更,但似乎是因为已经适应了在迷茫中前行,对于未解的问题反倒是失去了那种急于想知道答案的迫切感。比如,站在两面相对的镜子中间,会看到无数个自己吗...
  • java如何解决循环引用

    千次阅读 2021-03-15 17:59:39
    Excel 循环引用产生的原因及解决方法 来源:excel 格子社区 我们打开 ...(Garbage Collection Thread) , 来跟踪每块分配出去的内存空间, Java 虚拟机 当(Java Virtual Machine) 处于空闲循环时, 垃圾收集器线程会...
  • 循环引用问题解决

    2022-02-16 16:23:09
    正常情况下,使用个NSTimer或者CADisplayLink这种类型的计时器,就需要停止,而不清楚什么时候停止,就得需要个强引用的对象,而这种需要传入个target,又需要个强引用,那肯定会出现循环引用 解决方式:创建个...
  • c++循环引用

    千次阅读 2018-01-03 19:45:27
    虽然C++11引入了智能指针的,但是开发人员在与内存的斗争问题上并没有解放,如果我们使用不当仍然有内存泄漏问题,其中智能指针的循环引用缺陷是最大的问题。 // // main.cpp // test // // Created by 杜国超 ...
  • 禁止循环引用、可自定义脱敏规则、启用PathPattern等都是亮点
  • Mybatis的循环引用

    千次阅读 2020-06-27 09:41:19
    Mybatis的循环引用及关联查询循环引用关联查询二级目录三级目录 Mybatis中对循环引用及关联查询都做了很好的处理。博主觉得这块非常难。这里只是把我知道的讲出来。 循环引用 什么循环引用博主这里就不介绍了。网上...
  • 循环引用

    千次阅读 2018-12-26 18:45:44
    1循环引用指两个对象相互强引用了对方,即retain了对方,从而导致两个对象都无法被释放,引发了内存泄漏现象。 (2)在开发中很容易出现循环引用循环引用可能存在于代码的每个角落,会使内存消耗过高,性能...
  • vue组件的循环引用

    2022-05-18 14:44:00
    今天项目中遇到个场景,简单概括为:A...参见 组件之间的循环引用 解决方法有3种: 在A组件中需要在beforeCreate中注册B组件: beforeCreate() { this.$options.components.bonusSettlementSecondStep = () =>
  • 引用计数法的循环引用问题

    千次阅读 2020-07-31 21:14:18
    关于引用计数法,我们可以先看段wiki上的描述: As a collection algorithm, reference counting tracks, for each object, a count of the number of references to it held by other objects. If an object's ...
  • 1.美图 2.概述 bean的实例化仅仅是获得了bean的实例,该bean仍在继续创建之中,之后在该bean实例的基础之...当ClassA引用ClassB,ClassB又引用ClassA,那么两个类之间就会形成个闭环,导致循环依赖的出现。大家只...
  • springboot 循环引用问题

    千次阅读 2021-01-15 13:18:54
    ↑ ↓ | dataSourceInitializer └─────┘ 解决办法 然后抛出堆的: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘dataSourceDev’ defined in class path ...
  • 1.重复引用:因为引用同一对象而导致的重复引用,具体例子见如下: Map map = new HashMap<>(); map.put(7,7); List<Map> list3 = new ArrayList(); list3.add(map); list3.add(map); String str = ...
  • 、什么是闭包? 在 Swift 中,可以通过 func 定义个函数,也可以通过闭包表达式定义个函数,闭包是个捕获了上下文的常量或者是变量的函数。闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来...
  • 组件之间循环引用

    千次阅读 2018-11-03 16:06:00
    是为了便于区分两个组件,其实这两个组件互为 祖先和后代的关系,因为他们互相循环引用,不是简单的 父子关系 &lt;div id="div"&gt; &lt;!--用prop 获取实例data数据--&gt; &...
  • python循环引用的解决办法

    千次阅读 2018-12-25 21:40:46
    在python中常常会遇到循环import即circular import的问题。 现实中经常出现这种滑稽的情况, 安装无线网卡的时候,需要上网下载网卡驱动.. 安装压缩软件的时候,从网上下载的压缩软件安装程序居然是被压缩了的.. ...
  • 如何解决引用计数的循环引用问题

    千次阅读 2020-03-28 17:06:43
    循环引用 public class MyObject { public Object ref = null; public static void main(String[] args) { MyObject myObject1 = new MyObject(); MyObject myObject2 = new MyObject(); ...
  • 只要不是构造函数注入就不会产生循环引用的问题。这是因为:spring 容器对构造函数配置Bean 进行实例化的时候,有个前提,即 Bean 构造函数入参引用的对象必须已经准备就绪。由于这个机制,如果两个Bean 都循环...
  • iOS 循环引用

    千次阅读 2016-05-23 15:15:36
    、什么是循环引用 所有的引用计数系统,都存在循环应用的问题。 例如下面的引用关系对象: 对象a创建并引用到了对象b. 对象b创建并引用到了对象c. 对象c创建并引用到了对象b. 这时候b和c的引用计数分别是2和1...
  • iOS block循环引用问题深究

    千次阅读 2022-03-29 19:06:02
    在使用block我们都会默认在里面使用weakself,网上搜了很解释都是为了防止循环引用,以防self被持有导致内存泄露。 那么问题来了,到底是谁持有了self?我以前没有深究,一直以为是A和B互相持有导致的循环引用。 ...
  • 循环引用指的是对象A 中包含个指向对象B的指针,而对象B中也包含个指向对象A的引用。比如下面这个例子: function problem(){ var objectA = new Object(); var objectB = new Object(); objectA.someOther...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 769,038
精华内容 307,615
关键字:

一处或多处循环引用