只要学过 iOS 的人,都会对 strong、weak、copy等关键字应该都会很熟悉。weak 属性关键字就是弱引用,它不会增加引用计数但却能保证指针的安全访问,在对象释放后置为 nil,从而避免错误的内存访问。主要为了解决循环引用的问题。

接下来,我们会从 objc 库中的 NSObject.mm、 objc-weak.h 以及 objc-weak.mm 文件出发,去具体了解 weak 的实现过程。

weak 的内部结构

Runtime 维护了一个weak表,用于存储指向某个对象的所有weak指针。weak 表是由单个自旋锁管理的散列表。
weak表其实是一个hash表,key 是所指对象的指针,value是weak指针的地址(这个地址的值是所指向对象的地址)数组。

在下面涉及的源码中,我们会看到以下几个类型:
sideTableweak_table_tweak_entry_t 这几个结构体。

struct SideTable {// 自旋锁,用来保证线程安全spinlock_t slock;// 引用计数表RefcountMap refcnts;// weak 表weak_table_t weak_table;...
};

SideTable,它用来管理引用计数表和 weak 表,并使用 spinlock_lock 自旋锁来防止操作表结构时可能的竞态条件。它用一个 64*128 大小的uint8_t 静态数组作为 buffer 来保存所有的 SideTable 实例。这个结构体里面包含三个变量,第一个spinlock_t,它是一个自旋锁,用来保证线程安全。第二个RefcountMap,是引用计数表,每个对象的引用计数保存在全局的引用计数表中,一个对象地址对应一个引用计数。第三个就是我们接下来要讲的 weak 表,所有的 weak 变量会被加入到全局的weak表中,表的 key 是 weak 修饰的变量指向的对象, value 值就是 weak 修饰的变量。接下来,我们具体看看这个 weak 表

struct weak_table_t {// 保存了所有指向指定对象的 weak 指针weak_entry_t *weak_entries;// 存储空间,即 entries 的数目size_t    num_entries;// 参与判断引用计数辅助量uintptr_t mask;// hash key 最大偏移量uintptr_t max_hash_displacement;
};

这个是全局弱引用的 hash 表。它的作用就是在对象执行 dealloc 的时候将所有指向该对象的 weak 指针的值设为 nil, 避免悬空指针。它使用不定类型对象的地址的 hash 化后的数值作为 key,用 weak_entry_t 类型的结构体对象作为 value。其中 weak_entry_t 是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用 hash 表。其定义如下:

