iOS底层-类的属性底层原理

476 阅读6分钟

前言

  上一期我们探讨过类的本质,我们知道类的本质是结构体,讲到类我们自然离不开类的各种属性。平时在写属性的时候我们会用到各种关键词nonatomicatomicstrongcopyassign等,我们又常说用这些修饰词跟oc的内存管理机制有关系。为什么是这样的,属性底层是怎么实现的呢?

探索

  为方便下面的分析我们需要LLVM源码(LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。LLVM源码下载)

  • 利用clang分析oc代码底层实现  首先我们创建一个工程文件,在main文件里面定义一个类,并添加两组属性。
@interface Father : NSObject
@property (nonatomic, strong) NSString *aNikeName;
@property (nonatomic, copy) NSString *bNikeName;
@end

@implementation Father
@end

 接着使用终端输入指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp

//  aNikeName getter方法
static NSString * _I_Father_aNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)); }
//  aNikeName setter方法。(使用到了内存地址的平移)
static void _I_Father_setANikeName_(Father * self, SEL _cmd, NSString *aNikeName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)) = aNikeName; }

//  bNikeName getter方法
static NSString * _I_Father_bNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_bNikeName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//  bNikeName setter方法  (使用objc_setProperty)
static void _I_Father_setBNikeName_(Father * self, SEL _cmd, NSString *bNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _bNikeName), (id)bNikeName, 0, 1); }

 观察源码发现两个属性的setter方法存在差异,一个用到了内存地址的平移,另一个用到了objc_setProperty。Why?我们可以肯定的是代码变成这样,发生在clang编译之前。我们可以通过LLVM查找相关线索。

  • 利用LLVM源码分析

 用VSCode将下载下来的LLVM源码打开,因为上一步我们发现了objc_setProperty这个关键词,我们可以以它为线索。全局搜索objc_setProperty  接着分析getSetPropertyFn()查找线索  下一步分析GetPropertySetFunction()查找线索  通过逆推我们发现在构造函数PropertyImplStrategy strategy(CGM, propImpl)内部,有一个枚举,枚举为GetSetPropertySetPropertyAndExpressionGet时会调用GetPropertySetFunction()。下面我们从构造函数出发找线索。  从源码的PropertyImplStrategy这个枚举的注释中貌似发现了部分线索,再看下构造函数的实现

/// Pick an implementation strategy for the given property synthesis.
PropertyImplStrategy::PropertyImplStrategy(CodeGenModule &CGM,
                                     const ObjCPropertyImplDecl *propImpl) {
  const ObjCPropertyDecl *prop = propImpl->getPropertyDecl();
  ObjCPropertyDecl::SetterKind setterKind = prop->getSetterKind();

  IsCopy = (setterKind == ObjCPropertyDecl::Copy);
  IsAtomic = prop->isAtomic();
  HasStrong = false; // doesn't matter here.

  // 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;

  // If we have a copy property, we always have to use getProperty/setProperty.
  // TODO: we could actually use setProperty and an expression for non-atomics.
  
  // 跟修饰词copy有关哦
  if (IsCopy) {
    Kind = GetSetProperty;
    return;
  }

  // Handle retain.
  if (setterKind == ObjCPropertyDecl::Retain) {
    // In GC-only, there's nothing special that needs to be done.
    if (CGM.getLangOpts().getGC() == LangOptions::GCOnly) {
      // fallthrough

    // In ARC, if the property is non-atomic, use expression emission,
    // which translates to objc_storeStrong.  This isn't required, but
    // it's slightly nicer.
    } else if (CGM.getLangOpts().ObjCAutoRefCount && !IsAtomic) {
      // Using standard expression emission for the setter is only
      // acceptable if the ivar is __strong, which won't be true if
      // the property is annotated with __attribute__((NSObject)).
      // TODO: falling all the way back to objc_setProperty here is
      // just laziness, though;  we could still use objc_storeStrong
      // if we hacked it right.
      if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)
        Kind = Expression;
      else
        Kind = SetPropertyAndExpressionGet;
      return;

    // Otherwise, we need to at least use setProperty.  However, if
    // the property isn't atomic, we can use normal expression
    // emission for the getter.
    } else if (!IsAtomic) {
      Kind = SetPropertyAndExpressionGet;
      return;

    // Otherwise, we have to use both setProperty and getProperty.
    } else {
      Kind = GetSetProperty;
      return;
    }
  }

  // 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;
  }

  // GC-qualified or ARC-qualified ivars need to be emitted as
  // expressions.  This actually works out to being atomic anyway,
  // except for ARC __strong, but that should trigger the above code.
  if (ivarType.hasNonTrivialObjCLifetime() ||
      (CGM.getLangOpts().getGC() &&
       CGM.getContext().getObjCGCAttrKind(ivarType))) {
    Kind = Expression;
    return;
  }

  // Compute whether the ivar has strong members.
  if (CGM.getLangOpts().getGC())
    if (const RecordType *recordType = ivarType->getAs<RecordType>())
      HasStrong = recordType->getDecl()->hasObjectMember();

  // We can never access structs with object members with a native
  // access, because we need to use write barriers.  This is what
  // objc_copyStruct is for.
  if (HasStrong) {
    Kind = CopyStruct;
    return;
  }

  // Otherwise, this is target-dependent and based on the size and
  // alignment of the ivar.

  // If the size of the ivar is not a power of two, give up.  We don't
  // want to get into the business of doing compare-and-swaps.
  if (!IvarSize.isPowerOfTwo()) {
    Kind = CopyStruct;
    return;
  }

  llvm::Triple::ArchType arch =
    CGM.getTarget().getTriple().getArch();

  // Most architectures require memory to fit within a single cache
  // line, so the alignment has to be at least the size of the access.
  // Otherwise we have to grab a lock.
  if (IvarAlignment < IvarSize && !hasUnalignedAtomics(arch)) {
    Kind = CopyStruct;
    return;
  }

  // If the ivar's size exceeds the architecture's maximum atomic
  // access size, we have to use CopyStruct.
  if (IvarSize > getMaxAtomicAccessSize(CGM, arch)) {
    Kind = CopyStruct;
    return;
  }

  // Otherwise, we can use native loads and stores.
  Kind = Native;
}

 结论当使用copy修饰属性时,属性的setter方法会用到objc_setProperty函数方法。

 源码分析过程中,我们发现底层不仅有objc_setProperty 还有objc_getProperty。那么objc_getProperty的底层逻辑是怎么走的呢,哪些情况属性的getter方法会调用呢?  首先LLVM源码全局搜索objc_getProperty  接着查看函数getGetPropertyFn()实现  继续探索我们发现下面一段代码  结论

  • 当不使用nonatomic修饰属性,使用retaincopy修饰属性时,属性的getter方法会调用objc_getProperty
  • 当使用retaincopy修饰属性时,属性的setter方法会调用objc_setProperty。  下面通过案例验证以上结果  首先我们创建一个工程文件,在main文件里面定义一个类,并添加六组属性。
