iOS Block底层解析一

一、为啥要自己跟着解析

  • 前面搞过一篇《iOS Block用法与实现原理》这个有点乱,所以想着大佬们看着心累,我就用自己的理解加上查看的资料重新瞎折腾几番(毕竟自己太菜),没办法人老了,需要记录下来。
    图片.png
  • 根据我个人的理解,画了个思维导图,有助于解析理解,还是建议自己搞一遍 然后画个图 毕竟图毕竟直观

二、底层解析过程

  • 就按上面的思维图来,来来首先 捕获变量 是什么鬼,就是没有传参数,你又在block里面调用的变量,比如: 就是你去大宝剑,然后你被抓了,不是自愿的,嗯 就是这个意思,开搞 开搞
void test1() {
    void(^block)(void) =  ^{
         NSLog(@"fuck world");
     };
     block();
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test1();
    }
    return 0;
}

然后命令行cd到这个文件目录,在跑xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 文件名称.m 然后你会发现有一个.cpp的文件 然后拉到代码最下面,.cpp代码如下:

// 因为我是main函数调用test1的 从这里开始 第一步
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;  // 自动释放池 
        test1();// 方法调用
    }
    return 0;
}

// 第二步
void test1() {
    // 代码简化后 __test1_block_impl_0j就是block底层的结构体 括号里面的为参数
    void(*block)(void) = &__test1_block_impl_0(__test1_block_func_0, &__test1_block_desc_0_DATA);
    
     block->FuncPtr(block); // FuncPtr指向block
}

// 第四步
struct __block_impl {
  void *isa; // 指向类对象
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// 第三步
struct __test1_block_impl_0 {
  struct __block_impl impl; // __block_impl类型结构体
  struct __test1_block_desc_0* Desc; // __test1_block_desc_0类型结构指针
    // 方法函数调用
  __test1_block_impl_0(void *fp, struct __test1_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// 先不关注
static void __test1_block_func_0(struct __test1_block_impl_0 *__cself) {
         NSLog((NSString *)&__NSConstantStringImpl__var_folders_r3_94b6gvh96kj3bgwxst32yjh80000gn_T_main_71fc4b_mi_0);
     }
// 先不关注
static struct __test1_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __test1_block_desc_0_DATA = { 0, sizeof(struct __test1_block_impl_0)};

  • 上面的代码 有点多啊 有点麻烦,不过还是注释清楚了,不要觉得麻烦,自己去搞搞。先把block底层的基本结构弄清楚,那么差不多就懂一半了,万事开头难嘛。还是要好好说道 说道
  1. 自动释放池 这鬼东西不理先 我们今天只搞block,main函数 调用test1()
    2.__test1_block_impl_0一个指针调用后面跟的参数,注释也写清楚了就走个流程
    3.就是block的主要功能 封装了函数和调用环境的地方
    4.__block_impl结构体里面包含了一个isa指针,那么block本质就是一个封装了函数和调用环境的对象,就是这样了 后面都只是比较性质的了
  • 带参数的block,按上面的方法clang一下
void test2() {
    void(^block)(int,int) =  ^(int a, int b){
        NSLog(@"a = %d, b = %d",a,b);
    };
    block(20,20);
}
clang结构与没带参数的区别 只是多了两个参数 其他一样
void test2() {
    void(*block)(int,int) = &__test2_block_impl_0((void *)__test2_block_func_0, &__test2_block_desc_0_DATA));
   block->FuncPtr(block, 20, 20);
}
  • 未捕获变量的block 只是多了两个参数 其他一样,解析完了,是不是很简单,有同学会说简单毛线,那么多代码,其实只要把最基础的理解好 下面的block还不是分分钟,是吧。

三、捕获变量的Block解析过程

