当前位置: 首页 > news >正文

【iOS】 Block再学习

iOS Block再学习

文章目录

  • iOS Block再学习
    • 前言
    • Block的三种类型
      • __ NSGlobalBlock__
      • __ NSMallocBlock__
      • __ NSStackBlock__
        • 小结
    • Block底层分析
      • Block的结构
      • 捕获自由变量
      • 捕获全局(静态)变量
      • 捕获静态变量
      • __block修饰符
        • forwarding指针
    • Block的copy时机
      • block作为函数返回值
      • 将block赋给__strong指针
      • block作为函数中方法名含有usingBlock的方法参数时
      • block作为GCD API的方法参数时候
      • Block的三层拷贝
        • 外界变量的类型
    • Block循环引用
      • 解决循环引用
        • weak-strong-dance
        • __block修饰符
        • 对象self作为参数
    • 小结

前言

笔者之前学习过block的相关内容,但是掌握不牢,今天再重新学习一遍

Block的三种类型

__ NSGlobalBlock__

void(^block)(void) = ^ {NSLog(@"testBlock");};NSLog(@"%@", block);

此时block没有参数也没有返回值,属于全局block

image-20250526185545015

如果访问全局变量:

image-20250526191915758

从上面可以看出无论是否创建block变量,只要访问全局变量的话,他就会创建一个全局区的block

__ NSMallocBlock__

int loaclA = 10;void(^block)(void) = ^ {NSLog(@"testBlock %ld", loaclA);};NSLog(@"%@", block);

此时block捕获了一个临时变量,就是底层拷贝a,所以是堆区block

image-20250526185737373

__ NSStackBlock__

NSLog(@"%@", [^{NSLog(@"%d", loaclA);} class]);

image-20250526190334083

这里在自由变量没有处理之前是栈区block处理之后是堆区block,目前的栈区block就越来越少了

这里是不创建Block变量,且访问自由变量的时候才会出现这里的一个栈区Block

小结
  • block如果不访问自由变量的话,都是存储在全局区的,如果访问全局变量的话,也是存储在全局区的Block
  • block如果访问自由变量的话
    • 如果没有创建block变量,才会创建一个栈区Block变量
    • 创建了一个Block变量,且访问自由变量,才会创建出一个堆区的Block,这里创建出堆区Block的原因是 栈区的Block执行了拷贝操作

Block底层分析

Block的结构

先看这段代码:

int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;int localA = 3;void(^block)(void) = ^{NSLog(@"123%ld", localA);};}return 0;
}

这里是一段司空见惯的Block捕获自由变量的代码.这里我们来分析一下这里的源码来认识一下底层内容:

struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int localA; // 捕获的自由变量__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {impl.isa = &_NSConcreteStackBlock; // 设置isa指针//&_NSConcreteStackBlock:标识 Block 初始类型为 栈 Block(未进行 copy 操作时)impl.Flags = flags;impl.FuncPtr = fp; // 代码块的函数赋值Desc = desc;} // block的一个构造函数,创建Block调用的内容
};static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int localA = __cself->localA; // bound by copy 这里是一个值拷贝NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_3d8947_mi_0, localA);}static struct __main_block_desc_0 {size_t reserved;size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; extern NSString* nanString;int localA = 3;void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA)); //相当于block等于__main_block_impl_0,是一个函数}return 0;
}

这里我们先看block的一个结构体,这里可以说明block是一个 __main_block_impl_0类型的对象.从构造函数可以看出他又一个isa指针

总结

  • block其实是一个对象,结构体,函数,又因为block没有名称,所以也被叫做,匿名函数

关系图示

如果从更加底层的角度来看:

block的底层结构

捕获自由变量

可以看到Block的实现结构体里面新增了localA变量,它在上面这是一个单纯的一个值拷贝

这里我看一下这段核心代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {int localA = __cself->localA; // bound by copy 这里是一个值拷贝NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_3d8947_mi_0, localA);}

这里我们可以看到这里如果对于他的值其实是只读的,它并不会修改原来block中持有的变量的值,所以是有问题的

block捕获外界变量时,在内部会自动生成同一个属性来保存

捕获全局(静态)变量

这里我们看一下如果是一个全局变量会是什么形式?

int localA = 10;struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;__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;}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_4d4ace_mi_0, localA++);}

