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

【iOS】—— retain\release实现原理和属性关键字

【iOS】—— retain\release实现原理和属性关键字

    • 1. retain\reelase实现原理
      • 1.1 retain实现原理
      • 1.2 release实现原理
    • 2. 属性关键字
      • 2.1 属性关键字的分类
      • 2.2 内存管理关键字
        • 2.2.1 weak
        • 2.2.2 assgin
        • 2.3.3 strong和copy
      • 2.4 线程安全的关键字
      • 2.5 修饰变量的关键字
        • 2.5.1常量const
        • 2.5.2 static
        • 2.5.3 常量extern
        • 2.5.4 static与const联合使用
        • 2.5.5 extern与const联合使用

1. retain\reelase实现原理

1.1 retain实现原理

首先来看一下retain的源码:

 ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{// 如果是 Tagged Pointer 则直接返回 this (Tagged Pointer 不参与引用计数管理,它的内存在栈区,由系统处理)if (slowpath(isTaggedPointer())) return (id)this;// 临时变量,标记 SideTable 是否加锁bool sideTableLocked = false;// 临时变量,标记是否需要把引用计数迁移到 SideTable 中bool transcribeToSideTable = false;// 记录 objc_object 之前的 isaisa_t oldisa;// 记录 objc_object 修改后的 isaisa_t newisa;// 似乎是原子性操作,读取 &isa.bits。(&为取地址)oldisa = LoadExclusive(&isa.bits);if (variant == RRVariant::FastOrMsgSend) {// These checks are only meaningful for objc_retain()// They are here so that we avoid a re-load of the isa.// 这些检查仅对objc_retain()有意义// 它们在这里,以便我们避免重新加载isa。if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {ClearExclusive(&isa.bits);if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {return swiftRetain.load(memory_order_relaxed)((id)this);}return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));}}if (slowpath(!oldisa.nonpointer)) {// a Class is a Class forever, so we can perform this check once// outside of the CAS loopif (oldisa.getDecodedClass(false)->isMetaClass()) {ClearExclusive(&isa.bits);return (id)this;}}// 循环结束的条件是 slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))// StoreExclusive 函数,如果 &isa.bits 与 oldisa.bits 的内存内容相同,则返回 true,并把 newisa.bits 复制到 &isa.bits,// 否则返回 false,并把 &isa.bits 的内容加载到 oldisa.bits 中。// 即 do-while 的循环条件是指,&isa.bits 与 oldisa.bits 内容不同,如果它们内容不同,则一直进行循环,// 循环的最终目的就是把 newisa.bits 复制到 &isa.bits 中。// return __c11_atomic_compare_exchange_weak((_Atomic(uintptr_t) *)dst,//                                          &oldvalue, value, __ATOMIC_RELAXED, __ATOMIC_RELAXED)// _Bool atomic_compare_exchange_weak( volatile A *obj, C* expected, C desired );// 定义于头文件 <stdatomic.h>// 原子地比较 obj 所指向对象的内存的内容与 expected 所指向的内存的内容,若它们相等,则以 desired 替换前者(进行读修改写操作)。// 否则,将 obj 所指向的实际内存内容加载到 *expected (进行加载操作)。do {// 默认不需要转移引用计数到 SideTabletranscribeToSideTable = false;// 赋值给 newisa(第一次进来时 &isa.bits, oldisa.bits, newisa.bits 三者是完全相同的)newisa = oldisa;// 如果 newisa 不是优化的 isa (元类的 isa 是原始的 isa (Class cls))if (slowpath(!newisa.nonpointer)) {// 在 mac、arm64e 下不执行任何操作,只在 arm64 下执行 __builtin_arm_clrex();// 在 arm64 平台下,清除对 &isa.bits 的独占访问标记。ClearExclusive(&isa.bits);// 如果需要 tryRetain 则调用 sidetable_tryRetain 函数,并根据结果返回 this 或者 nil。// 执行此行之前是不需要在当前函数对 SideTable 加锁的// sidetable_tryRetain 返回 false 表示对象已被标记为正在释放,// 所以此时再执行 retain 操作是没有意义的,所以返回 nil。if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;// 如果不需要 tryRetain 则调用 sidetable_retain()else return sidetable_retain(sideTableLocked);}// don't check newisa.fast_rr; we already called any RR overrides// 不要检查 newisa.fast_rr; 我们已经调用所有 RR 的重载。// 如果 tryRetain 为真并且 objc_object 被标记为正在释放 (newisa.deallocating),则返回 nilif (slowpath(newisa.isDeallocating())) {ClearExclusive(&isa.bits);// SideTable 处于加锁状态if (sideTableLocked) {ASSERT(variant == RRVariant::Full);// 进行解锁sidetable_unlock();}// 需要 tryRetainif (slowpath(tryRetain)) {return nil;} else {return (id)this;}}// 下面就是 isa 为 nonpointer,并且没有被标记为正在释放的对象uintptr_t carry;// bits extra_rc 自增// x86_64 平台下:// # define RC_ONE (1ULL<<56)// uintptr_t extra_rc : 8// extra_rc 内容位于 56~64 位newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++// 如果 carry 为 true,表示要处理引用计数溢出的情况if (slowpath(carry)) {// newisa.extra_rc++ overflowed// 如果 variant 不为 Full,// 则调用 rootRetain_overflow(tryRetain) 它的作用就是把 variant 传为 Full// 再次调用 rootRetain 函数,目的就是 extra_rc 发生溢出时,我们一定要处理if (variant != RRVariant::Full) {ClearExclusive(&isa.bits);return rootRetain_overflow(tryRetain);}// Leave half of the retain counts inline and // prepare to copy the other half to the side table.// 将 retain count 的一半留在 inline,并准备将另一半复制到 SideTable.if (!tryRetain && !sideTableLocked) sidetable_lock();// 整个函数只有这里把 sideTableLocked 置为 truesideTableLocked = true;// 标记需要把引用计数转移到 SideTable 中transcribeToSideTable = true;// x86_64 平台下:// uintptr_t extra_rc : 8// # define RC_HALF  (1ULL<<7) 二进制表示为: 0b 1000,0000// extra_rc 总共 8 位,现在把它置为 RC_HALF,表示 extra_rc 溢出newisa.extra_rc = RC_HALF;// 把 has_sidetable_rc 标记为 true,表示 extra_rc 已经存不下该对象的引用计数,// 需要扩张到 SideTable 中newisa.has_sidetable_rc = true;}} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));if (variant == RRVariant::Full) {if (slowpath(transcribeToSideTable)) {// Copy the other half of the retain counts to the side table.// 复制 retain count 的另一半到 SideTable 中。sidetable_addExtraRC_nolock(RC_HALF);}// 如果 tryRetain 为 false 并且 sideTableLocked 为 true,则 SideTable 解锁if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();} else {ASSERT(!transcribeToSideTable);ASSERT(!sideTableLocked);}// 返回 thisreturn (id)this;
}

