OC - 方法的本质(下)

681 阅读8分钟

前言

上一篇文章 我们对 方法的本质 有了初步了解。还记的上一篇文章我们留的两个问题吗? 问题一: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 中的 类方法 存储在哪里呢?

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