肿瘤康复网,内容丰富有趣,生活中的好帮手!
肿瘤康复网 > iOS底层原理:weak的实现原理

iOS底层原理:weak的实现原理

时间:2020-06-23 10:17:22

相关推荐

作者丨夜幕降临耶链接:

https://juejin.im/post/5e7a322f6fb9a07ca24f79bb来源:掘金

在iOS开发过程中,会经常使用到一个修饰词weak,使用场景大家都比较清晰,避免出现对象之间的强强引用而造成对象不能被正常释放最终导致内存泄露的问题。weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。

1、weak 初探

下面的一段代码是我们在开发中常见的weak的使用

Person*object=[Personalloc];id__weakobjc=object;

如果在此打断点跟踪汇编信息,可以发现底层库调了objc_initWeak函数

那么我们来看一下objc_initWeak方法的实现代码是怎么样的呢?

1、objc_initWeak方法

如下是objc_initWeak方法的底层源码

idobjc_initWeak(id*location,idnewObj){if(!newObj){*location=nil;returnnil;}returnstoreWeak<DontHaveOld,DoHaveNew,DoCrashIfDeallocating>(location,(objc_object*)newObj);}

该方法的两个参数locationnewObj

location__weak指针的地址,存储指针的地址,这样便可以在最后将其指向的对象置为nil。

newObj:所引用的对象。即例子中的obj 。

从上面的代码可以看出objc_initWeak方法只是一个深层次函数调用的入口,在该方法内部调用了storeWeak方法。下面我们来看下storeWeak方法的实现代码。

2、storeWeak方法

如下是storeWeak方法的实现代码。

//Templateparameters.enumHaveOld{DontHaveOld=false,DoHaveOld=true};enumHaveNew{DontHaveNew=false,DoHaveNew=true};enumCrashIfDeallocating{DontCrashIfDeallocating=false,DoCrashIfDeallocating=true};template<HaveOldhaveOld,HaveNewhaveNew,CrashIfDeallocatingcrashIfDeallocating>staticidstoreWeak(id*location,objc_object*newObj){assert(haveOld||haveNew);if(!haveNew)assert(newObj==nil);ClasspreviouslyInitializedClass=nil;idoldObj;SideTable*oldTable;SideTable*newTable;//Acquirelocksforoldandnewvalues.//Orderbylockaddresstopreventlockorderingproblems.//Retryiftheoldvaluechangesunderneathus.retry:if(haveOld){//如果weakptr之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTableoldObj=*location;oldTable=&SideTables()[oldObj];}else{oldTable=nil;//如果weakptr之前没有弱引用过一个obj,则oldTable=nil}if(haveNew){//如果weakptr要weak引用一个新的obj,则将该obj对应的SideTable取出,赋值给newTablenewTable=&SideTables()[newObj];}else{newTable=nil;//如果weakptr不需要引用一个新obj,则newTable=nil}//加锁操作,防止多线程中竞争冲突SideTable::lockTwo<haveOld,haveNew>(oldTable,newTable);//location应该与oldObj保持一致,如果不同,说明当前的location已经处理过oldObj可是又被其他线程所修改if(haveOld&&*location!=oldObj){SideTable::unlockTwo<haveOld,haveNew>(oldTable,newTable);gotoretry;}//Preventadeadlockbetweentheweakreferencemachinery//andthe+initializemachinerybyensuringthatno//weakly-referencedobjecthasanun-+initializedisa.if(haveNew&&newObj){Classcls=newObj->getIsa();if(cls!=previouslyInitializedClass&&!((objc_class*)cls)->isInitialized())//如果cls还没有初始化,先初始化,再尝试设置weak{SideTable::unlockTwo<haveOld,haveNew>(oldTable,newTable);_class_initialize(_class_getNonMetaClass(cls,(id)newObj));//Ifthisclassisfinishedwith+initializethenwe"regood.//Ifthisclassisstillrunning+initializeonthisthread//(i.e.+initializecalledstoreWeakonaninstanceofitself)//thenwemayproceedbutitwillappearinitializingand//notyetinitializedtothecheckabove.//InsteadsetpreviouslyInitializedClasstorecognizeitonretry.previouslyInitializedClass=cls;//这里记录一下previouslyInitializedClass,防止改if分支再次进入gotoretry;//重新获取一遍newObj,这时的newObj应该已经初始化过了}}//Cleanupoldvalue,ifany.if(haveOld){weak_unregister_no_lock(&oldTable->weak_table,oldObj,location);//如果weak_ptr之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址}//Assignnewvalue,ifany.if(haveNew){//如果weak_ptr需要弱引用新的对象newObj//(1)调用weak_register_no_lock方法,将weakptr的地址记录到newObj对应的weak_entry_t中newObj=(objc_object*)weak_register_no_lock(&newTable->weak_table,(id)newObj,location,crashIfDeallocating);//weak_register_no_lockreturnsnilifweakstoreshouldberejected//(2)更新newObj的isa的weakly_referencedbit标志位//Setis-weakly-referencedbitinrefcounttable.if(newObj&&!newObj->isTaggedPointer()){newObj->setWeaklyReferenced_nolock();}//Donotset*locationanywhereelse.Thatwouldintroducearace.//(3)*location 赋值,也就是将weak ptr直接指向了newObj。可以看到,这里并没有将newObj的引用计数+1*location=(id)newObj;//将weakptr指向object}else{//Nonewvalue.Thestorageisnotchanged.}//解锁,其他线程可以访问oldTable,newTable了SideTable::unlockTwo<haveOld,haveNew>(oldTable,newTable);return(id)newObj;//返回newObj,此时的newObj与刚传入时相比,weakly-referencedbit位置1}