在这里插入图片描述

我们根据图来看一下retain步骤:

  1. 若对象是为TaggedPointer小对象,无需进行内存管理,直接返回。
  2. 若isa指针没有进过优化,!newisa.nonpointer成立,由于tryRetain=false直接进入sidetable_retain方法,此方法本质是直接操作散列表,最后让目标对象的引用计数+1;
  3. 判断对象是否正在释放,若正在释放,则执行dealloc流程,释放弱引用表和引用计数表。
  4. **若对象的isa经过优化,**执行newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry),即isa的位域extra_rc+1,且通过变量carry来判断位域extra_rc是否已满,如果位域extra_rc已满则执行newisa.extra_rc = RC_HALF,即将extra_rc满状态的一半拿出来存到extra_rc位域中,然后将另一半存储到散列表中,执行sidetable_addExtraRC_nolock(RC_HALF)函数;

1.2 release实现原理

看一下release的源代码:

 ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{if (isTaggedPointer()) return false;bool sideTableLocked = false;isa_t oldisa;isa_t newisa;retry:do {oldisa = LoadExclusive(&isa.bits);newisa = oldisa;if (slowpath(!newisa.nonpointer)) {// 未优化 isaClearExclusive(&isa.bits);if (sideTableLocked) sidetable_unlock();// 入参是否要执行 Dealloc 函数,如果为 true 则执行 SEL_deallocreturn sidetable_release(performDealloc);}// extra_rc --newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--if (slowpath(carry)) {// donot ClearExclusive()goto underflow;}// 更新 isa 值} while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits)));if (slowpath(sideTableLocked)) sidetable_unlock();return false;underflow:// 处理下溢,从 side table 中借位或者释放newisa = oldisa;// 如果使用了 sidetable_rcif (slowpath(newisa.has_sidetable_rc)) {if (!handleUnderflow) {// 调用本函数处理下溢ClearExclusive(&isa.bits);return rootRelease_underflow(performDealloc);}// 从 sidetable 中借位引用计数给 extra_rcsize_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);if (borrowed > 0) {// extra_rc 是计算额外的引用计数,0 即表示被引用一次newisa.extra_rc = borrowed - 1;  // redo the original decrement toobool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits);// 保存失败,恢复现场,重试                                    if (!stored) {isa_t oldisa2 = LoadExclusive(&isa.bits);isa_t newisa2 = oldisa2;if (newisa2.nonpointer) {uintptr_t overflow;newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);if (!overflow) {stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, newisa2.bits);}}}// 如果还是保存失败,则还回 side tableif (!stored) {sidetable_addExtraRC_nolock(borrowed);goto retry;}sidetable_unlock();return false;}else {// Side table is empty after all. Fall-through to the dealloc path.}}// 没有使用 sidetable_rc ,或者 sidetable_rc 计数 == 0 的就直接释放// 如果已经是释放中,抛个过度释放错误if (slowpath(newisa.deallocating)) {ClearExclusive(&isa.bits);if (sideTableLocked) sidetable_unlock();return overrelease_error();// does not actually return}// 更新 isa 状态newisa.deallocating = true;if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;if (slowpath(sideTableLocked)) sidetable_unlock();// 执行 SEL_dealloc 事件__sync_synchronize();if (performDealloc) {((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);}return true;
}

在这里插入图片描述
我们根据图来看一下release步骤:

  1. 若对象为TaggedPointer小对象,不需要做内存操作,直接返回。
  2. **若对象的isa没有经过优化,**即!newisa.nonpointer成立,直接进入sidetable_release方法,此方法本质是直接操作散列表,最后让目标对象的引用计数-1。
  3. 判断引用计数是否为0 ,如果是0则直接执行dealloc流程。
  4. 若对象的isa经过优化,则执行newisa.bits = subc(newisa.bits, RC_ONE, 0, &),即对象的isa位域extra_rc-1;且通过变量carry标识对象的isa的extra_rc是否为0, 如果对象的isa的extra_rc=0,则去访问散列表,判断对象在散列表中是否存在引用计数。
  5. 如果sidetable的引用计数为0,对象进行dealloc流程。

2. 属性关键字

属性关键字是用来修饰属性的关键字,保证程序的正常执行。

2.1 属性关键字的分类

  • 内存管理有关的关键字:weak, assgin, strong, retain, copy
  • 线程安全的关键字:monatomicatomic
  • 访问权限的关键字:readonlyreadwrite
  • 修饰变量的关键字:conststaticextern

2.2 内存管理关键字

2.2.1 weak

weak将常用来修饰OC对象数据类型,修饰的对象释放之后,指针会自动置nil,这是弱引用的表现。

**在ARC的环境下,为了避免循环引用,delegate往往是用的是weak修饰。在MRC下使用assgin修饰。**当某个对象不再拥有strong类型的指向的时候对象就会被释放,即使还有weak类型的指针指向它,weak指针也会被清除。

2.2.2 assgin

assgin常用于非指针变量,用于修饰基础数据类型和C的数据类型,用于基本数据类型进行复制操作。

asssgin不会修改引用计数,也可以用来修饰对象一般不建议如此,因为assgin修饰的对象被释放之后指针的地址还存着,成为了一个没有指向的野指针(垂悬指针)。

assgin修饰的基本类型都是基本数据类型,基本数据类型分配在栈上,栈上的变量是由系统自动管理,不会造成野指针以及MRC状态下的循环引用。

eg:当对象A通过retain持有了B,B的delegate对象是A,如果都是强引用则导致互相持有无法正确的释放,造成循环引用。

weak和assgin的区别:

  • 修饰的对象不同:weak修饰OC对象类型的数据,assgin修饰的基本数据类型。
  • 引用计数:两者都不会增加引用计数。
  • 释放后结果不同:weak修饰的对象释放之后指针自动为nil。assgin修饰的对象释放之后指针仍然存在,成为野指针。
  • 修饰delegate:MRC下assgin,ARC下weak,两者都是为了避免循环引用。
2.3.3 strong和copy

strong是常用的修饰符,主要用来修饰OC对象类型的数据(NSNumber,NSString,NSArray、NSDate、NSDictionary、模型类等)。 strong是强引用,在ARC下等于retain,这一点区别于weak。

strong就是指针拷贝(浅拷贝),内存地址不变,只是产生新的指针,新的指针和引用对象的指针指向同一个内存地址,没有生成新的对象只是多了一个指针。

**注意:**由于使用的是一个内存地址,当该内存地址存储的内容发生变更的时候导致属性也跟着变更。

同样用于修饰OC对象类型的数据,同时在MRC时期用来修饰block,因为MRC时期block要从栈区copy到堆区。现在的ARC系统自动给我们做了这个操作。也就是现在使用strong或者copy修饰block都可以。

copy和strong相同点在于都是属于强引用,引用计数+1,但是copy修饰的对象是内存拷贝,在引用的时候会生成新的内存地址和指针,和引用对象完全没有相同点,因此它不会因为引用属性的变更而改变。

copy关键字和strong的区别:

  • **copy:**内存拷贝-深拷贝,内存地址不同,指针地址也不同。

  • **strong:**指针拷贝-浅拷贝,内存地址不变,指针地址不同。

声明两个copy属性,两个strong属性,分别为可变和不可变类型:

@property (nonatomic, strong) NSString *Strstrong;
@property (nonatomic, copy) NSString *Strcopy;
@property (nonatomic, strong) NSMutableString *MutableStrongstr;
@property (nonatomic, copy) NSMutableString *MutableCopystr;

1. 不可变对象对属性进行赋值,查看两者的区别

 - (void)TestModel {//不可变对象对属性赋值NSString *otherString = @"我是谁";self.Strcopy = otherString;self.Strstrong = otherString;self.MutableCopystr = otherString;self.MutableStrongstr = otherString;  // 内容NSLog(@"原字符串=>%@\n normal:copy=>%@=====strong=>%@\nMutable:copy=>%@=====strong=>%@", otherString, _Strcopy, _Strstrong, _MutableCopystr, _MutableStrongstr);// 内存地址NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p", otherString, _Strcopy, _Strstrong, _MutableCopystr, _MutableStrongstr);// 指针地址NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p", &otherString, &_Strcopy, &_Strstrong, &_MutableCopystr, &_MutableStrongstr); }

在这里插入图片描述
由上面可以看出,strong修饰的对象,在引用一个对象的时候,内存地址都是一样的,只有指针地址不同,copy修饰的对象也是如此。
为什么呢?不是说copy修饰的对象是生成一个新的内存地址嘛?这里为什么内存地址还是原来的呢?用不可变对象对属性进行赋值,无论是strong还是copy,都是一样的,原内存地址不变,生成了新的指针地址。

2. 可变对象对属性进行赋值,查看strong和copy的区别

 - (void)testModel {//可变对象对属性赋值NSMutableString * OriginalMutableStr = [NSMutableString stringWithFormat:@"我已经开始测试了"];self.Strcopy = OriginalMutableStr;self.Strstrong = OriginalMutableStr;self.MutableCopystr = OriginalMutableStr;self.MutableStrongstr = OriginalMutableStr;[OriginalMutableStr appendFormat:@"改变了"];// 内容NSLog(@"原字符串=>%@\n normal:copy=>%@=====strong=>%@\nMutable:copy=>%@=====strong=>%@",OriginalMutableStr,_Strcopy,_Strstrong,_MutableCopystr,_MutableStrongstr);// 内存地址NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p",OriginalMutableStr,_Strcopy,_Strstrong,_MutableCopystr,_MutableStrongstr);// 指针地址NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p",&OriginalMutableStr,&_Strcopy,&_Strstrong,&_MutableCopystr,&_MutableStrongstr);
}

在这里插入图片描述
在上面的结果可以看出,strong修饰的属性内存地址依然没有改变,但是copy修饰的属性内存值产生了变化。
由此得出结论:对可变对象赋值 strong 是原地址不变,引用计数+1(浅拷贝)。 copy是生成一个新的地址和对象生成一个新指针指向新的内存地址(深拷贝)。

3. 此时改变OriginalMutableStr的值

 [OriginalMutableStr appendFormat:@"改变了"];

在这里插入图片描述
结论:

  1. strong修饰的属性,跟着进行改变。
  2. 由于OriginalMutableStr是可变类型,是在原有内存上进行修改,指针地址和内存地址都没有改变,由于strong修饰的属性虽然指针地址不同,但是指针指向的是原内存地址。
  3. 不同于strong,copy修饰的类型不仅指针地址不同,而且指向的内存地址也和OriginalMutableStr不一样,所以不会跟着 OriginalMutableStr的改变而改变。

注意的是:使用self.Strcopy 和 _Strcopy 来赋值也是两个不一样的结果,因为后者没有调用 set 方法,而 copy 和 strong 之所以会产生差别就是因为在 set 方法中,copy修饰的属性: 调用了 _Strcopy = [Strcopy copy] 方法

4. 深浅拷贝

1)深浅拷贝的区别?

浅拷贝:对内存地址的复制,两个指针指向同一个地址,增加被拷贝对象的引用计数,没有发生新的内存分配。

深拷贝:目标对象指针和原对象指针,指向两片内存空间。(不会增加被拷贝对象的引用计数,产生新的内存,出现两块内存。

总结区别:

  • 浅拷贝增加引用计数,不产生新的内存。
  • 深拷贝不增加引用计数,会新分配内存。

2)copy关键字影响了对象的可变和不可变属性吗?

  • 可变对象(mutable)copy和mutableCopy都是深拷贝
  • 不可变对象(immutable)的copy是浅拷贝,mutableCopy是深拷贝
  • copy方法返回的都是不可变对象,若被拷贝对象是可变对象,返回的也是不可变对象。
    在这里插入图片描述
    3)NSMutableArray用copy修饰会出现什么问题?

**出现调用可变方法不可控问题,会导致程序崩溃。**给Mutable 被声明为copy修饰的属性赋值, 过程描述如下:

如果赋值过来的是NSMutableArray对象,会对可变对象进行copy操作,拷贝结果是不可变的,那么copy后就是NSArray
如果赋值过来的是NSArray对象, 会对不可变对象进行copy操作,拷贝结果仍是不可变的,那么copy之后仍是NSArray。
所以不论赋值过来的是什么对象,只要对NSMutableArray进行copy操作,返回的对象都是不可变的。
原来属性声明的是NSMutableArray,可能会调用了add或者remove方法,拷贝后的结果是不可变对象,所以一旦调用这些方法就会程序崩溃(crash)。

4)说说strong和weak的区别是?

  • strong表示指向并拥有该对象,修饰的对象引用计数+1,只要引用计数不为0,就不会被销毁,
  • weak表示指向但是不拥有该对象,修饰的对象引用计数不会增加。无需手动该对象会自行在内存中销毁。

