阅读 108

类的原理分析(下)—— 类的成员变量和方法

《类的原理分析(上)——认识类的结构》中,我们认识了类的基本结构,本篇开始将介绍关于类的方法、属性、成员变量相关的知识。

一、成员变量和属性

1、成员变量和属性的定义

我们对成员变量的认识是,类或者扩展头文件中大括号包含的带 _ 的实例变量。

实际上,实例变量只是一个特殊的成员变量。

属性,实际上是一个成员变量 + setter-getter方法的组合,系统为我们提供了封装,可以命名为:@property(xxx , xxx)Type ivarName 的形式。

那么,一个属性的实现,底层到底经历了什么样的过程,让我们一起探索!

2、分析一个属性的C++代码


#pragma mark - 定义的类

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end

#pragma mark - 编译为cpp之后的文件

static NSString * _Nonnull _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1); }

static int _I_LGPerson_age(LGPerson * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGPerson$_age)); }

static void _I_LGPerson_setAge_(LGPerson * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGPerson$_age)) = age; }

复制代码

可以看到,类中的属性和方法,在转为c++代码之后,都分别生成了setter和getter方法。

① 使用关键字copy修饰的name,使用了objc_setProperty方法进行了赋值

② 未使用关键字修饰的age,是直接采用内存平移赋值操作

直接进行内存平移赋值的,这点我们可以理解,另外一种是使用objc_setProperty赋值的属性,由于涉及到一个编码问题,稍后我们介绍objc_setProperty的实现。

3、方法体的编码


{{(struct objc_selector *)"init", "@16@0:8", (void *)_I_LGPerson_init},
{(struct objc_selector *)"saySomething", "v16@0:8", (void *)_I_LGPerson_saySomething},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_},
{(struct objc_selector *)"age", "i16@0:8", (void *)_I_LGPerson_age},
{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_LGPerson_setAge_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_},
{(struct objc_selector *)"age", "i16@0:8", (void *)_I_LGPerson_age},
{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_LGPerson_setAge_}}

复制代码

OC中方法体的命名是遵循一定的规则的,如上所示,其中 "v20@0:8i16" 之类的,是代表特定的类型编码,苹果官网也做了相关说明 ( 我们可以通过 Command + Shift + 0,查看TypeEncoding,然后进入文档末端,进入苹果社区官网 ) ,我们直接看图。

image.png

那么,了解到这些规则,我们就随便找一个编码进行分析:如: v20@0:8i16

1、v 表示 void

2、20 表示 占用内存为20

3、@ 表示 id

4、0 表示 从0号位置开始

5、: 表示 SEL

6、8 表示 从8号位置开始

7、i 表示 int

8、16 表示 从16号为开始 (如果有参数,看参数类型,分配字节数,多参数以此类推)

了解了成员变量和属性的C++底层实现,我们就从更深层次探索objc_setProperty的实现过程,下面分别从runtime和llvm的源码着手分析。

二、属性的setter-getter方法分析

1、我们可以从runtime源码找到objc_Property函数


#pragma mark - 声明部分

OBJC_EXPORT void
objc_setProperty(id _Nullable self, SEL _Nonnull _cmd, ptrdiff_t offset,
                 id _Nullable newValue, BOOL atomic, signed char shouldCopy)
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);


#pragma mark - 实现部分

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

复制代码

我们在进行赋值的时候,需要传入的一些参数,类, _cmd ,新值 ,偏移量,根据这些参数对一个类的属性进行存储,下面是具体实现部分。

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    #pragma mark - 如果偏移为0,第一次赋值,直接赋值
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }
    
    #pragma mark - 如果不是第一次赋值
    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    
    #pragma mark - copy的逻辑
    if (copy) {
        #pragma mark - 如果使用了copy关键字,浅层copy,地址
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        #pragma mark - 使用了mutablecopy关键字,进行了地址copy+指针
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        #pragma mark - 其他情况,返回新值,引用计数 + 1
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
    
    #pragma mark - 原子类型判断
    
    if (!atomic) {
        #pragma mark - 非原子类型,直接赋值
        oldValue = *slot;
        *slot = newValue;
    } else {
    
        #pragma mark - 原子类型,加锁操作
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

复制代码

以上是对属性在runtime层次的分析,下面我们跟踪llvm,查看属性在编译层面做了哪些优化工作?

2、LLVM源码分析属性的实现过程

我们最初并不知道llvm的底层是怎么实现的,所以,就先从objc_setProperty这里开始探索吧!

  • 2.1、从 objc_setProperty开始,逆向查找实现过程

    #pragma mark - getGetPropertyFn的实现
    
    llvm::FunctionCallee getGetPropertyFn() {
    CodeGen::CodeGenTypes &Types = CGM.getTypes();
    ASTContext &Ctx = CGM.getContext();
    // id objc_getProperty (id, SEL, ptrdiff_t, bool)
    CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
    CanQualType SelType = ...
    llvm::FunctionType *FTy = ...(IdType, Params));
        
    #pragma mark - 这里创建了一个objc_getProperty的方法
    return CGM.CreateRuntimeFunction(FTy, "objc_getProperty");
  }


    #pragma mark - getSetPropertyFn的实现

  llvm::FunctionCallee getSetPropertyFn() {
    CodeGen::CodeGenTypes &Types = CGM.getTypes();
    ASTContext &Ctx = CGM.getContext();
    // void objc_setProperty (id, SEL, ptrdiff_t, id, bool, bool)
    CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
    CanQualType SelType = ...
    llvm::FunctionType *FTy = ...(IdType, Params));
    
     #pragma mark - 这里创建了一个objc_setProperty的方法
    return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
  }