storeWeak方法的实现代码虽然有些长,但是并不难以理解。下面我们来分析下该方法的实现。

storeWeak方法实际上是接收了5个参数,分别是haveOld、haveNew和crashIfDeallocating,这三个参数都是以模板的方式传入的,是三个bool类型的参数。分别表示weak指针之前是否指向了一个弱引用,weak指针是否需要指向一个新的引用,若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。

该方法维护了oldTablenewTable分别表示旧的引用弱表和新的弱引用表,它们都是SideTable的hash表。

如果weak指针之前指向了一个弱引用,则会调用weak_unregister_no_lock方法将旧的weak指针地址移除。

如果weak指针需要指向一个新的引用,则会调用weak_register_no_lock方法将新的weak指针地址添加到弱引用表中。

调用setWeaklyReferenced_nolock方法修改weak新引用的对象的bit标志位

那么这个方法中的重点也就是weak_unregister_no_lockweak_register_no_lock这两个方法。而这两个方法都是操作的SideTable这样一个结构的变量,那么我们需要先来了解下SideTable

3、SideTable

先来看下SideTable的定义。

structSideTable{spinlock_tslock;RefcountMaprefcnts;weak_table_tweak_table;}

SideTable的定义很清晰,有三个成员:

spinlock_t slock: 自旋锁,用于上锁/解锁 SideTable。

RefcountMap refcnts:用来存储OC对象的引用计数的hash表(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。

weak_table_t weak_table: 存储对象弱引用指针的hash表。是OC中weak功能实现的核心数据结构。

3.1、weak_table_t

先来看下weak_table_t的底层代码。

structweak_table_t{weak_entry_t*weak_entries;size_tnum_entries;uintptr_tmask;uintptr_tmax_hash_displacement;};

weak_entries:hash数组,用来存储弱引用对象的相关信息weak_entry_t

num_entries:hash数组中的元素个数

mask:hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)

max_hash_displacement:可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)

weak_table_t是一个典型的hash结构。weak_entries是一个动态数组,用来存储weak_entry_t类型的元素,这些元素实际上就是OC对象的弱引用信息。

3.2、weak_entry_t

weak_entry_t的结构也是一个hash结构,其存储的元素是弱引用对象指针的指针, 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。