5)weak属性修饰的变量,如何实现在变量没有强引用后自动置为 nil ?

runtime维护了一个weak_table _t弱引用表,用于存储某个对象的所有weak指针。weak是一个哈希表key是所指对象的的地址,value是weak指针的地址的数组。在回收对象的时候,根据对象的地址将所有的weak指针地址的数组便利,把其中的数据值为nil。

2.4 线程安全的关键字

  • nonatomic关键字

​ nonatomic:非原子操作,不加锁,线程执行快,但是多个线程同时访问同一属性会出现崩溃。

  • atomic关键字

​ atomic原子操作:加锁,保证setter和getter存取方法的线程安全(仅仅对setter和getter方法加锁)。因为线程加锁,别的线程访问当前属性的时候会先执行完属性当前的操作。

⚠️注意:atomic只针对属性的 getter/setter 方法进行加锁,所以安全只是针对getter/setter方法来说,并不是整个线程安全,因为一个属性并不只有 setter/getter 方法,例:(如果一个线程正在getter 或者 setter时,有另外一个线程同时对该属性进行release操作,如果release先完成,会造成crash)

2.5 修饰变量的关键字

2.5.1常量const

常量修饰符,表示不可变,可以用来修饰右边的基本变量和指针变量(放在谁的前面修饰谁(基本数据变量p,指针变量*p))。

