我们平时在开发的时候经常会使用分类来添加方法、协议、属性,但在添加属性的时候属性是不会自动生成成员变量的,这时候我们就需要关联对象来动态存储属性值。

分类

@interface NSObject(Study)@property (nonatomic, strong) NSObject *obj1;
@property (nonatomic, strong) NSObject *obj2;- (void)instanceMethod;
+ (void)classMethod;@endstatic const void *NSObjectObj1Name = "NSOBJECT_OBJ1";@implementation NSObject(Study)@dynamic obj2;- (void)setObj1:(NSObject *)obj1 {objc_setAssociatedObject(self, &NSObjectObj1Name, obj1, OBJC_ASSOCIATION_RETAIN);
}- (NSObject *)obj1 {return objc_getAssociatedObject(self, &NSObjectObj1Name);
}- (void)instanceMethod {NSLog(@"-类名:%@,方法名:%s,行数:%d",NSStringFromClass(self.class), __func__, __LINE__);
}+ (void)classMethod {NSLog(@"+类名:%@,方法名:%s,行数:%d",NSStringFromClass(self.class), __func__, __LINE__);
}@end

我们将上面的代码重写成c++代码,我们看一看关键部分:

static struct _category_t _OBJC_$_CATEGORY_NSObject_$_Study __attribute__ ((used, section ("__DATA,__objc_const"))) =
{"NSObject",0, // &OBJC_CLASS_$_NSObject,(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Study,(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_NSObject_$_Study,0,(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_NSObject_$_Study,
};

可以看到其根本的实现是_category_t这个结构,那么我们可以在objc源码来查找关于category_t的定义:

struct category_t {const char *name;classref_t cls;WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;struct protocol_list_t *protocols;struct property_list_t *instanceProperties;// Fields below this point are not always present on disk.struct property_list_t *_classProperties;method_list_t *methodsForMeta(bool isMeta) {if (isMeta) return classMethods;else return instanceMethods;}property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);protocol_list_t *protocolsForMeta(bool isMeta) {if (isMeta) return nullptr;else return protocols;}
};

根据category_t源码,我们可以总结:

  • 分类里面即有实例方法列表又有类方法列表
  • 分类没有成员变量列表

分类的加载

分类的加载是在objc中实现的。 在源码attachCategories的实现中:

