17.iOS底层学习之关联对象
本篇文章主要研究关联对象,承接上一篇分类的加载,补充一部分关于分类的问题,主要分为以下几个部分:
- 分类加载是否需要排序?
- methodList的数据结构?
- 分类和扩展
- 关联对象
分类加载是否需要排序
关联分类的关键方法是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)
翻译一下就是:从分类关联方法列表,协议,属性到一个类上,假如所有的类都被如期加载进来了那么分类的顺序就是加载的顺序,最旧的分类是第一个被加载的。
我们再来调试一下方法attachCategories
:
先来到这个方法的是分类B。
分类A执行attachCategories
- 第一次进入到方法
attachCategories
,cats_count=1,cats_list的类型是const locstamped_category_t *
;- 取出const locstamped_category_t里的具体内容发现里面有两个成员变量分别是上面👆🏻打印出来的:
cat = 0x0000000100008090
hi = 0x0000000108e0b260 - 取出cat,发现cat的类型是
category_t *const
; - 继续取cat里面装的内容发现如下结构:
- 取出const locstamped_category_t里的具体内容发现里面有两个成员变量分别是上面👆🏻打印出来的:
(lldb) p $25.cat
(category_t *const) $26 = 0x0000000100008090
(lldb) p *$26
(category_t) $27 = {
name = 0x0000000100003e5a "B"
cls = 0x00000001000084c8
instanceMethods = {
ptr = 0x0000000100008038
}
classMethods = {
ptr = 0x0000000100008070
}
protocols = nil
instanceProperties = nil
_classProperties = nil
}
其实就是分类的结构体内容,通过打印的内容可以看到现在进来的是分类B
,接着打印里面的instanceMethods的ptr,这个ptr的类型是method_list_t
,进一步打印得到了对应的两个实例方法:AAAA
和BBBB
;
打印classMethods中的ptr,拿到了一个+load方法:
以上的内容是把分类B关联到主类的过程!然后我们接着调试来看下A分类来到方法
attachCategories
的过程。
分类A执行attachCategories
现在的情况是A,B还有主类都是实现了load方法,所以是通过:attachCategories(cls, &lc, 1, ATTACH_EXISTING); 来到的attachCategories
方法,那么此时cats_count
固定是1。
这个调试的过程和B是一样的。
不一样的地方是此时B分类中有的方法AAAA已经存在了,我们来看一下A分类插入AAAA方法时的过程。我继续调试,方法通过
attachCategories
->prepareMethodLists
->fixupMethodList
->sel_registerName
->_ _sel_registerName
最终来到方法search_builtins(name)
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
SEL result = 0;
if (shouldLock) selLock.assertUnlocked();
else selLock.assertLocked();
if (!name) return (SEL)0;
result = search_builtins(name);
if (result) return result;
conditional_mutex_locker_t lock(selLock, shouldLock);
auto it = namedSelectors.get().insert(name);
if (it.second) {
// No match. Insert.
*it.first = (const char *)sel_alloc(name, copy);
}
return (SEL)*it.first;
}
static SEL search_builtins(const char *name)
{
#if SUPPORT_PREOPT
if (SEL result = (SEL)_dyld_get_objc_selector(name))
return result;
#endif
return nil;
}
这里方法search_builtins里有一个预编译,我这边跑的Mac 所以直接返回了nil,接下来走的是方法auto it = namedSelectors.get().insert(name)
,而这个insert方法如下
这里边的关键方法是try_emplace
所以这个方法会去通过LookupBucketFor方法传进去的key去查找相应的bucket如果找到,那么返回对应的iterator,并且pair的第二个元素设置为false,返回pair。
根据源码的注释提示:这个要插入的方法已经存在map中了。
否则的话会继续往下走执行InsertIntoBucket,根据注释提示:
插入新的元素
,插入新的key到对应的map中并且返回pair,此时的pair的第二个元素是true。pair
的定义这个pair是一个struct,并且带有模板template <class _T1, class _T2>。
pair.png
所以此时我们再回到方法__sel_registerName
,再来看:
auto it = namedSelectors.get().insert(name);
if (it.second) {
// No match. Insert.
*it.first = (const char *)sel_alloc(name, copy);
}
return (SEL)*it.first;
这一段就很清晰了,it的second就是上面pair过后的结果,为false的时候就是原来就有了,直接返回frist就行了因为前面通过pair处理过了,frist里面装的就是对应的value。
为ture的时候,结合注释,“未匹配到,插入”。
A分类有重名方法AAAA的时候.png
综上,这一段流程下来是没有对分类单独进行排序的,目前这种情况的cats_count都是1,是固定的,因为都是从方法 attachCategories(cls, &lc, 1, ATTACH_EXISTING);或者attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);这个进来的,这个cats_count是固定传的1。
然后我们再来看看从attachToClass-> attachCategories(cls, list.array(), list.count(), flags);这个地方进入的方法attachCategories
,这个后面的流程是一样的,都是对对应方法的查询和插入,而进入到attachCategories
之后的流程,也是去遍历cats_list每个分类里面的methodList,然后安卓流程attachList,就没有单独对分类排序的部分。
而分类的顺序之前源码的注释中也提到过,和加载顺序有关。
我的顺序是BCA,所以A是最后被load,注释说明,最旧的会被第一个加载。
这里我的理解是,分类最终要把方法合并到主类的相关关联ro或者是rwe中的方法列表里,每次操作都是对分类里的实质内容进行操作,比如分类里扩展了属性,方法,协议等等,核心操作是把这些相关的联系到主类里。所以分类不会再进行排序。(不管是Person的分类A,还是它的B,或者C,通过load最后关联的结构都是写入到Person类的rwe对应下面的methodlist,方法查找的时候也是从类的methodList找,也没有查找单独分类的方法列表,分类的设计是为了类服务的,为了更好的程序设计,提供了更好的动态性)(要是理解错了希望各位大佬帮忙纠正😊)。
methodList的数据结构
前面分析流程的时候,简单的说了下大概的类型和结构,这里总结下。
- cats_list:locstamped_category_t
locstamped_category_t
struct locstamped_category_t {
category_t *cat;
struct header_info *hi;
};
category_t
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
WrappedPtr<method_list_t, PtrauthStrip> 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;
}
};
method_list_t
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
bool isUniqued() const;
bool isFixedUp() const;
void setFixedUp();
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t I =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
ASSERT(i < count);
return I;
}
bool isSmallList() const {
return flags() & method_t::smallMethodListFlag;
}
bool isExpectedSize() const {
if (isSmallList())
return entsize() == method_t::smallSize;
else
return entsize() == method_t::bigSize;
}
method_list_t *duplicate() const {
method_list_t *dup;
if (isSmallList()) {
dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
dup->entsizeAndFlags = method_t::bigSize;
} else {
dup = (method_list_t *)calloc(this->byteSize(), 1);
dup->entsizeAndFlags = this->entsizeAndFlags;
}
dup->count = this->count;
std::copy(begin(), end(), dup->begin());
return dup;
}
};
所以通过cats_list去遍历locstamped_category_t,把locstamped_category_t中的category_t拿到,进而拿到category_t中的method_list_t。
-
cats_list中装的是分类的信息,分类信息里面有method_list信息。
cats_list.jpg
分类和扩展
category(类别、分类)
- 专门用来给类添加方法。
- 不能给类添加成员属性,添加了成员变量,也无法取到。
- 可以通过runtime给分类添加属性。
- 分类中用@property定义变量,只会生成变量的geter & setter方法的声明,不能生成方法的实现和带下划线的成员变量。
extension(扩展)
- 编译时决定的
- 可以给类添加成员属性,但是是私有变量。
- 可以给类添加方法,也是私有方法。
- 类扩展必须与主类在一起,最好定义在主类.m中,单独定义最终还是要合并到主类.m中。(单独写然并卵)
通过xcrun查看extension的底层实现可以看到extension的内容与类的内容合并到了一起。通过调试去打印ro相关的方法,发现extension中的内容,早就被加到了ro中。在编译的时候就完成了。不会像分类那样在启动的时候再去添加到类的methodList了,所以extension不会影响类的加载。
关联对象
首先,我给分类A增加了一个叫aName的属性,然后在main函数中进行实例化主类LGPerson,然后通过实例对象L
进行aName的赋值或者进行get的读取,嗯都不行,就报错了报错了报错了!
说明通过分类添加的属性,没有set还有get方法!然后在分类A的实现中发现了这样的警告⚠️
Property 'aName' requires method 'setAName:' to be defined - use @dynamic or provide a method implementation in this category
image.png
aName需要一个方法'setAName:'定义,通过动态性或者在这个分类里提供一个方法实现。于是我就尝试像往常一样实现set方法,然后又报错了。
分类创建的属性没有带下划线的属性名。
然后去查了下资料,这种可以用关联属性
实现。
-(void)setAName:(NSString *)aName{
objc_setAssociatedObject(self, "aName", aName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)aName{
return objc_getAssociatedObject(self, "aName");
}
实现完毕这两个方法就可以正常给分类的属性赋值或者读取了。我们来看下关联属性
的底层实现。
objc_setAssociatedObject
我们跟着这个方法点进去看一下,看到objc_setAssociatedObject调用的是_object_set_associative_reference。
_object_set_associative_reference源码解读
// objc_setAssociatedObject(self, "aName", aName, OBJC_ASSOCIATION_COPY_NONATOMIC);
//object->self key->"aName" value->aName policy->OBJC_ASSOCIATION_COPY_NONATOMIC
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/44094390
//判断要设置的对象的还有值都存不存在,有一个不存在就直接返回不再往下执行了。
if (!object && !value) return;
//判断是不是有元类禁止了关联对象,如果有会打印出来类名
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类的对象 disguised
DisguisedPtr<objc_object> disguised{(objc_object *)object};
//生成ObjcAssociation类的对象association
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
//根据_policy的值来确定value的处理方式
/**acquireValue的实现
switch (_policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
_value = objc_retain(_value);
break;
case OBJC_ASSOCIATION_SETTER_COPY:
_value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
break;
*/
association.acquireValue();
bool isFirstAssociation = false;
{
//类AssociationsManager管理锁/哈希表单例对。
//分配实例将获得锁
//这里在AssociationsManager创建对象的时候加了锁,在AssociationsManager的对象销毁的时候解锁
AssociationsManager manager;
//获取关联对象表
AssociationsHashMap &associations(manager.get());
if (value) {
//然后来到try_emplace方法,这方法前面讲到了,前一个元素是从bucket中查到的结果,后面是查没查到 第二个值second为true的时候是新插入的
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
//此时已经为这个key创建了对应bucket但是还没有存值,通过后面打印可以看到是空的
isFirstAssociation = true;
}
/* establish or replace the association */
//开始创建或者替换association 再拿refs_result的first的second
auto &refs = refs_result.first->second;
//往对应的bucket中存值
auto result = refs.try_emplace(key, std::move(association));
//因为前面已经执行过了try_emplace,如果这一次执行,返回的result.second为false说明这个已经存在了
//所以要把存在的这个刚查到的result.first的值和内层的交换一下??(这个地方没有太看懂)
if (!result.second) {
association.swap(result.first->second);
}
} else {
//没有值的情况 进行清空处理
auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
auto &refs = refs_it->second;
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();
}
objc_getAssociatedObject
基于上面关于set的分析,_object_get_associative_reference就好看多了👌🏻!
_object_get_associative_reference源码解读:
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
ObjectAssociationMap &refs = i->second;
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
association = j->second;
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();
}
通过get方法,可以了解到这是个HashMap的嵌套,第一层是AssociationsHashMap,第二层是ObjectAssociationMap。
最后的结构大致是这样的:
AssociationsHashMap结构.png
今天去打羽毛球,然后回来看到大爷发的羽毛球动态:
贵有恒,何必三更起五更眠;
最无益,只怕一日曝十日寒。
大爷永远是大爷🐂🍺😄