复制代码
  • 2.2、然后查看 getSetPropertyFn, setter方法的执行过程分析

 llvm::FunctionCallee getSetPropertyFn() {
    CodeGen::CodeGenTypes &Types = CGM.getTypes();
    ASTContext &Ctx = CGM.getContext();
    // void objc_setProperty (id, SEL, ptrdiff_t, id, bool, bool)
    CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
    CanQualType SelType = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
    CanQualType Params[] = {
        IdType,
        SelType,
        Ctx.getPointerDiffType()->getCanonicalTypeUnqualified(),
        IdType,
        Ctx.BoolTy,
        Ctx.BoolTy};
    llvm::FunctionType *FTy =
        Types.GetFunctionType(
          Types.arrangeBuiltinFunctionDeclaration(Ctx.VoidTy, Params));
    return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
  }

复制代码
  • 2.3、根据getSetPropertyFn找到调用层函数,然后找到调用层逻辑代码GetPropertySetFunction的实现。


#pragma mark -调用层函数
 
 llvm::FunctionCallee GetPropertySetFunction() override {
    return ObjCTypes.getSetPropertyFn();
  }
  
#pragma mark -调用层函数实现  generateObjCSetterBody
  void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
                                        const ObjCPropertyImplDecl *propImpl,
                                        llvm::Constant *AtomicHelperFn) {
  ObjCIvarDecl *ivar = propImpl->getPropertyIvarDecl();
  ObjCMethodDecl *setterMethod = propImpl->getSetterMethodDecl();

  // Just use the setter expression if Sema gave us one and it's
  // non-trivial.
  if (!hasTrivialSetExpr(propImpl)) {
    ...
  }
  PropertyImplStrategy strategy(CGM, propImpl);
  
  
 #pragma mark - 这里是个条件判断,传入不同的值,分别会创建不同的方法
  
  switch (strategy.getKind()) {
  
 #pragma mark - Native类型
  
  case PropertyImplStrategy::Native: {
  
    // We don't need to do anything for a zero-size struct.
    //成员变量大小为0字节,直接不创建setter-getter
    if (strategy.getIvarSize().isZero())
      return;
    //获取偏移量开始位置
    Address argAddr = GetAddrOfLocalVar(*setterMethod->param_begin());
    LValue ivarLValue =
      EmitLValueForIvar(TypeOfSelfObject(), LoadObjCSelf(), ivar, /*quals*/ 0);
    获取当前成员所在位置
    Address ivarAddr = ivarLValue.getAddress(*this);

    // Currently, all atomic accesses have to be through integer
    
    // 所有atomic修饰的都必须是integer类型
    llvm::Type *bitcastType =
      llvm::Type::getIntNTy(getLLVMContext(),
                            getContext().toBits(strategy.getIvarSize()));

    // Cast both arguments to the chosen operation type.
    argAddr = Builder.CreateElementBitCast(argAddr, bitcastType);
    ivarAddr = Builder.CreateElementBitCast(ivarAddr, bitcastType);

    // This bitcast load is likely to cause some nasty IR.
    llvm::Value *load = Builder.CreateLoad(argAddr);

    // Perform an atomic store.  There are no memory ordering requirements.
    llvm::StoreInst *store = Builder.CreateStore(load, ivarAddr);
    store->setAtomic(llvm::AtomicOrdering::Unordered);
    return;
  }
  
  
 #pragma mark - GetSetProperty类型
  
  case PropertyImplStrategy::GetSetProperty:
  case PropertyImplStrategy::SetPropertyAndExpressionGet: {

    llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
    llvm::FunctionCallee setPropertyFn = nullptr;
    if (UseOptimizedSetter(CGM)) {
      // 10.8 and iOS 6.0 code and GC is off
      setOptimizedPropertyFn =
          CGM.getObjCRuntime().GetOptimizedPropertySetFunction(
              strategy.isAtomic(), strategy.isCopy());
      if (!setOptimizedPropertyFn) {
        CGM.ErrorUnsupported(propImpl, "Obj-C optimized setter - NYI");
        return;
      }
    }
    else {
    #pragma mark - !!!!!!!!!!!!!!!!!!!!!!!!!!!
    #pragma mark - 这里调用GetPropertySetFunction去创建一个方法
    #pragma mark - !!!!!!!!!!!!!!!!!!!!!!!!!!!
      setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction();
      if (!setPropertyFn) {
        CGM.ErrorUnsupported(propImpl, "Obj-C setter requiring atomic copy");
        return;
      }
    }
    ...
    return;
  }
  
复制代码

至此,我们可以推理出,llvm是根据不同的条件,执行不同的创建操作,关键看PropertyImplStrategy的值。

 class PropertyImplStrategy {
  public:
    enum StrategyKind {
      /// The 'native' strategy is to use the architecture's provided
      /// reads and writes.
      Native,

      /// Use objc_setProperty and objc_getProperty.
      GetSetProperty,

      /// Use objc_setProperty for the setter, but use expression
      /// evaluation for the getter.
      SetPropertyAndExpressionGet,

      /// Use objc_copyStruct.
      CopyStruct,

      /// The 'expression' strategy is to emit normal assignment or
      /// lvalue-to-rvalue expressions.
      Expression
    };

复制代码

如上,在初期创建属性的时候,会根据修饰词的类型,首先判定属于哪种类型,然后分别生成不同的setter-getter方法,然后根据方法体PropertyImplStrategy,寻找最终调用者。


#pragma mark - 到这里算是找到了入口,包含了对一些关键字的判断,还有方法的生成策略等

PropertyImplStrategy::PropertyImplStrategy(CodeGenModule &CGM,
                                     const ObjCPropertyImplDecl *propImpl) {
  // prop 分隔符号                              
  const ObjCPropertyDecl *prop = propImpl->getPropertyDecl();
  //获取属性中Kind的类型
  ObjCPropertyDecl::SetterKind setterKind = prop->getSetterKind();

  #pragam mark - 判断,是否为copy、atomic、strong关键字修饰
  
  IsCopy = (setterKind == ObjCPropertyDecl::Copy);
  IsAtomic = prop->isAtomic();
  HasStrong = false; // doesn't matter here.
  
  #pragma mark 计算成员变量的大小和对齐
  // Evaluate the ivar's size and alignment.
  ObjCIvarDecl *ivar = propImpl->getPropertyIvarDecl();
  QualType ivarType = ivar->getType();
  auto TInfo = CGM.getContext().getTypeInfoInChars(ivarType);
  IvarSize = TInfo.Width;
  IvarAlignment = TInfo.Align;
#pragma mark **********************************************

#pragma mark 如果存在copy或nonatomic修饰的属性的处理都会定位到这里

  #pragma mark ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
  #pragma mark ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
  #pragma mark ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

  if (IsCopy) {
    Kind = GetSetProperty;
    return;
  }
  
  #pragma mark ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
  #pragma mark ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
  #pragma mark ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

  
#pragma mark 如果存在retain的处理逻辑,这里是MRC的处理过程,暂时不关注,涉及引用计数
  // Handle retain.
  if (setterKind == ObjCPropertyDecl::Retain) {
    .......
  }


#pragma mark 如果不是atomic类型的处理逻辑
  // If we're not atomic, just use expression accesses.
  if (!IsAtomic) {
    Kind = Expression;
    return;
  }

  // Properties on bitfield ivars need to be emitted using expression
  // accesses even if they're nominally atomic.
  if (ivar->isBitField()) {
    Kind = Expression;
    return;
  }
......

复制代码

三、总结llvm中属性的执行逻辑

✨ 上面我们采用了逆向推理的方式,查看了llvm中属性生成器的执行规则,再来正向去归纳下执行的过程。

① 在类中声明了@property,系统首先会根据后面修饰的类型,去判断是采用内存平移的方式赋值,还是通过objc_setProperty的方式。

② 如果是基本数据类型,计算机会通过内存平移的方式去赋值

③ 如果是对象类型,在编译中llvm中首先判断修饰的关键字,通过关键字去生成不同的setter方法(具体,凡是copy修饰的成员变量,都会执行objc_setProperty方法)

以上就是对系统setter-getter方法的探究过程,虽然探索过程比较麻烦,但是有这一层认识,后面再进行开发的过程中,会特别注意一些关键字的使用,合理使用可以避免内存使用不当造成的浪费,从深层次提升了自己的能力,这些探索,还只是初步尝试,关于更多的一些细节问题,等有时间再来探索吧!

文章分类
iOS
文章标签