#defineWEAK_INLINE_COUNT4#defineREFERRERS_OUT_OF_LINE2structweak_entry_t{DisguisedPtr<objc_object>referent;//被弱引用的对象//引用该对象的对象列表,联合。引用个数小于4,用inline_referrers数组。用个数大于4,用动态数组weak_referrer_t *referrersunion{struct{weak_referrer_t*referrers;//弱引用该对象的对象指针地址的hash数组uintptr_tout_of_line_ness:2;//是否使用动态hash数组标记位uintptr_tnum_refs:PTR_MINUS_2;//hash数组中的元素个数uintptr_tmask;// hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。uintptr_tmax_hash_displacement;//可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)};struct{//out_of_line_nessfieldislowbitsofinline_referrers[1]weak_referrer_tinline_referrers[WEAK_INLINE_COUNT];};};boolout_of_line(){return(out_of_line_ness==REFERRERS_OUT_OF_LINE);}weak_entry_t&operator=(constweak_entry_t&other){memcpy(this,&other,sizeof(other));return*this;}weak_entry_t(objc_object*newReferent,objc_object**newReferrer):referent(newReferent)//构造方法,里面初始化了静态数组{inline_referrers[0]=newReferrer;for(inti=1;i<WEAK_INLINE_COUNT;i++){inline_referrers[i]=nil;}}};

可以看到在weak_entry_t的结构定义中有联合体,在联合体的内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers两种方式来存储弱引用对象的指针地址。通过out_of_line()这样一个函数方法来判断采用哪种存储方式。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT时,使用定长数组。当超过WEAK_INLINE_COUNT时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。

到这里我们已经清楚了弱引用表的结构是一个hash结构的表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。那么接下来看看这个弱引用表是怎么维护这些数据的。

4、weak_register_no_lock方法添加弱引用

