iOS底层实例解析Swift闭包及OC闭包

   2023-02-07 学习力0
核心提示:目录基础OC-Block分类NSMallocBlock源码探究循环引用解决方案注意点Swift-Closure捕获值基础Block是⼀个自包含的(捕获了上下⽂的常量或者是变量的)函数代码块,可以在代码中被传递和使用。全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:

基础

Block是⼀个自包含的(捕获了上下⽂的常量或者是变量的)函数代码块,可以在代码中被传递和使用。

全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:

  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

OC-Block

分类

NSGlobalBlock

  • 位于全局区
  • 在Block内部不使用外部变量,或者只使用静态变量和全局变量

NSMallocBlock

  • 位于堆区
  • 被强持有
  • 在Block内部使用局部变量或OC属性,可以赋值给强引用/copy修饰的变量

NSStackBlock

  • 位于栈区
  • 没有被强持有
  • 在Block内部使用局部变量或OC属性,不能赋值给强引用/copy修饰的变量

如下简单demo code所示

int a = 10; // 局部变量
void(^Global)(void) = ^{
    NSLog(@"Global");
};
void(^Malloc)(void) = ^{
    NSLog(@"Malloc,%d",a);
};
void(^__weak Stack)(void) = ^{
    NSLog(@"Stack,%d",a);
};
NSLog(@"%@",Global); // <__NSGlobalBlock__: 0x101aa80b0>
NSLog(@"%@",Malloc); // <__NSMallocBlock__: 0x600003187900>
NSLog(@"%@",Stack); // <__NSStackBlock__: 0x7ff7b12c22f0>

下面重点介绍堆Block。

NSMallocBlock

Block拷贝到堆Block的时机:

  • 手动copy
  • Block作为返回值
  • 被强引用/copy修饰
  • 系统API包含using Block

所以总结一下堆Block判断依据:

  • Block内部有没有使用外部变量
  • 使用的变量类型?局部变量/OC属性/全局变量/静态变量
  • 有没有被强引用/copy修饰

源码探究

我们创建一个捕获了局部变量的block

#import <Foundation/Foundation.h>
void test() {
    int a = 10;
    void(^Malloc)(void) = ^{
        NSLog(@"%d",a);
    };
}

执行clang -rewrite-objc main.m -o main.cpp命令,查看main.cpp文件可以看到Malloc闭包的结构如下。

struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
	// 内部存储了变量a
  int a;
	/// 初始化函数。包含三个参数
	// - Parameters:
  ///   - fp: 函数指针
  ///   - desc: 描述
  ///   - _a: flag
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 创建Malloc闭包,传入参数如下
// fp: (void *)__test_block_func_0
// desc: &__test_block_desc_0_DATA
// _a: 变量a的值(值拷贝)
void(*Malloc)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a));
// __test_block_func_0实现如下
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
			NSLog(···);
    }

iOS底层实例解析Swift闭包及OC闭包

打开llvm可以看到,该block原本是在栈上,调用了objc_retainBlock方法,而在该方法中实际调用了_Block_copy方法。

在Block.h的源码中可以找到_Block_copy方法,其官方注释是“创建一个基于堆的Block副本,或者简单地添加一个对现有Block的引用。”,从而将这个栈block拷贝到了堆上,下面我们根据该方法的源码来探究一下堆Block的原理。(只截取重点代码)

void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, true);
}
static void *_Block_copy_internal(const void *arg, const bool wantsOne) {
    struct Block_layout *aBlock;
		···
		// 类型强转为Block_layout
    aBlock = (struct Block_layout *)arg;
		···
    // Its a stack block.  Make a copy.
		// 分配内存
		struct Block_layout *result = malloc(aBlock->descriptor->size);
		if (!result) return NULL;
		memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
		// reset refcount
		result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
		result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
		// isa重新标记为Malloc Block
		result->isa = _NSConcreteMallocBlock;
		_Block_call_copy_helper(result, aBlock);
		return result;
}

Block底层结构为Block_layout

struct Block_layout {
    void *isa;  // isa指针
    volatile int32_t flags; // contains ref count
    int32_t reserved; // 保留位
    void (*invoke)(void *, ...); // call out funtion
    struct Block_descriptor_1 *descriptor;
};

总结:

Block在运行时才会被copy,在堆上开辟内存空间。

循环引用

解决方案

__weak + __strong

