【iOS】——Blocks

文章目录

  • 前言
  • 一、Blocks概要
    • 1.什么是Blocks
  • 二、Block模式
  • 1.block语法
    • 2.block类型变量
    • 3.截获自动变量值
    • 4._Block修饰符
    • 5.截获的自动变量
  • 三、Blocks的实现
    • 1.Block的实质
    • 2.截获自动变量值
    • 3._Block说明符
    • 4.Block存储域


前言

一、Blocks概要

1.什么是Blocks

Blocks是C语言的扩充功能,并且可以用一句话来表示Blocks的扩充功能:Blocks是带有自动变量(局部变量)的匿名函数。

匿名函数:Blocks是一种匿名函数,也就是没有特定名称的函数。它们可以在需要的地方定义和使用,而无需提前声明或命名。这使得Blocks非常灵活,可以作为参数传递给其他函数或方法,或者作为变量保存和执行。
自动变量(局部变量):Blocks可以捕获其定义范围内的自动变量(也称为局部变量)。当一个Block被定义时,它会在其内部创建一个副本,用于在Block执行时访问该变量的值。这意味着即使变量超出了其定义范围,Block仍然可以访问和使用该变量的值。
闭包:由于Blocks可以捕获自动变量,它们形成了一个封闭的环境,即闭包。这意味着Block可以在其定义范围之外访问和使用自动变量。当Block被传递到其他函数或方法时,它会携带其封闭环境中的自动变量,以便在执行时可以访问这些变量的值。

二、Block模式

1.block语法

完整形式的Block语法和一般的C语言函数相比,只有两点不同。第一点是没有函数名,因为它是匿名函数。第二点是返回值类型前带有“^”(插入记号)记号。下面是Block语法格式:

^ 返回值类型 参数列表 表达式

^int (int count){return count + 1;}

Block语法可以省略返回值类型

^(int count){return count + 1;}

如果不使用参数,Block语法还可以省略参数列表

^void (void){printf("Blocks\n");}

2.block类型变量

在C语言中可以将函数地址赋值给函数指针的类型变量中,同样在Block语法下,可将Block语法赋值给Block类型的变量中。示例如下:

int (^blk)(int) = ^(int count) {return count + 1;};
int (^blk_t)(int);
blk_t = blk;

Block类型变量声明
返回参数 (^变量名称)(接受参数);

同时使用typedef重命名Block类型,因为Block类型变量和平时的使用类型相同,为了方便我们使用,我们通常都是用typedef来重命名Block。

typedef 返回参数 (^该Block的名称)(接收参数)

此时就可以用该Block的名称代表该Block了。

typedef int (^blk_t)(int)

blk_t blk = ^(int count) {return count + 1;};

3.截获自动变量值

Block截获自动变量值就是说在定义Block的时候,其之前的变量都会被该Block所截获,该Block后再改变变量的值然后再调用,其Block中所捕获的值不会受到改变。

int main(int argc, const char * argv[]) {
	int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
    };
    
    val = 2;
    fmt = "These values were changed. val = %d\n";
    
    blk();
    
    return 0;
}

在这里插入图片描述

此时我们发现,在定义Block之后的这val,fmt两个变量被改变了,但是再次调用该Block还是定义Block之前的值,这就是捕获。我的理解就是定义Block之前的自动变量都会被该Block捕获,在定义Block后改变变量其值还是该Block之前的定义的值,不会收到影响。

4._Block修饰符

自动变量截获只能保存执行Block语法瞬间的值并且保存后就不能修改这个值,如果要修改这个自动变量的值就需要在在声明时给这个自动变量附加_Block说明符。

int main(int argc, const char * argv[]) {
    __block int val = 10;
    __block const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, val);
        val = 20;
    };
    
    val = 2;
    fmt = "These values were changed. val = %d\n";
    
    blk();
    printf(fmt, val);
    
    return 0;
}

在这里插入图片描述

5.截获的自动变量

前面提到向Block截获的自动变量赋值会报错,如果调用变更该对象的方法则不会产生编译错误。
用OC中的NSMutableArray来说,截获它其实就是截获该类对象用的结构体实例指针,就是说你只要不改变其指针的指向和其指针的值,他就不会出错。

