美文网首页iOS学习
iOS-底层原理28-Block底层原理

iOS-底层原理28-Block底层原理

作者: 一亩三分甜 | 来源:发表于2021-02-09 18:51 被阅读0次

    《iOS底层原理文章汇总》
    上一篇文章《iOS-底层原理27-锁和Block》介绍了NSLock,NSCondition,NSConditionLock,条件变量和条件锁的底层原理及三种类型Block,本文接着介绍Block底层原理

    1.Block循环引用

    I.循环引用无法释放的原因

    image.png
    image.png

    self中持有block,block中持有self,pop时无法进行release,从而无法进入VC的dealloc方法从而无法给block发送release消息来进行block的retainCount减一到0释放


    image.png

    II.循环引用解决办法

    思维误区:我们经常会想到一个解决办法就是,通过中间变量WeakSelf解除相互强引用,中间变量WeakSelf储存在弱引用表中,不会对self的引用计数retainCount进行加1,为什么呢?后面分析。


    image.png

    此种解除循环引用的方式会有漏洞,若延时执行block中的内容将会无法释放,self的生命周期无法保全,于是就有下面的解决办法


    image.png
    A.强弱共舞:weakSelf在block内是临时变量,block内部语句执行完后strongSelf释放,weakSelf从弱引用表里面置为nil,self也可以进行释放了,属于自动释放
    image.png
    B.中介者模式:手动释放,vc使用完后就置为nil,vc使用__block修饰使block内部能对外界变量进行修改,vc虽为临时变量在viewDidLoad中,但vc在函数viewDidLoad执行完后并未释放,vc被block捕获到内存里面进行持有,底层进行了三层拷贝
    image.png
    C.传值模式:self作为临时变量传入block中压栈,block中不再持有self,不构成持有引用关系
    image.png
    D.proxy和NSObject平行的类,后续展开分析

    2.Block底层原理

    image.png

    I.Block的本质:能进行%@打印,是对象结构体,匿名函数,能存放代码也叫代码块,Blcok的本质是结构体,里面有结构体同名的构造函数__main_block_impl_0在初始化是传入两个参数__main_block_func_0和&__main_block_desc_0_DATA,也称为函数,函数没有名字引申为匿名函数

    image.png

    __main_block_func_0作为参数传入构造函数中impl.FuncPtr = fp,调用时传入block相当于((block)->FuncPtr)(block),相当于执行__main_block_func_0函数来执行block中的内容printf("LG_Cooci")


    image.png

    II.Block捕获外部变量:编译时就在结构体__main_block_impl_0中生成变量int a,值拷贝,a++无法改变a的值,编译时就存在和调不调用没有关系,编译时默认给的isa为NSConcreteStackBlock

    image.png

    编译时就在结构体__main_block_impl_0中生成变量int a
    编译时默认给的isa为NSConcreteStackBlock
    两个a并不是同一个a,值拷贝,a++无法改变__cself->a的值
    a只读不能写

    III.Block中的捕获外部变量加__block修饰,在block中对a进行++,会生成结构体__Block_byref_a_0并初始化赋值,&a = *__forwarding,传入结构体__Block_byref_a_0 a的地址&a到block中,传入a的指针地址_a->__forwarding赋值给block中的结构体__Block_byref_a_0指针变量a,(a->__forwarding->a)++就是生成的外部变量结构体__Block_byref_a_0中的变量a++,是指针拷贝,指针地址中指向的值进行++

    image.png

    3.__block在底层做了什么,__main_block_copy_0,__main_block_dispose_0,__main_block_desc_0,__main_block_dispose_0,_Block_object_assign底层做了什么

    I.Block的copy:在ViewDidLoad中对block下断点查看汇编,发现进入objc_retainBlock中,下符号断点往下执行_Block_copy,找到源码libsystem_blocks.dylib

    image.png
    image.png

    在通过clang编译的block.cpp文件中也能发下Block_private.h文件,Block在底层的形式是Block_layout类型


    image.png
    image.png

    探索Block内存变化-调用情况-签名
    开始的Block为全局Block类型NSGlobalBlock,若捕获外界变量,则会由NSStackBlock变为NSMallocBlock,在NSStackBlock中进行Block_copy操作变为NSMallocBlock
    通过汇编分析捕获外界变量的block类型为NSStackBlock经过拷贝类型为NSMallocBlock


    image.png
    由上文知道,捕获外界变量的Block本来为NSStackBlock,经过objc_retainBlock-->Block_copy之后变为NSMallocBlock
    image.png

    4.Block的invoke

    进入汇编查看block_invoke,经过平移操作后得到x8直接调用NSLog打印,开始输出


    image.png

    为什么会平移16个字节得到block_invoke呢?通过查看Block的源码结构,Block_layout中isa8字节,flags4字节,reserved4字节,8+4+4=16字节,平移16字节得到invoke,invoke为传入的Block中的代码块


    image.png

    5.Block的签名signature

    flags用来做辨识码,根据flags知道是否存在可选变量Block_descriptor_2和Block_descriptor_3,Block_descriptor_1一定会有,flags为BLOCK_HAS_COPY_DISPOSE,有Block_descriptor_2,flags为BLOCK_HAS_SIGNATURE有Block_descriptor_3


    image.png
    image.png

    flags是否为BLOCK_HAS_COPY_DISPOSE决定是否有Block_descriptor_2
    flags是否为BLOCK_HAS_SIGNATURE决定是否有Block_descriptor_3


    image.png
    若存在Block_descriptor_2和Block_descriptor_3则采取
    内存平移的方式获取
    存在Block_descriptor_2则从Block_descriptor_1起

    始地址平移Block_descriptor_1的size
    存在Block_descriptor_3则从Block_descriptor_1起
    始地址平移Block_descriptor_1的size,判断是否存在
    Block_descriptor_2,若存在Block_descriptor_2则再从
    当前地址平移Block_descriptor_2的size


    image.png
    通过Block的内存分布情况验证Block的结构
    flags和reserved共同组成8字节,reserved为0则在后面补0可忽略,Block_descriptor_1中的reserved为0,uintptr_t reserved占用8字节,uintptr_t size占8字节
    0x0000000102f9f153为Block_descriptor_3中的第一个元素char *signature
    1 << 25为BLOCK_HAS_COPY_DISPOSE的值,与上flags的值为0,则没有Block_descriptor_2
    1 << 30为BLOCK_HAS_SIGNATURE的值,与上flags的值不为0,则有Block_descriptor_3
    image.png

    底层继续深入block_hook biffi

    6.Block_copy怎么将栈区对象拷贝到堆区的,Block_copy源码

    • I.进行强制类型转换 Block_layout是block底层类型
    • II.判断是否需要释放
    • III.若是全局Block,不需要拷贝
    • IV.不是全局Block,只能是栈区或堆区Block,而堆区Block无法在编译时期编译出来,只能是栈区Block
    • V.申请堆区空间
    • VI.进行内存拷贝,原对象的Block拷贝到新的对象Block中
    • VII.原对象的invoke拷贝到新对象的invoke中,这儿就能执行之前例子中的内容NSLog
    • VIII.是否正在进行析构
    • IX.isa标识为NSMallocBlock


      image.png

    7.Block的三层拷贝:怎么对外界变量操作

    A.Block_discriptor_2中有copy变量和dispose变量进行copy和dispose操作,对外界变量进行copy处理底层调用Block_object_assign,对外界变量进行dispose操作,底层调用Block_object_dispose

    image.png
    Block结构体中flags是否为BLOCK_HAS_COPY_DISPOSE
    决定是否有Block_descriptor_2,若有会有copy和dispose
    函数赋值,copy对外界变量进行copy处理,底层调用_Block_object_assign((void*)&dst->lg_name,dispose对外界变量进行dispose处理,底层调用_Block_object_dispose((void*)src->lg_name
    image.png
    image.png

    B.对外界变量操作调用Block_object_assign,根据Block中flags的值与BLOCK_ALL_COPY_DISPOSE_FLAGS运算后的类型进行不同的操作,用的最多的是第一种纯对象类型BLOCK_FIELD_IS_OBJECT,在第三层拷贝,如NSString的值拷贝时会应用,第三种__block修饰的外界变量类型BLOCK_FIELD_IS_BYREF,编译时__block修饰的类型会变为结构体类型Block_byref如__Block_byref_lg_name_0,在第二层拷贝时会应用

    image.png

    C.byref_keep在什么时候赋值的?int a=10,不是对象类型,clang编译不会有byref_keep,可以进行第二层拷贝不会有第三层拷贝,先进入第二层拷贝*dest = _Block_byref_copy(object)Block里面的结构体指针指向的内存地址就是外部的结构体地址,故结构体内部修改a=11,能改变外部的变量的值

    image.png

    D.满足第三种情况BLOCK_FIELD_IS_BYREF,需要捕获的外部变量为对象类型即编译生成的结构体类型中有对象类型如NSString进行clang编译,NSString是对象类型,Block_byref结构体中包含对象类型NSString *lg_name,才满足进行第三层拷贝的条件,将byref_keep保存对象后在合适的时候进行调用,byref_keep在什么时候赋值的呢?及是否还有Block_byref_3?

    image.png
    image.png
    image.png
    image.png

    E.是否有Block_byref_2和Block_byref_3由flags的值BLOCK_BYREF_HAS_COPY_DISPOSE和BLOCK_BYREF_LAYOUT_EXTENDED决定,编译器拷贝,下层进行识别有Block_byref_2,进行拷贝时调用(*src2->byref_keep)(copy, src),即调用编译时传入的__Block_byref_id_object_copy,调用__Block_byref_id_object_copy,又一次调用_Block_object_assign,此时出入的为纯对象类型BLOCK_FIELD_IS_OBJECT,进行指针地址拷贝*dest = object,结构体__Block_byref_lg_name_0内存平移40得到NSString lg_name的内存地址的值,指向外部变量存放字符串常量LG_Cooci的地址,故Block内部对lg_name进行改变,外部也会跟着变,属于同一片地址空间,任何对象都是平移58吗?则由Block对象中flags的值是否为BLOCK_BYREF_LAYOUT_EXTENDED决定,即是否有Block_byref_3,若有则会增加一个变量char *layout,则去平移6 * 8 = 48个字节

    image.png
    image.png
    image.png

    针对捕获外界变量__block修饰的Block的三层拷贝

    • I.第一层拷贝:整个Block的拷贝从栈区NSStackBlock拷贝到堆区NSMallocBlock,前文在汇编代码中已经分析
    • II.__block修饰的捕获的外部变量结构体__Block_byref_lg_name_0的拷贝,调用*dest = _Block_byref_copy(object)第二层拷贝,对捕获的外部对象变量编译为结构体__Block_byref_lg_name_0后的拷贝,因为要在block内部去修改外部__block修饰的对象(编译为__Block_byref_lg_name_0类型的结构体了)的值,所以拷贝整个结构体到block中
    • III.捕获的外部变量为对象类型如NSString,作为成员NSString lg_name存在于结构体__Block_byref_lg_name_0中,通过内存平移58 = 40个字节得到值,要修改则进行指针拷贝,第三层拷贝,指针指向同一片内存地址*dest=object,内存地址中存放着字符串常量的值如LG_Cooci
      image.png

    相关文章

      网友评论

        本文标题:iOS-底层原理28-Block底层原理

        本文链接:https://www.haomeiwen.com/subject/hrycxltx.html