iOS底层原理-Block

Block

Block定义及本质

block本质上也是一个OC对象,它内部有个isa指针(有isa指针就可以认为是OC对象)
block是封装了函数调用以及函数调用环境的OC对象

函数调用环境:函数调用所需要的参数及外部参数等

//block表达式:
 ^ 返回值类型 (参数列表) {表达式}
 ^ int (int count) {
        return count + 1;
    };

//声明Block类型变量语法:
返回值类型 (^变量名)(参数列表) = Block表达式

int age = 20;
void (^Block)(void) = ^{
    NSLog(@"age = %d",age);
}

Block的内部结构

//以上block的内部结构
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

//impl内部结构
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
}

//Desc内部结构
struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
}

通过将oc代码转换为c++代码可以看出


int main(int argc, const char * argv[]) {
    // 定义block变量
    // 调用__main_block_impl_0函数,返回结构体变量地址
    void (*block)(void) = &__main_block_impl_0(
                                               __main_block_func_0,
                                               &__main_block_desc_0_DATA
                                               );
    
    // 执行block内部的代码
    // 此时,若block有参数时,会将参数一起传入
    block->FuncPtr(block);
    //之所以block能直接调用FuncPtr是因为存储FuncPtr的impl结构体位于结构体的第一位,所以其地址与结构体的地址是一样的
    //从另一方面看,由于impl直接是结构体对象,相当于可以直接将__block_impl结构体的东西赋值过去到__main_block_impl_0中,故从这方面看也是可以直接调用的
}


struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 构造函数(类似于OC的init方法),返回结构体对象
    __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;
    }
};

// 封装了block执行逻辑的函数
// 若block中含有参数,则参数会在这个函数中被当做参数传递进来
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //定义block时,block内部执行的函数
}

Snip20180716_11.png

Block的变量捕获

当在block内部使用外部局部变量时,block的结构体__main_block_impl_0中也会定义一个同名的成员变量,并在构造函数中,将外部局部变量的值赋值给内部的变量,在block定义中,若有使用到局部变量,会将外部的局部变量的值存储到结构体内部的成员变量中,所以外边的变量怎么改变,都不会修改函数定义中的变量值

变量捕获 : 专门新建一个成员变量来保存外部的变量,称为捕获
auto变量 : 自动变量,离开作用域就销毁

Snip20180716_8.png
  • 当使用static修饰的局部变量,在变量捕获时,会将变量的地址值传入,在结构体内部定义的,就是一个对应的指针类型

  • auto变量值传递,static变量指针传递是因为auto变量什么时候被释放是不确定的,而static修饰的局部变量会始终在内存中

  • 全局变量并没有捕获,是因为函数在哪里都是可以直接使用全局变量的,故不需要捕获,而局部变量需要捕获,是因为局部变量定义是在一个函数,使用又是在另一个函数,是跨函数使用的,故需要捕获

注:所有的方法都会附带两个参数,一个是self(方法调用者),一个是SEL _cmd(方法名),而参数都是局部变量,故在方法中的block使用self,也是会捕获的

  • 今后凡是涉及到会不会捕获,只需要判断是否为局部变量即可

  • 在方法中的block直接使用成员变量,即_name时,也是捕获方法调用者self,再通过self去取_name的值

Block的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

  • __NSGlobalBlock__(_NSConcreteGlobalBlock)
  • __NSStackBlock__(_NSConcreteStackBlock)
  • __NSMallocBlock__(_NSConcreteMallocBlock)

block的类型一切以运行时的结果为准
同时通过clang转成的代码(C++)并不是真正的底层代码
clang是属于LLVM中的一部分

Snip20180716_12.png

堆:动态分配内存,需要程序员申请,也需要程序员自己管理内存

Snip20180716_15.png
  • 只要没有访问auto变量就是global类型,哪怕有访问static变量或者局部变量
  • 在栈上的block由于处于栈区,受作用域影响,在作用域之外,有可能被销毁,导致数据错乱
  • globalBlock调用copy方法,依然为global类型

Block的copy操作

Snip20180716_17.png

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:

  • block作为函数的返回值
  • 将block赋值给__strong指针时
  • block作为cocoaAPI中方法含有usingBlock的方法参数时
  • block作为GCD方法的参数时

MRC环境下block属性建议写法:使用copy关键字

MRC环境下若使用retain,只会令block的引用计数+1,并不会将block复制到栈区

ARC环境下,copy和strong都是可以的,因为在ARC环境下,对block强引用也会对block进行一次copy操作,但还是建议统一写copy

Block的访问外部对象类型auto变量

  • 栈空间上的block,是不会持有外部对象的
  • 堆空间上的block,则会持有外部对象,当block使用到外部变量时,会对外部变量进行一次retain操作,在block自己销毁时,也会对内部用到的外部变量做一次release操作

当block内部访问了对象类型的auto变量时:

  • 如果block是在栈上:
    无论block内部对对象类型的auto变量是强引用还是弱引用,都不会持有该对象,即不会对auto变量产生强引用

  • 如果block被拷贝到堆上:
    会调用block内部的Desc中的copy函数
    copy函数内部会调用_Block_object_assign函数
    _Block_object_assign函数会根据auto变量的修饰符(__strong,__weak,__unsafe_unretained)做出相应的操作,类似于retain(形成强引用、弱引用)

  • 当block从堆上移除:
    会调用block内部的Desc中的dispose函数
    dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放引用的auto变量,类似于release