int main(int argc, const char * argv[]) {
    id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        id obj = [[NSObject alloc] init];
        [array addObject:obj];
    };
    
    blk();
    
    return 0;
}

这里因为你并没有改变其array指针的指向和值,而只是在其地址后边新加了一个值,所以他就不会报错。

int main(int argc, const char * argv[]) {
    id array = [[NSMutableArray alloc] init];
    
    void (^blk)(void) = ^{
        array = [[NSMutableArray alloc] init];
    };
    
    blk();
    
    return 0;
}

但是你这样写就改变了array的初始地址,所以他就会报错。

另外,在使用C语言数组是必须小心使用其指针。

int main(int argc, const char * argv[]) {
    const char text[] = "hello";  //这里是定义一个数组
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
    
    blk();
    
    return 0;
}

这里你看似没有什么错误,但是会出现下面的编译错误。
在这里插入图片描述
因为在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获,这时我们可以使用指针来解决该问题。

int main(int argc, const char * argv[]) {
    const char *text = "hello";  //这里是使用指针引用
    
    void (^blk)(void) = ^{
        printf("%c\n", text[2]);
    };
    
    blk();
    
    return 0;
}

三、Blocks的实现

1.Block的实质

Block实质是Objective-C对闭包的对象实现,简单说来,Block就是对象。

下面分别从表层到底层来分析一下:

表层分析Block的实质:它是一个类型

Block是一种类型,一旦使用了Block就相当于生成了可赋值给Block类型变量的值。举个例子:

int (^blk)(int) = ^(int count){
        return count + 1;
};

等号左侧的代码表示了这个Block的类型:它接受一个int参数,返回一个int值。
等号右侧的代码是这个Block的值:它是等号左侧定义的block类型的一种实现。

如果我们在项目中经常使用某种相同类型的block,我们可以用typedef来抽象出这种类型的Block:

typedef int(^AddOneBlock)(int count);

AddOneBlock block = ^(int count){
        return count + 1;//具体实现代码
};

这样一来,block的赋值和传递就变得相对方便一些了, 因为block的类型已经抽象了出来。

深层分析Block的实质:它是Objective-C对象

Block其实就是Objective-C对象,因为它的结构体中含有isa指针。

下面将Objective-C的代码转化为C++的代码来看一下block的实现。

int main()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    return 0;
}

通过clang编辑器转换为c++:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//block结构体
struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  //Block构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//isa指针
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

};

//将来被调用的block内部的代码:block值被转换为C的函数代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Block\n");
}

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)};

//main 函数
int main()
{
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    return 0;
}

首先我们看一下从原来的block值(OC代码块)转化而来的C++代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    printf("Block\n");
}

这里,*__cself 是指向Block的值的指针,也就相当于是Block的值它自己(相当于C++里的this,OC里的self)。
而且很容易看出来,cself 是指向mainblockimpl0结构体实现的指针。 结合上句话,也就是说Block结构体就是mainblockimpl0结构体。Block的值就是通过mainblockimpl_0构造出来的。


下面来看一下这个结构体的声明:

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;
  }
};

可以看出,_mainblockimpl0结构体有三个部分:

第一个是成员变量impl,它是实际的函数指针,它指向_mainblockfunc0。来看一下它的结构体的声明:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;  //今后版本升级所需的区域
  void *FuncPtr; //函数指针
};

第二个是成员变量是指向_mainblockdesc0结构体的Desc指针,是用于描述当前这个block的附加信息的,包括结构体的大小等等信息

static struct __main_block_desc_0 {

  size_t reserved;  //今后升级版本所需区域
  size_t Block_size;//block的大小

} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

第三个部分是mainblockimpl0结构体的构造函数,mainblockimpl0 就是该 block 的实现

__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;
  }

在这个结构体的构造函数里,isa指针保持这所属类的结构体的实例的指针。_mainblockimlp0结构体就相当于Objective-C类对象的结构体,这里的_NSConcreteStackBlock相当于Block的结构体实例,也就是说block其实就是Objective-C对于闭包的对象实现。

