只要學(xué)過 iOS 的人,都會(huì)對 strong、weak、copy等關(guān)鍵字應(yīng)該都會(huì)很熟悉。weak 屬性關(guān)鍵字就是弱引用,它不會(huì)增加引用計(jì)數(shù)但卻能保證指針的安全訪問,在對象釋放后置為 nil,從而避免錯(cuò)誤的內(nèi)存訪問。主要為了解決循環(huán)引用的問題。
尼勒克網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)!從網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、成都響應(yīng)式網(wǎng)站建設(shè)公司等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營維護(hù)。創(chuàng)新互聯(lián)2013年開創(chuàng)至今到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)。
接下來,我們會(huì)從 objc 庫中的 NSObject.mm、 objc-weak.h 以及 objc-weak.mm 文件出發(fā),去具體了解 weak 的實(shí)現(xiàn)過程。
weak 的內(nèi)部結(jié)構(gòu)
Runtime 維護(hù)了一個(gè)weak表,用于存儲(chǔ)指向某個(gè)對象的所有weak指針。weak 表是由單個(gè)自旋鎖管理的散列表。
weak表其實(shí)是一個(gè)hash表,key 是所指對象的指針,value是weak指針的地址(這個(gè)地址的值是所指向?qū)ο蟮牡刂罚?shù)組。
在下面涉及的源碼中,我們會(huì)看到以下幾個(gè)類型:
sideTable、weak_table_t、weak_entry_t 這幾個(gè)結(jié)構(gòu)體。
struct SideTable { // 自旋鎖,用來保證線程安全 spinlock_t slock; // 引用計(jì)數(shù)表 RefcountMap refcnts; // weak 表 weak_table_t weak_table; ... };
SideTable,它用來管理引用計(jì)數(shù)表和 weak 表,并使用 spinlock_lock 自旋鎖來防止操作表結(jié)構(gòu)時(shí)可能的競態(tài)條件。它用一個(gè) 64*128 大小的uint8_t 靜態(tài)數(shù)組作為 buffer 來保存所有的 SideTable 實(shí)例。這個(gè)結(jié)構(gòu)體里面包含三個(gè)變量,第一個(gè)spinlock_t,它是一個(gè)自旋鎖,用來保證線程安全。第二個(gè)RefcountMap,是引用計(jì)數(shù)表,每個(gè)對象的引用計(jì)數(shù)保存在全局的引用計(jì)數(shù)表中,一個(gè)對象地址對應(yīng)一個(gè)引用計(jì)數(shù)。第三個(gè)就是我們接下來要講的 weak 表,所有的 weak 變量會(huì)被加入到全局的weak表中,表的 key 是 weak 修飾的變量指向的對象, value 值就是 weak 修飾的變量。接下來,我們具體看看這個(gè) weak 表
struct weak_table_t { // 保存了所有指向指定對象的 weak 指針 weak_entry_t *weak_entries; // 存儲(chǔ)空間,即 entries 的數(shù)目 size_t num_entries; // 參與判斷引用計(jì)數(shù)輔助量 uintptr_t mask; // hash key 最大偏移量 uintptr_t max_hash_displacement; };
這個(gè)是全局弱引用的 hash 表。它的作用就是在對象執(zhí)行 dealloc 的時(shí)候?qū)⑺兄赶蛟搶ο蟮?weak 指針的值設(shè)為 nil, 避免懸空指針。它使用不定類型對象的地址的 hash 化后的數(shù)值作為 key,用 weak_entry_t 類型的結(jié)構(gòu)體對象作為 value。其中 weak_entry_t 是存儲(chǔ)在弱引用表中的一個(gè)內(nèi)部結(jié)構(gòu)體,它負(fù)責(zé)維護(hù)和存儲(chǔ)指向一個(gè)對象的所有弱引用 hash 表。其定義如下:
// 存儲(chǔ)在弱引用表中的一個(gè)內(nèi)部結(jié)構(gòu)體 #define WEAK_INLINE_COUNT 4 struct weak_entry_t { DisguisedPtrreferent; // 封裝 objc_object 指針,即 weak 修飾的變量指向的對象 union { struct { weak_referrer_t *referrers; uintptr_t out_of_line : 1; // LSB 最低有效元 當(dāng)標(biāo)志位為0時(shí),增加引用表指針緯度, // 當(dāng)其為0的時(shí)候, weak_referrer_t 成員將擴(kuò)展為靜態(tài)數(shù)組型的 hash table uintptr_t num_refs : PTR_MINUS_1; // 引用數(shù)值,這里記錄弱引用表中引用有效數(shù)字,即里面元素的數(shù)量 uintptr_t mask; uintptr_t max_hash_displacement; // hash 元素上限閥值 }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }; };
在 weak_entry_t 的結(jié)構(gòu)中, DisguisedPtr
它通過一個(gè)二維指針地址偏移,用下標(biāo)作為 hash 的 key,做成了一個(gè)弱引用散列。
每個(gè)對象的 SideTable 中的 weak_table_t 都是全局 weak 表的入口,以引用計(jì)數(shù)對象為鍵找到其所記錄的 weak 修飾的對象。weak_entry_t 中的 referrers 有兩種形式,當(dāng) out_of_line 為 0 的時(shí)候,referrers 是一個(gè)靜態(tài)數(shù)組型的表,數(shù)組大小默認(rèn)為 WEAK_INLINE_COUNT 大小,當(dāng) out_of_line 不為 0 的時(shí)候,referrers 是一個(gè)動(dòng)態(tài)數(shù)組,內(nèi)容隨之增加。
weak 實(shí)現(xiàn)原理的過程
當(dāng)我們用 weak 修飾屬性的時(shí)候,它是怎么實(shí)現(xiàn)當(dāng)所引用的對象被廢棄的時(shí)候,變量置為 nil,我們來探究一下。
{ id obj1 = [[NSObject alloc] init]; id __weak obj2 = obj1; }
經(jīng)過編譯期轉(zhuǎn)換之后,以上代碼會(huì)變成下面這樣
id obj2;
objc_initWeak(&obj2, obj1);
objc_destroyWeak(&obj2);
我們發(fā)現(xiàn),weak 修飾符變量是通過 objc_initWeak 函數(shù)來初始化的,在變量作用域結(jié)束的時(shí)候通過 objc_destroyWeak 函數(shù)來釋放該變量的。接下來,我們看看這兩個(gè)函數(shù)的源碼。
id objc_initWeak(id *location, id newObj) { // 查看對象實(shí)例是否有效 // 無效對象直接導(dǎo)致指針釋放 if (!newObj) { *location = nil; return nil; } // 這里傳遞了三個(gè) bool 數(shù)值 // 使用 template 進(jìn)行常量參數(shù)傳遞是為了優(yōu)化性能 return storeWeak(location, (objc_object*)newObj); }
void objc_destroyWeak(id *location) { (void)storeWeak(location, nil); }
對這兩個(gè)方法的分析后,我們發(fā)現(xiàn)它們都調(diào)用了storeWeak 這個(gè)函數(shù),但是兩個(gè)方法傳入的參數(shù)卻稍有不同。
init 方法中,第一個(gè)參數(shù)為 weak 修飾的變量,第二個(gè)參數(shù)為引用計(jì)數(shù)對象。但在 destoryWeak 函數(shù),第一參數(shù)依舊為 weak 修飾的變量,第二個(gè)參數(shù)為 nil。那這塊傳入不同的參數(shù)到底代表什么,我們繼續(xù)分析 storeWeak 這個(gè)函數(shù)。
// 更新一個(gè)弱引用變量 // 如果 HaveOld 是 true, 變量是個(gè)有效值,需要被及時(shí)清理。變量可以為 nil。 // 如果 HaveNew 是 true, 需要一個(gè)新的 value 來替換變量。變量可以為 nil // 如果crashifdeallocation 是 ture ,那么如果 newObj 是 deallocating,或者 newObj 的類不支持弱引用,則該進(jìn)程就會(huì)停止。 // 如果crashifdeallocation 是 false,那么 nil 會(huì)被存儲(chǔ)。 templatestatic id storeWeak(id *location, objc_object *newObj) { assert(HaveOld || HaveNew); if (!HaveNew) assert(newObj == nil); Class previouslyInitializedClass = nil; id oldObj; // 創(chuàng)建新舊散列表 SideTable *oldTable; SideTable *newTable; // Acquire locks for old and new values. // 獲得新值和舊值的鎖存位置 (用地址作為唯一標(biāo)示) // Order by lock address to prevent lock ordering problems. // 通過地址來建立索引標(biāo)志,防止桶重復(fù) // Retry if the old value changes underneath us. // 下面指向的操作會(huì)改變舊值 retry: if (HaveOld) { // 如果 HaveOld 為 true ,更改指針,獲得以 oldObj 為索引所存儲(chǔ)的值地址 oldObj = *location; oldTable = &SideTables()[oldObj]; } else { oldTable = nil; } if (HaveNew) { // 獲得以 newObj 為索引所存儲(chǔ)的值對象 newTable = &SideTables()[newObj]; } else { newTable = nil; } // 對兩個(gè) table 進(jìn)行加鎖操作,防止多線程中競爭沖突 SideTable::lockTwo (oldTable, newTable); // location 應(yīng)該與 oldObj 保持一致,如果不同,說明當(dāng)前的 location 已經(jīng)處理過 oldObj 可是又被其他線程所修改, 保證線程安全,這個(gè)判斷用來避免線程沖突重處理問題 if (HaveOld && *location != oldObj) { SideTable::unlockTwo (oldTable, newTable); goto retry; } // Prevent a deadlock between the weak reference machinery // and the +initialize machinery by ensuring that no // weakly-referenced object has an un-+initialized isa. // 防止弱引用之間發(fā)生死鎖,并且通過 +initialize 初始化構(gòu)造器保證所有弱引用的 isa 非空指向 if (HaveNew && newObj) { // 獲得新對象的 isa 指針 Class cls = newObj->getIsa(); // 判斷 isa 非空且已經(jīng)初始化 if (cls != previouslyInitializedClass && !((objc_class *)cls)->isInitialized()) { // 對兩個(gè)表解鎖 SideTable::unlockTwo (oldTable, newTable); _class_initialize(_class_getNonMetaClass(cls, (id)newObj)); // If this class is finished with +initialize then we're good. // If this class is still running +initialize on this thread // (i.e. +initialize called storeWeak on an instance of itself) // then we may proceed but it will appear initializing and // not yet initialized to the check above. // Instead set previouslyInitializedClass to recognize it on retry. // 如果該類已經(jīng)完成執(zhí)行 +initialize 方法是最好的,如果該類 + initialize 在線程中,例如 +initialize 正在調(diào)用storeWeak 方法,那么則需要手動(dòng)對其增加保護(hù)策略,并設(shè)置 previouslyInitializedClass 指針進(jìn)行標(biāo)記然后重新嘗試 previouslyInitializedClass = cls; goto retry; } } // Clean up old value, if any. 清除舊值 if (HaveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } // Assign new value, if any. 分配新值 if (HaveNew) { newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, CrashIfDeallocating); // weak_register_no_lock returns nil if weak store should be rejected // 如果弱引用被釋放則該方法返回 nil // Set is-weakly-referenced bit in refcount table. // 在引用計(jì)數(shù)表中設(shè)置弱引用標(biāo)記位 if (newObj && !newObj->isTaggedPointer()) { newObj->setWeaklyReferenced_nolock(); } // Do not set *location anywhere else. That would introduce a race. *location = (id)newObj; } else { // No new value. The storage is not changed. } SideTable::unlockTwo (oldTable, newTable); return (id)newObj; }
以上就是 store_weak 這個(gè)函數(shù)的實(shí)現(xiàn),它主要做了以下幾件事:
初始化弱引用對象流程一覽
弱引用的初始化,從上文的分析可以看出,主要的操作部分就是在弱引用表的取鍵、查詢散列、創(chuàng)建弱引用等操作,可以總結(jié)出如下的流程圖:
舊對象解除注冊操作 weak_unregister_no_lock
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_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))) { remove_referrer(entry, referrer); bool empty = true; if (entry->out_of_line && entry->num_refs != 0) { empty = false; } else { for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i]) { empty = false; break; } } } if (empty) { weak_entry_remove(weak_table, entry); } } // Do not set *referrer = nil. objc_storeWeak() requires that the // value not change. }
該方法主要作用是將舊對象在 weak_table 中接觸 weak 指針的對應(yīng)綁定。根據(jù)函數(shù)名,稱之為解除注冊操作。
來看看這個(gè)函數(shù)的邏輯。首先參數(shù)是 weak_table_t 表,鍵和值。聲明 weak_entry_t 變量,如果key,也就是引用計(jì)數(shù)對象為空,直接返回。根據(jù)全局入口表和鍵獲取對應(yīng)的 weak_entry_t 對象,也就是 weak 表記錄。獲取到記錄后,將記錄表以及 weak 對象作為參數(shù)傳入 remove_referrer 函數(shù)中,這個(gè)函數(shù)就是解除操作。然后判斷這個(gè) weak 記錄是否為空,如果為空,從全局記錄表中清除相應(yīng)的引用計(jì)數(shù)對象的 weak 記錄表。
接下來,我們了解一下,如何獲取這個(gè) weak_entry_t 這個(gè)變量。
static weak_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) return nil; size_t index = hash_pointer(referent) & weak_table->mask; size_t hash_displacement = 0; while (weak_table->weak_entries[index].referent != referent) { index = (index+1) & weak_table->mask; hash_displacement++; if (hash_displacement > weak_table->max_hash_displacement) { return nil; } } return &weak_table->weak_entries[index]; }
這個(gè)函數(shù)的邏輯就是先獲取全局 weak 表入口,然后將引用計(jì)數(shù)對象的地址進(jìn)行 hash 化后與 weak_table->mask 做與操作,作為下標(biāo),在全局 weak 表中查找,若找到,返回這個(gè)對象的 weak 記錄表,若沒有,返回nil。
再來了解一下解除對象的函數(shù):
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer) { if (! entry->out_of_line) { for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i] == old_referrer) { entry->inline_referrers[i] = nil; return; } } _objc_inform("Attempted to unregister unknown __weak variable " "at %p. This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", old_referrer); objc_weak_error(); return; } size_t index = w_hash_pointer(old_referrer) & (entry->mask); size_t hash_displacement = 0; while (entry->referrers[index] != old_referrer) { index = (index+1) & entry->mask; hash_displacement++; if (hash_displacement > entry->max_hash_displacement) { _objc_inform("Attempted to unregister unknown __weak variable " "at %p. This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", old_referrer); objc_weak_error(); return; } } entry->referrers[index] = nil; entry->num_refs--; }
這個(gè)函數(shù)傳入的是 weak 對象,當(dāng) out_of_line 為0 時(shí),遍歷數(shù)組,找到對應(yīng)的對象,置nil,如果未找到,報(bào)錯(cuò)并返回。當(dāng) out_of_line 不為0時(shí),根據(jù)對象的地址 hash 化并和 mask 做與操作作為下標(biāo),查找相應(yīng)的對象,若沒有,報(bào)錯(cuò)并返回,若有,相應(yīng)的置為 nil,并減少元素?cái)?shù)量,即 num_refs 減 1。
新對象添加注冊操作 weak_register_no_lock
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating) { objc_object *referent = (objc_object *)referent_id; objc_object **referrer = (objc_object **)referrer_id; if (!referent || referent->isTaggedPointer()) return referent_id; // ensure that the referenced object is viable bool deallocating; 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) { return nil; } deallocating = ! (*allowsWeakReference)(referent, SEL_allowsWeakReference); } if (deallocating) { if (crashIfDeallocating) { _objc_fatal("Cannot form weak reference to instance (%p) of " "class %s. It is possible that this object was " "over-released, or is in the process of deallocation.", (void*)referent, object_getClassName((id)referent)); } else { return nil; } } // now remember it and where it is being stored weak_entry_t *entry; if ((entry = weak_entry_for_referent(weak_table, referent))) { append_referrer(entry, referrer); } else { weak_entry_t new_entry; new_entry.referent = referent; new_entry.out_of_line = 0; new_entry.inline_referrers[0] = referrer; for (size_t i = 1; i < WEAK_INLINE_COUNT; i++) { new_entry.inline_referrers[i] = nil; } weak_grow_maybe(weak_table); weak_entry_insert(weak_table, &new_entry); } // Do not set *referrer. objc_storeWeak() requires that the // value not change. return referent_id; }
一大堆 if-else, 主要是為了判斷該對象是不是 taggedPoint 以及是否正在調(diào)用 dealloca 等。下面操作開始,同樣是先獲取 weak 表記錄,如果獲取到,則調(diào)用 append_referrer 插入對象,若沒有,則新建一個(gè) weak 表記錄,默認(rèn)為 out_of_line,然后將新對象放到 0 下標(biāo)位置,其他位置置為 nil 。下面兩個(gè)函數(shù) weak_grow_maybe 是用來判斷是否需要重申請內(nèi)存重 hash,weak_entry_insert 函數(shù)是用來將新建的 weak 表記錄插入到全局 weak 表中。插入時(shí)同樣是以對象地址的 hash 化和 mask 值相與作為下標(biāo)來記錄的。
接下來看看 append_referrer 函數(shù),源代碼如下:
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer) { if (! entry->out_of_line) { // Try to insert inline. for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i] == nil) { entry->inline_referrers[i] = new_referrer; return; } } // Couldn't insert inline. Allocate out of line. weak_referrer_t *new_referrers = (weak_referrer_t *) calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t)); // This constructed table is invalid, but grow_refs_and_insert // will fix it and rehash it. for (size_t i = 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 = 1; 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) { return grow_refs_and_insert(entry, new_referrer); } size_t index = w_hash_pointer(new_referrer) & (entry->mask); size_t hash_displacement = 0; while (entry->referrers[index] != NULL) { index = (index+1) & entry->mask; hash_displacement++; } if (hash_displacement > entry->max_hash_displacement) { entry->max_hash_displacement = hash_displacement; } weak_referrer_t &ref = entry->referrers[index]; ref = new_referrer; entry->num_refs++; }
當(dāng) out_of_line 為 0,并且靜態(tài)數(shù)組里面還有位置存放,那么直接存放并返回。如果沒有位置存放,則升級(jí)為動(dòng)態(tài)數(shù)組,并加入。如果 out_of_line 不為 0,先判斷是否需要擴(kuò)容,然后同樣的,使用對象地址的 hash 化和 mask 做與操作作為下標(biāo),找到相應(yīng)的位置并插入。
對象的銷毀以及 weak 的置 nil 實(shí)現(xiàn)
釋放時(shí),調(diào)用clearDeallocating函數(shù)。clearDeallocating 函數(shù)首先根據(jù)對象地址獲取所有weak指針地址的數(shù)組,然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)為nil,最后把這個(gè)entry從weak表中刪除,最后清理對象的記錄。
當(dāng)weak引用指向的對象被釋放時(shí),又是如何去處理weak指針的呢?當(dāng)釋放對象時(shí),其基本流程如下:
objc_clear_deallocating的具體實(shí)現(xiàn)如下:
void objc_clear_deallocating(id obj) { assert(obj); assert(!UseGC); if (obj->isTaggedPointer()) return; obj->clearDeallocating(); }
這個(gè)函數(shù)只是做一些判斷以及更深層次的函數(shù)調(diào)用,
void objc_object::sidetable_clearDeallocating() { SideTable& table = SideTables()[this]; // clear any weak table items // clear extra retain count and deallocating bit // (fixme warn or abort if extra retain count == 0 ?) table.lock(); // 迭代器 RefcountMap::iterator it = table.refcnts.find(this); if (it != table.refcnts.end()) { if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) { weak_clear_no_lock(&table.weak_table, (id)this); } table.refcnts.erase(it); } table.unlock(); }
我們可以看到,在這個(gè)函數(shù)中,首先取出對象對應(yīng)的SideTable實(shí)例,如果這個(gè)對象有關(guān)聯(lián)的弱引用,則調(diào)用weak_clear_no_lock來清除對象的弱引用信息,我們在來深入一下,
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { objc_object *referent = (objc_object *)referent_id; weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { /// XXX shouldn't happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } // zero out references weak_referrer_t *referrers; size_t count; if (entry->out_of_line) { referrers = entry->referrers; count = TABLE_SIZE(entry); } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } for (size_t i = 0; i < count; ++i) { objc_object **referrer = referrers[I]; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } weak_entry_remove(weak_table, entry); }
這個(gè)函數(shù)根據(jù) out_of_line 的值,取得對應(yīng)的記錄表,然后根據(jù)引用計(jì)數(shù)對象,將相應(yīng)的 weak 對象置 nil。最后清除相應(yīng)的記錄表。
通過上面的描述,我們基本能了解一個(gè)weak引用從生到死的過程。從這個(gè)流程可以看出,一個(gè)weak引用的處理涉及各種查表、添加與刪除操作,還是有一定消耗的。所以如果大量使用__weak變量的話,會(huì)對性能造成一定的影響。那么,我們應(yīng)該在什么時(shí)候去使用weak呢?《Objective-C高級(jí)編程》給我們的建議是只在避免循環(huán)引用的時(shí)候使用__weak修飾符。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。