**const 类型 * 变量名a:可以改变指针的指向,不能改变指针指向的内容。 **

const放在 * 号的前面约束参数,表示*a只读。只能修改地址a,不能通过a修改访问的内存空间

int x = 12;
int new_x = 21;
const int *px = &x; 
px = &new_x; // 改变指针px的指向,使其指向变量y

**类型 * const 变量名:可以改变指针指向的内容,不能改变指针的指向。 **

const放后面约束参数,表示a只读,不能修改a的地址,只能修改a访问的值,不能修改参数的地址

int y = 12;
int new_y = 21;
int * const py = &y;
(*py) = new_y; // 改变px指向的变量x的值

const和define的区别:

使用宏和常量所占的内存差别不大,宏定义的是常量,常量都放在常量区,只会生成一份内存。

缺点:

  • 编译时刻:宏是预编译,const是编译阶段。
  • 导致使用宏定义过多的话,随着工程越来越大,编译速度会越来越慢
    宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。

优点:

  • 宏可以定义一些函数,方法。const不能。
2.5.2 static

定义所修饰的对象只能在当前文件访问,不能通过extern来引用

  1. static修饰全局变量:只能在本文件中访问,修改全局变量的作用域,生命周期不会改。避免重复定义全局变量(单例模式)
  2. static修饰局部变量:
  • 有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用的时候该变量已经有值。这时就应该指定该局部变量为静态变量,用关键字 static 进行声明。
  • 延长局部变量的生命周期(没有改变变量的作用域,只在当前作用域有用),程序结束才会销毁。