2.截获自动变量值

使用Block的时候,不仅可以使用其内部的参数,还可以使用Block外部的局部变量。而一旦在Block内部使用了其外部变量,这些变量就会被Block保存。
通过先前的例子我们知道
Block可以截获局部变量。
修改Block外部的局部变量,Block内部被截获的局部变量不受影响。
修改Block内部到局部变量,编译不通过。

通过C++的代码来看一下Block在截获变量的时候都发生了什么:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  const char *fmt;  //被添加
  int val;          //被添加

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy

        printf(fmt,val);
    }

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 dmy = 256;
    int val = 10;

    const char *fmt = "var = %d\n";

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

    val = 2;
    fmt = "These values were changed. var = %d\n";

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

单独抽取_mainblockimpl0来看一下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt; //截获的自动变量
  int val;         //截获的自动变量

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们可以看到,在block内部语法表达式中使用的自动变量(fmt,val)被作为成员变量追加到了_mainblockimpl0结构体中(注意:block没有使用的自动变量不会被追加,如dmy变量)。
在初始化block结构体实例时(请看mainblockimpl0的构造函数),还需要截获的自动变量fmt和val来初始化mainblockimpl0结构体实例,因为增加了被截获的自动变量,block的体积会变大。

再来看一下函数体的代码:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  printf(fmt,val);
}

从这里看就更明显了:fmt,var都是从__cself里面获取的,更说明了二者是属于block的。而且从注释来看(注释是由clang自动生成的),这两个变量是值传递,而不是指针传递,也就是说Block仅仅截获自动变量的值,所以这就解释了即使改变了外部的自动变量的值,也不会影响Block内部的值。

既然我们无法在Block中改变外部变量的值,所以也就没有必要在Block内部改变变量的值了,因为Block内部和外部的变量实际上是两种不同的存在:前者是Block内部结构体的一个成员变量,后者是在栈区里的临时变量。

现在我们知道:被截获的自动变量的值是无法直接修改的,但是有两个方法可以解决这个问题:

改变存储于特殊存储区域的变量。
通过__block修饰符来改变。

3._Block说明符

前面提到Block中使用自动变量后,在Blockj的结构体实例中重写该自动变量也不会改变原先截获的自动变量的值。不过这样的话就不能在Block中保存值了,不是很方便。因此下面提供两种方法来解决。
第一种方法是:C语言中有一个变量允许Block改写值,即静态变量,也就是用static修饰的变量。但是他并不太好,因为变量作用域结束的同时,原来的自动变量被废弃,因此Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。
第二种方法是:使用_Block说明符。更准确的表达方式是“_Block存储域类说明符”。C语言中有以下存储域类说明符:

  • typedef
  • extern
  • static
  • auto
  • register

使用__block修饰符,它类似于static、auto、和register说明符,用于指定将变量值设置到哪个存储域中。

下面是个例子:

int main(int argc, const char * argv[]) {
    
    __block int val = 10;
    void (^blk)(void) = ^{
        val = 1;
    };
    
    return 0;
}

其源代码如下:

//__block说明符修饰后的变量的结构体
struct __Block_byref_val_0 {
	void *__isa;  //指向所属类的指针
	__Block_byref_val_0 *__forwarding;  //指向自己的内存地址的指针
	int __flags;  //标志性参数,暂时没用到所以默认为0
	int __size;  //该结构体所占用的大小
	int val;  //该结构体存储的值,即原变量的赋的值
};