这里我们可以看到它对于全局变量就是直接引用了全局变量的内容

捕获静态变量

这里我们看一下如果是一个静态局部变量:

int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;static int loaclB = 10;void(^block)(void) = ^{loaclB++;NSLog(@"123%ld", localA++);};}return 0;
}

如果是一个静态局部变量的话他是通过指针来获取的,同时静态局部变量与Block建立关联的是指针(int *),也就是说Block捕获的静态局部变量捕获的是变量的指针,因此当我们对静态局部变量进行修改时,Block内部的静态局部变量的值也会随之改变.

struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;int *loaclB;__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_loaclB, int flags=0) : loaclB(_loaclB) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};

__block修饰符

如果给localA添加一个修饰符__block,然后在block中对localA进行一个加加操作

int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;__block int localA = 3;void(^block)(void) = ^{NSLog(@"123%ld", localA++);};}return 0;
}

这时候我们在把它重写成我们的结构体:

struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;__Block_byref_localA_0 *localA; // by ref__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_localA_0 *_localA, int flags=0) : localA(_localA->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->localA, (void*)src->localA, 8/*BLOCK_FIELD_IS_BYREF*/);}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->localA, 8/*BLOCK_FIELD_IS_BYREF*/);}//重新看一下函数部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_localA_0 *localA = __cself->localA; // bound by refNSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_fd8242_mi_0, (localA->__forwarding->localA)++);}
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; extern NSString* nanString;//创建一个__Block_byref_a_0 是一个结构体,相当于把外界对象村对象__attribute__((__blocks__(byref))) __Block_byref_localA_0 localA = {(void*)0,(__Block_byref_localA_0 *)&localA, 0, sizeof(__Block_byref_localA_0), 3};void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_localA_0 *)&localA, 570425344));}return 0;
}

这里我们发现原先的localA变成了__Block_byref_localA_0的部分

struct __Block_byref_localA_0 { //__block修饰的外界变量的结构体void *__isa;
__Block_byref_localA_0 *__forwarding;int __flags;int __size;int localA;
};

总结:

  • 外界变量如果被__block修饰会变成这个结构体
  • 结构体用来保存变量的指针和值
  • 将变量生成的结构体对象中的指针地址 传递给block, 然后在block内部就可以对外界变量进行操作了

两种拷贝对比如下

  • 值拷贝 - 深拷贝,只是拷贝数值,且拷贝的值不可更改,指向不同的内存空间,案例中普通变量loacaA就是值拷贝
  • 指针拷贝 - 浅拷贝,生成的对象指向同一片内存空间,案例中经过__block修饰的变量localA就是指针拷贝
forwarding指针

这里我们首先看下面这段代码:

__block int val = 0;void (^block)(void) = ^{val++;};++val;block();NSLog(@"%ld", val);

这里我们看一下这里的一个val在堆区上面,一个在栈区上面,如果我们不做处理的话对于他的数据计算会有问题:

所以这就是forwarding这个指针的使命所在–确保可以正确的访问__block变量

我们把它转译成c++源码来看一下:


int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; extern NSString* nanString;__attribute__((__blocks__(byref))) __Block_byref_loaclB_0 loaclB = {(void*)0,(__Block_byref_loaclB_0 *)&loaclB, 0, sizeof(__Block_byref_loaclB_0), 10};void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_loaclB_0 *)&loaclB, 570425344));(loaclB.__forwarding->loaclB)++;}return 0;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_loaclB_0 *loaclB = __cself->loaclB; // bound by ref(loaclB->__forwarding->loaclB)++;NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_589979_mi_0, localA++);}

从上面我们可以看出对于这种自由变量的加减操作,都是通过我们的(loaclB->__forwarding->loaclB)++这一句指令来执行的,无论是堆区的block还是栈区的block都是这样操作的,这时候我们看一下Block copy的相关源码:

这部分源码内容会在下面的Block的拷贝部分讲一下,这里先不提起,先让笔者了解一下有关于__Block变量拷贝的图