注意:当在对象A里这么写static int i = 10; 当A销毁掉之后 这个i还存在当再次alloc init一个A的对象之后 在新对象里 依然可以拿到i = 90,除非杀死程序 再次进入才能得到i = 0。

局部变量只会生成一份内存,只会初始化一次。把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在

- (void)test{// static修饰局部变量1static int age = 0;age++;NSLog(@"%d",age);
}
-(void)test2{// static修饰局部变量2static int age = 0;age++;NSLog(@"%d",age);
}[self test];
[self test2];
[self test];
[self test2];
[self test];
[self test2];打印 1 1 2 2 3 3

由此可见 变量生命周期延长了,作用域没有变。

2.5.3 常量extern

只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量。

查找优先级: 先在当前文件查找有没有全局变量,没有找到,才会去其他文件查找。

#import "JMProxy.h"
@implementation JMProxy
int ageJMProxy = 20;
@end@implementation TableViewController
- (void)viewDidLoad {[super viewDidLoad];extern int ageJMProxy;NSLog(@"%d",ageJMProxy);
}
@end

⚠️ extern不能用于定义变量。

2.5.4 static与const联合使用

声明一个静态的全局只读常量。开发中声明的全局变量,有些不希望外界改动,只允许读取。

iOS中staic和const常用使用场景,是用来代替宏,把一个经常使用的字符串常量,定义成静态全局只读变量.

 // 开发中经常拿到key修改值,因此用const修饰key,表示key只读,不允许修改。