// Attach method lists and properties and protocols from categories to a class.
// Assumes the categories in cats are all loaded and sorted by load order,
// oldest categories first.
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,int flags)
{...bool fromBundle = NO;bool isMeta = (flags & ATTACH_METACLASS);//新建rweauto rwe = cls->data()->extAllocIfNeeded();//debug代码可以放这里//遍历每个分类for (uint32_t i = 0; i < cats_count; i++) {auto& entry = cats_list[i];//获取分类里面的方法method_list_t *mlist = entry.cat->methodsForMeta(isMeta);if (mlist) {if (mcount == ATTACH_BUFSIZ) {prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);rwe->methods.attachLists(mlists, mcount);mcount = 0;}mlists[ATTACH_BUFSIZ - ++mcount] = mlist;fromBundle |= entry.hi->isBundle();}...}if (mcount > 0) {prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,NO, fromBundle, __func__);//添加分类的方法到rwe中rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);if (flags & ATTACH_EXISTING) {flushCaches(cls, __func__, [](Class c){// constant caches have been dealt with in prepareMethodLists// if the class still is constant here, it's fine to keepreturn !c->cache.isConstantOptimizedCache();});}}rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

我们在这个函数里加上:

//调试新增
const char *mangledName = cls->nonlazyMangledName();
//你添加分类的类名
const char *className = "MyObject";
if (strcmp(mangledName, className) == 0 && !isMeta) {printf("debug find it\n");
}

打上断点,注意:分类和本类都需要实现+load方法才可以。我们看堆栈信息:

可以看到是load_images中调用的。前面的文章已经讲解过load_images的调用时机。在里面也可以最终找到attachCategories的调用时机(当然,这只是一种情况,还有一种情况是在realizeClassWithoutSwift最后的methodizeClass调用):

接下来我们通过lldb来调试。

在这里,我获得的方法列表里面方法数为0。越过断点,使用count()获取:

现在数量为3。因为我们写了三个实例方法,所以数量是3。

关联对象

回到我们一开始的代码,还有一个关联对象。我们先在objc源码中找到关联对象api的实现部分:

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{_object_set_associative_reference(object, key, value, policy);
}void objc_removeAssociatedObjects(id object)
{if (object && object->hasAssociatedObjects()) {_object_remove_assocations(object, /*deallocating*/false);}
}

objc_setAssociatedObject

可以看到是调用了内部函数_object_set_associative_reference,解析注解如下:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{// This code used to work when nil was passed for object and key. Some code// probably relies on that to not crash. Check and handle it explicitly.// rdar://problem/44094390if (!object && !value) return;//isa有一位信息为禁止关联对象,如果设置了,直接报错if (object->getIsa()->forbidsAssociatedObjects())_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));//包装对象,转换类型DisguisedPtr<objc_object> disguised{(objc_object *)object};//包装值和属性信息ObjcAssociation association{policy, value};// retain the new value (if any) outside the lock.//设置属性信息association.acquireValue();bool isFirstAssociation = false;{//调用构造函数,构造函数内加锁操作AssociationsManager manager;//获取全局的HasMapAssociationsHashMap &associations(manager.get());//如果值不为空if (value) {//去关联对象表中找对象对应的二级表,如果没有内部会重新生成一个⚠️auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//如果没有找到if (refs_result.second) {/* it's the first association we make *///说明是第一次设置关联对象,把是否关联对象设置为YESisFirstAssociation = true;}/* establish or replace the association */auto &refs = refs_result.first->second;//在二级表中找key对应的内容,auto result = refs.try_emplace(key, std::move(association));//如果已经有内容了,没有内容上面根据association已经插入了值,所以啥也不用干if (!result.second) {//替换掉association.swap(result.first->second);}//如果value为空} else {//通过object找对应的二级表auto refs_it = associations.find(disguised);// 如果有if (refs_it != associations.end()) {auto &refs = refs_it->second;//通过key再在二级表里面找对应的内容auto it = refs.find(key);//如果有if (it != refs.end()) {//删除掉association.swap(it->second);refs.erase(it);if (refs.size() == 0) {associations.erase(refs_it);}}}}}// Call setHasAssociatedObjects outside the lock, since this// will call the object's _noteAssociatedObjects method if it// has one, and this may trigger +initialize which might do// arbitrary stuff, including setting more associated objects.// 第一次时候标记对象是否有关联对象if (isFirstAssociation)object->setHasAssociatedObjects();// release the old value (outside of the lock).// 释放association.releaseHeldValue();
}

方法需要传入四个参数:

参数名称 解释
id object 需要关联的对象
void *key 对应的key
id value 对应的值
objc_AssociationPolicy policy 内存管理策略

AssociationsManager

是一个构造函数,内部构造函数AssociationsManager()和析构函数~AssociationsManager()进行了加锁和解锁(不是单例)。 构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量。

// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock
// 类关联管理器管理锁/哈希表单例对。
// 分配实例获取锁
class AssociationsManager {using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;static Storage _mapStorage;public:AssociationsManager()   { AssociationsManagerLock.lock(); }~AssociationsManager()  { AssociationsManagerLock.unlock(); }AssociationsHashMap &get() {return _mapStorage.get();}static void init() {_mapStorage.init();}
};

通过观察代码和注释可以看出:其通过AssociationsHashMap &associations(manager.get());获取的关联表是全局唯一的,_mapStorage是全局静态变量,获取的AssociationsHashMap是全局唯一的。
接下来,我们需要注意try_emplace这个方法。

try_emplace

value有的情况下 try_emplace 会走2次。

  1. 第一次参数传入: DisguisedPtr<objc_object> disguised{(objc_object *)object} 闭包
  2. 第二次参数传入: key, objc_setAssociatedObjectkey为传进来的第二个参数:自定义的key
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
// 如果key不在map中,插入key,value进map
// 如果key不在map中,则会在适当的位置构造该value,否则不会移动该value
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {// 创建BucketT通知BucketT *TheBucket;// 通过LookupBucketFor方法查询TheBucket值情况,要么有值走下面if (LookupBucketFor(Key, TheBucket))return std::make_pair(makeIterator(TheBucket, getBucketsEnd(), true),false); // Already in map.已经有值// Otherwise, insert the new element.// 没有值,给TheBucket插入新值TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);return std::make_pair(makeIterator(TheBucket, getBucketsEnd(), true),true);
}

这里返回的是一个迭代器,如果有内容返回对应的迭代器,如果没有的话,添加一个,并返回迭代器。使用了两次try_emplace方法,可以得知他是嵌套两层的HashMap结构,根据代码的理解,可以得到以下结构图:

LookupBucketFor

/// LookupBucketFor - Lookup the appropriate bucket for Val, returning it in
/// FoundBucket.  If the bucket contains the key and a value, this returns
/// true, otherwise it returns a bucket with an empty marker or tombstone and
/// returns false.
/// 查找 Val 的相应存储桶,将其返回到 FoundBucket 中。
/// 如果存储桶包含键和值,则返回 true,否则返回带有空标记或逻辑删除的存储桶并返回 false。
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,const BucketT *&FoundBucket) const {// 获取buckets的首地址const BucketT *BucketsPtr = getBuckets();// 获取可存储的buckets的总数const unsigned NumBuckets = getNumBuckets();// 如果NumBuckets = 0 返回 falseif (NumBuckets == 0) {FoundBucket = nullptr;return false;}// FoundTombstone - Keep track of whether we find a tombstone while probing.// 在探查的时候留意我们是否找到了tombstoneconst BucketT *FoundTombstone = nullptr;const KeyT EmptyKey = getEmptyKey();const KeyT TombstoneKey = getTombstoneKey();assert(!KeyInfoT::isEqual(Val, EmptyKey) &&!KeyInfoT::isEqual(Val, TombstoneKey) &&"Empty/Tombstone value shouldn't be inserted into map!");// 计算bucket的hash下标unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);unsigned ProbeAmt = 1;while (true) {// 内存平移:找到hash下标对应的Bucketconst BucketT *ThisBucket = BucketsPtr + BucketNo;// Found Val's bucket?  If so, return it.if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {// 如果查询到`Bucket`的`key`和`Val`相等 返回当前的Bucket,说明查询到了FoundBucket = ThisBucket;return true;}// If we found an empty bucket, the key doesn't exist in the set.// Insert it and return the default value.// 如果bucket为空,说明当前key还不在表中,返回false// 返回默认值if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {// If we've already seen a tombstone while probing, fill it in instead// of the empty bucket we eventually probed to.// 如果我们在探测时已经看到了tombstone,请将其填充,而不是我们最终探测到的空bucket。FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;return false;}// If this is a tombstone, remember it.  If Val ends up not in the map, we// prefer to return it than something that would require more probing.// Ditto for zero values.// 如果这是tombstone,记住它。如果Val最终不在map中,我们宁愿返回它,而不是需要更多探测的东西。// 对于零值也是如此if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&!FoundTombstone)//记录发现的第一个tombstoneFoundTombstone = ThisBucket;  // Remember the first tombstone found.if (ValueInfoT::isPurgeable(ThisBucket->getSecond())  &&  !FoundTombstone)FoundTombstone = ThisBucket;// Otherwise, it's a hash collision or a tombstone, continue quadratic// probing.// 否则,它是一个哈希冲突或tombstone,继续二次探索。if (ProbeAmt > NumBuckets) {FatalCorruptHashTables(BucketsPtr, NumBuckets);}// 重新计算hash下标BucketNo += ProbeAmt++;BucketNo &= (NumBuckets-1);}
}