//block本体
struct __main_block_impl_0 {
	struct __block_impl impl;  //block的主体
	struct __main block desc 0* Desc;  //存储该block的大小
	__Block_byref_val_0 *val;  //__block修饰的变量的值
	//构造函数
	__main_block_impl_0(void *fp, struct __main_block_desc 0 *desc, __Block_byrefval_0 *_val, int flags=0) : val(_val->__forwarding) {
	impl.isa = &_NSConcreteStackBlock;
	impl.Flags = flags;
	impl.FuncPtr = fp;
	Desc = desc;
};

//封装的block逻辑,存储了block的代码块
static void __main_block_func_0(struct__main_block_impl_0 *_cself) {
	__Block_byref_val_0 *val =__cself->val;
	
	(val->__forwarding->val) = 1;
}
static void_main_block_copy_0(struct __main_block_impl_0* dst, struct __main_block_impl_0* src) {
    //根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_imp1_0* src) {
    //自动释放引用的auto变量(相当于release)
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
	unsigned long reserved;  //保留字段
	unsigned long Block_size;  //block大小
	void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);  //copy的函数指针,下面使用构造函数进行了初始化
	void (*dispose)(struct __main_block_impl_0*);  //dispose的函数指针,下面使用构造函数进行了初始化
}
    //构造函数,初始化保留字段、block大小及两个函数
    __main_block_desc_0_DATA = {
	0,
	sizeof(structmain_block_impl_0),
	__main_block_copy_O, 
	__main_block_dispose_0
};
int main() {
    //之前的 __block int val = 10;变成了结构体实例
	struct __Block_byref_val_0 val = {
		0,  //isa指针
		&val,  //指向自身地址的指针
		0,  //标志变量
		sizeof(__Block_byref_val_0),  //block大小
		10  //该数据的值
	};
	blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}

通过源代码发现__block说明符修饰的变量变成了一个结构体,它是利用这个结构体来进行存储数据的:

struct __Block_byref_val_0 val = {
		0,  //isa指针
		&val,  //指向自身地址的指针
		0,  //标志变量
		sizeof(__Block_byref_val_0),  //block大小
		10  //该数据的值
	};

那么为什么会有成员变量__forwarding呢?
因为__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。

4.Block存储域

Block转换为Block的结构体类型的自动变量,_Block变量转换为 block变量的结构体类型的自动变量。所谓结构体类型的自动变量就是栈上生成的该结构体的实例。

Block的所属类:

_NSConcreteStackBlock //栈
_NSConcreteGlobalBlock //全局(.data区)
_NSConcreteMallocBlock //堆

全局Block:

在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。

void (^blk)(void) = ^{printf("Global Block\n");};

int main(void) {
    ······
}

其isa指针初始化如下:

impl.isa = &_NSConcreteGlobalBlock;

该类Block用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。也就是说,全局Block不可以截获自动变量,否则其就不可以设置为全局Block。

也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。以上的这些情况下,Block为_NSConcreteGlobalBlock类对象。即Block配置在应用程序的数据区域中。

栈Block:

其isa指针初始化如下:

impl.isa = &_NSConcreteStackBlock;

除上述的设置两种全局Block之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。

配置在全局变量上的Block,从变量作用域外也可以通过指针安全地使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block变量也会被废弃。我们也就无法再访问到该变量了。

为了解决无法访问被废弃变量这个问题,就出现了堆block,同时也出现了将Block和__block变量从栈上复制到堆上的方法。这样即使Block 语法记述的变量作用域结束,堆上的Block还可以继续存在。下面我们就来说说:

堆Block:

其isa指针初始化如下:

impl.isa = &_NSConcreteMallocBlock;

堆上的Block其实就是将栈上的Block复制过来而成的,有时我们在_block变量配置在堆上的状态下,也可以访问栈上的__block变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block 变量还是从堆上的__block 变量都能够正确访问。

在这里插入图片描述

当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成将Block从栈上复制到堆上的代码。这是为了防止其被废弃而导致我们访问错误。
下面这个例子就是堆Block,用它来说明:

typedef int (^blk_t)(int);

blk_t func(int rate) {
	return ^(int count) {return rate * count;};
}

源代码:

blk_t func(int rate) {
    //因为ARC处于有效的状态,所以blk_t tmp实际上与附有__strong 修饰符的blk_t __strong tmp 相同
    blk_t tmp = &__func_block_impl_0(__func_block_func_0,&__func_block_desc_0_DATA, rate);
    
    //通过 objc4运行时库的runtime/objc-arrmm可知,objc_retainBlock函数实际上就是_Block_copy 函数
    tmp = objc_retainBlock(tmp);
    //等同于 tmp = _Block_copy(tmp);
 
    //最后将tmp放入自动释放池中进行返回
    return objc_autoreleaseReturnValue(tmp);
}    

