前面两篇文章类的原理(上)和类的原理(中)我们探索了类的属性、成员变量、类方法、对象方法存储位置,大概了解了类的一些原理,那么这篇文章我们去探索下,属性、成员变量、实例变量之间的关系?系统是如何给属性赋值的?
属性和成员变量的关系
1、属性 = 成员变量 + setter
+ getter
- 例如:@property(nonatomic,copy) NSString *name;这里的属性
name
就等于_name
(成员变量)+-(void)setName:(NSString *)name
+-(NSString *)name
。
2、成员变量系统不会主动为我们生成setter
和getter
方法。
3、实例变量是成员变量的一种,比如:NSObject *_obj
这里的_obj
就是实例变量,实例变量是指,除却基本数据类型以外的成员变量。
总结出来属性、成员变量、实例变量之间的关系如下图:
类的属性
1、这节我们探索类的属性的存储以及取值,我们创建一个继承于NSObject
的类KGPerson
,然后里面声明一些成员变量以及属性,具体代码如下:
2、然后我们将KGPerson.m
文件通过clang -rewrite-objc KGPerson.m -o KGPerson.cpp
编译成.cpp
文件,然后打开文件全局搜索KGPerson
,最后我们能够找到如下代码:
3、我们可以看到系统将属性在底层编译成了成员变量,同时会编译extern "C" unsigned long OBJC_IVAR_$_KGPerson$_属性名;
这种代码,当使用extern
关键词搭配"C"
时告诉编译器使用OBJC_IVAR_$_KGPerson$_属性名
时可以到其它的文件去查找声明,因为我们在函数中使用变量时必须保证变量已经声明,而且声明在头文件内,外部访问不到,所以用extern
修饰的话就可以从声明出开始到文件结束都可以使用了。
4、然我们可以看到后面紧跟的就是类的属性的setter
和getter
,先直接上代码如图:
5、我们能够发现都是成对出现的属性的setter
和getter
,但是其中有个细节点,同样是NSString
类型的属性,但是有的是使用位移去赋值取值的,但是有的是用objc_setProperty
去赋值取值的,所以这块就出来问题了,什么情况下回出现这种情况呢?是什么因素影响的呢?下面我们修改下KGPerson
类,代码如下:
6、然后我们通过clang
编译成.cpp
文件看下具体效果:
7、通过上面我们基本上可以得出以下结论:
是否原子性 | 修饰符 | 结果 |
---|---|---|
nonatomic | strong | 内存平移 |
nonatomic | copy | objc_setProperty |
nonatomic | retain | objc_setProperty |
nonatomic | readonly | 内存平移 |
nonatomic | readwrite | 内存平移 |
nonatomic | assign | 内存平移 |
atomic | strong | 内存平移 |
atomic | copy | objc_setProperty |
atomic | retain | objc_setProperty |
atomic | readonly | 内存平移 |
atomic | readwrite | 内存平移 |
atomic | assign | 内存平移 |
8、然后我们在LLVM
源码中去查看,我们能够搜到如下代码:
9、从上图我们能够看到调用objc_setProperty
方法的函数是在llvm::FunctionCallee getSetPropertyFn()
中,那么我们继续查找在哪调用的这个函数,最后能够找到如下函数:
10、然后我们继续去查找GetPropertySetFunction
函数在哪调用的,最后我们搜索到如下一段代码:
11、然后这段代码主要是通过case
分支进行判断,然后我们重点看下PropertyImplStrategy
,因为都是围绕它进行的条件判断,然后我们进行搜索后,能够看到如下定义:
12、在以上代码中系统明确说明,如果使用copy
属性,我们必须使用getProperty/setProperty
。那么到此我们再次证明了上面整理的表格内容,对于属性的探索也是到此为止。
扩展-编码
1、我们在编译得到的KGPerson.cpp
文件中能够看到如下类型的代码,看着比较熟悉,但是又有一些不认识的,如下:
2、在上图中我们能够看到{{(struct objc_selector *)"sayHello", "v16@0:8", (void *)_I_KGPerson_sayHello}
这种objc_selector *
类型的_cmd
,前半部分我们能够看出来是方法,但是后面v16@0:8
代表什么意思呢?下面我们通过一张图来解释:
3、具体这写符号代表什么意思我们可以在类型编码中找到相应的解释。
属性赋值
1、我们都知道,给属性赋值的方式比较多,如下图:
2、然后我们看看系统是如何进行赋值操作的呢?下面我们通过clang -rewrite-objc main.m -o main.cpp
将main.m
转成main.cpp
文件,去看下我们这段代码系统是如何处理的,结果如下:
3、我们可以通过main.cpp
文件看出来,无论是使用person.name = @"KG"
方式还是[person setHomeTown:@"China"]
或者是[person setValue:@"? ? ? ? ? ?" forKey:@"nikeName"]
方式去给类的属性或者成员变量赋值,系统都会给我们编译成objc_msgSend(person, sel_registerName("setName:"), @"KG")
这种形式,通过objc_msgSend
进行消息发送。
4、进行消息发送前,我们能看到先调用了一个sel_registerName
函数,然后我们在objc
源码中进行全局查找,最后找到如下函数实现:
SEL sel_registerName(const char *name) {
// 在这里传入的两个1,表示为了操作安全已经进行了加锁,而且进行了拷贝操作
return __sel_registerName(name, 1, 1);
}
复制代码
5、然后继续走流程能够找到__sel_registerName
函数的实现,具体实现如下:
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);
// 只有第一次走的时候it.second为true,否则都是false
if (it.second) {
// 没有找到唯一匹配的缓存,进行插入操作
*it.first = (const char *)sel_alloc(name, copy);
}
return (SEL)*it.first;
}
复制代码
6、在上述函数中search_builtins
函数是进行方法查找,然后会调用dyld
的_dyld_get_objc_selector
函数进行查找,如果查找失败直接返回nil
,_dyld_get_objc_selector
函数在objc
源码中解释为只被objc调用来查看dyld是否有唯一的这个选择器。如果dyld具有惟一的值,则返回该值;如果没有惟一的值,则返回nullptr。注意,这个函数必须在_dyld_objc_notify_register后面调用.
7、然后重点看下sel_alloc
函数,看函数命名大概就能看出来函数的作用:为SEL
开辟内存。为什么这么说呢,因为每个类对象它的缓存中开始是没有缓存方法也就是SEL
和IMP
对应关系的缓存,所以系统并没有开辟方法缓存,所以才会第一次走的时候去开始内存,进行存储。接下来我们看下sel_alloc
函数的具体实现:
static SEL sel_alloc(const char *name, bool copy)
{
selLock.assertLocked();
// 因为从开始这个函数传入的copy是true,所以这块会走strdupIfMutable
return (SEL)(copy ? strdupIfMutable(name) : name);
}
复制代码
8、然后我们能够看到系统在这里做了一个三元运算,条件是copy
,我们跟着流程走下来都明白,开始在sel_registerName
函数中传入的copy
是true
,所以我们直接进入函数strdupIfMutable
的实现:
// Strdup不复制只读内存
static inline char *
strdupIfMutable(const char *str)
{
// 在原有内存的大小基础上进行添加
size_t size = strlen(str) + 1;
// 判断是否需要开辟新的内存空间去存储
if (_dyld_is_memory_immutable(str, size)) {
return (char *)str;
} else {
// 开辟内存
return (char *)memdup(str, size);
}
}
复制代码
9、这个函数中走了一个条件分支,我们先看下判断条件_dyld_is_memory_immutable
函数,这个函数在objc
源码中只有定义,没有具体实现,但是定义的时候给我们一个非常重要的注释:Returns if the specified address range is in a dyld owned memory that is mapped read-only and will never be unloaded.
,意思就是如果在给定的地址指向的内存空间中拥有可以继续使用的内存,那么直接返回改地址,而且这个内存映射是只读永远不会被释放。也就是说只要内存足够继续返回这块内存空间地址。然后我们接下来看下memdup
函数的实现:
static inline void *
memdup(const void *mem, size_t len)
{
// 根据需要的大小开辟新的内存空间,获取地址
void *dup = malloc(len);
// 将旧地址mem指向的内存空间的起始位置开始拷贝指定大小字节到目标地址dup指向的空间中
memcpy(dup, mem, len);
// 返回新的内存地址
return dup;
}
复制代码
10、通过以上的观察,我们能够看到objc
在赋值时是通过msg_send
进行消息发送调用setter
方法。
11、然后我们修改一下main.m
代码如下:
12、然后通过clang -rewrite-objc main.m -o main.cpp
转换,我们能够看到取值也是走的msg_send
,而且也调用了sel_registerName
函数。
总结
探索中跳的比较快,所以文章看起来也比较乱,但是大概探索的结果是明确的,我们得到了属性赋值取值的差别。