和在cache中的找bucket流程一样。

InsertIntoBucket

template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,ValueArgs &&... Values) {// 根据Key 找到TheBucket的内存地址TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);// 将 Key 和 Values保存到TheBucket中TheBucket->getFirst() = std::forward<KeyArg>(Key);::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);return TheBucket;
}

主要的插入工作都是在InsertIntoBucketImpl方法中完成的:

template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,BucketT *TheBucket) {// If the load of the hash table is more than 3/4, or if fewer than 1/8 of// the buckets are empty (meaning that many are filled with tombstones),// grow the table.// 如果哈希表的加载量大于3/4,或者小于1/8的桶是空的(这意味着很多桶都装了tombstones),那么就对哈希表扩容。// The later case is tricky.  For example, if we had one empty bucket with// tons of tombstones, failing lookups (e.g. for insertion) would have to// probe almost the entire table until it found the empty bucket.  If the// table completely filled with tombstones, no lookup would ever succeed,// causing infinite loops in lookup.//后一种情况比较棘手。例如,如果我们有一个空桶,里面有大量的tombstone,那么失败的查找(例如插入)将不得不探测几乎整个表,直到找到空桶。如果表完全被tombstone填满,那么任何查找都无法成功,导致无限循环的查找。// 计算实际占用buckets的个数,如果超过负载因子(3/4),进行扩容操作unsigned NewNumEntries = getNumEntries() + 1;// 获取buckets的总容量unsigned NumBuckets = getNumBuckets();if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {// 如果哈希表的负载大于等于3/4,进行二倍扩容this->grow(NumBuckets * 2);// 首次分配 4 的容量LookupBucketFor(Lookup, TheBucket);//查找BucketNumBuckets = getNumBuckets();} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=NumBuckets/8)) {this->grow(NumBuckets);//查找BucketLookupBucketFor(Lookup, TheBucket);}ASSERT(TheBucket);// Only update the state after we've grown our bucket space appropriately// so that when growing buckets we have self-consistent entry count.// If we are writing over a tombstone or zero value, remember this.// 只有在我们适当地增大bucket存储空间后,才更新状态,以便在增加存储bucket时,我们具有自洽的条目计数。// 如果我们在tombstone或零值上书写,请记住这一点。if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {// Replacing an empty bucket.incrementNumEntries();} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {// Replacing a tombstone.// 更换空桶// 更新占用的容量个数incrementNumEntries();decrementNumTombstones();} else {// we should be purging a zero. No accounting changes.// 我们应该清除一个零。没有占用更改。ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));TheBucket->getSecond().~ValueT();}return TheBucket;
}

