前言
上一篇分类的加载,我们分析了分类的加载,以及类的被迫营业,即如果主类没有实现load()方法而分类实现了load(),编译时会把主类强制优化成非懒加载类同时把分类优化掉。我们知道类中添加属性,在编译时会生成set、get方法,但是在分类中添加的属性,编译时并不会生成set、get方法,那么分类中的属性该如何存储呢?下面就分析下类的关联对象以及类的扩展。
类扩展 Extension
- 可以给类添加成员属性,但是是
私有变量 - 可以给类添加方法,也是
私有方法
其实我们一直在用类扩展,比如在.m文件中
@interface ViewController ()
@property(nonatomic,strong)UIWindow* __nullable newwindow;
@end
@implementation ViewController
-(void)viewDidAppear:(BOOL)animated{
}
-(void)viewWillDisappear:(BOOL)animated{
}
属性newwindow就是我们的扩展属性,注意扩展必须是放在声明之后,实现之前。我们新建一个类的扩展只是一个.h文件,编译时会被合并进.m中。我们写个demo通过clang编译验证下。
@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
在main.m中写了一个LGStudent的扩展,定义了属性ext_name和ext_age,以及方法ext_instanceMethod和ext_classMethod,clang看一下编译后的情况main.cpp文件
struct LGStudent_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _ext_age;
NSString *_name;
NSString *_ext_name;
};
static void _C_LGStudent_ext_classMethod(Class self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_40_l7v4nh0543xbbd0lfg8dy5g00000gn_T_main_50c04f_mi_3,__func__);
}
static NSString * _I_LGStudent_name(LGStudent * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGStudent$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGStudent_setName_(LGStudent * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGStudent, _name), (id)name, 0, 1); }
static int _I_LGStudent_age(LGStudent * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_age)); }
static void _I_LGStudent_setAge_(LGStudent * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_age)) = age; }
static NSString * _I_LGStudent_ext_name(LGStudent * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGStudent$_ext_name)); }
static void _I_LGStudent_setExt_name_(LGStudent * self, SEL _cmd, NSString *ext_name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGStudent, _ext_name), (id)ext_name, 0, 1); }
static int _I_LGStudent_ext_age(LGStudent * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_ext_age)); }
static void _I_LGStudent_setExt_age_(LGStudent * self, SEL _cmd, int ext_age) { (*(int *)((char *)self + OBJC_IVAR_$_LGStudent$_ext_age)) = ext_age; }
我们看到在编译时类的扩展属性以及方法就被合并到了主类中,所以类的扩展不会像分类那样影响类的加载。
关联对象
我们知道如果在分类中添加属性以及读写属性,必须使用关联对象,分类中的属性是动态添加的所以没有get和set方法,那么关联对象是如何读写属性的呢?我们先用关联对象实现一下分类中属性的读写。
@interface LGPerson (wgy)
@property(nonatomic,copy)NSString* wgyname;
@end
@implementation LGPerson (wgy)
-(void)setWgyname:(NSString *)wgyname{
objc_setAssociatedObject(self, "wgyname", wgyname, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)wgyname{
return objc_getAssociatedObject(self, "wgyname");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson * person = [LGPerson alloc];
person.wgyname=@"NB";
}
return 0;
}
分析:添加一个LGPerson分类,在分类中添加属性wgyname,通过关联方法objc_setAssociatedObject实现属性的存储,通过objc_getAssociatedObject实现属性的读取。下面看一下objc_setAssociatedObject源码是如何实现属性值的存储的,跟进objc_setAssociatedObject源码发现调用了_object_set_associative_reference方法。
_object_set_associative_reference存储值
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
if (!object && !value) return;
if (object->getIsa()->forbidsAssociatedObjects())
//把object构造成一个统一的对象disguised,
DisguisedPtr<objc_object> disguised{(objc_object *)object};
//把value构造成统一的对象
ObjcAssociation association{policy, value};
association.acquireValue();
bool isFirstAssociation = false;
{ //AssociationsManager线程安全获取和释放锁
//通过构造函数获取锁,跳出作用域通过析构函数释放锁
AssociationsManager manager;
//获取关联对象hash表的单例
AssociationsHashMap &associations(manager.get());
if (value) {
//根据对象查找对象关联表,如果存在就返回,如果没有就创建对象关联表
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
isFirstAssociation = true;
}
//在对象关联表中,根据key查找属性关联表,如果找到就返回,没找到就创建,并且把value赋值
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
association.swap(result.first->second);
}
} else {//如果value为空,就erase移除关联属性
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);
}
}
}
}
}
if (isFirstAssociation)
//对象标记为存在关联对象,这个很重要,对象释放的时候会根据这个标记状态释放关联对象
object->setHasAssociatedObjects();
//release旧值
association.releaseHeldValue();
}
分析:
- 把对象和value
构造成统一的对象,便于hash存储。 - 通过
AssociationsManager获取关联表单例,为了线程安全通过构造函数获取和释放锁 - 如果新值value不为空,根据对象查找
对象关联表,如果没有就新建对象关联表;在对象关联表中,根据key查找属性关联表,如果找到就返回,没找到就创建,并且把value赋值。 - 如果新值
value为空,根据key移除关联关系。 - 第一次的时候把
对象标记为存在关联对象,这个很重要,在对象释放的时候会根据这个标记状态释放关联对象表
通过上面的分析首先关联表是一个双层hash表,一个是通过对象查找的对象关联表,一个是在对象中通过key查找的属性关联表,try_emplace调用了两次,第一次是操作的对象disguised,第二次是操作的属性key。我们再来看下AssociationsManager对象以及是try_emplace是如何存储value的。
AssociationsManager
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();
}
};
分析:
AssociationsManager本身只是一个对象而非单例,只是提供一个全局静态变量_mapStorage去获取一张关联表。- 在AssociationsManager作用域内获取锁,出了作用域
析构函数释放锁
try_emplace
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
//根据key去查找对应的Bucket
if (LookupBucketFor(Key, TheBucket))
//如果找到,false表示不是第一次插入关联表
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// 如果没有查询到 将数据插入bucket中,返回bucket
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
//通过 make_pair生成相应的键值对,true表示第一次往哈希关联表中添加bucket
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
分析:
- 根据key查找对应的
bucket,如果找到了就封装返回bucket - 如果没有,就
新建空的bucket,然后将数据存在bucket中 - bucket使我们联想到探索类时cache中的bucket,类中的bucket存储的是SEL和IMP
第一次调用try_emplace,key是对象,目的是在关联表中查找对象,如果找不到就插入对象;第二次调用try_emplace,key是属性,目的是在关联表中查找属性如果找不到就插入属性
关联对象存储过程双hash表图
总结:
- 编译时类的扩展属性以及方法就被合并到了主类中,所以类的扩展不会像分类那样影响类的加载
分类中属性的读写是通过一张全局的关联表维护的,如果类释放了,关联表中相对应的关联关系也释放。