  • 局部变量捕获过程,代码如下:
//捕获auto变量
void test3() {
     // 局部变量默认 auto 修饰
    int age = 10;  // 相当于 auto int age = 10;
    void(^block)(void) =  ^{
        NSLog(@"age is %d",age);
    };
    age = 20;
    block();
}
  • clang出来的.cpp是这样,我只是把不一样的地方贴出来,一样的我就不贴了
// 第三步
void test3() {
    int age = 10;
    // 传age的值进去
    void(*block)(void) = ((void (*)())&__test3_block_impl_0((void *)__test3_block_func_0, &__test3_block_desc_0_DATA, age));
    // 这个是block后面的赋值 没有赋值进block里面 所以修改不了block里面age的值
    age = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
// 第四步
struct __test3_block_impl_0 {
  struct __block_impl impl;
  struct __test3_block_desc_0* Desc;
  int age; // 多出来的 age 变量
     // int _age, int flags=0) : age(_agez)这里赋值
  __test3_block_impl_0(void *fp, struct __test3_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

  • 上面的代码看出来了吧, 捕获局部变量为啥要搞这么麻烦

1.局部变量的作用域出了函数的大括号就释放了,而block什么时候调用呢,不知道 ,万一局部变量释放了,还调用block那不GG了,所以苹果直接把他传进去放在block的结构里面

  • 然后到 静态局部变量 block捕获
    //捕获静态变量
void test4() {
    static int age = 10;
     void(^block)(void) =  ^{
         NSLog(@"age is %d",age);
     };
     age = 20;
     block();
}
打印结果:
2020-04-13 16:23:28.410241+0800 blockTest[3000:112691] age is 20

-还是clang一下看看代码 我们还是只看代码不同的地方 如下:

// 第二步
void test4() {
    static int age = 10;
    // &age指针引用
     void(*block)(void) = &__test4_block_impl_0((void *)__test4_block_func_0, &__test4_block_desc_0_DATA, &age));
     age = 20;
     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
// 第三步
struct __test4_block_impl_0 {
  struct __block_impl impl;
  struct __test4_block_desc_0* Desc;
  int *age; // 指针类型属于地址传递
  __test4_block_impl_0(void *fp, struct __test4_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • 看到了吧 就是指针引用,age的内存地址传到block里面了, 保存的也是int *类型的指针,所以打印的机构不是10,而是20

1.为啥是20 因为传进去的是static int age = 10;的地址,跟这个指向的是同一块内存地址,所以外面的age变了,block里面的age也跟着变
2.为啥不是直接把值传递进去呢? 是因为static 是静态变量,static修饰的意思就是有且只有一份的意思,所以直接传指针,反正我是这样理解 苹果就是这样

  • 那么接下来就是 block 捕获 全局变量和静态全局变量的过程,
//全局变量

static int age = 10;
int height = 30;
void test5() {
    void(^block)(void) =  ^{
        NSLog(@"age is %d,height is %d",age,height);
    };
    age = 20;
    height = 40;
    block();
}
打印结果:
2020-04-13 16:39:05.945902+0800 blockTest[3070:119722] age is 20,height is 40

1.clang的代码不用看了,为啥?根据苹果的尿性 ,想想 全局变量 作用域是全局的,类里面随时可以访问,还引用个屁啊,直接拿来用就是了
2.同学们可以自己看看,跟平常函数调用全局变量一个鬼样,底层代码不会有任何的不同

  • 捕获变量到最后的 对象类型 捕获了 代码如下:
 // 对象类型
void test6() {
    Person *p = [Person new];
    p.name = @"hello block!";
    void(^block)(void) = ^{
        NSLog(@"--- %@",p.name);
    };
    block();
}
打印结果:
2020-04-13 16:42:14.056053+0800 blockTest[3093:121623] --- hello block!
  • 按照惯例clang搞他:
// 第二步
void test6() {

    // person初始化 objc_msgSend
    Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));
    //  p.name = @"hello block!"; setName 方法
    ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_r3_94b6gvh96kj3bgwxst32yjh80000gn_T_main_31b833_mi_0);
    // 开始调用block
    void(*block)(void) = &__test6_block_impl_0((void *)__test6_block_func_0, &__test6_block_desc_0_DATA, p, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
// 第三步
struct __test6_block_impl_0 {
  struct __block_impl impl;
  struct __test6_block_desc_0* Desc;
  Person *p; // 拿到对象的指针
    // Person *_pz对象的指针传过来
  __test6_block_impl_0(void *fp, struct __test6_block_desc_0 *desc, Person *_p, int flags=0) : p(_p) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// 第五步 copy调用 _Block_object_assign
static void __test6_block_copy_0(struct __test6_block_impl_0*dst, struct __test6_block_impl_0*src) {_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 第六步 dispose 调用 _Block_object_dispose
static void __test6_block_dispose_0(struct __test6_block_impl_0*src) {_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 第四步
static struct __test6_block_desc_0 {
  size_t reserved;
  size_t Block_size;
    /*
     生成了copy和 dispose的两个函数指针
     */
  void (*copy)(struct __test6_block_impl_0*, struct __test6_block_impl_0*);
    
  void (*dispose)(struct __test6_block_impl_0*);
    
} __test6_block_desc_0_DATA = { 0, sizeof(struct __test6_block_impl_0), __test6_block_copy_0, __test6_block_dispose_0};
  • 得慢慢看,狗日,这么多不同,不过都差不多

1.第四步:copy 和dispose 分别调用低五步和第六步,__test6_block_copy_0会对block做一次retain操作,__test6_block_dispose_0会对block做一次release操作,引用计数管理嘛,
2.为啥要这么做?Person对象是局部变量,然后block拿去用了,那Person对象的内存管理就得你自己管理咯,比如:你大宝剑被抓了,人家叔叔肯定管吃管住是一样的

  • 捕获变量终于搞完了 贼累,但是要为下一篇__block 修饰这些捕获的变量 做个铺垫,了解下block的内存管理

四、Block内存管理

  • 直接照搬了,内存管理这种东西必须要记的吖,理论性的,Block 类型:

在前面的 Block 结构体中都存在一个 isa 指针,且在构造函数的时候赋值 &_NSConcreteStackBlock。所以可以猜测认为 block 其实也是对象的一种, 尝试对 block 调用 class 方法来看看会有什么输出:


图片.png

可以看出来 block 确实是对象且主要的 block 类型(都是继承自NSBlock)有以下三种:

  • NSGlobalBlock( _NSConcreteGlobalBlock )存放在 数据段 中
  • NSStackBlock( _NSConcreteStackBlock ) 存放在 栈 中
  • NSMallocBlock( _NSConcreteMallocBlock )存放在 堆 中

block 是属于哪一种类型总结下来可以用下面的图片表示:

图片.png

图片.png
  • 上面就是这样 MRC的代码不写了,搬一下别人的,
补充: ARC 环境下下列操作会自动将 block 进行 copy 操作:
  • block 作为方法的返回值
  • 将 block 赋值给 __strong 指针时
  • block 作为Cocoa API中方法名含有usingBlock的方法参数时
  • block 作为GCD API的方法参数时
  • 最后今天就这样,内存管理这个,自己理解吧 我也没啥好办法,但是还是根据我自己的理解说道 说道
1.代码段的block{}出了这个大括号,ARC会帮我们释放了
2.栈上的block系统分配的,啥时候释放我们不知道了
3.堆上的block,就是要自己管理的了,怎么说:就是你把栈上的block copy就会到堆上
Block对变量的内存管理总结:
  • block 在栈上的时候,不会强引用外部的任何变量
  • block 从栈到堆上的时候,会有一次 copy 操作,在 copy 操作的时__main_block_copy_0 函数会根据捕获外部变量的 strong、weak修饰,来直接对强/弱引用外部变量。
  • 如果捕获变量是 __block 修饰的,copy 到堆上的 block 会将变量转换成的结构体 copy 到堆上同时生成强引用,变量转换成的结构体自身对外部变量的强弱引用则是根据捕获变量时变量自身的强弱修饰符决定。
  • 如果堆上的 block 被销毁了,__main_block_dispose_0 会对 block 引用的变量进行 release 操作。
  • 好了,搞得腰酸背痛,去按一下,下回分解__block
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容