iOS底层探索-类的原理之属性、方法

323 阅读4分钟

1、成员变量与属性

举例与解释

  • 对比他们的区别我们先从1个简单的例子入手。
    • 首先我们先定义几个成员变量和属性 image.png
    • 然后cd进要转译的文件目录(例子是main.m) ,转成cpp文件
    clang -rewrite-objc main.m -o main.cpp
    
    • 在cpp文件中搜索定义的LZPerson类得到: image.png
      image.png

    • 那么为什么用copy修饰的属性会产生一个objc_setProperty呢?

      • objc_setProperty是一个中间层代码。为了每一个属性在使用setter方法的时候不必为其在底层创建单独的实现方法,当检测到属性对象存在copy属性的时候,会将此对象的setter方法重定向到objc_setProperty(),然后只要在底层实现setProperty方法就可以将属性对象的setter方法实现了。
    • 向下查方法参数能发现如下内容,他们代表什么呢? image.png

      • 其实这是返回实例变量的编码类型类型字符串(以对象类型声明的特殊的成员变量) d34adfafd32741ffb1fd83bdb8e8a2ff~tplv-k3u1fbpfcp-watermark.image.png
      {{(struct objc_selector *)"ncName", "@16@0:8",(void *)_I_LZPerson_ncName},
      {(struct objc_selector *)"setNcName:", "v24@0:8@16", (void *)_I_LZPerson_setNcName_},
      

      根据上边官网TypeEncoding的表一一对应可知:

      • @16@0:8 image.png

      • v24@0:8@16 image.png

1.1、ivar、imp、_cmd、method

  • ivar : 成员变量
  • imp : 指向实际执行函数体的函数指针
  • _cmd : SEL 类型的一个变量,Objective C的函数的前两个隐藏参数为self 和 _cmd
  • method : 指向Objective C中的方法的指针

1.2、分析首地址+内存偏移objc_setProperty

  • getter方法时,copy + atomic修饰通过objc_setProperty进行get,其余修饰可以通过首地址+内存偏移来获取想要获取的那个成员变量
  • setter方法时,strong修饰的也可以通过首地址+内存偏移这种方式进行set,但Copy修饰的会使用objc_setProperty进行set
  • 原因是进行setter时,无论是setName还是setAge等等,本质是对内存进行赋值,只有_cmd方法名不同,底层进行的操作完全一样,所以对底层的赋值方法进行了抽取,在编译时LLVM),所有的ivarsel通过imp重定向到objc_setProperty

image.png

1.3、属性被StrongCopy修饰时的区别

  • strong修饰时,对象的getter/setter方法是通过对象的首地址+内存偏移找到对应对象的内存地址,然后进行值的getset

    //getter方法
    static NSString * _I_LZPerson_nName(LZPerson * self, SEL _cmd) {
        return (*(NSString **)((char *)self + OBJC_IVAR_$_LZPerson$_nName)); 
    }
    
    //setter方法
    static void _I_LZPerson_setNName_(LZPerson * self, SEL _cmd, NSString *nName) {
        (*(NSString **)((char *)self + OBJC_IVAR_$_LZPerson$_nName)) = nName; 
    }
    

    image.png

    image.png

  • copy修饰时get方法与strong时相同 ,但setter方法则是通过objc_setProperty进行set

    image.png 但是造成这个的原因时什么呢?我们分步分析:

    1. 判断在什么阶段确定的 setter/getter 方法:因为用MachOView查看可执行性文件可以看到已经生成了方法,所以可以确定是编译阶段就已经完成
    2. 确定了在编译阶段,那么就需要查看LLVM源码中是如何编译的:
      • 全局搜索 objc_setProperty ,发现是在getSetPropertyFn()方法中通过CreateRuntimeFunction(FTy,"objc_setProperty"),在runtime运行时中创建,接下来找 getSetPropertyFn() 是在哪被调用 image.png

      • 找到在GetPropertySetFunction()中间层中,继续找GetPropertySetFunction() image.png

      • 发现是PropertyImplStrategy条件判断中调用了 GetPropertySetFunction() ,但它有两种类型GetSetPropertySetPropertyAndExpressionGet image.png

      • Copy属性修饰时,枚举值strategy值被设置为GetSetProperty image.png

        image.png

    3. 通过此案例得出结论:
      • 只要为对象设置了copy属性,无论是原子性还是非原子性,都没有影响,setter方法都会被重定向到objc_setProperty
      • 如果声明的对象不设置如何除原子性之外的属性,那么默认属性是strong,不会触发objc_setProperty
      • 中间层的优点:底层变化上层不受影响上层变化底层也不会受影响
    4. 对于getter方法:atomiccopy修饰的属性使用objc_getProperty方式实现,其它属性使用内存偏移实现,还是需要看LLVM源码,类似于 setter 的查找方法
  • objc_setProperty源码 image.png

    image.png

2、类、元类的方法

Runtime API查看类和元类

  • 获取方法名 image.png

  • 获取实例方法 image.png

  • 获取类方法 image.png

  • 获取IMP image.png

总结:

  • 实例方法存在中,因此类调用不到类方法
  • 类方法存在元类中,因此元类调用不到实例方法
  • 找不到IMP实现的也返回方法,这个方法就是_objc_msgFarward;
  • 编译器自动生成元类,目的是存放类方法
  • 万物皆对象,在底层并没有区分对象方法和类方法,其实类和元类在底层都是调用的实例方法,类方法只是OC层的区分(类方法本质是元类的实例方法

实例方法: image.png 类方法: image.png IMP: image.png