static  NSString * const key = @"name";// 如果 const修饰 *key1,表示*key1只读,key1还是能改变。static  NSString const *key1 = @"name";
2.5.5 extern与const联合使用

在多个文件中经常使用的同一个字符串常量,可以使用extern与const组合
extern与const组合:只需要定义一份全局变量,多个文件共享

@interface Person : NSObject
extern NSString * const nameKey = @"name"; 
@end#import "ViewController.h"
@interface ViewController ()@end
NSString * const nameKey; // 必须用xonst才能访问到 extern与const组合组合修饰的全局变量 

相关文章:

【iOS】—— retain\release实现原理和属性关键字

【iOS】—— retain\release实现原理和属性关键字 1. retain\reelase实现原理1.1 retain实现原理1.2 release实现原理 2. 属性关键字2.1 属性关键字的分类2.2 内存管理关键字2.2.1 weak2.2.2 assgin2.3.3 strong和copy 2.4 线程安全的关键字2.5 修饰变量的关键字2.5.1常量const…...

这一文,关于Java泛型的点点滴滴 一

作为一个 Java 程序员&#xff0c;用到泛型最多的&#xff0c;我估计应该就是这一行代码&#xff1a; List<String> list new ArrayList<>();这也是所有 Java 程序员的泛型之路开始的地方啊。 不过本文讲泛型&#xff0c;先不从这里开始讲&#xff0c;而是再往前…...

