实话实说吧 發表於 2022-11-15 10:41:52

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

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>基础</li><li>OC-Block</li><ul class="second_class_ul"><li>分类</li><li>NSMallocBlock</li><ul class="third_class_ul"><li>源码探究</li></ul><li>循环引用</li><ul class="third_class_ul"><li>解决方案</li></ul><li>注意点</li><ul class="third_class_ul"></ul></ul><li>Swift-Closure</li><ul class="second_class_ul"><li>捕获值</li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"></p><h2>基础</h2>
<p>Block是⼀个自包含的(捕获了上下⽂的常量或者是变量的)函数代码块,可以在代码中被传递和使用。</p>
<p>全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:</p>
<ul><li>全局函数是一个有名字但不会捕获任何值的闭包</li><li>嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包</li><li>闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包</li></ul>
<p class="maodian"></p><h2>OC-Block</h2>
<p class="maodian"></p><h3>分类</h3>
<p><code>NSGlobalBlock</code></p>
<ul><li>位于全局区</li><li>在Block内部不使用外部变量,或者只使用静态变量和全局变量</li></ul>
<p><code>NSMallocBlock</code></p>
<ul><li>位于堆区</li><li>被强持有</li><li>在Block内部使用局部变量或OC属性,可以赋值给强引用/copy修饰的变量</li></ul>
<p><code>NSStackBlock</code></p>
<ul><li>位于栈区</li><li>没有被强持有</li><li>在Block内部使用局部变量或OC属性,不能赋值给强引用/copy修饰的变量</li></ul>
<p>如下简单demo code所示</p>
<div class="jb51code"><pre class="brush:cpp;">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); // &lt;__NSGlobalBlock__: 0x101aa80b0&gt;
NSLog(@"%@",Malloc); // &lt;__NSMallocBlock__: 0x600003187900&gt;
NSLog(@"%@",Stack); // &lt;__NSStackBlock__: 0x7ff7b12c22f0&gt;
</pre></div>
<p>下面重点介绍堆Block。</p>
<p class="maodian"></p><h3>NSMallocBlock</h3>
<p>Block拷贝到堆Block的时机:</p>
<ul><li>手动copy</li><li>Block作为返回值</li><li>被强引用/copy修饰</li><li>系统API包含using Block</li></ul>
<p>所以总结一下堆Block判断依据:</p>
<ul><li>Block内部有没有使用外部变量</li><li>使用的变量类型?局部变量/OC属性/全局变量/静态变量</li><li>有没有被强引用/copy修饰</li></ul>
<p class="maodian"></p><h4>源码探究</h4>
<p>我们创建一个捕获了局部变量的block</p>
<div class="jb51code"><pre class="brush:cpp;">#import &lt;Foundation/Foundation.h&gt;
void test() {
    int a = 10;
    void(^Malloc)(void) = ^{
      NSLog(@"%d",a);
    };
}
</pre></div>
<p>执行<code>clang -rewrite-objc main.m -o main.cpp</code>命令,查看main.cpp文件可以看到Malloc闭包的结构如下。</p>
<div class="jb51code"><pre class="brush:cpp;">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 = &amp;_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}
};
// 创建Malloc闭包,传入参数如下
// fp: (void *)__test_block_func_0
// desc: &amp;__test_block_desc_0_DATA
// _a: 变量a的值(值拷贝)
void(*Malloc)(void) = ((void (*)())&amp;__test_block_impl_0((void *)__test_block_func_0, &amp;__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-&gt;a; // bound by copy
                        NSLog(···);
    }
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221115092828013.jpg" /></p>
<p>打开llvm可以看到,该block原本是在栈上,调用了<code>objc_retainBlock</code>方法,而在该方法中实际调用了<code>_Block_copy</code>方法。</p>
<p>在Block.h的源码中可以找到<code>_Block_copy</code>方法,其官方注释是&ldquo;创建一个基于堆的Block副本,或者简单地添加一个对现有Block的引用。&rdquo;,从而将这个栈block拷贝到了堆上,下面我们根据该方法的源码来探究一下堆Block的原理。(只截取重点代码)</p>
<div class="jb51code"><pre class="brush:cpp;">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-&gt;descriptor-&gt;size);
                if (!result) return NULL;
                memmove(result, aBlock, aBlock-&gt;descriptor-&gt;size); // bitcopy first
                // reset refcount
                result-&gt;flags &amp;= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
                result-&gt;flags |= BLOCK_NEEDS_FREE | 2;// logical refcount 1
                // isa重新标记为Malloc Block
                result-&gt;isa = _NSConcreteMallocBlock;
                _Block_call_copy_helper(result, aBlock);
                return result;
}
</pre></div>
<p>Block底层结构为<code>Block_layout</code></p>
<div class="jb51code"><pre class="brush:cpp;">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;
};
</pre></div>
<p>总结:</p>
<p>Block在运行时才会被copy,在堆上开辟内存空间。</p>
<p class="maodian"></p><h3>循环引用</h3>
<p class="maodian"></p><h4>解决方案</h4>
<p><code>__weak</code> + <code>__strong</code></p>
<p>思路: 在block里短暂持有self的生命周期。(<code>weak</code> 自动置空)</p>
<div class="jb51code"><pre class="brush:cpp;">self.name = @"YK";
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf;
    strongSelf.callFunc();
};
</pre></div>
<p><code>__block</code></p>
<p>思路: 值拷贝。(手动置空)</p>
<p>我们有如下代码,生成cpp文件看一下</p>
<div class="jb51code"><pre class="brush:cpp;">#import &lt;Foundation/Foundation.h&gt;
void test() {
    __block int a = 10;
    void(^Malloc)(void) = ^{
      a++;
      NSLog(@"%d",a);
    };
    Malloc();
}
</pre></div>
<div class="jb51code"><pre class="brush:cpp;">// 可以看到传入的第三个参数,是__Block_byref_a_0结构体类型的a变量地址,而不是上面讲过的直接存储int类型
void(*Malloc)(void) =
((void (*)())&amp;__test_block_impl_0((void *)__test_block_func_0,
                                  &amp;__test_block_desc_0_DATA,
                                  (__Block_byref_a_0 *)&amp;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-&gt;__forwarding) {
    impl.isa = &amp;_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 *)&amp;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; // 值拷贝存储
};
</pre></div>
<p>总结 <code>__block</code> 原理</p>
<ul><li>创建<code>__Block_byref_a_0</code> 结构体</li><li>传给block指针地址</li><li>block內修改的是与原始值同一片的内存空间</li></ul>
<p class="maodian"></p><h3>注意点</h3>
<p>根据上述分析我们可以得出结论,如果在OC的block中捕获了没有加<code>__block</code> 的外部变量,在编译时就会将变量值传入(值拷贝),如果捕获了加<code>__block</code> 的外部变量,则会获取到变量指针对应的内存空间的地址。代码验证如下</p>
<div class="jb51code"><pre class="brush:cpp;">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
</pre></div>
<p class="maodian"></p><h2>Swift-Closure</h2>
<ul><li>Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:<ul><li>利用上下文推断参数类型和返回值类型</li><li>隐式返回单表达式闭包(单表达式闭包可以省略&nbsp;<code>return</code>&nbsp;关键字)</li><li>参数名称缩写,可以用0,0,0,1表示</li><li>尾随闭包语法:如果函数的最后一个参数是闭包,则闭包可以写在形参小括号的外面。为了增强函数的可读性。</li></ul></li><li>Swift 的闭包是一个引用类型,验证如下。我们知道Swift的引用类型在创建时都会调用<code>swift_allocObject</code>方法</li></ul>
<div class="jb51code"><pre class="brush:cpp;">// 未调用swift_allocObject
let closure1 = { () -&gt; () in
    print("closure1")
}
// 调用swift_allocObject
let a = 10
let closure2 = { () -&gt; () in
    print("closure2 \(a)")
}
</pre></div>
<p class="maodian"></p><h3>捕获值</h3>
<ul><li>在闭包中如果通过<code></code>的形式捕获外部变量,捕获到的变量为<code>let</code>类型,即不可变</li><li>在闭包中如果直接捕获外部变量,获取的是指针,也就是说在闭包内修改变量值的话,原始变量也会被改变。</li><li>如果捕获的是指针类型(<code>Class</code>),无论是否用[],在闭包内对该变量进行修改,都会影响到原始变量</li></ul>
<p>简单验证如下:</p>
<div class="jb51code"><pre class="brush:cpp;">var variable = 10
let closure = { () -&gt; () in
    variable += 1
    print("closure \(variable)")
}
closure() // closure 11
print(variable) // 11
</pre></div>
<p>可见直接获取变量的话,会修改到原始值。</p>
<p>如果改成下面这样会编译报错&rdquo;可变运算符的左侧不可变&rdquo;</p>
<div class="jb51code"><pre class="brush:cpp;">var variable = 10
let closure = { () -&gt; () in
    variable += 1
    print("closure \(variable)")
}
closure()
print(variable)
</pre></div>
<p>捕获指针类型验证</p>
<div class="jb51code"><pre class="brush:cpp;">class YKClass {
    var name = "old"
}
let demoS = YKStruct()
let demoC = YKClass()
let closure1 = { () -&gt; () in
    demoC.name = "new"
    print("closure1 \(demoC.name)")
}
closure1() // closure1 new
print(demoC.name) // new
let closure2 = { () -&gt; () in
    demoC.name = "new2"
    print("closure2 \(demoC.name)")
}
closure2() // closure2 new2
print(demoC.name) // new2</pre></div>
<p>以上就是iOS底层实例解析Swift闭包及OC闭包的详细内容,更多关于iOS底层Swift OC闭包的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>react native reanimated实现动画示例详解</li><li>react-native&nbsp;封装视频播放器react-native-video的使用</li><li>React&nbsp;Native全面屏状态栏和底部导航栏适配教程详细讲解</li><li>java Nio使用NioSocket客户端与服务端交互实现方式</li><li>ios下OC与JS交互之WKWebView</li><li>iOS&nbsp;16&nbsp;CocoaAsyncSocket&nbsp;崩溃修复详解</li><li>React Native与iOS OC之间的交互示例详解</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: iOS底层实例解析Swift闭包及OC闭包