OC – 方法的本质(下)

前言

上一篇文章 我们对 方法的本质 有了初步了解。还记的上一篇文章我们留的两个问题吗? 问题一:TPerson 中的 ivar 存储在哪里呢? 问题二:TPerson 中的 类方法 存储在哪里呢? 今天我们通过这边文章再对 方法的本质 做进一步的学习和探究。

类的内存数据 — or数据

一、类

首先我们先来看一段 视频 ,苹果官方对内存的优化。数据结构的变化 Objective-C 运行时会使用它们来追踪类Objective-C 方法列表变化Tagged pointer 格式的变化

可以由以下几张图简单描述。

1. 类结构(类对象本身)

类对象本身包含了最常被访问的信息指向元类超类方法缓存的指针 还有一个指向更多数据的指针。
它包括 像类名称方法协议实例变量 的信息。Swift 类和 Objective-C 类共享这一基本数据,所以每个 Swift 类也有这些基本数据结构。

类的结构图

image.png

2. 类第一次从磁盘加载

当类第一次从磁盘中加载到内存中时,它们一开始也是这样的,但是一经使用,它们就会发生变化。在了解这些变化之前,我们先了解一下 clean memorydirty memory 的区别。

clean memory

clean memory 是指加载后不会发生更改的内存。class_ro_t 就属于 clean memory,因为它是只读的。
image.png

dirty memory

dirty memory 是指在进程运行时会发生更改的内存,类结构一经使用就会变成 dirty memory,因为运行时会向它写入新的数据,例如:创建一个新的方法缓存并从类中指向
它。

小结

dirty memoryclean memory 要昂贵得多,只要进程在运行,它就必须一直存在。另一方面 clean memory 可以进行移除从而节省更多的内存空间。因为如果你需要 clean memory 系统可以从磁盘中重新加载。macOS 可以选择换出 dirty memory,但因为 iOS 不使用 swap,所以 dirty memory 在 iOS 中代价很大。dirty memory 是这个类数据被分成两部分的原因,可以保持清洁的数据越来越好。通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为 clean memory

3. 类第⼀次被使⽤时的结构(runtime)

当类首次被使用时,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是 class_rw_t 用于 读取-编写数据。在这个数据结构中,我们存储了只有再运行时才会生成的新信息。例如:所有的类都会链接成一个树状结构。这是通过使用 First SubclassNext Sibling Class 指针实现的。这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。当 category 被加载时,它可以向类中添加新的方法。而且程序员可以使用运行时 API 动态地添加它们。因为 class_ro_t只读的,所以我们需要在 class_rw_t 中追踪这些东西。

image.png

4. 将需要动态更新的部分提取出来,存⼊class_rw_ext_t

我们可以拆掉那些平时不用的部分,这将 class_rw_t 的大小减少了一半,对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个并把它滑到类中供其他使用。

image.png

5. 类的整体结构

大约 90% 的类从来不需要这些扩展数据,这些内存可以用于更有效的用途。比如:存储 APP数据

image.png

二、类的成员变量和属性以及编码

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 进行探索。

image.png

发现属性在底层 C++ 里面全部都没有了。取而代之的是在成员变量 中生成了带 _的,但是它和成员变量的区别在于它还生成了响应 setget方法。

image.png

也可以通过以下代码进行区分辨别:

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. 编码

image.png

Xcode 中操作 command + shift + 0 打开。直接打开地址:developer.apple.com/library/arc…

image.png

image.png

或者直接运行代码

#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内存偏移区别?

TPersonname 属性底层是通过 objc_setProperty 实现的,而 nickName 属性底层是通过 内存偏移 实现的。这两者到底有什么区别呢?

setget 方法在编译时函数地址就已经被确定。那么 objc_setProperty 只能通过LLVM 源码进行分析和探究。LLVM下载地址:github.com/apple/llvm-…

首先全局搜索 objc_setProperty,发现 objc_setProperty 是在 getSetPropertyFn() 中被创建的。

image.png

全局搜索 getSetPropertyFn(),发现是在 GetPropertySetFunction() 中间层中调用了 getSetPropertyFn()
image.png

全局搜索 GetPropertySetFunction() ,发现 是根据 switch 条件调用的 GetPropertySetFunction(),条件是由 PropertyImplStrategy 策略的类型决定,接下来我们就看看是在什么时候给策略进行赋值的。

image.png

全局搜索 PropertyImplStrategy ,发现当 IsCopytrue 时,赋值了 GetSetProperty ,即为当 copy 修饰属性时,底层会使用 objc_setProperty 方式实现。

image.png

类方法的存储

一、类存储

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)。

image.png

发现在这里是有我们定义的 + (void)personNickName; 类方法。

分析:- (void)formatPerson; 是一个实例方法(对象方法),那它为什么会在类里面呢?猜测方法的存储和 内存变量 的存储是不一样的。

补充:Objective-C+- 方法在底层 CC++ 中,统称为函数

如果当前的 对象方法类方法 都放在 里面,那么编译器将无法区分那个是哪个,无法查找。苹果为了解决这一问题就设计了 元类。将 类方法 存在 元类 里面。记下来我们就验证一下:

(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. 打印 ClassIMP

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 中的 类方法 存储在哪里呢?

类方法的存储 中我们已经知道了 类方法 存在 元类 里面。也做了响应的验证。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享