iOS底层之类扩展和关联对象
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
在前面文章中,从底层源码探索了类和分类的加载流程,今天从源码层面探索一下类扩展及关联对象的本质。
类扩展
通过cpp文件查看类扩展
定义类LGStudent的属性方法,以及类扩展实现
@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod;
+ (void)classMethod;
@end
// 类扩展
@interface LGStudent ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, assign) int ext_age;
- (void)ext_instanceMethod;
+ (void)ext_classMethod;
@end
@implementation LGStudent
- (void)instanceMethod{
NSLog(@"%s", __func__ );
}
+ (void)classMethod{
NSLog(@"%s", __func__ );
}
- (void)ext_instanceMethod{
NSLog(@"%s", __func__ );
}
+ (void)ext_classMethod{
NSLog(@"%s", __func__ );
}
@end
通过clang然后生成cpp文件,查看类扩展属性
![]()
在类扩展中定义的属性
ext_name和ext_age,在cpp文件和主类的属性name和age一样处理生成带下划线的成员变量。
![]()
![]()
这里对比可以看到,
- 在成员列表
_ivar_list_t中是有_age、_ext_age、_name、_ext_name4个成员变量;- 在属性列表
_prop_list_t中只有name和age两个属性,说明在类扩展中的属性为私有;
看一下类扩展中的方法
![]()
类扩展中定义的实例方法
ext_instanceMethod以及属性ext_name和ext_age的setter和getter方法,都编译在主类实例方法中。
![]()
类扩展中定义的类方法
ext_classMethod和主类的类方法classMethod在编译在主类的类方法中。
通过类的加载查看类扩展
定义类LGPerson和类扩展LGPerson+LG
![]()
在《iOS底层之类的加载》中我们知道了
非懒加载类加载到内存是在启动时完成的。
为方便调试在主类实现+load方法,类扩展方法以ext开头,在LGperson的.m中不实现类扩展方法ext_saySomthing。
![]()
在methodizeClass方法中对LGPerson准确下断点,运行调试
![]()
先来到的为
LGPerson的元类,在从ro读取的method_list_t中,包含主类中的类方法+load和类扩展中的类方法ext_classMethod
过掉断点会再次进来,这次是对类LGPerson的加载
![]()
LGPerson类加载时,从
ro读取的method_list_t中,包含主类中的实例方法sayHello和sayByeBye,同时还有分类中的实例方法ext_instanceMethod,属性ext_name的setter和getter方法,并没有未实现的方法ext_saySomthing。
类扩展总结
- 类扩展可以给类添加成员属性,但是是私有变量;
- 类扩展可以给类添加方法,也是私有方法;
关联对象
分类的作用是给类添加新的方法,但是不能添加属性,即使添加了属性,也只会生成属性的setter和getter方法声明,不能生成方法实现和带下划线的成员变量。
我们可以通过Runtime给分类添加属性,就是通过关联对象的形式实现,今天从源码层面实例探究一下关联对象是如何实现的给分类添加属性的。
源码分析
关联对象存储
关联对象存储值时使用的api为objc_setAssociatedObject,参数有4个
object: 关联对象;key:标识符;value:关联值;policy:关联策略;
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
在objc_setAssociatedObject直接调用_object_set_associative_reference,这样做是维持最上层的api稳定,不管底层怎么更新,我们做关联对象使用的就是objc_setAssociatedObject
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.
// 关联对象为nil切关联值也为nil
if (!object && !value) return;
// object不允许关联对象,直接崩
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};
// 将value和policy包装成ObjcAssociation类型
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false; // 首次关联对象标识
{
AssociationsManager manager; // 实例化关联对象处理manager
AssociationsHashMap &associations(manager.get()); // 获取关联对象表
if (value) { // 关联值判断
// 从关联对象表中,根据disguised获取关联对象存储地址,没有就创建插入
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) { // 是否为第一次关联值
/* it's the first association we make */
isFirstAssociation = true;
}
/* establish or replace the association */
// 获取disguised的关联对象存储地址
auto &refs = refs_result.first->second;
// 以key-value的形式,存储association
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
// 如果之前存储过关联值,用之前的值first替换调value,后面将association释放
association.swap(result.first->second);
}
} else { // 关联值位nil,如果之前有key对应关联值,则清除
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()) { // 判断key对应的关联值,有的话就清除释放掉
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.
// 给关联对象object的isa中的has_assoc设置值true,表示有关联对象
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue(); // 如果有新值替换则释放旧值
}
在_object_set_associative_reference中的工作处理流程:
1.value有值
- 实例化全局关联表
associations;associations调用try_emplace将关联对象disguised作为key,获取disguised对应的关联对象表ObjectAssociationMap的地址,如果没有就创建一个ObjectAssociationMap,返回为refs_result;- 如果
refs_result.second有值,则disguised为第一次做关联值;- 通过
refs_result.first->second获取关联对象表的存储地址refs;refs调用try_emplace以标识符key存储关联值association,返回为result;- 如果
result.second为false,说明之前已经做过key对应的关联值,调用association.swap方法存储旧值地址存入association准备释放旧值;- 如果为第一次关联值,调用
setHasAssociatedObjects将isa的has_assoc设置为true,表示object有关联对象,在dealloc时将关联表释放。- 如果不是第一次关联,此时
association存储的是旧值,调用releaseHeldValue释放掉旧值
2.value为空值
- 如果关联值为空,获取
disguised的关联表,清除掉key对应的关联值,如果disguised关联表中没有关联值,则将disguised关联表释放。
关联对象取值
关联对象取值时使用的api为objc_getAssociatedObject,参数为关联对象object和标识符key
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
同样是一层封装过渡,直接调用_object_get_associative_reference
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
// 通过关联对象object获取关联表
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
// 获取关联表的地址
ObjectAssociationMap &refs = i->second;
// 通过key值获取关联值
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
association = j->second;
association.retainReturnedValue(); // retain处理
}
}
}
return association.autoreleaseReturnedValue(); // 返回value
}
- 通过关联对象object获取关联表;
- 获取关联表的地址,通过key值获取关联值
关联对象销毁
对象销毁时会调用dealloc方法
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
// TaggedPointer小对象直接返回
if (isTaggedPointer()) return; // fixme necessary?
/**
* nonpointer & !weakl & !has_*assoc & !has_cxx_dtor & !has_sidetable_rc
* nonpointer类型
* 无弱引用
* 无关联对象
* 无C++析构函数
* 无类的C++析构函数
* 无引用计数
*/
if (fastpath(isa().nonpointer &&
!isa().weakly_referenced &&
!isa().has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa().has_cxx_dtor &&
#else
!isa().getClass(**false**)->hasCxxDtor() &&
#endif
!isa().has_sidetable_rc))
{
assert(!sidetable_present());
free(this);// 直接释放
}
else {
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
// 获取是否有关联对象
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
// 如果有关联对象则移除关联对象
if (assoc) _object_remove_associations(obj, /*deallocating*/true);
obj->clearDeallocating();
}
return obj;
}
inline bool
objc_object::hasAssociatedObjects()
{
if (isTaggedPointer()) return true;
if (isa().nonpointer) return isa().has_assoc;
return true;
}
dealloc中调用_objc_rootDealloc;_objc_rootDealloc中调用对象的rootDealloc方法;rootDealloc中判断对象为TaggedPointer不用处理;- 不是
TaggedPointer,如果为nonpointer指针且无弱引用、无关联对象、无C++析构函数、无类的C++析构函数、无引用计数则直接free,否则就调用object_dispose;- 在
object_dispose中先调用objc_destructInstance,然后再free;- 在
objc_destructInstance中根据hasAssociatedObjects判断有无关联对象,如果有就调用_object_remove_associations移除关联对象;- 在
hasAssociatedObjects中nonpointer类型的isa,根据值has_assoc判断有无关联对象;
看一下_object_remove_associations的源码是怎么移除关联对象的
void
_object_remove_associations(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.
bool didReInsert = false;
if (!deallocating) { // deallocating值为true
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.
if (deallocating)
laterRefs.append(&i.second);
} else {
i.second.releaseHeldValue();
}
}
for (auto *later: laterRefs) {
later->releaseHeldValue();
}
}
- 全局关联表
associations通过对象object获取关联对象表对应的表i; - 获取表地址空间
refs; -
for循环取出所有的关联值,先清除释放策略不是OBJC_ASSOCIATION_SYSTEM_OBJECT,策略为OBJC_ASSOCIATION_SYSTEM_OBJECT放入laterRefs; - 再对
laterRefs循环清除。
实例验证
定义主类LGPerson和分类LGPerson+CatA,在分类中设置属性cate_name,并在分类中实现cate_name的setter和getter方法
- (void)setCate_name:(NSString *)cate_name{
/**对象,标识符,value,策略* */
objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)cate_name{
return objc_getAssociatedObject(self, "cate_name");
}
在mian函数中给cate_name赋值
在_object_set_associative_reference中加断点调试
第一次关联值
![]()
- 第一次关联值为
Hello cat!,这里isFirstAssociation值为true- 此时
refs还没有调用try_emplace,Buckets的值为nil;
继续Step Over一下
![]()
这里
refs的存储了值,result.second为true说明第一次关联不需要association.swap
在setHasAssociatedObjects打断点,进入调试
![]()
这里对
newisa的has_assoc赋值为true。
在releaseHeldValue打断点,进入调试
![]()
这里
_value为nil,且_policy为0。
第二次关联值
放掉断点,进入第二次关联值流程,断点来到refs.try_emplace处
![]()
- 第二次关联值为
Hello dog!,这里isFirstAssociation值为false;refs的存储了值,Buckets的值为关联值的地址;
继续Step Over一下
![]()
![]()
在
refs.try_emplace后result.second的值为false,进入swap方法,association的存储值为上一次关联的旧值。
在releaseHeldValue打断点,进入调试
![]()
这里要释放掉的
_value为上一次关联的Hello cat!。
关联值值nil
过掉断点,进入关联值为nil的流程,断点到erase处
![]()
- 这里
value为nil;association.swap的值为second,_value为Hello dog!,最后存储的关联值,准备释放;refs.size的值为1,有一个关联值;
继续Step Over一下,过掉erase
![]()
refs.size的值为0,没有关联值,调用associations的erase方法释放关联对象表refs_it。
关联对象取值
注释掉关联对象赋值为nil,打印cate_name
![]()
在_object_get_associative_reference加断点调试
这里取到的值为ObjectAssociationMap的second最新值为Hello dog!。
关联对象释放
走完main的作用域,person就会进入dealloc流程
在dealloc中根据字符串匹配LGPerson精准下断点,然后进入rootDealloc
这里通过isa()输出,has_assoc值为1,进入object_dispose流程,在_object_remove_associations打断点
看到这里object为LGPerson对象,关联值有1个
输出值为Hello dog!。
关联对象总结
关联对象的存储结构为
- 总哈希表
AssociationsHashMap,存储的是关联对象和关联对象对应的关联表ObjectAssociationMap;- 关联表
ObjectAssociationMap中的Buckets为表的地址值;- 关联表中存储的是标识符
key,和将value与policy包装在一起的ObjcAssociation。
![]()
iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
以上是对类扩展和关联对象的探索总结,难免有不足和错误之处,如有疑问请在评论区留言。