当block变量从stack copy到heap上面的时候,stack上的forwarding被修改程指向heap上的block变量,通过这个机制,保证我们无论是在stack和heap上面都可以访问到同一个block变量

img

Block的copy时机

在 Objective-C 中,Block 最初是在栈上创建的。栈上的 Block(NSStackBlock)生命周期与其定义的作用域相关联,一旦该作用域结束,栈上的 Block 将不再有效。这意味着如果你需要在 Block 的定义作用域外使用它,比如将它作为回调传递或保存为后续使用,你需要将它复制到堆上(成为 NSMallocBlock)。

就好比我们在子线程回调到主线程的时候,我们的Block如果在栈上的话,那么超出作用域就会被销毁,无法回到主线程被调用,因此需要拷贝到堆上

在ARC中进行了很多优化的内容,Block的copy操作会在下面这些情况下执行:

block作为函数返回值

- (nxBlock)test {int a = 0;return ^{NSLog(@"1233123 %d", a);};
}
nxBlock testBlock = [self test];
NSLog(@"%@", [testBlock class]);

image-20250527163628890

将block赋给__strong指针

int loaclA = 10;void(^block)(void) = ^ {NSLog(@"testBlock %ld", loaclA);};NSLog(@"%@", block);

block作为函数中方法名含有usingBlock的方法参数时


NSArray *array = @[@1,@4,@5];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {}];

block作为GCD API的方法参数时候

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{}); 

Block的三层拷贝

这里有一个Block的三层copy,我们先分析学习有关于

static void *_Block_copy_internal(const void *arg, const int flags) 
{struct Block_layout *aBlock;const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;if (!arg) return NULL;aBlock = (struct Block_layout *)arg; // 强制转化成Block_layout对象,防止对外界造成影响if (aBlock->flags & BLOCK_NEEDS_FREE)         // NSConcreteMallocBlock//是否需要释放{latching_incr_int(&aBlock->flags);return aBlock;}else if (aBlock->flags & BLOCK_IS_GLOBAL)     // NSConcreteGlobalBlock//如果是全局block,直接返回{return aBlock;}//	为栈block或者是堆区block,由于堆区需要申请内存,所以是栈区的block的操作// Its a stack block.  Make a copy.struct Block_layout *result = malloc(aBlock->descriptor->size);if (!result) return (void *)0;//通过memmove内存拷贝,将aBlock拷贝到resultmemmove(result, aBlock, aBlock->descriptor->size); // bitcopy first// reset refcountresult->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not neededresult->flags |= BLOCK_NEEDS_FREE | 1;result->isa = _NSConcreteMallocBlock;if (result->flags & BLOCK_HAS_COPY_DISPOSE) {(*aBlock->descriptor->copy)(result, aBlock); // do fixup}return result;
}

下面简单讲述一下这个步骤:

  • 如果需要释放,如果需要就直接释放
  • 如果是globalBlock不需要copy,直接返回
  • 否则就只有两种情况,栈区block和堆区block,但是由于需要拷贝,所以堆区Block需要申请内存,所以最后是有关于栈区Block的拷贝代码
    • 通过malloc申请内存来接受block
    • 通过memmove将block拷贝到新申请的内存中
    • 设置block对象的类型为堆区block,就是result->isa = _NSConcreteMallocBlock;
外界变量的类型
// Runtime support functions used by compiler when generating copy/dispose helpers// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {// see function implementation for a more complete description of these fields and combinations//普通对象,即没有其他的引用类型,也就是我们任何的一个Object都是这个逻辑BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...//block类型作为变量BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable//经过__block修饰的变量和对象BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable//weak 弱引用变量BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers//返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};

这里我们在看一下这里的_Block_object_assign的源码,以及和下面这段Block拷贝的代码进行对比:

