欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

block本质探寻七之内存管理

程序员文章站 2022-05-30 15:04:41
说明: <1>阅读本问,请参照block前述文章加以理解; <2>环境:ARC; <3>变量类型:基本数据类型或者对象类型的auto局部变量; 一、三种情形 //代码 //打印 //clang:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc - ......

说明:

<1>阅读本问,请参照block前述文章加以理解;

<2>环境:arc;

<3>变量类型:基本数据类型或者对象类型的auto局部变量;

一、三种情形

//代码

void test1()
{
    int num = 10;
    __block int age = 20;
    person *per = [[person alloc] init];
    void(^block)(void) = ^{
        nslog(@"%d %d %p", num, age, per);
    };
    block();
}

//打印

2019-01-16 15:42:38.974947+0800 mj_test[2405:192414] 10 20 0x100654220
2019-01-16 15:42:38.975258+0800 mj_test[2405:192414] -[person dealloc]
program ended with exit code: 0

//clang:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m

struct __block_byref_age_0 {
  void *__isa;
__block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};


struct __test1_block_impl_0 {
  struct __block_impl impl;
  struct __test1_block_desc_0* desc;
  int num;
  person *__strong per;
  __block_byref_age_0 *age; // by ref
  __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int _num, person *__strong _per, __block_byref_age_0 *_age, int flags=0) : num(_num), per(_per), age(_age->__forwarding) {
    impl.isa = &_nsconcretestackblock;
    impl.flags = flags;
    impl.funcptr = fp;
    desc = desc;
  }
};


static void __test1_block_copy_0(struct __test1_block_impl_0*dst, struct __test1_block_impl_0*src) {_block_object_assign((void*)&dst->age, (void*)src->age, 8/*block_field_is_byref*/);_block_object_assign((void*)&dst->per, (void*)src->per, 3/*block_field_is_object*/);}


static void __test1_block_dispose_0(struct __test1_block_impl_0*src) {_block_object_dispose((void*)src->age, 8/*block_field_is_byref*/);_block_object_dispose((void*)src->per, 3/*block_field_is_object*/);}


static struct __test1_block_desc_0 {
  size_t reserved;
  size_t block_size;
  void (*copy)(struct __test1_block_impl_0*, struct __test1_block_impl_0*);
  void (*dispose)(struct __test1_block_impl_0*);
}