// 存储在弱引用表中的一个内部结构体
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {DisguisedPtr<objc_object> referent;                 // 封装 objc_object 指针,即 weak 修饰的变量指向的对象union {struct {weak_referrer_t *referrers;uintptr_t        out_of_line : 1;           // LSB 最低有效元 当标志位为0时,增加引用表指针纬度,// 当其为0的时候, weak_referrer_t 成员将扩展为静态数组型的 hash tableuintptr_t        num_refs : PTR_MINUS_1;    // 引用数值,这里记录弱引用表中引用有效数字,即里面元素的数量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 的结构中, DisguisedPtr<objc_object> 是对 objc_object * 指针及其一些操作进行的封装,目的就是为了让它给人看起来不会有内存泄露的样子,其内容可以理解为对象的内存地址。out_of-line 成员为最低有效位,当其为 0 的时候,weak_referrer_t 成员将扩展为一个静态数组型的 hash table。其实 weak_referrer 是objc_objcet 的别名,定义如下:typedef objc_object ** weak_referrer_t;

它通过一个二维指针地址偏移,用下标作为 hash 的 key,做成了一个弱引用散列。

下图,就是weak表结构的总结:

每个对象的 SideTable 中的 weak_table_t 都是全局 weak 表的入口,以引用计数对象为键找到其所记录的 weak 修饰的对象。weak_entry_t 中的 referrers 有两种形式,当 out_of_line 为 0 的时候,referrers 是一个静态数组型的表,数组大小默认为 WEAK_INLINE_COUNT 大小,当 out_of_line 不为 0 的时候,referrers 是一个动态数组,内容随之增加。

weak 实现原理的过程

当我们用 weak 修饰属性的时候,它是怎么实现当所引用的对象被废弃的时候,变量置为 nil,我们来探究一下。

{id obj1 = [[NSObject alloc] init];id __weak obj2 = obj1;
}

经过编译期转换之后,以上代码会变成下面这样

id obj2;
objc_initWeak(&obj2, obj1);
objc_destroyWeak(&obj2);

我们发现,weak 修饰符变量是通过 objc_initWeak 函数来初始化的,在变量作用域结束的时候通过 objc_destroyWeak 函数来释放该变量的。接下来,我们看看这两个函数的源码。

id objc_initWeak(id *location, id newObj)
{// 查看对象实例是否有效// 无效对象直接导致指针释放if (!newObj) {*location = nil;return nil;}// 这里传递了三个 bool 数值// 使用 template 进行常量参数传递是为了优化性能return storeWeak<false/*old*/, true/*new*/, true/*crash*/>(location, (objc_object*)newObj);
}
void objc_destroyWeak(id *location)
{(void)storeWeak<true/*old*/, false/*new*/, false/*crash*/>(location, nil);
}

对这两个方法的分析后,我们发现它们都调用了storeWeak 这个函数,但是两个方法传入的参数却稍有不同。
init 方法中,第一个参数为 weak 修饰的变量,第二个参数为引用计数对象。但在 destoryWeak 函数,第一参数依旧为 weak 修饰的变量,第二个参数为 nil。那这块传入不同的参数到底代表什么,我们继续分析 storeWeak 这个函数。

// 更新一个弱引用变量
// 如果 HaveOld 是 true, 变量是个有效值,需要被及时清理。变量可以为 nil。
// 如果 HaveNew 是 true, 需要一个新的 value 来替换变量。变量可以为 nil
// 如果crashifdeallocation 是 ture ,那么如果 newObj 是 deallocating,或者 newObj 的类不支持弱引用,则该进程就会停止。
// 如果crashifdeallocation 是 false,那么 nil 会被存储。template <bool HaveOld, bool HaveNew, bool CrashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj)
{assert(HaveOld  ||  HaveNew);if (!HaveNew) assert(newObj == nil);Class previouslyInitializedClass = nil;id oldObj;// 创建新旧散列表SideTable *oldTable;SideTable *newTable;// Acquire locks for old and new values.// 获得新值和旧值的锁存位置 (用地址作为唯一标示)// Order by lock address to prevent lock ordering problems.// 通过地址来建立索引标志,防止桶重复// Retry if the old value changes underneath us.// 下面指向的操作会改变旧值retry:if (HaveOld) {// 如果 HaveOld 为 true ,更改指针,获得以 oldObj 为索引所存储的值地址oldObj = *location;oldTable = &SideTables()[oldObj];} else {oldTable = nil;}if (HaveNew) {// 获得以 newObj 为索引所存储的值对象newTable = &SideTables()[newObj];} else {newTable = nil;}// 对两个 table 进行加锁操作,防止多线程中竞争冲突SideTable::lockTwo<HaveOld, HaveNew>(oldTable, newTable);//  location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改, 保证线程安全,这个判断用来避免线程冲突重处理问题if (HaveOld  &&  *location != oldObj) {SideTable::unlockTwo<HaveOld, HaveNew>(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.// 防止弱引用之间发生死锁,并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向if (HaveNew  &&  newObj) {// 获得新对象的 isa 指针Class cls = newObj->getIsa();// 判断 isa 非空且已经初始化if (cls != previouslyInitializedClass  &&  !((objc_class *)cls)->isInitialized()) {// 对两个表解锁SideTable::unlockTwo<HaveOld, HaveNew>(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.// 如果该类已经完成执行 +initialize 方法是最好的,如果该类 + initialize 在线程中,例如 +initialize 正在调用storeWeak 方法,那么则需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记然后重新尝试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.// 在引用计数表中设置弱引用标记位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<HaveOld, HaveNew>(oldTable, newTable);return (id)newObj;
}

以上就是 store_weak 这个函数的实现,它主要做了以下几件事:

  • 声明了新旧散列表指针,因为 weak 修饰的变量如果之前已经指向一个对象,然后其再次改变指向另一个对象,那么按理来说我们需要释放旧对象中该 weak 变量的记录,也就是要将旧记录删除,然后在新记录中添加。这里的新旧散列表就是这个作用。

    • 根据新旧变量的地址获取相应的 SideTable
    • 对两个表进行加锁操作,防止多线程竞争冲突
    • 进行线程冲突重处理判断
    • 判断其 isa 是否为空,为空则需要进行初始化
    • 如果存在旧值,调用 weak_unregister_no_lock 函数清除旧值
    • 调用 weak_register_no_lock 函数分配新值
    • 解锁两个表,并返回第二参数

初始化弱引用对象流程一览

弱引用的初始化,从上文的分析可以看出,主要的操作部分就是在弱引用表的取键、查询散列、创建弱引用等操作,可以总结出如下的流程图:

旧对象解除注册操作 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 指针的对应绑定。根据函数名,称之为解除注册操作。
来看看这个函数的逻辑。首先参数是 weak_table_t 表,键和值。声明 weak_entry_t 变量,如果key,也就是引用计数对象为空,直接返回。根据全局入口表和键获取对应的 weak_entry_t 对象,也就是 weak 表记录。获取到记录后,将记录表以及 weak 对象作为参数传入 remove_referrer 函数中,这个函数就是解除操作。然后判断这个 weak 记录是否为空,如果为空,从全局记录表中清除相应的引用计数对象的 weak 记录表。

接下来,我们了解一下,如何获取这个 weak_entry_t 这个变量。

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];
}