int main(int argc, const char * argv[]) {@autoreleasepool {extern NSString* nanString;int loaclB = 10;__block NSObject* obj = [[NSObject alloc] init];NSMutableArray* ary = [NSMutableArray array];void(^block)(void) = ^{[ary addObject:@"123"];NSLog(@"123%ld %@", loaclB, obj);};block();[ary addObject:@"23333"];NSLog(@"%@", ary);loaclB++;}return 0;
}//反编译之后变成下面这样:
//block的拷贝函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->ary, (void*)src->ary, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);} //这里调用了这个_Block_object_assign方法//block里面的匿名函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {__Block_byref_obj_0 *obj = __cself->obj; // bound by refNSMutableArray *ary = __cself->ary; // bound by copyint loaclB = __cself->loaclB; // bound by copy((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)ary, sel_registerName("addObject:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_d58752_mi_0);NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_main_d58752_mi_1, loaclB, (obj->__forwarding->obj));}
//block的结构
struct __main_block_impl_0 {struct __block_impl impl;struct __main_block_desc_0* Desc;NSMutableArray *ary;int loaclB;__Block_byref_obj_0 *obj; // by ref__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_ary, int _loaclB, __Block_byref_obj_0 *_obj, int flags=0) : ary(_ary), loaclB(_loaclB), obj(_obj->__forwarding) {impl.isa = &_NSConcreteStackBlock;impl.Flags = flags;impl.FuncPtr = fp;Desc = desc;}
};

这里我们看到了这里有两种类型:

BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF这里在这三层拷贝的最后一层进行一个讲解

这里如果有__block修饰的变量就会先调用一次这个方法:

static struct Block_byref *_Block_byref_copy(const void *arg) {//强转为Block_byref结构体类型,保存一份struct Block_byref *src = (struct Block_byref *)arg;if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {// src points to stack 申请内存struct Block_byref *copy = (struct Block_byref *)malloc(src->size);copy->isa = NULL;// byref value 4 is logical refcount of 2: one for caller, one for stackcopy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;//block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力//copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力copy->forwarding = copy; // patch heap copy to point to itself // 把堆区的指针指向自己src->forwarding = copy;  // patch stack to point to heap copy // 把栈区的指针指向堆区,保证数值一致copy->size = src->size;//如果有copy能力if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { // 有自己的一个copy的逻辑// Trust copy helper to copy everything of interest// If more than one field shows up in a byref block this is wrong XXX//Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);copy2->byref_keep = src2->byref_keep; // 调用自定义复制逻辑copy2->byref_destroy = src2->byref_destroy;if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);copy3->layout = src3->layout;}//等价于 __Block_byref_id_object_copy(*src2->byref_keep)(copy, src);}else {// Bitwise copy.// This copy includes Block_byref_3, if any.memmove(copy+1, src+1, src->size - sizeof(*src)); //如果为简单类型就直接进行一个内存拷贝}}// already copied to heapelse if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {latching_incr_int(&src->forwarding->flags);}return src->forwarding;
}

这个函数完成了copy之后会调用我们的最后一个函数_Block_object_assign这个函数由编译器来处理

第三层拷贝是block对传入对象的变量进行_Block_object_assign,将block内部将要使用的对象的变量拷贝到block内部。

上面我们自己给出的那段代码中有BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF

这两种都会进入我们的最后的_Block_object_assign然后进行不同的处理

  • 如果是普通对象,则交给系统ARC处理,并拷贝对象指针,就是引用计数甲一,所以外界变量不能释放
  • 如果是block类型的变量,则通过_Block_copy操作,将block拷贝到堆区
  • 如果是__block修饰的变量,就通过_Block_byref_copy函数进行内存拷贝以及常规处理

三层拷贝总结:

第一层通过_Block_copy实现对象的自身拷贝,从栈区拷贝到堆区

第二层通过调用_Block_byref_copy这个来实现对于对象拷贝成Block_byref类型

第三次调用_Block_object_assign对于__block修饰的当前变量内部对象的内存管理

当且仅当用__block变量的时候才会有三次拷贝.

Block循环引用

先认识一下什么是循环引用:

  • 正常使用: 是指A持有B的引用,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的retainCount为0的时候,则调用B的dealloc方法
  • 循环引用:A, B互相持有,所以导致A无法调用dealloc方法给Breleasse信号,所以B也无法接受到release信号,所以A,B此时都无法释放