void test1()
{
    int num = 10;
    __attribute__((__blocks__(byref))) __block_byref_age_0 age = {(void*)0,(__block_byref_age_0 *)&age, 0, sizeof(__block_byref_age_0), 20};
    person *per = ((person *(*)(id, sel))(void *)objc_msgsend)((id)((person *(*)(id, sel))(void *)objc_msgsend)((id)objc_getclass("person"), sel_registername("alloc")), sel_registername("init"));
    void(*block)(void) = ((void (*)())&__test1_block_impl_0((void *)__test1_block_func_0, &__test1_block_desc_0_data, num, per, (__block_byref_age_0 *)&age, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->funcptr)((__block_impl *)block);
}

分析:根据前述文章可知

<1>arc模式下,block对象(等号右边大括号)被强指针(等号左边)变量持有(默认为strong)时,系统会自动将block对象从栈区copy到堆区;

<2>当访问实例对象或者__block修饰的基础数据类型变量时,block对象结构体中自动会增加两个函数指针——copy和dispose,分别指向__test1_block_copy_0函数和__test1_block_dispose_0函数;

<3>__block修饰的基础数据类型的变量,系统会自动生成一个新的对象(__block_byref_age_0),block对象通过该新的对象指针变量(__block_byref_age_0 *age)来访问基础数据类型变量(int age);

<4>num:直接被block捕捉到其结构体内部,随block一起copy到堆区;其随block对象本身一起销毁(不管是栈区的block还是堆区的block);

<5>per:类对象的访问,当block对象被copy到堆区时,block对象会通过copy函数指针来调用__test1_block_copy_0函数完成对per对象的拷贝;同时此时堆区的block对象会根据该类对象的类型(strong还是weak)来决定对其强引用还是弱引用;

说明:所谓的强引用,是指指针指向某个对象(实质为,存放指针变量的内存指向对象的内存);所谓的指向,即是对该对象的引用计数加1即retain操作(即此时该指针持有该对象),但是一个对象可能被多个指针持有,因此realease并不必然导致对象销毁(内存回收)而只是被释放即某个指针变量不再指向(持有)该内存区域,只有当对象的引用计数为0时,系统才会自动回收其内存;

注:引用计数为对象的属性,而非指针;

<6>__block修饰的基本数据类型的变量和对象类型变量,block被拷贝时,都会通过调用__test1_block_copy_0函数中_block_object_assign函数完成对自身的拷贝——其中,_block_object_assign函数的第三个参数,8表示由__block修饰的基本数据类型变量,3表示访问的事对象类型变量(那么,__block修饰的对象类型是多少呢?往下看);

当block对象销毁时,二者都是通过调用__test1_block_dispose_0函数中的_block_object_dispose函数来被销毁;

问题:为什么要增加copy和dispose两个函数指针呢?因为block对象要持有上述两种对象(__block修饰的基本数据类型生成的对象和per实例对象),那么自然要对其进行内存管理,达到持有/释放的可控目的;

因此,__block修饰生成的对象(会随着block的copy而一起被copy到堆区,而拷贝后的栈区的结构体依然会存在,只不过其作用域结束后,系统会对其内存自动回收),block对象要对其持有,肯定是强引用,否则弱引用,该对象的内存管理不受控制,那么block内部修改变量的值存在极大风险——这点没问题;

补充:__weak不能修饰基础类型变量

block本质探寻七之内存管理

如上,我么知道,__weak仅能修饰对象类型变量和block指针类型——为什么?

以上我们已经分析过,__strong和__weak:前者指针持有对象达到对该对象内存管理可控的目的(只要该对象的引用计数>0,其内存就不可能被回收,指针就可以合法指向该内存),会进行retain操作即对象的retaincount会自动+1;后者不持有,该对象的内存管理不可控(什么时候释放,跟该指针没关系),不会retain,对象的引用计数不会自动+1;

所以,__strong和__weak修饰的目的是对堆区的内存管理是否管控,而只有对象类型的变量(在堆区创建)才会有管控的问题,基础数据类型变量起始是在栈区存储,其内存(创建/回收)由系统自动管理;

 

二、__forwarding指针

我们在前面的文章提到,block对int类型的age变量的访问,为什么还要通过__forwarding指针而不是直接访问__block_byref_age_0结构体中的age变量呢?

block本质探寻七之内存管理

 

 分析:

<1>__forwarding指针本身是指向__block_byref_age_0结构体本身;第一个age又是__block_byref_age_0结构体类型的指针;第二个age是__block_byref_age_0结构体中int型成员变量;

<2>在栈区:age->__forwarding->age <=> age->age 没有任何问题;但是在堆区:因为block对象结构体会被copy到堆区,而原先留在栈区的block中的__forwarding指针会自动指向堆区的__block_byref_age_0结构体;

<3>从上述分析,我们很清楚地知道,将__block_byref_age_0结构体一并copy到堆区的目的就是堆区的block对象强引用该结构体,所以指向堆区的block对象的各类指针(包括对象本身)都可以通过该block对象达到对__block_byref_age_0结构体中age变量值的改变等操作的目的,而不必担心int型age变量内存随时会被系统回收的风险

————问题来了:如果栈区的指针或者block对象本身要对age变量的值进行修改,是要面临该风险的,那如何规避呢?

就是通过__forwarding指针,因为此时栈区的_block_byref_age_0结构体中的__forwarding指针变量是指向堆区的_block_byref_age_0结构体,除非堆区_block_byref_age_0结构体内存被手动销毁,否则会一直存在;

//图解

 

block本质探寻七之内存管理

 

三、__block修饰对象类型变量

//代码

void test2()
{
    person *per = [[person alloc] init];
    per.age = 20;
    __block person *blockper = per;
    void(^block)(void) = ^{
        blockper.age = 30;
        nslog(@"%d", blockper.age);
    };
    block();
}

//打印

2019-01-17 11:17:12.020409+0800 mj_test[1304:76856] 30
2019-01-17 11:17:12.020869+0800 mj_test[1304:76856] -[person dealloc]
program ended with exit code: 0

//clang

struct __block_byref_blockper_1 {
  void *__isa;
__block_byref_blockper_1 *__forwarding;
 int __flags;
 int __size;
 void (*__block_byref_id_object_copy)(void*, void*);
 void (*__block_byref_id_object_dispose)(void*);
 person *blockper;
};


struct __test2_block_impl_0 {
  struct __block_impl impl;
  struct __test2_block_desc_0* desc;
  __block_byref_blockper_1 *blockper; // by ref
  __test2_block_impl_0(void *fp, struct __test2_block_desc_0 *desc, __block_byref_blockper_1 *_blockper, int flags=0) : blockper(_blockper->__forwarding) {
    impl.isa = &_nsconcretestackblock;
    impl.flags = flags;
    impl.funcptr = fp;
    desc = desc;
  }
};


static void __test2_block_copy_0(struct __test2_block_impl_0*dst, struct __test2_block_impl_0*src) {_block_object_assign((void*)&dst->blockper, (void*)src->blockper, 8/*block_field_is_byref*/);}


static void __test2_block_dispose_0(struct __test2_block_impl_0*src) {_block_object_dispose((void*)src->blockper, 8/*block_field_is_byref*/);}


static struct __test2_block_desc_0 {
  size_t reserved;
  size_t block_size;
  void (*copy)(struct __test2_block_impl_0*, struct __test2_block_impl_0*);
  void (*dispose)(struct __test2_block_impl_0*);
}

分析:

<1>我们发现,__block修饰的blockperl实例对象,系统也会自动生成一个新的对象__block_byref_blockper_1;被引用的类对象以指针的形式存在于该结构体中(person *blockper),该指针指向[[person alloc] init]这个实例对象(位于堆区);

<2>在__block_byref_blockper_1结构体中,还存在__block_byref_id_object_copy和__block_byref_id_object_dispose两个函数指针,分别指向__block_byref_id_object_copy_131函数和__block_byref_id_object_dispose_131函数(作用同上),如下:

static void __block_byref_id_object_copy_131(void *dst, void *src) {
 _block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __block_byref_id_object_dispose_131(void *src) {
 _block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

很显然,这两个函数的作用也是针对某个对象的内存管理,那是哪个对象呢?

首先,_block_object_assign和_block_object_dispose的第三个参数为131,是指__block修饰实例对象的情形;

其次,dst是__block_byref_blockper_1对象的地址,加40是什么?我们算出person *blockper指针的地址偏移量正好为40(8+8+4+4+8+8,指针变量占8个字节),那么可以肯定,上述两个函数就是对person *blockper指向的实例对象[[person alloc] init]的内存管理;

所以,我们可以推测出以下结构:block对象__test2_block_impl_0通过其内部成员变量blockper持有__block_byref_blockper_1对象,而__block_byref_blockper_1对象又通过其内部成员变量blockper持有[[person alloc] init]实例对象;

我们知道,前者通过_test2_block_copy_0函数和__test2_block_dispose_0函数进行内存管理,其持有必定是强引用,,这点没问题;而后者的持有是通过__block_byref_id_object_copy_131函数和__block_byref_id_object_dispose_131函数进行内存管理,但其持有是强引用还是弱引用呢?往下看;

//__weak修饰

__block __weak person *blockper = per;

//clang:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-9.0.0 main.m(注:要设置runtime,否则编译会报错)

struct __block_byref_blockper_1 {
  void *__isa;
__block_byref_blockper_1 *__forwarding;
 int __flags;
 int __size;
 void (*__block_byref_id_object_copy)(void*, void*);
 void (*__block_byref_id_object_dispose)(void*);
 person *__weak blockper;
};

//打印

2019-01-17 12:17:20.776779+0800 mj_test[1598:105742] 30
2019-01-17 12:17:20.777113+0800 mj_test[1598:105742] -[person dealloc]
program ended with exit code: 0

分析:

<1>我们发现,如果没有__weak修饰,blockper格式为person *blockper,默认为strong类型;__weak修饰后,则会变成weak类型;

<2>根据之前的分析,其实__block_byref_blockper_1对象对[[person alloc] init]实例对象的引用,取决于指向该实例对象的指针类型(因为对象引用是指针传递,前面已讲过);

这里有个问题,为什么__weak修饰后,person实例对象打印前没有被销毁呢?因为该实例对象的作用域在test2()函数体内,而block的回调也在函数体内,因此回调时,该实例对象并没有被销毁;

接下来,我们可以验证下: 

block本质探寻七之内存管理

分析:此时,block回调前,person实例对象就被销毁了,说明block对象对实例对象的引用取决于person对象指针的引用类型;

我们再切换到mrc模式下看看:

//代码

void test4()
{
    person *per = [[person alloc] init];
    
    myblock block = [^{
        nslog(@"%p", per);
    } copy];
    
    [per release];
    
    block();
    
    [block release];
}

//打印

2019-01-17 13:52:05.667590+0800 mj_test[2106:151409] 0x10061add0
2019-01-17 13:52:05.668200+0800 mj_test[2106:151409] -[person dealloc]
program ended with exit code: 0

分析:当per指针象release时,[[person alloc] init]实例对象并没有被释放,而当block指针release时,[[person alloc] init]实例对象才被释放(block对象不再持有该实例对象),这也印证了上述的分析;

 

 

github