/*
 *将通过Block语法生成的Block,
 *即配置在栈上的Block用结构体实例
 *赋值给相当于Block类型的变量tmp中。
 */
tmp = _Block_copy(tmp);
/*
 *_Block_copy 函数
 *将栈上的Block复制到堆上。
 *复制后,将堆上的地址作为指针赋值给变量tmp。
 */

return objc_autoreleaseReturnValue(tmp);
/*
 *将堆上的Block作为Objective-c对象
 *注册到autoreleasepool中,然后返回该对象。
 */

通过上面的例子就可以知道将 Block 作为函数返回值返回时,编译器会自动生成复制到堆上的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/273233.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Redis 八种常用数据类型详解

夯实基础,这篇文章带着大家回顾一下 Redis 中的 8 种常用数据类型: 5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zse…

IDEA直接打包Docker镜像

以下为使用IDEA打包Docker镜像并推送到远程仓库(使用Windows打包Docker镜像并推送到远程仓库)教程 1 安装Docker Desktop 下载地址:https://www.docker.com/products/docker-desktop/ 安装成功后,可在cmd查看版本号 2 启动Do…

基于Qt 和python 的自动升级功能

需求: 公司内部的一个客户端工具,想加上一个自动升级功能。 服务端: 1,服务端使用python3.7 ,搭配 fastapi 和uvicorn 写一个简单的服务,开出一个get接口,用于客户端读取安装包的版本&#…

微服务:高并发带来的问题的容错方案

1.相关脚本(陈天狼) 启动nacos客户端: startup.cmd -m standalone 启动sentinel控制台: # 直接使⽤jar命令启动项⽬(控制台本身是⼀个SpringBoot项⽬) java -Dserver.port8080 -Dcsp.sentinel.dashboard.serverlocalhost:808…

通过点击按钮实现查看全屏和退出全屏的效果

动态效果如图&#xff1a; 可以通过点击按钮&#xff0c;或者esc键实现全屏和退出全屏的效果 实现代码&#xff1a; <template><div class"hello"><el-button click"fullScreen()" v-if"!isFullscreen">查看全屏</el-butt…

Obsidian使用200+插件与70+种主题分享

主题资源 下载方式一&#xff1a; 网盘下载 密码:a3eu 下载方式二&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1fOgP8lY29sYxkUAbTlQQCw 提取码&#xff1a;qhxa 下载解压打开红色框文件夹 上面的是插件&#xff0c;下面的是主题 以下介绍安装主题 打开Obsidi…

苍穹外卖-day08:导入地址簿功能代码(单表crud)、用户下单(业务逻辑)、订单支付(业务逻辑,cpolar软件)

苍穹外卖-day08 课程内容 导入地址簿功能代码用户下单订单支付 功能实现&#xff1a;用户下单、订单支付 用户下单效果图&#xff1a; 订单支付效果图&#xff1a; 1. 导入地址簿功能代码&#xff08;单表crud&#xff09; 1.1 需求分析和设计 1.1.1 产品原型&#xff08…

Swift:.ignoresSafeArea():自由布局的全方位掌握

ignoresSafeArea(_ regions : edges:)修饰符的说明 SwiftUI布局系统会调整视图的尺寸和位置&#xff0c;以避免特定的安全区域。这就确保了系统内容&#xff08;比如软件键盘&#xff09;或设备边缘不会遮挡您的视图。要将您的内容扩展到这些区域&#xff0c;您可以通过应用该修…

Centos strema 9 环境部署Glusterfs9

本文档只是创建复制卷&#xff0c;分布式卷&#xff0c;分布式复制卷&#xff0c;纠删卷 操作系统 内核 角色 Ip地址 说明 CentOS Stream 9 x86_64 5.14.0-427.el9.x86_64 客户端 client 192.168.80.119 挂载存储业务机器 CentOS Stream 9 x86_64 5.14.0-427.el9.x8…

idea项目mapper.xml中的SQL语句黄色下划线去除