image

解决循环引用

self.name = @"123";
self.testBlock = ^(void){NSLog(@"%@", self.name);
};
UIView animateWithDuration:1 animations:^{NSLog(@"%@",self.name);
};

代码第一种发生了一个循环引用的问题,因为在block中持有了外部变量name,导致block也持有了self,而self本来是持有block的,所以导致了self和block的互相持有代码二中没有循环引用,因为self中没有持有animation的block,不构成互相持有

weak-strong-dance

如果block内部并未嵌套block,直接使用__weak修饰self即可

self.name = @"123";__weak typeof (self) weakSelf = self;self.testBlock = ^(void){NSLog(@"%@", weakSelf.name);};

此时的weakSelfself指向同一片内存空间,且使用__weak不会导致self的引用计数发生变化

__weak typeof (self) weakSelf = self;self.testBlock = ^(void){NSLog(@"%@", weakSelf.name);};

如果block里面嵌套了block,那么就要同时使用__weak__strong

self.testBlock = ^(void){__strong typeof(weakSelf) strongSelf = weakSelf;dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", weakSelf.name);});};

如果不采用强弱共舞的话,可能会出现一个过了一会self被释放导致我们获取不到对应的数据,所以要采用__strong修饰一下,

其中strongSelf是一个临时变量,在cjlBlock的作用域内,即内部block执行完就释放strongSelf,所以并不会出现block持有self的一个情况导致这些问题

__block修饰符

我们可以采用__block修饰符,在主动调用完后手动释放self

__block PersonViewController* vc = self;self.testBlock = ^(void){NSLog(@"%@", vc.name);vc = nil;};
self.testBlock();

需要注意的是这里的block必须调用如果不调用block的话,vc就不会主动置为nil,那么仍旧是循环引用,self和block都不会被释放

对象self作为参数

主要是把对象self作为参数,提供给block内部使用,不会有引用计数问题

self.testBlock = ^(PersonViewController* vc){NSLog(@"%@", vc.name);}; 

小结

笔者这里简单总结了有关于Block的一个知识,复习了有关于Block捕获变量,以及使用__block修饰的时候forwarding指针的有一个变化,还有一个它发生拷贝的一个时机,以及复习了几种有关于解决Block循环引用的内容.

相关文章:

  • Ubuntu20.04中 Redis 的安装和配置
  • Cursor 编辑器, 使用技巧,简单记录一下
  • 【人工智能 | 项目开发】Python Flask实现本地AI大模型可视化界面
  • 2025年与2030年AI及AI智能体 (Agent) 市场份额分析报告
  • 【GO性能优化】第十五章:性能优化艺术——揭秘Go程序的性能调优技巧
  • CppCon 2015 学习:Live Lock-Free or Deadlock
  • MS39531N 是一款正弦驱动的三相无感直流电机驱动器,具有最小振动和高效率的特点
  • Perplexity AI:重塑你的信息探索之旅
  • 树莓派超全系列教程文档--(57)如何设置 Apache web 服务器
  • VLM引导的矢量草图生成AutoSketch
  • JS手写代码篇---手写ajax
  • 【ROS2】核心概念8——参数设置(Parameters)
  • Java 面向对象进阶之多态:从概念到实践的深度解析
  • ckeditor5的研究 (9):写一个自定义插件,包括自定义的toolbar图标、插入当前时间,并复用 CKEditor5 内置的 UI 组件
  • Unity中的Mathf.Clamp01
  • 端午编程小游戏--艾草驱邪
  • Unity使用代码分析Roslyn Analyzers
  • 【动画】Unity2D骨骼动画-Animation2D
  • Linux系统编程中的_GNU_SOURCE宏
  • 【Blender】Blender 基础:导入导出
  • 网站空间自己做/最近的疫情情况最新消息
  • 自己的博客和自己的网站做友链/深圳小程序开发公司
  • 网站建设实训报告意见和建议/网络推广是做什么的
  • 网站推广的定义/各城市首轮感染高峰期预测
  • 西安三网合一网站建设/宁波seo外包代运营
  • 做学校的网站推广发展前景/西安网络推广公司大全