刨根问底Objective-C Runtime(4)- 成员变量与属性
2015-07-22 18:53
531 查看
刨根问底Objective-C Runtime(4)- 成员变量与属性
上一篇笔记讲述了objc runtime中消息和Category的细节,本篇笔记主要是讲述objc runtime的成员变量和
属性。
习题内容
下面代码会? Compile Error / Runtime Crash / NSLog…?[code]@interface Sark : NSObject @property (nonatomic, copy) NSString *name; @end @implementation Sark - (void)speak { NSLog(@"my name is %@", self.name); } @end @interface Test : NSObject @end @implementation Test - (instancetype)init { self = [super init]; if (self) { id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak]; } return self; } @end int main(int argc, const char * argv[]) { @autoreleasepool { [[Test alloc] init]; } return 0; }
答案:代码正常输出,输出结果为:
[code]2014-11-07 14:08:25.698 Test[1097:57255] my name is <Test: 0x1001002d0>
为什么呢?
前几节博文中多次讲到了objc_class结构体,今天我们再拿出来看一下:
[code]struct objc_class { Class isa OBJC_ISA_***AILABILITY; #if !__OBJC2__ Class super_class OBJC2_UN***AILABLE; const char *name OBJC2_UN***AILABLE; long version OBJC2_UN***AILABLE; long info OBJC2_UN***AILABLE; long instance_size OBJC2_UN***AILABLE; struct objc_ivar_list *ivars OBJC2_UN***AILABLE; struct objc_method_list **methodLists OBJC2_UN***AILABLE; struct objc_cache *cache OBJC2_UN***AILABLE; struct objc_protocol_list *protocols OBJC2_UN***AILABLE; #endif } OBJC2_UN***AILABLE;
其中
objc_ivar_list结构体存储着
objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息。
那么什么是Ivar呢?
Ivar在objc中被定义为:
[code]typedef struct objc_ivar *Ivar;
它是一个指向objc_ivar结构体的指针,结构体有如下定义:
[code]struct objc_ivar { char *ivar_name OBJC2_UN***AILABLE; char *ivar_type OBJC2_UN***AILABLE; int ivar_offset OBJC2_UN***AILABLE; #ifdef __LP64__ int space OBJC2_UN***AILABLE; #endif } OBJC2_UN***AILABLE;
这里我们注意第三个成员
ivar_offset。它表示基地址偏移字节。
在编译我们的类时,编译器生成了一个
ivar布局,显示了在类中从哪可以访问我们的 ivars 。看下图:
上图中,左侧的数据就是地址偏移字节,我们对 ivar 的访问就可以通过
对象地址 + ivar偏移字节的方法。但是这又引发一个问题,看下图:
我们增加了父类的ivar,这个时候布局就出错了,我们就不得不重新编译子类来恢复兼容性。
而Objective-C Runtime中使用了
Non Fragile ivars,看下图:
使用Non Fragile ivars时,Runtime会进行检测来调整类中新增的ivar的偏移量。 这样我们就可以通过
对象地址 + 基类大小 + ivar偏移字节的方法来计算出ivar相应的地址,并访问到相应的ivar。
我们来看一个例子:
[code]@interface Student : NSObject { @private NSInteger age; } @end @implementation Student - (NSString *)description { return [NSString stringWithFormat:@"age = %d", age]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Student *student = [[Student alloc] init]; student->age = 24; } return 0; }
上述代码,Student有两个被标记为private的ivar,这个时候当我们使用
->访问时,编译器会报错。那么我们如何设置一个被标记为private的ivar的值呢?
通过上面的描述,我们知道ivar是通过计算字节偏量来确定地址,并访问的。我们可以改成这样:
[code]@interface Student : NSObject { @private int age; } @end @implementation Student - (NSString *)description { NSLog(@"current pointer = %p", self); NSLog(@"age pointer = %p", &age); return [NSString stringWithFormat:@"age = %d", age]; } @end int main(int argc, const char * argv[]) { @autoreleasepool { Student *student = [[Student alloc] init]; Ivar age_ivar = class_getInstanceVariable(object_getClass(student), "age"); int *age_pointer = (int *)((__bridge void *)(student) + ivar_getOffset(age_ivar)); NSLog(@"age ivar offset = %td", ivar_getOffset(age_ivar)); *age_pointer = 10; NSLog(@"%@", student); } return 0; }
上述代码的输出结果为:
[code]2014-11-08 18:24:38.892 Test[4143:466864] age ivar offset = 8 2014-11-08 18:24:38.893 Test[4143:466864] current pointer = 0x1001002d0 2014-11-08 18:24:38.893 Test[4143:466864] age pointer = 0x1001002d8 2014-11-08 18:24:38.894 Test[4143:466864] age = 10
我们可以清晰的看到指针地址的变化和偏移量,和我们上述描述一致。
说完了Ivar, 那Property又是怎么样的呢?
使用
clang -rewrite-objc main.m重写题目中的代码,我们发现
Sark类中的
name属性被转换成了如下代码:
[code]struct Sark_IMPL { struct NSObject_IMPL NSObject_IVARS; NSString *_name; }; // @property (nonatomic, copy) NSString *name; /* @end */ // @implementation Sark static NSString * _I_Sark_name(Sark * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Sark$_name)); } static void _I_Sark_setName_(Sark * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Sark, _name), (id)name, 0, 1); }
类中的Property属性被编译器转换成了
Ivar,并且自动添加了我们熟悉的
Set和
Get方法。
我们这个时候回头看一下
objc_class结构体中的内容,并没有发现用来专门记录Property的list。我们翻开objc源代码,在objc-runtime-new.h中,发现最终还是会通过在
class_ro_t结构体中使用
property_list_t存储对应的propertyies。
而在刚刚重写的代码中,我们可以找到这个
property_list_t:
[code]static struct /*_prop_list_t*/ { unsigned int entsize; // sizeof(struct _prop_t) unsigned int count_of_properties; struct _prop_t prop_list[1]; } _OBJC_$_PROP_LIST_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_prop_t), 1, name }; static struct _class_ro_t _OBJC_CLASS_RO_$_Sark __attribute__ ((used, section ("__DATA,__objc_const"))) = { 0, __OFFSETOFIVAR__(struct Sark, _name), sizeof(struct Sark_IMPL), (unsigned int)0, 0, "Sark", (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Sark, 0, (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Sark, 0, (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Sark, };
解惑
1)为什么能够正常运行,并调用到speak方法?
[code]id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak];
obj被转换成了一个指向Sark Class的指针,然后使用
id转换成了
objc_object类型。这个时候的
obj已经相当于一个Sark的实例对象(但是和使用[Sark
new]生成的对象还是不一样的),我们回想下Runtime的第二篇博文中
objc_object结构体的构成就是一个指向Class的isa指针。
这个时候我们再回想下上一篇博文中
objc_msgSend的工作流程,在代码中的obj指向的Sark
Class中能够找到
speak方法,所以代码能够正常运行。
2) 为什么
self.name的输出为
<Test: 0x1001002d0>?
我们在测试代码中加入一些调试代码和Log如下:
[code]- (void)speak { unsigned int numberOfIvars = 0; Ivar *ivars = class_copyIvarList([self class], &numberOfIvars); for(const Ivar *p = ivars; p < ivars+numberOfIvars; p++) { Ivar const ivar = *p; ptrdiff_t offset = ivar_getOffset(ivar); const char *name = ivar_getName(ivar); NSLog(@"Sark ivar name = %s, offset = %td", name, offset); } NSLog(@"my name is %p", &_name); NSLog(@"my name is %@", *(&_name)); } @implementation Test - (instancetype)init { self = [super init]; if (self) { NSLog(@"Test instance = %@", self); void *self2 = (__bridge void *)self; NSLog(@"Test instance pointer = %p", &self2); id cls = [Sark class]; NSLog(@"Class instance address = %p", cls); void *obj = &cls; NSLog(@"Void *obj = %@", obj); [(__bridge id)obj speak]; } return self; } @end
输出结果如下:
[code]2014-11-11 00:56:02.464 Test[10475:1071029] Test instance = <Test: 0x10010fb60> 2014-11-11 00:56:02.464 Test[10475:1071029] Test instance pointer = 0x7fff5fbff7c8 2014-11-11 00:56:02.465 Test[10475:1071029] Class instance address = 0x1000023c8 2014-11-11 00:56:02.465 Test[10475:1071029] Void *obj = <Sark: 0x7fff5fbff7c0> 2014-11-11 00:56:02.465 Test[10475:1071029] Sark ivar name = _name, offset = 8 2014-11-11 00:56:02.465 Test[10475:1071029] my name is 0x7fff5fbff7c8 2014-11-11 00:56:02.465 Test[10475:1071029] my name is <Test: 0x10010fb60>
Sark中Property
name最终被转换成了Ivar加入到了类的结构中,Runtime通过计算成员变量的地址偏移来寻找最终Ivar的地址,我们通过上述输出结果,可以看到
Sark的对象指针地址加上Ivar的偏移量之后刚好指向的是Test对象指针地址。
这里的原因主要是因为在C中,局部变量是存储到内存的栈区,程序运行时栈的生长规律是从地址高到地址低。C语言到头来讲是一个顺序运行的语言,随着程序运行,栈中的地址依次往下走。
看下图,可以清楚的展示整个计算的过程:
我们可以做一个另外的实验,把
TestClass
的
init方法改为如下代码:
[code]@interface Father : NSObject @end @implementation Father @end @implementation Test - (instancetype)init { self = [super init]; if (self) { NSLog(@"Test instance = %@", self); id fatherCls = [Father class]; void *father; father = (void *)&fatherCls; id cls = [Sark class]; void *obj; obj = (void *)&cls; [(__bridge id)obj speak]; } return self; } @end
你会发现这个时候的输出变成了:
[code]2014-11-08 21:40:36.724 Test[4845:543231] Test instance = <Test: 0x10010fb60> 2014-11-08 21:40:36.725 Test[4845:543231] ivar name = _name, offset = 8 2014-11-08 21:40:36.726 Test[4845:543231] Sark instance = 0x7fff5fbff7b8 2014-11-08 21:40:36.726 Test[4845:543231] my name is 0x7fff5fbff7c0 2014-11-08 21:40:36.726 Test[4845:543231] my name is <Father: 0x7fff5fbff7c8>
关于C语言内存分配和使用的问题可参考这篇文章 http://www.th7.cn/Program/c/201212/114923.shtml。
本文是关于Objective C Runtime的学习笔记。有不对的地方,欢迎大家指正。
感谢@唐巧_boy和@sunnyxx分享题目。
本文由@Chun发表于Chun
Tips .
版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
分享到:2
Posted by Chun Ye Nov 8th, 2014 1:52
pm objective-c runtime
黑幕背后的__block修饰符 »
Comments
最新最早最热3条评论
Pavel1986
请问博主,2014-11-08 18:24:38.892 Test[4143:466864] age ivar offset = 8
这age的offset为什么是8,我理解Student对象的前边就一个isa,之后就是age,这样理解age的offset是4。有点不解,望博主赐教。
2014年12月4日回复顶转发
Chun Ye
回复 Pavel1986: hi,你好,因为文中Ivar一节中配图所标示的运行环境是 32Bit,此时的offset == 4, 而我的测试代码中的运行环境是 64Bit,此时的offset == 8
2014年12月5日回复顶转发
乙
请问下,我改装了下,我把int换成NSString,就报指针错误了哇?
5月26日回复顶转发
相关文章推荐
- [学习笔记—Objective-C]《Objective-C-基础教程 第2版》第二章~第七章
- 刨根问底Objective-C Runtime(3)- 消息 和 Category
- 刨根问底Objective-C Runtime(2)- Object & Class & Meta Class
- 刨根问底Objective-C Runtime(1)- Self & Super
- NSObject 的实现分析
- Associative机制使用场景[objective-c有两个扩展机制:category扩展方法和associative扩展属性]
- 关于pch文件
- Objective-C GCC Code Block Evaluation C Extension ({…})语法
- object-c 知识点01
- 【iOS开发系列】NSObject方法介绍
- OC学习笔记之self关键字
- Objective-C学习之旅 第二篇
- OC学习笔记之description
- OC学习笔记之多态
- Objective-C学习笔记(二)——OC基本语法概述
- OC学习之类的进阶
- JSONObject与JSONArray的使用
- 2015年Objective-C有哪些新功能?
- Objective-C学习笔记(一)——OC语言的特点
- Objective-C中应用断言_assert()