前言
上一篇文章 我们对 方法的本质
有了初步了解。还记的上一篇文章我们留的两个问题吗? 问题一:TPerson
中的 ivar
存储在哪里呢? 问题二:TPerson
中的 类方法
存储在哪里呢? 今天我们通过这边文章再对 方法的本质
做进一步的学习和探究。
类的内存数据 — or数据
一、类
首先我们先来看一段 视频 ,苹果官方对内存的优化。数据结构的变化 Objective-C 运行时会使用它们来追踪类
、Objective-C 方法列表变化
、Tagged pointer 格式的变化
。
可以由以下几张图简单描述。
1. 类结构(类对象本身)
类对象本身包含了最常被访问的信息指向元类
、超类
、方法缓存的指针
还有一个指向更多数据的指针。
它包括 像类名称
、方法
、协议
、实例变量
的信息。Swift
类和 Objective-C
类共享这一基本数据,所以每个 Swift
类也有这些基本数据结构。
类的结构图
2. 类第一次从磁盘加载
当类第一次从磁盘中加载到内存中时,它们一开始也是这样的,但是一经使用,它们就会发生变化。在了解这些变化之前,我们先了解一下 clean memory
和 dirty memory
的区别。
clean memory
clean memory
是指加载后不会发生更改的内存。class_ro_t
就属于 clean memory
,因为它是只读的。
dirty memory
dirty memory
是指在进程运行时会发生更改的内存,类结构一经使用就会变成 dirty memory
,因为运行时会向它写入新的数据,例如:创建一个新的方法缓存并从类中指向
它。
小结
dirty memory
比 clean memory
要昂贵得多,只要进程在运行,它就必须一直存在。另一方面 clean memory
可以进行移除从而节省更多的内存空间。因为如果你需要 clean memory
系统可以从磁盘中重新加载。macOS
可以选择换出 dirty memory
,但因为 iOS
不使用 swap
,所以 dirty memory
在 iOS 中代价很大。dirty memory
是这个类数据被分成两部分的原因,可以保持清洁的数据越来越好。通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为 clean memory
。
3. 类第⼀次被使⽤时的结构(runtime)
当类首次被使用时,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是 class_rw_t
用于 读取-编写数据
。在这个数据结构中,我们存储了只有再运行时才会生成的新信息。例如:所有的类都会链接成一个树状结构。这是通过使用 First Subclass
和 Next Sibling Class
指针实现的。这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。当 category
被加载时,它可以向类中添加新的方法。而且程序员可以使用运行时 API
动态地添加它们。因为 class_ro_t
是只读
的,所以我们需要在 class_rw_t
中追踪这些东西。
4. 将需要动态更新的部分提取出来,存⼊class_rw_ext_t
我们可以拆掉那些平时不用的部分,这将 class_rw_t
的大小减少了一半,对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个并把它滑到类中供其他使用。
5. 类的整体结构
大约 90%
的类从来不需要这些扩展数据,这些内存可以用于更有效的用途。比如:存储 APP
的数据
。
二、类的成员变量和属性以及编码
1. 成员变量和属性的区别
{}
中的是成员变量
,用 @property
描述的是 属性
。NSObject * objc
属于 实例变量
,是 成员变量
中的一种。实例变量
:是一个 对象类型
的变量。
// 空号中的是成员变量
@interface TPerson : NSObject {
NSString * desc;
NSObject * objc; // 属于 实例变量
}
// 用 @property 描述的是 属性
@property (nonatomic, copy) NSString * name;
@property (nonatomic, retain) NSString * nickName;
- (void)formatPerson;
+ (void)personNickName;
@end
复制代码
2. 成员变量和属性的分析和探索
对象的探索
在 OC-对象的本质 这篇文章中有讲解到如何使用 clang
进行探索。
发现属性
在底层 C++
里面全部都没有了。取而代之的是在成员变量
中生成了带 _
的,但是它和成员变量的区别在于它还生成了响应 set
、get
方法。
也可以通过以下代码进行区分辨别:
void lgObjc_copyIvar_copyProperies(Class pClass){
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
//获取实例变量名
const char*cName = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:cName];
LGLog(@"class_copyIvarList:%@",ivarName);
}
free(ivars);
unsigned int pCount = 0;
objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
for (unsigned int i=0; i < pCount; i++) {
objc_property_t const property = properties[i];
//获取属性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
//获取属性值
LGLog(@"class_copyProperiesList:%@",propertyName);
}
free(properties);
}
复制代码
扩展
1. 编码
在 Xcode
中操作 command
+ shift
+ 0
打开。直接打开地址:developer.apple.com/library/arc…
或者直接运行代码
#pragma mark - 各种类型编码
void lgTypes(void){
NSLog(@"char --> %s",@encode(char));
NSLog(@"int --> %s",@encode(int));
NSLog(@"short --> %s",@encode(short));
NSLog(@"long --> %s",@encode(long));
NSLog(@"long long --> %s",@encode(long long));
NSLog(@"unsigned char --> %s",@encode(unsigned char));
NSLog(@"unsigned int --> %s",@encode(unsigned int));
NSLog(@"unsigned short --> %s",@encode(unsigned short));
NSLog(@"unsigned long --> %s",@encode(unsigned long long));
NSLog(@"float --> %s",@encode(float));
NSLog(@"bool --> %s",@encode(bool));
NSLog(@"void --> %s",@encode(void));
NSLog(@"char * --> %s",@encode(char *));
NSLog(@"id --> %s",@encode(id));
NSLog(@"Class --> %s",@encode(Class));
NSLog(@"SEL --> %s",@encode(SEL));
int array[] = {1,2,3};
NSLog(@"int[] --> %s",@encode(typeof(array)));
typedef struct person{
char *name;
int age;
}Person;
NSLog(@"struct --> %s",@encode(Person));
typedef union union_type{
char *name;
int a;
}Union;
NSLog(@"union --> %s",@encode(Union));
int a = 2;
int *b = {&a};
NSLog(@"int[] --> %s",@encode(typeof(b)));
}
复制代码
例子:
@16@0:8
1: @ // id
2: 16 // 占用的内存
3: @ // id
4: 0 // 从0号位置开始
5: : // SEL
6: 8 // 从8号位置开始
复制代码
2. objc_setProperty
和 内存偏移
区别?
TPerson
中 name
属性底层是通过 objc_setProperty
实现的,而 nickName
属性底层是通过 内存偏移
实现的。这两者到底有什么区别呢?
set
和 get
方法在编译时函数地址就已经被确定。那么 objc_setProperty
只能通过LLVM 源码
进行分析和探究。LLVM下载地址:github.com/apple/llvm-…
首先全局搜索 objc_setProperty
,发现 objc_setProperty
是在 getSetPropertyFn()
中被创建的。
全局搜索 getSetPropertyFn()
,发现是在 GetPropertySetFunction()
中间层中调用了 getSetPropertyFn()
。
全局搜索 GetPropertySetFunction()
,发现 是根据 switch
条件调用的 GetPropertySetFunction()
,条件是由 PropertyImplStrategy
策略的类型决定,接下来我们就看看是在什么时候给策略进行赋值的。
全局搜索 PropertyImplStrategy
,发现当 IsCopy
为 true
时,赋值了 GetSetProperty
,即为当 copy
修饰属性时,底层会使用 objc_setProperty
方式实现。
类方法的存储
一、类存储
TPerson
代码
@interface TPerson : NSObject {
NSString * desc;
NSObject * objc;
}
@property (nonatomic, copy) NSString * name;
@property (nonatomic, retain) NSString * nickName;
- (void)formatPerson;
+ (void)personNickName;
@end
复制代码
回顾上一篇文章中,类对象方法探索
(lldb) p/x TPerson.class
(Class) $0 = 0x0000000100008708 TPerson
(lldb) p/x 0x0000000100008708+0x20
(long) $1 = 0x0000000100008728
(lldb) p (class_data_bits_t *)0x0000000100008728
(class_data_bits_t *) $2 = 0x0000000100008728
(lldb) p $2->data()
(class_rw_t *) $3 = 0x000000010070da90
(lldb) p $3->methods()
(const method_array_t) $4 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000083f0
}
arrayAndFlag = 4295001072
}
}
}
(lldb) p $4.list
(const method_list_t_authed_ptr<method_list_t>) $5 = {
ptr = 0x00000001000083f0
}
(lldb) p $5.ptr
(method_list_t *const) $6 = 0x00000001000083f0
(lldb) p *$6
(method_list_t) $7 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 7)
}
(lldb) p $7.get(0).big()
(method_t::big) $8 = {
name = "formatPerson"
types = 0x0000000100003f7c "v16@0:8"
imp = 0x0000000100003c40 (KCObjcBuild`-[TPerson formatPerson])
}
(lldb)
复制代码
但是它的 类方法
在哪里呢?接下来我们使用上一篇文章提到的 烂苹果
[下载地址](链接: pan.baidu.com/s/1nRvNHX-D… 密码: 8pes)。
发现在这里是有我们定义的 + (void)personNickName;
类方法。
分析:- (void)formatPerson;
是一个实例方法(对象方法)
,那它为什么会在类里面呢?猜测方法
的存储和 内存变量
的存储是不一样的。
补充:Objective-C
中 +
、-
方法在底层 C
、C++
中,统称为函数
。
如果当前的 对象方法
和 类方法
都放在 类
里面,那么编译器
将无法区分那个是哪个,无法查找。苹果为了解决这一问题就设计了 元类
。将 类方法
存在 元类
里面。记下来我们就验证一下:
(lldb) x/4gx TPerson.class
0x100008708: 0x0000000100008730 0x000000010036a140
0x100008718: 0x00000001003623c0 0x0000803400000000
(lldb) p/x 0x0000000100008730 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100008730
(lldb) po 0x0000000100008730
TPerson
(lldb) p/x 0x0000000100008730+0x20
(long) $3 = 0x0000000100008750
(lldb) p/x (class_data_bits_t *)0x0000000100008750
(class_data_bits_t *) $4 = 0x0000000100008750
(lldb) p $4->data()
(class_rw_t *) $5 = 0x000000010121a2b0
(lldb) p *$5
(class_rw_t) $6 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4302789841
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff802eceb0
}
(lldb) p $5.methods()
(const method_array_t) $7 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008598
}
arrayAndFlag = 4295001496
}
}
}
Fix-it applied, fixed expression was:
$5->methods()
(lldb) p $7.list
(const method_list_t_authed_ptr<method_list_t>) $8 = {
ptr = 0x0000000100008598
}
(lldb) p $8.ptr
(method_list_t *const) $9 = 0x0000000100008598
(lldb) p *$9
(method_list_t) $10 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $10.get(0).big()
(method_t::big) $11 = {
name = "personNickName"
types = 0x0000000100003f7c "v16@0:8"
imp = 0x0000000100003d60 (KCObjcBuild`+[TPerson personNickName])
}
(lldb)
复制代码
这里我们就得到我们再 TPerson
中定义的 类方法
:+[TPerson personNickName]
。
二、runtime API
把所有 Class
中的方法
和函数
通过runtime API
打印。
1. 打印 Class
中方法名
void lgObjc_copyMethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
LGLog(@"Method, name: %@", key);
}
free(methods);
}
复制代码
2. 打印 Class
中实例方法
void lgInstanceMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
LGLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
复制代码
3. 打印 Class
中类方法
void lgClassMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
// - (void)sayHello;
// + (void)sayHappy;
Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
LGLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
复制代码
4. 打印 Class
中IMP
void lgIMP_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));// 0
// sel -> imp 方法的查找流程 imp_farw
IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy)); // 0
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
NSLog(@"%s",__func__);
}
复制代码
总结
1、class_rw_t
的优化,其实就是将 class_rw_t
中不常用的部分剥离到 class_rw_ext_t
。2、实例方法
存储在类
中,类方法
存储在 元类
中,编译器自动生成元类
,就是为了存储 类方法
。3、copy
修饰的属性使用 objc_setProperty
方式实现,其它属性使用 内存偏移
实现。
Tips:在学习的过程中,如果有不理解的地方或者描述错误的地方欢迎指错!欢迎一起探讨和交流!!!
上一篇文章问题解答
一、问题一:TPerson
中的 ivar
存储在哪里呢?
通过 WWDC
讲解我们知道 成员变量
存储在 RO
里面,接下来我们就一起验证一下。
(lldb) p/x TPerson.class
(Class) $0 = 0x00000001000086e0 TPerson
(lldb) p/x 0x00000001000086e0+0x20
(long) $1 = 0x0000000100008700
(lldb) p (class_data_bits_t *)0x0000000100008700
(class_data_bits_t *) $2 = 0x0000000100008700
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101c1eb30
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295001000
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.ro() // 获取 or
(const class_ro_t *) $5 = 0x00000001000083a8
(lldb) p *$5 // 获取 ro 中存储的数据
(const class_ro_t) $6 = {
flags = 388
instanceStart = 8
instanceSize = 32
reserved = 0
= {
ivarLayout = 0x0000000100003e3d "\U00000003"
nonMetaclass = 0x0000000100003e3d
}
name = {
std::__1::atomic<const char *> = "TPerson" {
Value = 0x0000000100003e3f "TPerson"
}
}
baseMethodList = 0x00000001000083f0
baseProtocols = nil
ivars = 0x00000001000084a0
weakIvarLayout = 0x0000000000000000
baseProperties = 0x0000000100008508
_swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $6.ivars
(const ivar_list_t *const) $7 = 0x00000001000084a0
(lldb) p *$7
(const ivar_list_t) $8 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 3)
}
(lldb) p $8.get(0)
(ivar_t) $9 = {
offset = 0x00000001000085d8
name = 0x0000000100003f2d "desc"
type = 0x0000000100003f53 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb)
复制代码
p $8.get(0)
就拿到 TPerson
中的 成员变量
。
二、问题二:TPerson
中的 类方法
存储在哪里呢?
在 类方法的存储 中我们已经知道了 类方法
存在 元类
里面。也做了响应的验证。