这个函数的逻辑就是先获取全局 weak 表入口,然后将引用计数对象的地址进行 hash 化后与 weak_table->mask 做与操作,作为下标,在全局 weak 表中查找,若找到,返回这个对象的 weak 记录表,若没有,返回nil。

再来了解一下解除对象的函数:

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--;
}

这个函数传入的是 weak 对象,当 out_of_line 为0 时,遍历数组,找到对应的对象,置nil,如果未找到,报错并返回。当 out_of_line 不为0时,根据对象的地址 hash 化并和 mask 做与操作作为下标,查找相应的对象,若没有,报错并返回,若有,相应的置为 nil,并减少元素数量,即 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 viablebool 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 storedweak_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 以及是否正在调用 dealloca 等。下面操作开始,同样是先获取 weak 表记录,如果获取到,则调用 append_referrer 插入对象,若没有,则新建一个 weak 表记录,默认为 out_of_line,然后将新对象放到 0 下标位置,其他位置置为 nil 。下面两个函数 weak_grow_maybe 是用来判断是否需要重申请内存重 hash,weak_entry_insert 函数是用来将新建的 weak 表记录插入到全局 weak 表中。插入时同样是以对象地址的 hash 化和 mask 值相与作为下标来记录的。

接下来看看 append_referrer 函数,源代码如下:

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++;
}

当 out_of_line 为 0,并且静态数组里面还有位置存放,那么直接存放并返回。如果没有位置存放,则升级为动态数组,并加入。如果 out_of_line 不为 0,先判断是否需要扩容,然后同样的,使用对象地址的 hash 化和 mask 做与操作作为下标,找到相应的位置并插入。

对象的销毁以及 weak 的置 nil 实现

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

当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:
- 调用 objc_release
- 因为对象的引用计数为0,所以执行dealloc
- 在dealloc 中,调用了_objc_rootDealloc 函数
- 在 _objc_rootDealloc 中,调用了 objec_dispose 函数
- 调用objc_destructInstance
- 最后调用 objc_clear_deallocating

objc_clear_deallocating的具体实现如下:

void objc_clear_deallocating(id obj)
{assert(obj);assert(!UseGC);if (obj->isTaggedPointer()) return;obj->clearDeallocating();
}

这个函数只是做一些判断以及更深层次的函数调用,

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();
}

我们可以看到,在这个函数中,首先取出对象对应的SideTable实例,如果这个对象有关联的弱引用,则调用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 referencesweak_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);
}

这个函数根据 out_of_line 的值,取得对应的记录表,然后根据引用计数对象,将相应的 weak 对象置 nil。最后清除相应的记录表。

通过上面的描述,我们基本能了解一个weak引用从生到死的过程。从这个流程可以看出,一个weak引用的处理涉及各种查表、添加与删除操作,还是有一定消耗的。所以如果大量使用__weak变量的话,会对性能造成一定的影响。那么,我们应该在什么时候去使用weak呢?《Objective-C高级编程》给我们的建议是只在避免循环引用的时候使用__weak修饰符。