isFirstAssociation

首次关联对象,需要更新对象isa的标志位has_assoc,表示是否有关联对象。

// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
// 首次关联对象调用setHasAssociatedObjects方法
// 通过setHasAssociatedObjects方法`标记对象存在关联对象`设置`isa指针`的`has_assoc`属性为`true`
if (isFirstAssociation)object->setHasAssociatedObjects();

setHasAssociatedObjects

接下来看一下设置关联对象的函数。

inline void
objc_object::setHasAssociatedObjects()
{if (isTaggedPointer()) return;if (slowpath(!hasNonpointerIsa() && ISA()->hasCustomRR()) && !ISA()->isFuture() && !ISA()->isMetaClass()) {void(*setAssoc)(id, SEL) = (void(*)(id, SEL)) object_getMethodImplementation((id)this, @selector(_noteAssociatedObjects));if ((IMP)setAssoc != _objc_msgForward) {(*setAssoc)((id)this, @selector(_noteAssociatedObjects));}}isa_t newisa, oldisa = LoadExclusive(&isa.bits);do {newisa = oldisa;if (!newisa.nonpointer  ||  newisa.has_assoc) {ClearExclusive(&isa.bits);return;}newisa.has_assoc = true;//⚠️⚠️⚠️} while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
}

可以看到设置了这个对象存在关联对象。
通过setHasAssociatedObjects方法设置对象存在关联对象,即isa指针的has_assoc位域设置为true最后通过releaseHeldValue方法释放旧值。

objc_getAssociatedObject

id
objc_getAssociatedObject(id object, const void *key)
{return _object_get_associative_reference(object, key);
}

可以看到,objc_getAssociatedObject调用了_object_get_associative_reference方法。进入了_object_get_associative_reference方法,关联对象的获取就是查表,源码如下:

id
_object_get_associative_reference(id object, const void *key)
{// 创建一个空关联对象ObjcAssociation association{};{// 实例化AssociationsManagerAssociationsManager manager;// 实例化全局的关联表AssociationsHashMap(单例)AssociationsHashMap &associations(manager.get());// iterator迭代器,相当于找到object和对应的ObjectAssociationMap(对象关联表)AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到了object对应的ObjectAssociationMap(对象关联表)if (i != associations.end()) {// 获取ObjectAssociationMapObjectAssociationMap &refs = i->second;// 在二级表内迭代获取key对应的数据ObjectAssociationMap::iterator j = refs.find(key);//找到了key对应的数据if (j != refs.end()) {// 获取 associationassociation = j->second;// retain 新值association.retainReturnedValue();}}}// 取值并返回然后放到自动释放池中return association.autoreleaseReturnedValue();
}

看一下find()

iterator find(const_arg_type_t<KeyT> Val) {BucketT *TheBucket;if (LookupBucketFor(Val, TheBucket))return makeIterator(TheBucket, getBucketsEnd(), true);return end();
}

里面还是调用了前面讲过的LookupBucketFor,就不多讲了。

objc_removeAssociatedObjects

void objc_removeAssociatedObjects(id object)
{if (object && object->hasAssociatedObjects()) {_object_remove_assocations(object, /*deallocating*/false);}
}

内部调用_object_remove_assocations,看一下:

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
// 与设置/获取关联引用不同,此函数对性能敏感,因为原始isa对象(如OS对象)不能跟踪它们是否有关联对象。
void
_object_remove_assocations(id object, bool deallocating)
{ObjectAssociationMap refs{};{AssociationsManager manager;AssociationsHashMap &associations(manager.get());AssociationsHashMap::iterator i = associations.find((objc_object *)object);if (i != associations.end()) {refs.swap(i->second);// If we are not deallocating, then SYSTEM_OBJECT associations are preserved.//如果我们没有回收,那么SYSTEM_OBJECT关联会被保留。bool didReInsert = false;if (!deallocating) {for (auto &ref: refs) {if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {i->second.insert(ref);didReInsert = true;}}}if (!didReInsert)associations.erase(i);}}// Associations to be released after the normal ones.// 在正常关联之后释放关联。SmallVector<ObjcAssociation *, 4> laterRefs;// release everything (outside of the lock).// 释放锁外的所有内容。for (auto &i: refs) {if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {// If we are not deallocating, then RELEASE_LATER associations don't get released.//如果我们不是在释放,那么RELEASE_LATER关联不会被释放if (deallocating)laterRefs.append(&i.second);} else {i.second.releaseHeldValue();}}for (auto *later: laterRefs) {later->releaseHeldValue();}
}

关联对象总结

设值

  • 创建一个AssociationsManager 管理类
  • 获取唯一全局静态HashMap
  • 判断是否存在关联对象值
    • 存在 :

      • 创建一个空的 ObjectAssociationMap 去取查询的键值对
      • 如果发现没有这个 key 就先插入一个 空的 BucketT
      • 标记对象存在关联对象
      • 用当前 策略 policyvalue 组成了一个 ObjcAssociation 替换之前空的BucketT
      • 标记 ObjectAssociationMap 为第二次
    • 不存在 :
      • 根据DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
      • 清理迭代器

取值

  • 创建一个 AssociationsManager 管理类
  • 获取唯一的全局静态HashMap
  • 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
  • 如果这个迭代查询器不是最后一个,那么获取 : ObjectAssociationMap (这里有策略 policy 和值 value)
  • ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
  • 返回value

销毁

对象销毁dealloc时,销毁相关的关联对象。
调用流程:dealloc --> _objc_rootDealloc --> rootDealloc --> object_dispose --> objc_destructInstance --> _object_remove_assocations

本文参考博客:
iOS八股文(十)分类和关联对象源码解析
OC底层探索(十八): 类扩展和关联对象

[OC学习笔记]分类和关联对象源码解析相关推荐

  1. Kubernetes学习笔记之Calico CNI Plugin源码解析(二)

    女主宣言 今天小编继续为大家分享Kubernetes Calico CNI Plugin学习笔记,希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在"360云计算" ...

  2. Kubernetes学习笔记之Calico CNI Plugin源码解析(一)

    女主宣言 今天小编为大家分享Kubernets Calico CNI Plugin的源码学习笔记,希望对正在学习k8s相关部分的同学有所帮助: PS:丰富的一线技术.多元化的表现形式,尽在" ...

  3. Qt学习笔记,再次分析EVA源码之后得出的结论-QListView,QListViewItem(Qt3);Q3ListView,Q3ListViewItem(Qt4)...

    Qt学习笔记,再次分析EVA源码之后得出的结论-QListView,QListViewItem(Qt3);Q3ListView,Q3ListViewItem(Qt4) 今天再次分析了Eva的源码,也看 ...

  4. Ui学习笔记---EasyUI的EasyLoader组件源码分析

    Ui学习笔记---EasyUI的EasyLoader组件源码分析 技术qq交流群:JavaDream:251572072   1.问题1:为什么只使用了dialog却加载了那么多的js   http: ...

  5. Netty网络框架学习笔记-16(心跳(heartbeat)服务源码分析)

    Netty网络框架学习笔记-16(心跳(heartbeat)服务源码分析_2020.06.25) 前言: Netty 作为一个网络框架,提供了诸多功能,比如编码解码等,Netty 还提供了非常重要的一 ...

  6. 《OpenHarmony开源鸿蒙学习入门》-- 系统相机应用源码解析(一)

    OpenHarmony开源鸿蒙学习入门–系统相机应用源码解析(一) 一.源码解析的目的: 为什么要去做源码解析这件事?我个人认为,首先可以提高我们对代码书写的能力,毕竟官方系统级的应用,会比demo的 ...

  7. Spring源码深度解析(郝佳)-学习-Spring消息-整合RabbitMQ及源码解析

      我们经常在Spring项目中或者Spring Boot项目中使用RabbitMQ,一般使用的时候,己经由前人将配置配置好了,我们只需要写一个注解或者调用一个消息发送或者接收消息的监听器即可,但是底 ...

  8. MyBatis源码学习笔记(从设计模式看源码)

    文章目录 1.源码分析概述 ①.Mybatis架构分析 ②.门面模式 ③.设计模式的原则 2.日志模块分析 ①.适配器模型 ②.动态代理 ③.日志模块分析 3.数据源模块分析 ①.工厂模式 ②.数据源 ...

  9. NODEMCU学习笔记-01 esp8266 WIFI杀手 源码上传版

    NODEMCU学习笔记-01 esp8266WIFI杀手 动手前的准备 NODEMCU和ESP8266 ARDUINO IDE GITHUB CSDN 让我们开始吧 连接开发板并安装驱动 安装ardu ...

最新文章

  1. 关于学习Python的一点学习总结(35->关联超类)
  2. android 应用变量,Android全局应用变量的使用
  3. 正则表达式在js和java中的使用
  4. Java自动装箱与拆箱
  5. db2 本地db 到实例_如何登录到FreeCodeCamp的本地实例
  6. 使用.NET为Window Mobile写自动化工具的无奈之处.
  7. 新风口?人造肉第一股表现强劲 股价累计上涨近600%
  8. vb.ne textbox数字保存excel_Excel 另类保护:锁死页面布局、保存、审阅标签右键等菜单禁编辑...
  9. IMAXB6充电器使用教程
  10. 线程wait和notify方法的demo详解
  11. 舒尔特表-最终版 js
  12. 女士品茶 - 简单摘录
  13. 随机梯度下降算法SGD
  14. java语言编译系统_请问C语言,JAVA之类的语言编译程序是属于 系统软件 还是 应用软件??...
  15. JAVA架构演变过程
  16. 92.发光文字加载特效
  17. Intro to Copy Elision and (N)RVO
  18. 微信html5展示页,H5科普|微信H5页面的展示形式
  19. Java job interview:网页设计HTML+CSS前端开发与PS前台美化案例分析
  20. uniapp vue3版本 引用color-ui的cu-custom组件问题

热门文章

  1. linux如何查看有几个网卡,linux 查看有几块网卡
  2. 几个非洲鼓的基本节奏
  3. Java代码混淆和加密--Jocky
  4. 全面理解ES6模块化编程
  5. 信息技术课程计算机硬件,初中信息技术课程关键思路分析
  6. 四轮两驱小车(四):STM32驱动5路灰度传感器PID循迹
  7. yii2 框架使用gii工具创建模块
  8. vs2017控制台应用程序调用DLL
  9. 基于C语言的G代码解释器,G-Code
  10. 怎么利用完成端口监听多个不同端口的socket