iOS 从源码深入探究 weak 的实现
<h2>__weak</h2> <pre> <code class="language-objectivec">id __week obj1 = obj;</code></pre> <p>编译器的模拟代码</p> <pre> <code class="language-objectivec">id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); objc_storeWeak(&obj1, 0);</code></pre> <p>objc_storeWeak</p> <p>函数把第二参数的赋值对象的地址作为键值,将第一参数的附有__weak修饰的变量的地址注册到weak表中。</p> <p>如果第二参数为0,则把变量的地址从weak表中删除。</p> <p>initWeak的实现</p> <pre> <code class="language-objectivec">id objc_initWeak(id *object, id value) { *object = 0; return objc_storeWeak(object, value); }</code></pre> <p>storeWeak是Objective-C的开源部分</p> <p>让我们来看看storeWeak到底是怎么实现的</p> <h3>objc_storeWeak</h3> <p>storeWeak的源码</p> <p>官方英文注释挺全的,可以直接理解~</p> <pre> <code class="language-objectivec">// 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 // Set is-weakly-referenced bit in refcount table. 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. }</code></pre> <p>来看看storeWeak的实现</p> <p>获取oldObj/newObj</p> <pre> <code class="language-objectivec">if (HaveOld) { oldObj = *location; oldTable = &SideTables()[oldObj]; } else { oldTable = nil; } if (HaveNew) { newTable = &SideTables()[newObj]; } else { newTable = nil; }</code></pre> <p>首先是根据weak指针找到其指向的老的对象:</p> <pre> <code class="language-objectivec">oldObj = *location;</code></pre> <p>然后获取到与新旧对象相关的SideTable对象:</p> <pre> <code class="language-objectivec">oldTable = &SideTables()[oldObj]; newTable = &SideTables()[newObj];</code></pre> <p>&SideTables()[oldObj]这是什么鬼??</p> <p>其时是 实现了一个类 StripedMap 重载了[]操作符</p> <p>(c++: 哪里都能看到我 233)</p> <pre> <code class="language-objectivec">public: T& operator[] (const void *p) { return array[indexForPointer(p)].value; }</code></pre> <p>下面要做的就是在老对象的weak表中移除指向信息,而在新对象的weak表中建立关联信息:</p> <pre> <code class="language-objectivec">if (HaveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } if (HaveNew) { newObj = weak_register_no_lock(&newTable->weak_table, newObj,location); // weak_register_no_lock returns NULL if weak store should be rejected }</code></pre> <p>接下来让弱引用指针指向新的对象:</p> <pre> <code class="language-objectivec">*location = newObj;</code></pre> <p>最后会返回这个新对象:</p> <pre> <code class="language-objectivec">return newObj;</code></pre> <p>以上我们能发现weak的管理实际上跟weak_table有这千丝万缕的联系,接下来就对weak_table进行分析!</p> <p>weakTable</p> <p>(关先上源码还是先总结...我思考了很久...。。。。)</p> <ul> <li>weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了所有对象相关的的所有的弱引用信息</li> <li>其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。</li> <li>weak_entry_t中的referrers 存储了指向weak对象的所有变量</li> </ul> <p>来张图直观感受一下:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/76d3fbc089340ef23e04f7e120eb7744.png"></p> <p>下面开始对这些结构体进行分析:</p> <ul> <li>SideTable是一个用C++实现的类,它的具体定义在NSObject.mm中 <pre> <code class="language-objectivec">class SideTable { private: static uint8_t table_buf[SIDE_TABLE_STRIPE * SIDE_TABLE_SIZE]; public: RefcountMap refcnts;//引用计数表 weak_table_t weak_table;//弱引用表 ...... }</code></pre> </li> </ul> <p>weak表的结构定义:</p> <pre> <code class="language-objectivec">/** * The global weak references table. Stores object ids as keys, * and weak_entry_t structs as their values. */ struct weak_table_t { weak_entry_t *weak_entries; size_t num_entries; uintptr_t mask; uintptr_t max_hash_displacement; };</code></pre> <p>根据注释我们可以得到这是一张全局的存储object的id 和 keys的表.</p> <p>weak_entry_t 作为他们的值.</p> <p>来看weak_entry_t的结构体</p> <pre> <code class="language-objectivec">struct weak_entry_t { DisguisedPtr<objc_object> referent; union { struct { weak_referrer_t *referrers; uintptr_t out_of_line : 1; uintptr_t num_refs : PTR_MINUS_1; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }; };</code></pre> <p>referrers: 是指向weak对象的所有变量</p> <p>referent: 是内存上的weak对象</p> <p>现在我们可以得出什么结论了呢</p> <ol> <li>OC中 弱引用变量的管理是利用 weak表(Hash表)来管理的</li> <li>weak表中的weak_entries负责管理指向weak对象的变量</li> </ol> <p>weak对象的释放</p> <p>释放对象时,废弃谁都不持有的对象的同时,程序的动作是怎么样的呢?</p> <ul> <li>objc_release</li> <li>因为引用计数为0所以执行dealloc</li> <li>objc rootDealloc</li> <li>objc dispose</li> <li>objc destructInstance</li> <li>objc clear dealloctaing</li> </ul> <p>对象被废弃时最后调用的objc_clear_deallocating函数的动作如下:</p> <p>(1) 从weak表中获取废弃对象的地址为键值的记录。</p> <p>(2) 将包含在记录中的所有附有__weak修饰符变量的地址,赋值为nil</p> <p>(3) 从weak表中删除该记录。</p> <p>(4) 从引用计数表中删除废弃对象的地址为键值的记录。</p> <p>由此可知,如果大量使用附有weak修饰符的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用weak修饰符。</p> <h3>立即释放对象</h3> <pre> <code class="language-objectivec">{ id __weak obj = [[NSObject alloc] init]; }</code></pre> <p>因为该源码将自己生成并持有的对象赋值给附有__weak修饰符的变量中,所以自己不能持有该对象,这是会被释放并且废弃。</p> <h3>使用附有__weak修饰符的变量,即是使用注册到autoreleasepool中的对象</h3> <pre> <code class="language-objectivec">{ id __weak obj1 = obj; NSLog(@"%@",obj1); }</code></pre> <p>编译器模拟的代码</p> <pre> <code class="language-objectivec">id obj1; objc_initWeak(&obj,obj); id tmp = objc_loadWeakRetained(&obj); objc_autorelease(tmp); NSLog(@"%@", tmp); objc_destoryWeak(&obj1);</code></pre> <p>注意这两行代码</p> <pre> <code class="language-objectivec">id tmp = objc_loadWeakRetained(&obj); objc_autorelease(tmp);</code></pre> <p>与赋值时相比,在使用附有__weak修饰符变量的情况下,增加了对objc_loadWeakRetained函数和objc_autorelease函数的调用。</p> <p>(1) objc_loadWeakRetained 函数取出附有__weak修饰符变量所引用的对象并retain</p> <p>(2) objc_autorelease 函数将对象注册到autoreleasepool中。</p> <p>注意:</p> <p>每次使用</p> <p>weak修饰的变量,会使变量所引用的对象注册到autoreleasepool中。</p> <p>如果要避免这种情况可以将附有</p> <p>weak修饰符的变量赋值给附有__strong修饰符的变量后再次使用。</p> <pre> <code class="language-objectivec">id __weak o = obj; id tmp = o;</code></pre> <h3>allowWeakReference/retainWeakReference</h3> <p>当allowsWeakReference/retainWeakReference实例方法(没有写入NSObject接口说明文档中)返回NO的情况。</p> <pre> <code class="language-objectivec">- (BOOL)allowsWeakReference; - (BOOL)retainWeakReference;</code></pre> <p>在赋值给__weak修饰符的变量时,如果allowsWeakReference方法返回NO,程序将异常终止。</p> <p>对象retain时,如果retainWeakReference方法返回NO, 该变量将使用nil</p> <h3>具体源码分析</h3> <p>以上关于weak的</p> <ul> <li>weak_register_no_lock</li> <li>weak_unregister_no_lock</li> <li>。。。<br> 很多具体实现都没有讲...<br> 我把自己看的代码加上注释贴出来了...感兴趣的可以看一下具体的实现...感受源码实现的魅力</li> </ul> <p>weak_unregister_no_lock</p> <p>怎么理解呢objc_object **referrer?</p> <pre> <code class="language-objectivec">objc_object *referent = (objc_object *)referent_id; objc_object **referrer = (objc_object **)referrer_id;</code></pre> <p>我们要结合remove_referrer这个函数来理解</p> <p>remove_referrer</p> <pre> <code class="language-objectivec">for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i] == old_referrer) { entry->inline_referrers[i] = nil; return; } }</code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/46bb1b8f2906177d4b5c0a447b1b16c0.png"></p> <p>所以我们要拿到referrer 根据这个值来和entry链表中的指针进行比较,如果发现,就nil</p> <p>(指针搞得我都晕了...佩服c/c++系统工程师)</p> <p>(阅读源码真的是一件有意思的是哈哈)</p> <pre> <code class="language-objectivec">#include "objc-private.h" #include "objc-weak.h" #include <stdint.h> #include <stdbool.h> #include <sys/types.h> #include <libkern/OSAtomic.h> #define TABLE_SIZE(entry) (entry->mask ? entry->mask + 1 : 0) static void append_referrer(weak_entry_t *entry, objc_object **new_referrer); BREAKPOINT_FUNCTION( void objc_weak_error(void) ); /** * Unique hash function for object pointers only. * * @param key The object pointer * * @return Size unrestricted hash of pointer. */ static inline uintptr_t hash_pointer(objc_object *key) { return ptr_hash((uintptr_t)key); } /** * Unique hash function for weak object pointers only. * * @param key The weak object pointer. * * @return Size unrestricted hash of pointer. */ static inline uintptr_t w_hash_pointer(objc_object **key) { return ptr_hash((uintptr_t)key); } /** * Grow the entry's hash table of referrers. Rehashes each * of the referrers. * * @param entry Weak pointer hash set for a particular object. */ __attribute__((noinline, used)) static void grow_refs_and_insert(weak_entry_t *entry, objc_object **new_referrer) { assert(entry->out_of_line); size_t old_size = TABLE_SIZE(entry); size_t new_size = old_size ? old_size * 2 : 8; size_t num_refs = entry->num_refs; weak_referrer_t *old_refs = entry->referrers; entry->mask = new_size - 1; entry->referrers = (weak_referrer_t *) calloc(TABLE_SIZE(entry), sizeof(weak_referrer_t)); entry->num_refs = 0; entry->max_hash_displacement = 0; for (size_t i = 0; i < old_size && num_refs > 0; i++) { if (old_refs[i] != nil) { append_referrer(entry, old_refs[i]); num_refs--; } } // Insert append_referrer(entry, new_referrer); if (old_refs) free(old_refs); } /** * Add the given referrer to set of weak pointers in this entry. * Does not perform duplicate checking (b/c weak pointers are never * added to a set twice). * * @param entry The entry holding the set of weak pointers. * @param new_referrer The new weak pointer to be added. */ static void append_referrer(weak_entry_t *entry, objc_object **new_referrer) { // if is Array implementation 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); } //find a place to insert ref //weak_entry_remove() may bzero() some place 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++; } /** * Remove old_referrer from set of referrers, if it's present. * Does not remove duplicates, because duplicates should not exist. * * @todo this is slow if old_referrer is not present. Is this ever the case? * * @param entry The entry holding the referrers. * @param old_referrer The referrer to remove. */ 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--; } /** * Add new_entry to the object's table of weak references. * Does not check whether the referent is already in the table. */ static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry) { weak_entry_t *weak_entries = weak_table->weak_entries; assert(weak_entries != nil); //mask may keep entry in array size_t index = hash_pointer(new_entry->referent) & (weak_table->mask); size_t hash_displacement = 0; //hash index 处理 while (weak_entries[index].referent != nil) { index = (index+1) & weak_table->mask; hash_displacement++; } weak_entries[index] = *new_entry; weak_table->num_entries++; //update max_hash_displacement if (hash_displacement > weak_table->max_hash_displacement) { weak_table->max_hash_displacement = hash_displacement; } } static void weak_resize(weak_table_t *weak_table, size_t new_size) { size_t old_size = TABLE_SIZE(weak_table); weak_entry_t *old_entries = weak_table->weak_entries; weak_entry_t *new_entries = (weak_entry_t *) calloc(new_size, sizeof(weak_entry_t)); weak_table->mask = new_size - 1; //new weak_table->weak_entries = new_entries; weak_table->max_hash_displacement = 0; weak_table->num_entries = 0; // restored by weak_entry_insert below //use pointer if (old_entries) { weak_entry_t *entry; weak_entry_t *end = old_entries + old_size; for (entry = old_entries; entry < end; entry++) { if (entry->referent) { weak_entry_insert(weak_table, entry); } } free(old_entries); } } // Grow the given zone's table of weak references if it is full. static void weak_grow_maybe(weak_table_t *weak_table) { size_t old_size = TABLE_SIZE(weak_table); // Grow if at least 3/4 full. if (weak_table->num_entries >= old_size * 3 / 4) { weak_resize(weak_table, old_size ? old_size*2 : 64); } } // Shrink the table if it is mostly empty. static void weak_compact_maybe(weak_table_t *weak_table) { size_t old_size = TABLE_SIZE(weak_table); // Shrink if larger than 1024 buckets and at most 1/16 full. if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) { weak_resize(weak_table, old_size / 8); // leaves new table no more than 1/2 full } } /** * Remove entry from the zone's table of weak references. */ static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry) { // remove entry if (entry->out_of_line) free(entry->referrers); //bzero()函数在由s指向的区域中放置n个0。 bzero(entry, sizeof(*entry)); weak_table->num_entries--; //maybe resize weak_table weak_compact_maybe(weak_table); } /** * Return the weak reference table entry for the given referent. * If there is no entry for referent, return NULL. * Performs a lookup. * * @param weak_table * @param referent The object. Must not be nil. * * @return The table of weak referrers to this object. */ 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]; } /** * Unregister an already-registered weak reference. * This is used when referrer's storage is about to go away, but referent * isn't dead yet. (Otherwise, zeroing referrer later would be a * bad memory access.) * Does nothing if referent/referrer is not a currently active weak reference. * Does not zero referrer. * * FIXME currently requires old referent value to be passed in (lame) * FIXME unregistration should be automatic if referrer is collected * * @param weak_table The global weak table. * @param referent The object. * @param referrer The weak reference. */ 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; //after unregister the entry's referrers is empty? // Hash implementation if (entry->out_of_line && entry->num_refs != 0) { empty = false; } // Array implementation else { for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) { if (entry->inline_referrers[i]) { empty = false; break; } } } // if entry.references empty if (empty) { weak_entry_remove(weak_table, entry); } } // Do not set *referrer = nil. objc_storeWeak() requires that the // value not change. } /** * Registers a new (object, weak pointer) pair. Creates a new weak * object entry if it does not exist. * * @param weak_table The global weak table. * @param referent The object pointed to by the weak reference. * @param referrer The weak pointer address. */ id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating) { //object objc_object *referent = (objc_object *)referent_id; //The Point which point the object objc_object **referrer = (objc_object **)referrer_id; if (!referent || referent->isTaggedPointer()) return referent_id; // ensure that the referenced object is viable // judge is Allows Weak Reference 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 DEBUG bool weak_is_registered_no_lock(weak_table_t *weak_table, id referent_id) { return weak_entry_for_referent(weak_table, (objc_object *)referent_id); } #endif /** * Called by dealloc; nils out all weak pointers that point to the * provided object so that they can no longer be used. * * @param weak_table * @param referent The object being deallocated. */ void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { //referent objc objc_object *referent = (objc_object *)referent_id; //referent objc entry(which save many referents) 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; } //entry->referrers all nil 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); } /** * This function gets called when the value of a weak pointer is being * used in an expression. Called by objc_loadWeakRetained() which is * ultimately called by objc_loadWeak(). The objective is to assert that * there is in fact a weak pointer(s) entry for this particular object being * stored in the weak-table, and to retain that object so it is not deallocated * during the weak pointer's usage. * * @param weak_table * @param referrer The weak pointer address. */ /* Once upon a time we eagerly cleared *referrer if we saw the referent was deallocating. This confuses code like NSPointerFunctions which tries to pre-flight the raw storage and assumes if the storage is zero then the weak system is done interfering. That is false: the weak system is still going to check and clear the storage later. This can cause objc_weak_error complaints and crashes. So we now don't touch the storage until deallocation completes. */ id weak_read_no_lock(weak_table_t *weak_table, id *referrer_id) { objc_object **referrer = (objc_object **)referrer_id; objc_object *referent = *referrer; //Detection Tagged Pointer if (referent->isTaggedPointer()) return (id)referent; weak_entry_t *entry; // referent == nil or entry == nil if (referent == nil || !(entry = weak_entry_for_referent(weak_table, referent))) { return nil; } //Custom RR denotes a custom retain-release implementation // if (! referent->ISA()->hasCustomRR()) { //???question if (! referent->rootTryRetain()) { return nil; } } //has isa else { BOOL (*tryRetain)(objc_object *, SEL) = (BOOL(*)(objc_object *, SEL)) object_getMethodImplementation((id)referent, SEL_retainWeakReference); //IMP != _objc_magForward if ((IMP)tryRetain == _objc_msgForward) { return nil; } //IMP != nil if (! (*tryRetain)(referent, SEL_retainWeakReference)) { return nil; } } return (id)referent; }</code></pre> <p> </p> <p>来自:https://juejin.im/post/58ffe5fb5c497d0058158fee</p> <p> </p>
本文由用户 404355009 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!