iOS 中 weak 的实现相关推荐

  1. iOS 中delegate的理解与使用(传值)

    之前做了半年的iOS,刚入了门,又被拉去转战java,现在iOS的那位大佬离职了,又被弄过来维护app,之前对于iOS中的delegate一直都是半知半解,所以刚好趁着这个机会把我所了解的记下来,以便 ...

  2. iOS中的传感器---摇一摇, 计步器,距离感应,陀螺仪

    前几天项目中用到了一下CoreMotion框架,觉得iOS中的传感器还是挺好玩的,又花了点时间去了解了一下iOS中其他一些常用的传感器应用,今天简单做下总结. iOS中的传感器大致有以下几种: 运动传 ...

  3. [iOS开发]iOS中的Hash

    文章目录 前言 关联对象的底层原理 weak的实现原理 KVO的实现原理 iOS App签名的原理 对象引用计数存储的位置 Runloop与线程的存储关系 NSDictionary的原理 哈希表 哈希 ...

  4. iOS arc weak指针原理

    iOS arc weak指针原理 ARC 都帮我们做了什么? weak是什么? weak是怎么实现的? 1. weak原理简介 2. weak简单测试 3. weak原理分析 3.1 weak指针帮我 ...

  5. iOS中 流媒体播放和下载 韩俊强的博客

    iOS中关于流媒体的简介:介于下载本地播放与实时流媒体之间的一种播放形式,下载本地播放必须全部将文件下载完成后才能播放,而渐进式下载不必等到全部下载完成后再播放,它可以一边下载一边播放,在完成播放内容 ...

  6. 【iOS】—— iOS中的相关锁

    文章目录 自旋锁 1.OSSpinLock 2.os_unfair_lock 3.atomic 互斥锁 pthread_mutex @synchronized objc_sync_enter objc ...

  7. iOS 中的 timer 任务(寻找内存恶鬼之旅)

    前言 在 iOS 的开发过程中定时任务中能找到使用的场景,然而在 iOS 中默认的有关 timer 的 api 总是那么晦涩难用,而且暗坑不断,一旦遇上,会让你一脸懵逼,为了不再同一个地方跌倒两次,我 ...

  8. iOS中常用的设计模式

    iOS中常用的几种设计模式 iOS中常用的几种设计模式 1.代理模式 2. 观察者模式(通知机制,KVO机制) 4. 单例 5. 适配器() 6. 策略 9. 装饰器(Decorator) 10. 原 ...

  9. ios中使用SegmentedControl来切换视图

    From 效果图 设计图 结构与原理 视图结构 切换视图原理 代码 From ios中使用SegmentedControl来切换视图 效果图 设计图 结构与原理 视图结构 共有3个ViewContro ...

最新文章

  1. HTTP基础认证Basic Authentication
  2. android 定位 闪退_Android使用百度地图出现闪退及定位时显示蓝屏问题
  3. Spring Cloud配置–外部化应用程序配置
  4. RHEL5.8配置开机自动挂载磁盘
  5. 20151212Jquery 工具函数代码备份
  6. qq修改群名服务器失败,如何解决qq群名片改不了的问题
  7. 小米8手机android版本下载地址,小米手机8 MIUI 10稳定版完整包发布(附下载链接)...
  8. 创邻科技荣登机器之心Pro·AI 趋势先锋 Insight 榜单
  9. tensorflow2系类知识-4 :RNN
  10. 非常详细的图文安装wordpress安装教程
  11. python爬虫:lxml爬取链家网二手房信息
  12. 数字电路 时序逻辑电路
  13. 写入iCloud在模拟器和真机上失败的解决办法
  14. jsp+servlet搭建在线投票问卷系统
  15. C/C++描述 第十一届蓝桥杯省赛 C/C++ 大学C组 第一场(2020.7.5) 题目+题解
  16. 【解题总结】SEERC 2019(Codeforces Gym 102392)
  17. 你好,法语!A1课文背诵汇总
  18. symfony框架Twig模板语言的使用
  19. android 选择答题功能,Android实现选择题答题(包括单选、多选和答题卡)
  20. 中间件weblogic部署详情

热门文章

  1. HashMap遍历的三种方式
  2. 【原创】ES5高效封装WIN10系统教程2020系列(三)母盘安装及系统调整
  3. java 正则 空格_java 正则匹配空格字符串 正则表达式截取字符串
  4. bat命令删除指定文件夹下的空文件夹
  5. 【王喆-推荐系统】线上服务篇-(task1)线上高并发的推荐服务
  6. 【第六周:统计学】7周成为数据分析师
  7. YC创始人格雷厄姆:如何才能发现创业思路?
  8. 初学安卓:安卓小游戏之2048
  9. AWS初试:CloudWatch账单告警 和IAM
  10. 【网络通信 -- 直播】视频流编码 -- H.264 编码的一般概念