@interface Father : NSObject

@property (nonatomic, strong) NSString *aNikeName;
@property (nonatomic, copy) NSString *bNikeName;
@property (nonatomic, retain) NSString *cNikeName;

@property (atomic, strong) NSString *dNikeName;
@property (atomic, copy) NSString *eNikeName;
@property (atomic, retain) NSString *fNikeName;
@end

@implementation Father

@end


int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

 接着使用终端输入指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp

// @implementation Father

// aNikeName getter方法 (使用内存平移)
static NSString * _I_Father_aNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)); }
// aNikeName setter方法  (使用内存平移)
static void _I_Father_setANikeName_(Father * self, SEL _cmd, NSString *aNikeName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)) = aNikeName; }

// bNikeName getter方法 (使用内存平移)
static NSString * _I_Father_bNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_bNikeName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// bNikeName setter方法  (使用objc_setProperty)
static void _I_Father_setBNikeName_(Father * self, SEL _cmd, NSString *bNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _bNikeName), (id)bNikeName, 0, 1); }
// cNikeName getter方法 (使用内存平移)
static NSString * _I_Father_cNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_cNikeName)); }
// cNikeName setter方法  (使用objc_setProperty)
static void _I_Father_setCNikeName_(Father * self, SEL _cmd, NSString *cNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _cNikeName), (id)cNikeName, 0, 0); }
// dNikeName getter方法 (使用内存平移)
static NSString * _I_Father_dNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_dNikeName)); }
// dNikeName setter方法  (使用内存平移)
static void _I_Father_setDNikeName_(Father * self, SEL _cmd, NSString *dNikeName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_dNikeName)) = dNikeName; }

extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
// eNikeName getter方法 (使用objc_getProperty)
static NSString * _I_Father_eNikeName(Father * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct Father, _eNikeName), 1); }
// eNikeName setter方法  (使用objc_setProperty)
static void _I_Father_setENikeName_(Father * self, SEL _cmd, NSString *eNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _eNikeName), (id)eNikeName, 1, 1); }
// fNikeName getter方法 (使用objc_getProperty)
static NSString * _I_Father_fNikeName(Father * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct Father, _fNikeName), 1); }
// fNikeName setter方法  (使用objc_setProperty)
static void _I_Father_setFNikeName_(Father * self, SEL _cmd, NSString *fNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _fNikeName), (id)fNikeName, 1, 0); }
// @end

 由上面结果发现LLVM源码探索出来调用objc_getPropertyobjc_setProperty的条件与实际运行结果一致。

总结

  • 使用retaincopy修饰属性且不要使用nonatomic修饰时生成的getter方法会调用objc_getProperty
  • 使用retaincopy修饰的属性生成的setter方法会调用objc_setProperty
  • 以上两种情况以外的其他条件都是使用内存平移进行赋值。