微信小程序之调查问卷

一、设计思路 1、界面 调查问卷又称调查表&#xff0c;是以问题的形式系统地记载调查内容的一种形式。微信小程序制作的调查问卷&#xff0c;可以在短时间内快速收集反馈信息。具体效果如下所示&#xff1a; 2、思路 此调查问卷采用服务器客户端的方式进行设计&#xff0c;服…...

基于Qt的视频剪辑

在Qt中进行视频剪辑可以通过多种方式实现&#xff0c;但通常需要使用一些额外的库来处理视频数据。以下是一些常见的方法和步骤&#xff1a; 使用FFmpeg FFmpeg是一个非常强大的多媒体框架&#xff0c;可以用来处理视频和音频数据。你可以使用FFmpeg的命令行工具或者其库来实现…...

electron 网页TodoList工具打包成win桌面应用exe

参考&#xff1a; electron安装&#xff08;支持win、mac、linux桌面应用&#xff09; https://blog.csdn.net/weixin_42357472/article/details/140643624 TodoList工具 https://blog.csdn.net/weixin_42357472/article/details/140618446 electron打包过程&#xff1a; 要将…...

数据结构之判断二叉树是否为搜索树(C/C++实现)

文章目录 判断二叉树是否为搜索树方法一&#xff1a;递归法方法二&#xff1a;中序遍历法总结 二叉树是一种非常常见的数据结构&#xff0c;它在计算机科学中有着广泛的应用。二叉搜索树&#xff08;Binary Search Tree&#xff0c;简称BST&#xff09;是二叉树的一种特殊形式&…...

golang长连接的误用

误用一&#xff1a;忘记读取响应的body 由于忘记读取响应的body导致创建大量处于TIME_WAIT状态的连接&#xff08;同时产生大量处于transport.go的readLoop和writeLoop的协程&#xff09; 在linux下运行下面的代码: package mainimport ("fmt""html"&qu…...

Springboot @Validate @Valid 基于复杂嵌套对象的参数校验示例

