作者丨夜幕降临耶链接:
https://juejin.im/post/5e7a322f6fb9a07ca24f79bb来源:掘金
1、weak 初探在iOS开发过程中,会经常使用到一个修饰词
weak
,使用场景大家都比较清晰,避免出现对象之间的强强引用而造成对象不能被正常释放最终导致内存泄露的问题。weak 关键字的作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。
下面的一段代码是我们在开发中常见的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);}
该方法的两个参数location
和newObj
。
location:
的地址,存储指针的地址,这样便可以在最后将其指向的对象置为nil。
__weak指针
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。该方法维护了
oldTable
和newTable
分别表示旧的引用弱表和新的弱引用表,它们都是SideTable
的hash表。如果weak指针之前指向了一个弱引用,则会调用
weak_unregister_no_lock
方法将旧的weak指针地址移除。如果weak指针需要指向一个新的引用,则会调用
weak_register_no_lock
方法将新的weak指针地址添加到弱引用表中。调用
setWeaklyReferenced_nolock
方法修改weak新引用的对象的bit标志位
那么这个方法中的重点也就是weak_unregister_no_lock
和weak_register_no_lock
这两个方法。而这两个方法都是操作的SideTable
这样一个结构的变量,那么我们需要先来了解下SideTable
。
3、SideTable
先来看下SideTable的定义。
structSideTable{spinlock_tslock;RefcountMaprefcnts;weak_table_tweak_table;}
SideTable的定义很清晰,有三个成员:
3.1、weak_table_tspinlock_t slock: 自旋锁,用于上锁/解锁 SideTable。
RefcountMap refcnts:用来存储OC对象的引用计数的
hash表
(仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。weak_table_t weak_table: 存储对象弱引用指针的
hash表
。是OC中weak功能实现的核心数据结构。
先来看下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_table:
weak_table_t
结构类型的全局的弱引用表。referent_id:weak指针。
*referrer_id:weak指针地址。
crashIfDeallocating:若果被弱引用的对象正在析构,此时再弱引用该对象是否应该crash。
从上面的代码我么可以知道该方法主要的做了如下几个方便的工作。
4.1、weak_entry_for_referent取元素如果referent为nil 或 referent 采用了
TaggedPointer
计数方式,直接返回,不做任何操作。如果对象正在析构,则抛出异常。
如果对象不能被weak引用,直接返回nil。
如果对象没有再析构且可以被weak引用,则调用
weak_entry_for_referent
方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer
方法向其中插入weak指针地址。否则新建一个weak_entry。
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);}}
6.1、object_dispose首先判断对象是否是
Tagged Pointer
,如果是则直接返回。如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用
!isa.weakly_referenced
、没有关联对象!isa.has_assoc
、没有自定义的C++析构方法!isa.has_cxx_dtor
、没有用到SideTable来引用计数!isa.has_sidetable_rc
则直接快速释放。如果不能满足2中的条件,则会调用
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、对象释放时,调用
函数根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。clearDeallocating
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的实现原理》对你有帮助,请点赞、收藏,并留下你的观点哦!