问题描述 当我们使用idea开发java项目时&#xff0c;经常会与数据库打交道&#xff0c;一般在使用mybatis的时候需要写一大堆的mapper.xml以及SQL语句&#xff0c;每当写完SQL语句的时候总是有黄色下划线&#xff0c;看着很不舒服。 解决方案&#xff1a; 修改idea的配置 Edi…

高效使用git流程分享

准备 假设你已经 clone 了当前仓库&#xff0c;并且你的终端位置已经位于仓库目录中。 查询状态 查询状态常用的命令有 git status 和 git branch。 前者用于查询更改文件情况&#xff0c;后者用于展示所有分支。 chatbot-system$ git status On branch develop Your bran…

Java项目:62 基于ssm的校园驿站管理系统+jsp

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 管理员管理快递仓库信息&#xff0c;管理待发货信息&#xff0c;管理已收快递&#xff0c;管理物流以及留言信息&#xff0c;管理员工和用户资…

影响交易收益的因素有哪些?

在尝试做交易时&#xff0c;你可能会问自己一个问题&#xff1a;交易一天能赚多少钱&#xff1f;“如果我全职投入交易&#xff0c;一天能赚多少&#xff1f;”或者更广泛地说&#xff0c;“交易能为我带来怎样的财富&#xff1f;”这些问题本质上都充满了不确定性&#xff0c;…

【蓝桥杯选拔赛真题69】python小松鼠运坚果 第十五届青少年组蓝桥杯python选拔赛真题 算法思维真题解析

目录 python小松鼠运坚果 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python小松鼠运坚果 第十五届蓝桥杯青少年组python比赛选拔赛真题 一…

编曲学习:如何编写钢琴织体 Cubase12逻辑预置 需要弄明白的问题

钢琴织体是指演奏形式、方式,同一个和弦进行可以用很多种不同的演奏方法。常用织体有分解和弦,柱式和弦,琶音织体,混合织体。 在编写钢琴织体前,先定好歌曲的调。 Cubase小技巧:把钢琴轨道向上拖动打和弦轨道,就可以显示和弦!如果你有一些参考工程,不知道用了哪些和…

Vue通用后台管理项目实战-1

接下来&#xff0c;我将在csdn平台发布我学习Vue通用后台管理项目时的一系列笔记&#xff0c;并放在当前的专栏中&#xff0c;感兴趣的朋友可以订阅专栏&#xff0c;大家一起学习。Vue通用后台管理项目视频链接 【VUE项目&#xff0c;VUE项目实战&#xff0c;vue后台管理系统&…

Day17 深入类加载机制

Day17 深入类加载机制 文章目录 Day17 深入类加载机制一、初识类加载过程二、深入类加载过程三、利用类加载过程理解面试题四、类加载器五、类加载器分类六、类加载器之间的层次关系七、双亲委派模型 - 概念八、双亲委派模型 - 工作过程九、双亲委派模型 - 好处十、双亲委派原则…

杭州市医疗器械经营监督管理规定(景区分局)

杭市政管[2023]92号 各区县&#xff08;市&#xff09;市场监管局&#xff08;景区局&#xff09;、市局办公室、行政执法支队、药检中心&#xff1a; 《杭州市医疗器械经营监督管理条例》已经市局局长办公会议审议通过&#xff0c;现印发给你们。 请遵照执行。 杭州市市场监…

腾讯云优惠券领取指南:让你省钱又省心

随着云计算技术的日益发展&#xff0c;越来越多的企业和个人开始采用云服务来满足业务需求。腾讯云作为国内领先的云服务提供商&#xff0c;以其高效、稳定、安全的服务赢得了众多用户的青睐。为了回馈广大用户&#xff0c;腾讯云经常推出各种优惠券活动&#xff0c;旨在帮助用…

文件系统I/O FATFS RW 源码分析

文件系统I/O FATFS RW 源码分析 0 参考 FatFs 是用于小型嵌入式系统的通用 FAT/exFAT 文件系统模块。FatFs 整个项目都按照 ANSI C (C89) 编写。与存储器 I/O 解耦良好&#xff0c;便于移植到 8051、PIC、AVR、ARM、Z80、RX 等小型微控制器中。 下面是关于 FAT 文件系统格式…
最新文章