Springboot Validate Valid 基于复杂嵌套对象的参数校验示例 复杂对象 Data public class Object1 {Length(max 50,message "长度不能超过50位字符")NotBlank(message "名称不能为空")private String name;NotNull(message "不能为空")pri…...

算力共享下的,分级路由转发报文协议与通告

目录 网络双 SLA 约束 一、双SLA约束的定义与背景 二、双SLA约束的应用场景 三、双SLA约束的管理与实施 四、双SLA约束的优势与挑战 算力共享下的,分级路由转发报文协议与通告 基础设施即服务(IaaS)类 型算力资源 函数即服务(FaaS)类型算力服务 软件即服务(SaaS…...

滚动数组详解

滚动数组详解 何为滚动数组&#xff1f;滚动数组是如何优化空间的&#xff1f;交替滚动例题&#xff1a;来自某某轮廓线DP的题目 自我滚动(~~不如交替~~ 完结&#xff01;&#xff01;&#xff01; ( 宇宙免责任书&#xff1a;我用的是C) 何为滚动数组&#xff1f; 什么是滚动…...

C 语言动态链表

线性结构->顺序存储->动态链表 一、理论部分 从起源中理解事物&#xff0c;就是从本质上理解事物。 -杜勒鲁奇 动态链表是通过结点&#xff08;Node&#xff09;的集合来非连续地存储数据&#xff0c;结点之间通过指针相互连接。 动态链表本身就是一种动态分配内存的…...

【Leetcode】二十、记忆化搜索:零钱兑换

文章目录 1、记忆化搜索2、leetcode509&#xff1a;斐波那契数列3、leetcode322&#xff1a;零钱兑换 1、记忆化搜索 也叫备忘录&#xff0c;即把已经计算过的结果存下来&#xff0c;下次再遇到&#xff0c;就直接取&#xff0c;不用重新计算。目的是以减少重复计算。 以前面提…...

json数据格式 继续学习

1.定义 轻量级的数据交互格式&#xff0c;可以按照json数据格式去组织和封装数据。 本质是一个带有特定格式的字符串。 2.功能 负责不同编程语言中的数据传递和交互。 3.json数据格式转化 """ 演示json数据和python字典之间的转换 """ impor…...

gradle 构建项目添加版本信息

gradle 构建项目添加版本信息&#xff0c;打包使用 spring boot 的打包插件 build.gradle 配置文件 bootJar {manifest {attributes(Project-Name: project.name,Project-Version: project.version,"project-Vendor": "XXX Corp","Built-By": &…...

vue3 学习笔记17 -- 基于el-menu封装菜单

vue3 学习笔记17 – 基于el-menu封装菜单 前提条件&#xff1a;组件创建完成 配置路由 // src/router/index.ts import { createRouter, createWebHashHistory } from vue-router import type { RouteRecordRaw } from vue-router export const Layout () > import(/lay…...

使用 Redis 实现验证码、token 的存储,用自定义拦截器完成用户认证、并使用双重拦截器解决 token 刷新的问题

可以看一下我以前做过的笔记&#xff1a;黑马点评 短信登录部分 基于session实现登录流程 1.发送验证码 用户在提交手机号后&#xff0c;会校验手机号是否合法&#xff0c;如果不合法&#xff0c;则要求用户重新输入手机号 如果手机号合法&#xff0c;后台此时生成对应的验…...

反转链表 - 力扣(LeetCode)C语言

206. 反转链表 - 力扣&#xff08;LeetCode&#xff09;( 点击前面链接即可查看题目) /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode* reverseList(struct ListNode* head) {if(head NULL)…...

【Linux】进程间通信(1):进程通信概念与匿名管道

人与人之间是如何通信的&#xff1f;举个简单的例子&#xff0c;假如我是月老&#xff0c;我要为素不相识的但又渴望爱情的男女两方牵红线。我需要收集男方的信息告诉女方&#xff0c;收集女方的信息告诉男方&#xff0c;然后由男女双方来决定是否继续。对于他们而言&#xff0…...

Spring从入门到精通 01

文章目录 1. 依赖注入 (Dependency Injection, DI)2. 面向切面编程 (Aspect-Oriented Programming, AOP)3. 事务管理4. 简化 JDBC 开发5. 集成各种框架和技术6. 模块化和扩展性&#xff1a;主要的 Spring 模块&#xff1a;Core Container&#xff1a;AOP 模块&#xff1a;Data …...

C语言经典习题25

冒泡排序 对一维数组进行升序排序&#xff0c;然后在数组中输入20个数&#xff0c;将排序后的结果打印输出。 #include<stdio.h> #define N 20 int main() {int a[N];int i;for(i0;i<N;i) //初始化数组的数 {scanf("%d",&a);}for(i0;…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

系统设计 --- MongoDB亿级数据查询优化策略

系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log&#xff0c;共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题&#xff0c;不能使用ELK只能使用…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?

Otsu 是一种自动阈值化方法&#xff0c;用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理&#xff0c;能够自动确定一个阈值&#xff0c;将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...

linux 错误码总结

1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

MODBUS TCP转CANopen 技术赋能高效协同作业

在现代工业自动化领域&#xff0c;MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步&#xff0c;这两种通讯协议也正在被逐步融合&#xff0c;形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

现代密码学 | 椭圆曲线密码学—附py代码

Elliptic Curve Cryptography 椭圆曲线密码学&#xff08;ECC&#xff09;是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础&#xff0c;例如椭圆曲线数字签…...

【分享】推荐一些办公小工具

1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由&#xff1a;大部分的转换软件需要收费&#xff0c;要么功能不齐全&#xff0c;而开会员又用不了几次浪费钱&#xff0c;借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...