思路: 在block里短暂持有self的生命周期。(weak 自动置空)

self.name = @"YK";
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf;
    strongSelf.callFunc();
};

__block

思路: 值拷贝。(手动置空)

我们有如下代码,生成cpp文件看一下

#import <Foundation/Foundation.h>
void test() {
    __block int a = 10;
    void(^Malloc)(void) = ^{
        a++;
        NSLog(@"%d",a);
    };
    Malloc();
}
// 可以看到传入的第三个参数,是__Block_byref_a_0结构体类型的a变量地址,而不是上面讲过的直接存储int类型
void(*Malloc)(void) =
((void (*)())&__test_block_impl_0((void *)__test_block_func_0,
                                  &__test_block_desc_0_DATA,
                                  (__Block_byref_a_0 *)&a,
                                  570425344));
// __test_block_impl_0结构体中存储的变量也是__Block_byref_a_0类型
struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 初始化__Block_byref_a_0如下
__attribute__((__blocks__(byref))) __Block_byref_a_0 a =
{(void*)0,
        (__Block_byref_a_0 *)&a,
        0,
        sizeof(__Block_byref_a_0),
        10};
// __Block_byref_a_0结构体
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding; // 指针指向原始值
 int __flags;
 int __size;
 int a; // 值拷贝存储
};

总结 __block 原理

  • 创建__Block_byref_a_0 结构体
  • 传给block指针地址
  • block內修改的是与原始值同一片的内存空间

注意点

根据上述分析我们可以得出结论,如果在OC的block中捕获了没有加__block 的外部变量,在编译时就会将变量值传入(值拷贝),如果捕获了加__block 的外部变量,则会获取到变量指针对应的内存空间的地址。代码验证如下

int a = 1;
__block int b = 2;
void(^Malloc)(void) = ^{
    NSLog(@"a,%d",a);
    NSLog(@"b,%d",b);
};
a = 3;
b = 4;
Malloc();
// 输出结果如下
// a,1
// b,4

Swift-Closure

  • Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
    • 利用上下文推断参数类型和返回值类型
    • 隐式返回单表达式闭包(单表达式闭包可以省略 return 关键字)
    • 参数名称缩写,可以用0,0,0,1表示
    • 尾随闭包语法:如果函数的最后一个参数是闭包,则闭包可以写在形参小括号的外面。为了增强函数的可读性。
  • Swift 的闭包是一个引用类型,验证如下。我们知道Swift的引用类型在创建时都会调用swift_allocObject方法
// 未调用swift_allocObject
let closure1 = { () -> () in
    print("closure1")
}
// 调用swift_allocObject
let a = 10
let closure2 = { () -> () in
    print("closure2 \(a)")
}

捕获值

  • 在闭包中如果通过[variable1, variabla2]的形式捕获外部变量,捕获到的变量为let类型,即不可变
  • 在闭包中如果直接捕获外部变量,获取的是指针,也就是说在闭包内修改变量值的话,原始变量也会被改变。
  • 如果捕获的是指针类型(Class),无论是否用[],在闭包内对该变量进行修改,都会影响到原始变量

简单验证如下:

var variable = 10
let closure = { () -> () in
    variable += 1
    print("closure \(variable)")
}
closure() // closure 11
print(variable) // 11

可见直接获取变量的话,会修改到原始值。

如果改成下面这样会编译报错”可变运算符的左侧不可变”

var variable = 10
let closure = { [variable] () -> () in
    variable += 1
    print("closure \(variable)")
}
closure()
print(variable)

捕获指针类型验证

class YKClass {
    var name = "old"
}
let demoS = YKStruct()
let demoC = YKClass()
let closure1 = { [demoC] () -> () in
    demoC.name = "new"
    print("closure1 \(demoC.name)")
}
closure1() // closure1 new
print(demoC.name) // new
let closure2 = { () -> () in
    demoC.name = "new2"
    print("closure2 \(demoC.name)")
}
closure2() // closure2 new2
print(demoC.name) // new2

以上就是iOS底层实例解析Swift闭包及OC闭包的详细内容,更多关于iOS底层Swift OC闭包的资料请关注其它相关文章!

原文地址:https://juejin.cn/post/7165696168160067620
 
