iOS weak关键字实现原理
在iOS中,使用weak关键字能够对内存对象进行弱引用,基于这个特性,使用weak关键字能够解决许多问题,例如delegate中对象的循环持有问题、Block对对象的强引用导致的对象无法及时释放问题。
为何weak关键字能够实现对内存对象的弱引用,今天我们就来探究一下。
首先在分析weak关键字实现原理之前,先介绍一下相关的数据结构,这些数据结构其中一部分可能在其他地方有所提及,但本文只列出与weak关键字有关的一部分。
这些数据结构全部存在于runtime源码中,相关内容可以在 objc-weak
文件中查看。
一、数据结构
1. SideTables
SideTables
本质上是一个全局的 StripedMap
。
StripedMap
本质是一个数组,且在iOS系统下,容量为64。
该数据结构通过实现[]
操作,实现了类似字典的功能:可通过传入一个对象作为key值,来获取对应的Item。
在 SideTables
中, Item类型为 SideTable
,由此可见,对于任何一个对象, SideTables
都能根据其地址对应到具体的一个 SideTable
上。
2. SideTable
SideTable
中包含三个元素,分别是 1.自旋锁 2.记录对象引用计数的字典 3.记录对象弱引用信息的数据结构 weak_table_t
。
其中 weak_table_t
是与weak关键字有关的数据结构,其余二者暂可不用关注。
3. weak_table_t
weak_table_t
本质上是一个数组,其中每个Item为 weak_entry_t
。
4. weak_entry_t
weak_entry_t
就比较有意思了,它本质上是个字典。
其中的key值为对象,而value对应为一个数组,该数组最初为内部的一个大小为4的数组,当数组大小超过4后,则变为内部一个可变大小数组。
无论value值对应的数组是固定大小还是可变大小,数组中保存的值均为 weak_referrer_t
类型的数据。
5. weak_referrer_t
weak_referrer_t
本质上是 objc_object **
,即Objective-C对象的地址。
所以,weak_entry_t
value数组中,每一个Item均为一个地址,即weak对象的地址。
以上就是weak实现原理中所涉及到的所有数据结构,具体关系如下图:
二、weak_table_t
与 weak_entry_t
相关方法
在正式探究weak关键字实现原理之前,先来看一些操作 weak_table_t
与 weak_entry_t
的方法。
1. 从 weak_table_t
中查询对应的 weak_entry_t
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{//获取weak_table_t的数组结构weak_entry_t *weak_entries = weak_table->weak_entries;if (!weak_entries) return nil;//获取对象地址,并根据地址映射到数组结构长度内,得到对应下标size_t index = hash_pointer(referent) & weak_table->mask;//线性探寻数组结构中对应的value所在indexsize_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;}}//返回查询到的weak_entry_treturn &weak_table->weak_entries[index];
}
2. 向 weak_table_t
中增加新的 weak_entry_t
static void
weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{//获取weak_table_t的数组结构weak_entry_t *weak_entries = weak_table->weak_entries;assert(weak_entries != nil);//获取对象地址,并根据地址映射到数组结构长度内,得到对应下标size_t index = hash_pointer(new_entry->referent) & (weak_table->mask);//线性探寻数组结构中value所应在的位置size_t hash_displacement = 0;while (weak_entries[index].referent != nil) {index = (index+1) & weak_table->mask;hash_displacement++;}//将```weak_entry_t```放入```weak_table_t```对应位置,并更新相关数据weak_entries[index] = *new_entry;weak_table->num_entries++;if (hash_displacement > weak_table->max_hash_displacement) {weak_table->max_hash_displacement = hash_displacement;}
}
3. 扩展 weak_table_t
容积
weak_entry_insert
方法不需要考虑 weak_table_t
容积,因为runtime代码中在调用 weak_entry_insert
方法前都会调用 weak_grow_maybe
方法来在必要的时候扩展 weak_table_t
容积。
static void
weak_grow_maybe(weak_table_t *weak_table)
{size_t old_size = TABLE_SIZE(weak_table);//当weak_table_t容积超过3/4时,进行容积扩展if (weak_table->num_entries >= old_size * 3 / 4) {//weak_table_t容积扩展为原先容积的2倍,且保证了最小容积为64weak_resize(weak_table, old_size ? old_size*2 : 64);}
}
4. 从 weak_table_t
中移除 weak_entry_t
static void
weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{//移除weak_entry_t中相关数据if (entry->out_of_line) free(entry->referrers);//将weak_entry_t所在位置内存全部重置,相当于将weak_table_t数组结构对应位置置为NULL,等同于从数组结构中移除bzero(entry, sizeof(*entry));//weak_table_t中数据个数减一weak_table->num_entries--;//调用weak_compact_maybe方法进行必要的压缩weak_compact_maybe(weak_table);
}
5. 压缩 weak_table_t
在 weak_entry_t
从 weak_table_t
中移除后,runtime会对 weak_table_t
进行必要的压缩,减少内存的使用。
static void
weak_compact_maybe(weak_table_t *weak_table)
{size_t old_size = TABLE_SIZE(weak_table);//当weak_table_t容积大于1025,并且其中有效数据个数少于容积的1/16时,进行压缩if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {//将weak_table_t容积压缩到之前的1/8,保证压缩后有效个数尽量占有新容积的1/2但不超过1/2weak_resize(weak_table, old_size / 8);// leaves new table no more than 1/2 full}
}
6. 向 weak_entry_t
中添加新的 objc_object **
之前我们介绍 weak_entry_t
时提到它本身是一个字典,key为内存对象,value为所有指向该内存对象的weak对象数组,这个数组有两个,分别为 inline_referrers
和 referrers
,那么这两个数组是怎么使用的,答案就在下面的方法中。
static void
append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{//若out_of_line标志位为false,即表明应向inline_referrers数组中插入数据if (! entry->out_of_line) {//尝试向inline_referrers数组中插入weak对象,若有空位并插入成功,则直接退出for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {if (entry->inline_referrers[i] == nil) {entry->inline_referrers[i] = new_referrer;return;}}//若向inline_referrers数组中插入失败,则开始启用inferrers数组//首先将inline_referrers数组中数据用于初始化inferrers数组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;//置out_of_line标志位为true,表明数组数据存在于inferrers数组中entry->out_of_line = 1;//初始化线性探寻所需要的数据entry->mask = WEAK_INLINE_COUNT-1;entry->max_hash_displacement = 0;}assert(entry->out_of_line);//若inferrers数组有效数据超过容积的3/4时,调用grow_refs_and_insert方法扩展inferrers数组//grow_refs_and_insert方法扩展容积的关键代码为old_size ? old_size * 2 : 8,即容积扩展为原先的2倍,且保证最小为8if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {return grow_refs_and_insert(entry, new_referrer);}//使用线性探寻,找到应当存放weak对象的位置,并将weak对象插入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++;
}
7. 从 weak_entry_t
中移除 objc_object **
static void
remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{//若out_of_line标志位为false,即表明数组数据保存在inline_referrers数组中if (! entry->out_of_line) {//从inline_referrers数组中找到对应weak对象并删除for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {if (entry->inline_referrers[i] == old_referrer) {entry->inline_referrers[i] = nil;return;}}return;}//线性探寻,找到weak对象所在位置,并从referrers数组中删除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) {return;}}entry->referrers[index] = nil;entry->num_refs--;
}
从代码中可见,weak_entry_t
在移除对象后,并不会进行类似 weak_table_t
压缩数据结构的操作,故应尽量保证weak对象个数较少。
三、weak关键字修饰对象初始化及重新赋值实现原理
介绍完weak关键字涉及到的数据结构,接下来就该分析weak关键字实现原理了,我们先从weak关键字修饰对象初始化开始分析。
1. objc_initWeak
在 NSObject.mm
文件中,有一个 objc_initWeak
方法,官方文档描述为:当初始化一个weak对象并将内存对象赋值给该weak对象时会调用该方法。
id
objc_initWeak(id *location, id newObj)
{//对内存对象进行非nil判断if (!newObj) {*location = nil;return nil;}//调用storeWeak方法,三个参数的意义在storeWeak方法中描述return storeWeak<false/*old*/, true/*new*/, true/*crash*/>(location, (objc_object*)newObj);
}
2. objc_storeWeak
在 NSObject.mm
文件中,有一个 objc_storeWeak
方法,官方文档描述为:当为一个weak对象赋新值时会调用该方法。
id
objc_storeWeak(id *location, id newObj)
{//调用storeWeak方法,三个参数的意义在storeWeak方法中描述return storeWeak<true/*old*/, true/*new*/, true/*crash*/>(location, (objc_object *)newObj);
}
3. storeWeak
由官方文档可知,无论是初始化weak对象还是为weak对象赋值,最终都会调用到 storeWeak
方法,不同点在于,二者传入三个模板值不同。
storeWeak
有三个模板值:HaveOld,HaveNew,CrashIfDeallocating,这三个值代表的含义如下:
- HaveOld:当该值为true时,weak对象此时已有指向的内存对象,需要将该指向清除。
- HaveNew:当该值为true时,weak对象需要指向新的内存对象。
- CrashIfDeallocating:当该值为true时,若内存对象正在被释放或不支持弱引用(-(BOOL)allowsWeakReference或-(BOOL)retainWeakReference方法返回NO),立刻crash;当该值为false时,返回nil。
id
storeWeak(id *location, objc_object *newObj)
{id oldObj;SideTable *oldTable;SideTable *newTable;retry://获取weak对象所指的旧内存对象以及旧内存对象所对应的SideTableif (HaveOld) {oldObj = *location;oldTable = &SideTables()[oldObj];} else {oldTable = nil;}//获取新内存对象所对应的SideTableif (HaveNew) {newTable = &SideTables()[newObj];} else {newTable = nil;}//调用相关方法清除weak对象与旧内存对象的关联if (HaveOld) {weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);}//调用相关方法将weak对象放入新内存对象所对应的```weak_entry_t```value数组中if (HaveNew) {newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, CrashIfDeallocating);//将新内存对象weakly_referenced标志位置为trueif (newObj && !newObj->isTaggedPointer()) {newObj->setWeaklyReferenced_nolock();}//将weak对象指向新内存对象*location = (id)newObj;}return (id)newObj;
}
4. weak_unregister_no_lock
我们先来看一下weak对象是如何与旧内存对象解除指向关系的。
void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id)
{//内存对象objc_object *referent = (objc_object *)referent_id;//weak对象objc_object **referrer = (objc_object **)referrer_id;weak_entry_t *entry;if (!referent) return;//获取内存对象对应的weak_entry_tif ((entry = weak_entry_for_referent(weak_table, referent))) {//调用remove_referrer方法,将weak对象从内存对象对应的weak_entry_t中删除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;}}}//若weak_entry_t在删除之后有效数据为空,则表明该内存对象没有任何weak对象指向,可以将对应的weak_entry_t从weak_table_t中删除,并在必要条件下对weak_table_t进行空间压缩if (empty) {weak_entry_remove(weak_table, entry);}}
}
5. weak_register_no_lock
在将weak对象与旧内存对象的关联解除后,就调用 weak_register_no_lock
方法将weak对象与新内存对象进行关联。
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;//weak对象objc_object **referrer = (objc_object **)referrer_id;//tagged类型对象无需进行处理if (!referent || referent->isTaggedPointer()) return referent_id;bool deallocating;if (!referent->ISA()->hasCustomRR()) {//若内存对象所属类没有自定义retain/release/dealloc/allowsWeakReference等方法,可以直接读取当前是否正在释放deallocating = referent->rootIsDeallocating();}else {//若内存对象所属类自定义了retain/release/dealloc/allowsWeakReference等方法,读取该类是否支持弱引用,若不支持弱引用,直接标识对象为正在释放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) {//crashIfDeallocating为true,则直接crash;crashIfDeallocating为false,返回nilif (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;}}weak_entry_t *entry;if ((entry = weak_entry_for_referent(weak_table, referent))) {//若该内存对象对应的weak_entry_t已经在weak_table_t中存在,则直接调用append_referrer将weak对象插入weak_entry_t中append_referrer(entry, referrer);} else {//若该内存对象对应的weak_entry_t不存在,则创建weak_entry_t,并初始化weak_entry_t的inline_referrers,将weak对象放入weak_entry_t的inline_referrers数组第一位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_entry_t插入weak_table_t中,在此之前对weak_table_t做必要的扩容weak_grow_maybe(weak_table);weak_entry_insert(weak_table, &new_entry);}return referent_id;
}
至此,weak关键字修饰对象的初始化和重新赋值流程就完成了,本质来说,每个内存对象都会在全局的 SideTables
中对应至一个 SideTable
中, SideTable
中的 weak_table_t
记录了该 SideTable
下所有内存对象weak引用信息,内存对象可在 weak_table_t
中找到与自己一一对应的 weak_entry_t
,weak_entry_t
中记录了所有指向该内存对象且weak修饰的对象信息。
weak关键字修饰对象的初始化和重新赋值流程如下图:
四、使用weak关键字修饰对象时原理
weak关键字修饰的对象,在使用时可以访问到所指的内存对象,但是如果是直接使用该内存对象,当在多线程情况下,并不能保证内存对象在weak对象执行语句中被释放,那么weak关键字是如何保证在weak对象执行语句时内存对象不被释放的呢?其实很简单,就是对内存对象进行计数增加。
每次在使用weak对象时,都相当于调用一次 objc_loadWeak
id objc_loadWeak(id *location)
{if (!*location) return nil;//使用时,计数+1,并在合适aoturelease_pool中进行-1return objc_autorelease(objc_loadWeakRetained(location));
}id objc_loadWeakRetained(id *location)
{id result;SideTable *table;table = &SideTables()[result];//根据weak对象找到所指向的内存对象result = weak_read_no_lock(&table->weak_table, location);return result;
}id weak_read_no_lock(weak_table_t *weak_table, id *referrer_id)
{//weak对象objc_object **referrer = (objc_object **)referrer_id;//内存对象objc_object *referent = *referrer;//tagged对象无需处理if (referent->isTaggedPointer()) return (id)referent;//weak对象指向为nil或内存对象没有对应的weak_entry_t,即没有weak对象指向时,返回nilweak_entry_t *entry;if (referent == nil || !(entry = weak_entry_for_referent(weak_table, referent))) {return nil;}if (! referent->ISA()->hasCustomRR()) {//若内存对象所属类没有自定义retain/release/dealloc/allowsWeakReference等方法,则调用rootTryRetain直接对对象计数+1if (! referent->rootTryRetain()) {return nil;}}else {//若内存对象所属类自定义了retain/release/dealloc/allowsWeakReference等方法,会调用tryRetain方法,并返回该方法返回值BOOL (*tryRetain)(objc_object *, SEL) = (BOOL(*)(objc_object *, SEL))object_getMethodImplementation((id)referent, SEL_retainWeakReference);if ((IMP)tryRetain == _objc_msgForward) {return nil;}if (! (*tryRetain)(referent, SEL_retainWeakReference)) {return nil;}}return (id)referent;
}
由此可见,在weak对象执行的语句中,weak对象所指向的内存对象计数会+1,这样就保证在语句中不会发生执行一半而释放内存对象的问题。
五、weak关键字修饰对象所指内存对象释放时
除了文章开头提到的特征外,weak关键字还具有一个特征:当weak对象指向的内存对象被释放后,weak对象自动置为nil。
那底层原理是如何实现的呢?
当内存对象释放时,会一次调用以下方法:
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance
clearDeallocating
sidetable_clearDeallocating
weak_clear_no_lock
在 weak_claer_no_lock
方法中,会进行对weak对象的置空操作:
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);//若内存对象对应的weak_entry_t不存在,则无需做更多操作if (entry == nil) {return;}//对weak_entry_t存储weak对象的数组中有效数据依次置nilweak_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_weak_error();}}}weak_entry_remove(weak_table, entry);
}
至此,我们知道了内存对象在释放时所做的操作,也知道了weak对象是在内存对象dealloc时被置为nil的。
但是,如果我们在MRC下,强制重写内存对象的dealloc方法,使之无法正常调用[super dealloc],意味着内存对象无法正常调用到 weak_clear_no_lock
,也就无法完成weak对象的置nil,而此时再去获取weak对象,发现获取到的值已经为nil了,这是为什么呢?
在使用weak对象时,当调用到 weak_read_no_lock
方法时,我们知道,若内存对象有自定义retain/release/dealloc/allowsWeakReference等方法时,会直接返回tryRetain
方法的返回值。
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{if (tryRetain && newisa.deallocating) goto tryfail;tryfail:if (!tryRetain && sideTableLocked) sidetable_unlock();return nil;
}
由此可见,当内存对象被标记为deallocting,即使在还没调用dealloc等方法时,对该对象进行计数+1,也会被返回nil,这就解释了上面的问题。
六、Weak-Strong搭配使用的误解
在使用Block时,我们可以使用weak关键字来避免外部变量被Block强引用而导致的循环引用,同时为了Block中的代码能够正常执行,许多开发者提出了Weak-Strong搭配使用的方式,类似如下:
{__weak typeof(self) weakSelf = self;self.block = ^{__strong typeof(weakSelf) strongSelf = weakSelf;[strongSelf test1];[strongSelf test2];};
}
以上代码相对于单独使用weak来说还是有好处的,在单独使用weak时,可以保证在执行 [weakSelf test1]
或 [weakSelf test2]
单条语句时,weakSelf所指的self不会被释放或self已经释放而直接向nil发送消息。
若使用Weak-Strong搭配的方式的话,可以保证在执行 [strongSelf test1]
和 [strongSelf test2]
时,是向同一对象发送消息。
为什么这么说呢?当开始执行Block语句时,若self还存在,那么strongSelf可以保证在整个Block代码块中不会被释放,即使Block中调用无数次strongSelf,strongSelf也不会因为多线程而在半途被释放;若开始执行Block时,self已经被释放,那么之后所有的消息都会被发送至nil。所以Weak-Strong搭配可以保证Block中语句被处理为一个事务。
所以说,Weak-Strong并不能保证Block中语句一定会被执行,它只能保证Block中语句作为一个事务被发送到同一对象处。只要理解了weak实现原理,我们就能明白何时单独使用weak也能完成代码功能,而何时必须使用Weak-Strong来保证代码事务能力。
七、参考资料
- iOS 从源码深入探究weak的实现
iOS weak关键字实现原理相关推荐
- iOS之深入解析weak关键字的底层原理
一.weak 关键字 在 iOS 开发过程中,会经常使用到一个修饰词 weak,使用场景大家都比较清晰,避免出现对象之间的强强引用而造成对象不能被正常释放最终导致内存泄露的问题. weak 关键字的作 ...
- iOS底层原理:weak的实现原理
作者丨夜幕降临耶 链接: https://juejin.im/post/5e7a322f6fb9a07ca24f79bb 来源:掘金 在iOS开发过程中,会经常使用到一个修饰词weak,使用场景大家都 ...
- iOS 5中的strong和weak关键字
from:http://blog.csdn.net/yhawaii/article/details/7291134 iOS 5 中对属性的设置新增了strong 和weak关键字来修饰属性(iOS 5 ...
- iOS进阶之底层原理-weak实现原理
基本上每一个面试都会问weak的实现原理,还有循环引用时候用到weak,今天我们就来研究下weak的实现原理到底是什么. weak入口 我们在这里打个断点,然后进入汇编调试. 这里就很明显看到了入口, ...
- iOS底层weak的实现原理
weak是弱引用,所引用对象的计数器不会加一,并在引用对象被释放的时候自动被设置为nil.那么weak的原理是什么呢? weak表其实是一个hash(哈希)表 (字典也是hash表),Key是所指对象 ...
- iOS 底层解析weak的实现原理
weak表是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址数组.weak是弱引用,所引用对象的计数器不会加一,并在引用对象被释放的时候自动被设置为nil.通常用于解决循 ...
- iOS weak的使用
iOS开发中,我们经常会听到weak这个词,它是Objective-C语言中的一个特性,用于解决循环引用的问题.在本文中,我们将深入探讨iOS weak的使用,包括它的定义.原理.优缺点以及使用场景等 ...
- iOS weak 自动置为nil的实现
1 weak 自动置为nil的实现 runtime 维护了一个Weak表,weak_table_t 用于存储指向某一个对象的所有Weak指针.Weak表其实是一个哈希表, key是所指对象的地址,va ...
- ARM 之十一__weak 和 __attribute__((weak)) 关键字的使用
今天在使用 Keil (主要是 armcc 编译器)编译代码(华大的 MCU 驱动库hc32f46x_interrupts.h / c)的时候遇到了有 __weak 关键字的函数不起作用的问题,甚 ...
- 什么情况使用 weak 关键字,相比 assign 有什么不同?
什么情况使用 weak 关键字? 在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性 自身已经对它进行一次强引用,没有必要再强引 ...
最新文章
- 获取当前横竖屏_Chrome扩展程序一键生成网页骨架屏
- 吴恩达 coursera AI 专项五第一课(下)总结+作业答案
- 实现一个函数,对一个正整数n,算得到1
- 如何写出优雅的异常处理
- 邮件系统磁盘监控脚本
- ubuntu自定义安装里怎么选_超市里的五香粉怎么选?看懂配料表,两个小技巧,不怕选不好。...
- 【UVA - 10037】Bridge(过河问题,经典贪心)
- java source folder作用_java项目把源码放到folder里,不是source folder,这个java代码还能被调用吗?...
- RHEL账号总结一:账号的分类
- 读书笔记—《发现你的行为模式(钻石版)》-DiSC测试
- Bitmap Style Designer非官方说明
- 系统内存太少,VirtualBox无法启动虚拟机
- excel函数修改服务器端数据,勤哲Excel服务器表达式函数详解
- cpc客户端网络不通
- 关于AMS1117-ADJ 电压调节计算
- sap系统搭建教程_SAP系统和微信集成的系列教程之一:微信开发环境的搭建
- PES实况足球2018 中文版下载解说 中超德甲亚冠世界杯夏季转会 全dlc
- MySQL数据库锁机制
- 华为交换机或路由器释放DHCP已分配的地址
- C/C++ 机房预约系统
热门文章
- 上野千鹤子名誉教授的东大祝辞中提到的“元知识”是什么?
- 网站域名https显示证书错误如何解决
- 适用于 Windows 7 SP1 和 Windows Server 2008 R2 SP1 的扩展安全更新(ESU)许可准备程序包
- SpringBoot 项目鉴权的 4 种方式
- 卡1有信号 卡2无服务器,为什么卡1无服务卡2有
- dispatch_group_async
- 轩小陌的Python笔记-Pandas时间序列与日期
- u盘推荐知乎_市面上的U盘怎么选择?U盘那个牌子好?
- SpringBoot中这样定义全局异常处理器Global Exception Handler
- Raspberry Pi 上手准备