idweak_register_no_lock(weak_table_t*weak_table,idreferent_id,id*referrer_id,boolcrashIfDeallocating){objc_object*referent=(objc_object*)referent_id;objc_object**referrer=(objc_object**)referrer_id;//如果referent为nil或referent采用了TaggedPointer计数方式,直接返回,不做任何操作if(!referent||referent->isTaggedPointer())returnreferent_id;//确保被引用的对象可用(没有在析构,同时应该支持weak引用)booldeallocating;if(!referent->ISA()->hasCustomRR()){deallocating=referent->rootIsDeallocating();}else{BOOL(*allowsWeakReference)(objc_object*,SEL)=(BOOL(*)(objc_object*,SEL))object_getMethodImplementation((id)referent,SEL_allowsWeakReference);if((IMP)allowsWeakReference==_objc_msgForward){returnnil;}deallocating=!(*allowsWeakReference)(referent,SEL_allowsWeakReference);}//正在析构的对象,不能够被弱引用if(deallocating){if(crashIfDeallocating){_objc_fatal("Cannotformweakreferencetoinstance(%p)of""class%s.Itispossiblethatthisobjectwas""over-released,orisintheprocessofdeallocation.",(void*)referent,object_getClassName((id)referent));}else{returnnil;}}//nowrememberitandwhereitisbeingstored//在weak_table中找到referent对应的weak_entry,并将referrer加入到weak_entry中weak_entry_t*entry;if((entry=weak_entry_for_referent(weak_table,referent))){//如果能找到weak_entry,则讲referrer插入到weak_entry中append_referrer(entry,referrer);//将referrer插入到weak_entry_t的引用数组中}else{//如果找不到,就新建一个weak_entry_tnew_entry(referent,referrer);weak_grow_maybe(weak_table);weak_entry_insert(weak_table,&new_entry);}//Donotset*referrer.objc_storeWeak()requiresthatthe//valuenotchange.returnreferent_id;}

这个方法需要传入四个参数,它们代表的意义如下:

weak_tableweak_table_t结构类型的全局的弱引用表。

referent_id:weak指针。

*referrer_id:weak指针地址。

crashIfDeallocating:若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。

从上面的代码我么可以知道该方法主要的做了如下几个方便的工作。

如果referent为nil 或 referent 采用了TaggedPointer计数方式,直接返回,不做任何操作。

如果对象正在析构,则抛出异常。

如果对象不能被weak引用,直接返回nil。

如果对象没有再析构且可以被weak引用,则调用weak_entry_for_referent方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer方法向其中插入weak指针地址。否则新建一个weak_entry。

4.1、weak_entry_for_referent取元素

staticweak_entry_t*weak_entry_for_referent(weak_table_t*weak_table,objc_object*referent){assert(referent);weak_entry_t*weak_entries=weak_table->weak_entries;if(!weak_entries)returnnil;size_tbegin=hash_pointer(referent)&weak_table->mask;//这里通过&weak_table->mask的位操作,来确保index不会越界size_tindex=begin;size_thash_displacement=0;while(weak_table->weak_entries[index].referent!=referent){index=(index+1)&weak_table->mask;if(index==begin)bad_weak_table(weak_table->weak_entries);//触发badweaktablecrashhash_displacement++;if(hash_displacement>weak_table->max_hash_displacement){//当hash冲突超过了可能的maxhash冲突时,说明元素没有在hash表中,返回nilreturnnil;}}return&weak_table->weak_entries[index];}

4.2、append_referrer添加元素

staticvoidappend_referrer(weak_entry_t*entry,objc_object**new_referrer){if(!entry->out_of_line()){//如果weak_entry尚未使用动态数组,走这里//Trytoinsertinline.for(size_ti=0;i<WEAK_INLINE_COUNT;i++){if(entry->inline_referrers[i]==nil){entry->inline_referrers[i]=new_referrer;return;}}//如果inline_referrers的位置已经存满了,则要转型为referrers,做动态数组。//Couldn"tinsertinline.Allocateoutofline.weak_referrer_t*new_referrers=(weak_referrer_t*)calloc(WEAK_INLINE_COUNT,sizeof(weak_referrer_t));//Thisconstructedtableisinvalid,butgrow_refs_and_insert//willfixitandrehashit.for(size_ti=0;i<WEAK_INLINE_COUNT;i++){new_referrers[i]=entry->inline_referrers[I];}entry->referrers=new_referrers;entry->num_refs=WEAK_INLINE_COUNT;entry->out_of_line_ness=REFERRERS_OUT_OF_LINE;entry->mask=WEAK_INLINE_COUNT-1;entry->max_hash_displacement=0;}//对于动态数组的附加处理:assert(entry->out_of_line());//断言:此时一定使用的动态数组if(entry->num_refs>=TABLE_SIZE(entry)*3/4){//如果动态数组中元素个数大于或等于数组位置总空间的3/4,则扩展数组空间为当前长度的一倍returngrow_refs_and_insert(entry,new_referrer);//扩容,并插入}//如果不需要扩容,直接插入到weak_entry中//注意,weak_entry是一个哈希表,key:w_hash_pointer(new_referrer) value: new_referrer//细心的人可能注意到了,这里weak_entry_t的hash算法和weak_table_t的hash算法是一样的,同时扩容/减容的算法也是一样的size_tbegin=w_hash_pointer(new_referrer)&(entry->mask);//"&(entry->mask)"确保了begin的位置只能大于或等于数组的长度size_tindex=begin;//初始的hashindexsize_thash_displacement=0;//用于记录hash冲突的次数,也就是hash再位移的次数while(entry->referrers[index]!=nil){hash_displacement++;index=(index+1)&entry->mask;// index + 1, 移到下一个位置,再试一次能否插入。(这里要考虑到entry->mask取值,一定是:0x111, 0x1111, 0x11111, ... ,因为数组每次都是*2增长,即8, 16, 32,对应动态数组空间长度-1的mask,也就是前面的取值。)if(index==begin)bad_weak_table(entry);// index == begin 意味着数组绕了一圈都没有找到合适位置,这时候一定是出了什么问题。}if(hash_displacement>entry->max_hash_displacement){//记录最大的hash冲突次数,max_hash_displacement意味着:我们尝试至多max_hash_displacement次,肯定能够找到object对应的hash位置entry->max_hash_displacement=hash_displacement;}//将ref存入hash数组,同时,更新元素个数num_refsweak_referrer_t&ref=entry->referrers[index];ref=new_referrer;entry->num_refs++;}

这段代码首先是使用定长数组还是动态数组,如果是使用定长数组,则直接weak指针地址直接添加到数组即可,如果定长数组已经用尽,则需要将定长数组中的元素转存到动态数组中。

5、weak_unregister_no_lock移除引用

如果weak指针之前指向了一个弱引用,则会调用weak_unregister_no_lock方法将旧的weak指针地址移除。

voidweak_unregister_no_lock(weak_table_t*weak_table,idreferent_id,id*referrer_id){objc_object*referent=(objc_object*)referent_id;objc_object**referrer=(objc_object**)referrer_id;weak_entry_t*entry;if(!referent)return;if((entry=weak_entry_for_referent(weak_table,referent))){//查找到referent所对应的weak_entry_tremove_referrer(entry,referrer);//在referent所对应的weak_entry_t的hash数组中,移除referrer//移除元素之后,要检查一下weak_entry_t的hash数组是否已经空了boolempty=true;if(entry->out_of_line()&&entry->num_refs!=0){empty=false;}else{for(size_ti=0;i<WEAK_INLINE_COUNT;i++){if(entry->inline_referrers[i]){empty=false;break;}}}if(empty){//如果weak_entry_t的hash数组已经空了,则需要将weak_entry_t从weak_table中移除weak_entry_remove(weak_table,entry);}}

首先,它会在weak_table中找出referent对应的weak_entry_t

在weak_entry_t中移除referrer

移除元素后,判断此时weak_entry_t中是否还有元素 (empty==true?)

如果此时weak_entry_t已经没有元素了,则需要将weak_entry_t从weak_table中移除

到这里为止就是对于一个对象做weak引用时底层做的事情,用weak引用对象后引用计数并不会加1,当对象释放时,所有weak引用它的指针又是如何自动设置为nil的呢?

6、dealloc

当对象的引用计数为0时,底层会调用_objc_rootDealloc方法对对象进行释放,而在_objc_rootDealloc方法里面会调用rootDealloc方法。如下是rootDealloc方法的代码实现。

inlinevoidobjc_object::rootDealloc(){if(isTaggedPointer())return;//fixmenecessary?if(fastpath(isa.nonpointer&&!isa.weakly_referenced&&!isa.has_assoc&&!isa.has_cxx_dtor&&!isa.has_sidetable_rc)){assert(!sidetable_present());free(this);}else{object_dispose((id)this);}}

首先判断对象是否是Tagged Pointer,如果是则直接返回。

如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced、没有关联对象!isa.has_assoc、没有自定义的C++析构方法!isa.has_cxx_dtor、没有用到SideTable来引用计数!isa.has_sidetable_rc则直接快速释放。

如果不能满足2中的条件,则会调用object_dispose方法。

6.1、object_dispose

object_dispose方法很简单,主要是内部调用了objc_destructInstance方法。

void*objc_destructInstance(idobj){if(obj){//Readalloftheflagsatonceforperformance.boolcxx=obj->hasCxxDtor();boolassoc=obj->hasAssociatedObjects();//Thisorderisimportant.if(cxx)object_cxxDestruct(obj);if(assoc)_object_remove_assocations(obj);obj->clearDeallocating();}returnobj;}

上面这一段代码很清晰,如果有自定义的C++析构方法,则调用C++析构函数。如果有关联对象,则移除关联对象并将其自身从Association Manager的map中移除。调用clearDeallocating方法清除对象的相关引用。

6.2、clearDeallocating

inlinevoidobjc_object::clearDeallocating(){if(slowpath(!isa.nonpointer)){//Slowpathforrawpointerisa.sidetable_clearDeallocating();}elseif(slowpath(isa.weakly_referenced||isa.has_sidetable_rc)){//Slowpathfornon-pointerisawithweakrefsand/orsidetabledata.clearDeallocating_slow();}assert(!sidetable_present());}

clearDeallocating中有两个分支,现实判断对象是否采用了优化isa引用计数,如果没有的话则需要清理对象存储在SideTable中的引用计数数据。如果对象采用了优化isa引用计数,则判断是都有使用SideTable的辅助引用计数(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合这两种情况中一种的,调用clearDeallocating_slow方法。

6.3、clearDeallocating_slow

NEVER_INLINEvoidobjc_object::clearDeallocating_slow(){assert(isa.nonpointer&&(isa.weakly_referenced||isa.has_sidetable_rc));SideTable&table=SideTables()[this];//在全局的SideTables中,以this指针为key,找到对应的SideTabletable.lock();if(isa.weakly_referenced){//如果obj被弱引用weak_clear_no_lock(&table.weak_table,(id)this);//在SideTable的weak_table中对this进行清理工作}if(isa.has_sidetable_rc){//如果采用了SideTable做引用计数table.refcnts.erase(this);//在SideTable的引用计数中移除this}table.unlock();}

在这里我们关心的是weak_clear_no_lock方法。这里调用了weak_clear_no_lock来做weak_table的清理工作。

6.4、weak_clear_no_lock

voidweak_clear_no_lock(weak_table_t*weak_table,idreferent_id){objc_object*referent=(objc_object*)referent_id;weak_entry_t*entry=weak_entry_for_referent(weak_table,referent);//找到referent在weak_table中对应的weak_entry_tif(entry==nil){///XXXshouldn"thappen,butdoeswithmismatchedCF/objc//printf("XXXnoentryforcleardeallocating%p\n",referent);return;}//zerooutreferencesweak_referrer_t*referrers;size_tcount;//找出weak引用referent的weak指针地址数组以及数组长度if(entry->out_of_line()){referrers=entry->referrers;count=TABLE_SIZE(entry);}else{referrers=entry->inline_referrers;count=WEAK_INLINE_COUNT;}for(size_ti=0;i<count;++i){objc_object**referrer=referrers[i];//取出每个weakptr的地址if(referrer){if(*referrer==referent){//如果weakptr确实weak引用了referent,则将weakptr设置为nil,这也就是为什么weak指针会自动设置为nil的原因*referrer=nil;}elseif(*referrer){//如果所存储的weakptr没有weak引用referent,这可能是由于runtime代码的逻辑错误引起的,报错_objc_inform("__weakvariableat%pholds%pinsteadof%p.""Thisisprobablyincorrectuseof""objc_storeWeak()andobjc_loadWeak().""Breakonobjc_weak_errortodebug.\n",referrer,(void*)*referrer,(void*)referent);objc_weak_error();}}}weak_entry_remove(weak_table,entry);//由于referent要被释放了,因此referent的weak_entry_t也要移除出weak_table}

7、总结

1、weak的原理在于底层维护了一张weak_table_t结构的hash表,key是所指对象的地址,value是weak指针的地址数组。

2、weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。

3、对象释放时,调用clearDeallocating函数根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

4、文章中介绍了SideTable、weak_table_t、weak_entry_t这样三个结构,它们之间的关系如下图所示。

参考资料

SideTables, SideTable, weak_table, weak_entry_t

/u013378438/article/details/82790332

weak引用的底层实现原理

/u013378438/article/details/82767947

近期精彩内容推荐:

直播界要哭了!罗永浩进军电商直播

人家裁员我加薪,他凭什么身价1200亿?

什么?你还在使用fastjson,性能太差了

抖音用户画像报告

在看点这里

好文分享给更多人↓↓

如果觉得《iOS底层原理:weak的实现原理》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。