标签: iOS 底层 Swift OC 闭包
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • iOS扩展——Objective-C开发编程规范 简单io扩展实验代码
    iOS扩展——Objective-C开发编程规范 简单io扩
      最近准备开始系统学习一个完整项目的开发流程和思路,在此之前,我们需要对iOS的开发变成规范进行更系统和详尽的学习,随意对编程规范进行了整理和学习。本文内容主要转载自:Objective-C-Coding-Guidelines-In-Chinese  此外,这篇文章所说的一些常见
    03-08
  • iOS打包framework - Swift完整项目打包Framework,嵌入OC项目使用
    iOS打包framework - Swift完整项目打包Framewor
    场景说明:-之前做的App,使用Swift框架语言,混合编程,内含少部分OC代码。-需要App整体功能打包成静态库,完整移植到另一个App使用,该App使用OC。-所以涉及到一个语言互转的处理,以及一些AppDelegate的代码减除变化。 --------------------------------
    02-09
  • iOS8 蓝牙设备的重连接(retrieve) Swift实现
      今天App写到了蓝牙重连的阶段,以前针对sdk 6.0写的代码,蓝牙设备的回复是通过- (void)retrievePeripherals:(NSArray *)peripheralUUIDs然后回调 centralManager:didRetrievePeripherals:函数来得到可以回复设备的Array。在SDK7之后,- (void)retrievePe
    02-09
  • iOS高效开发必备的10款Objective-C类库 object c ios
    iOS高效开发必备的10款Objective-C类库 object
    因为iOS SDK相对比较底层,所以开发者就得受累多做一些体力活。不过幸运的是,有很多第三方的类库可以用来简化很多不必要的工作.经过作者团队的慎重讨论,他们评选出了10款能够极大提高iOS开发效率的类库,根据原文作者的评价来看,基本上有了这10款工具,做i
    02-09
  • IOS基础:Objective-C 字符串处理
    //一、NSString/*----------------创建字符串的方法----------------*/ //1、创建常量字符串。NSString *astring = @"This is a String!";//2、创建空字符串,给予赋值。NSString *astring = [[NSString alloc] init];astring = @"This is a String!";[astri
    02-09
  • 微信小程序ios下禁用默认滚动事件 微信小程序ios下禁用默认滚动事件提醒
    微信小程序ios下禁用默认滚动事件 微信小程序io
    在微信小程序ios下,如果有一个scroll-view滚动列表,开始触摸点点在滚动列表外,向列表里拖拽列表是不会滚动的,这很正常,因为开始触摸点点在了滚动列表外。可是不正常的是ios下会出现“反应不过来”的现象,这很难表述,看下图图中view:A用了fixed主要是
    02-09
  • 小程序camera组件ios上出现授权的问题
    camera需要调用手机系统的摄像头,正常进入页面就会询问用户权限。但是ios上面,前提是已经打开了摄像头的权限,加载之后隐藏掉camera,再次显示的时候,就会进入binderror事件,该事件是当用户未打开摄像头权限才会进入的监听事件,尝试了用wx.openSetting再
    02-09
  • 【小程序】使用uni-app搭建小程序环境---时间格式问题 new Date(str) IOS系统跟Android系统不兼容
    【小程序】使用uni-app搭建小程序环境---时间格
    今天做了一个需求,要在列表中把后台返回来的时间给显示出来,使用 new Date(str)  在微信开发者工具上显示是没有问题的,然后在IOS系统上显示是NAN。原因是 IOS系统只识别 " / " 不识别 " - ". 后台返回来的时间类型一般有三种 时间、时间搓、字符串。我
    02-09
  • Delphi xe7 FireMonkey / Mobile (Android, iOS)生成 QR Code完整实例
    Delphi xe7 FireMonkey / Mobile (Android, iOS
    这个实例在windows、OS X、IOS和Android等平台运行正常。本文参考这个网站提供的方法:http://zarko-gajic.iz.hr/firemonkey-mobile-android-ios-qr-code-generation-using-delphi-xe-5-delphizxingqrcode/代码中用到的DelphiZXingQRCode.Pas点这下载1 unit U
    02-09
  • 预热篇- 总结Delphi Xe4 做App的的可行性分析.
    首先澄清一个问题, 很多同学其实是误会了, 以为只要搞定了Delphi 就能很快写快餐程序了.  ios 本身的知识还是需要一些的, 并没有什么捷径可以走. 但如果一个团队有分工协作的话, DelphiXe4 也可以考虑作为一种技术方向.  用对了地方, 就可以发挥Delphi的长
    02-09
点击排行