copy函数和dispose函数都是做内存管理用的,只要见到这两个函数,就大概知道与对象有关

Snip20180717_19.png

当访问的是个对象类型对象,会自动生成_Block_object_assign函数和_Block_object_dispose函数对该对象进行内存管理

看强引用什么时候被释放,则该对象就什么时候释放,与弱引用无关

Block的修改外部auto变量

默认情况下,在block中是无法修改外部的变量的
从本质上看,定义变量是在一个函数,而block调用的函数又是另一个函数,二者作用域不同,故肯定是无法修改(代码)

要想修改外部变量,做法:

  • 将变量变为static或者为全局变量
  • __block关键字

__block可以用于解决block内部无法修改auto变量值的问题
__block不能修饰全局变量,静态变量(static)

编译器会将__block包装成一个对象,故其也有copy与dispose函数

//包装的对象结构体为:
struct __Block_byref_age_0 {
    void *__isa;
    ___Block_byref_age_0 *__forwarding;//该指针指向自己
    int __flags;
    int __size;
    int age;
}

在block中定义了一个指针,指向上面的这个结构体,当需要修改值时,通过指针找到对应的__forwarding,在找到对应的age,从而能够成功地修改和读取值了

此时在外边打印age的地址值,实际上就是__block_byref_age_0内部的age的地址,即实际上外部的age就是结构体内部的age

注意点1:若block外部定义了一个可变数组array,在block内部使用方法[array addObject:@“111”],是可以的,因为其本质是将array用来使用,而不是直接修改array的指针的东西,例如array = nil,(该语句就会报错 )
注意点2:若对象类型外部在使用__block修饰,则可以修改对象类型的指针变量,即可以令array = nil,其内部也会生成一个__Block_byref_XXX的结构体,结构体内部还会存在一个copy函数和dispose函数(因为block外部需要管理结构体对象,故需要一个copy和dispose,而结构体内部需要管理真正的对象类型,故还是需要一个copy和dispose函数)

__block的内存管理

  • 当block在栈上时,并不会对__block变量产生强引用
  • 当block被copy到堆上的时候,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)(一般都是强引用)
Snip20180717_22.png

当block从栈copy到堆上,会将内部用到的__block变量也一起拷贝到堆上,并对其进行强引用,当时__block修饰的对象类型,在__block的变量结构体被拷贝到堆上时,会调用结构体内部的copy函数对结构体里边的对象变量进行管理
若另外一个block进行同样的操作,也使用到一样的__block变量,则该block拷贝到堆上的同时,对该变量进行强引用,由于之前__block已经在堆上了,故直接强引用就好了,不需要再次复制

  • 当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)
Snip20180717_23.png

block与对象类型的auto对象变量,__block变量内存管理总结

  • 当block在栈上时,对他们都不会产生强引用
  • 当block拷贝到堆上时,都会通过copy函数来处理它们
  • 当block从堆中移除时,都会调用dispose函数来释放

使用__block修饰基本数据类型和直接传入OC对象的不同是:
使用__block修饰的对象,在block内部都是对其有着强引用的
使用OC对象传入会根据外部是强指针(__strong)还是弱引用(__weak)来决定内部对该对象是强引用还是弱引用

注:不存在__block __weak int age这样的写法,因为__weak只能用来修饰普通OC对象

__forwarding指针

  • __block修饰的变量在栈上时,其__forwarding指针指向自己
  • __block修饰的变量被拷贝到堆上时,栈上的对象中的__forwarding指针指向堆上的对象,堆上对象的__forwarding指针指向自己
Snip20180717_25.png

这样的好处是,无论访问堆上还是栈上的__block对象,都能确保读取的对象一定是堆中的__block对象

__block修饰的对象类型

  • block内部指向被__block包装的对象结构体的指针一定是强指针,而结构体内部的指向外部对象的指针会根据外部是强指针还是弱指针,对应的是强引用还是弱引用
  • 当栈上的block调用了copy操作,会调用block中Desc函数内部的copy函数将修饰的变量中的结构体也复制一份到堆上
  • 在MRC环境中,使用__block修饰的对象变量中结构体指向外部对象变量的指针始终都是弱引用(即不会进行retain操作),只有__block对象才会这样.比较特殊

循环引用

解决方法:

  • ARC:
    1.使用__weak__unsafe_unretained指针
    __weak:不会产生强引用,当指向的对象被销毁,会自动将指针置为nil
    __unsafe_unretained :不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变(有可能产生野指针错误)
    2.使用__block,在block内部将__block修饰的指针置为空,且必须要执行block
__block MXPerson *person = [[MXPerson alloc] init];
person.age = 10;
person.block = ^{
    NSLog(@"%d",person.age);
    person = nil;
}

person.block();
  • MRC
    1.因为MRC是不支持__weak的,所以在MRC环境中,可以使用__unsafe_unretained
    2.可以使用__block
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容