iOS类的原理探索分析(二)

431 阅读3分钟

前言

之前有讲到类的结构,了解到元类、isa走向关系和继承关系,还从类的bits里拿到的方法和属性。这一篇我们继续深入研究,探索类里面还有什么。

探索DDAnimal.m的底层源码

@interface DDAnimal : NSObject {
    NSString *_sex;
}

@property (nonatomic, copy) NSString *name;    ///<
@property (nonatomic, strong) NSString *nickName;    ///<
@property (nonatomic, assign) NSInteger height;    ///< 

@end

这是文件的上层代码,通过clang,将DDAnimal.m编译成DDAnimal.cpp文件。

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk DDAnimal.m -o DDAnimal.cpp 编译后的代码

#ifndef _REWRITER_typedef_DDAnimal
#define _REWRITER_typedef_DDAnimal
typedef struct objc_object DDAnimal;
typedef struct {} _objc_exc_DDAnimal;
#endif

extern "C" unsigned long OBJC_IVAR_$_DDAnimal$_name;
extern "C" unsigned long OBJC_IVAR_$_DDAnimal$_nickName;
extern "C" unsigned long OBJC_IVAR_$_DDAnimal$_height;
struct DDAnimal_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_sex;
	NSString * _Nonnull _name;
	NSString * _Nonnull _nickName;
	NSInteger _height;
};


// @property (nonatomic, copy) NSString *name;
// @property (nonatomic, strong) NSString *nickName;
// @property (nonatomic, assign) NSInteger height;

/* @end */

#pragma clang assume_nonnull end

// @implementation DDAnimal


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

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

static NSString * _Nonnull _I_DDAnimal_nickName(DDAnimal * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_nickName)); }
static void _I_DDAnimal_setNickName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull nickName) { (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_nickName)) = nickName; }

static NSInteger _I_DDAnimal_height(DDAnimal * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_DDAnimal$_height)); }
static void _I_DDAnimal_setHeight_(DDAnimal * self, SEL _cmd, NSInteger height) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_DDAnimal$_height)) = height; }
// @end

这里可以看出类就是一个struct,所有的属性也被注释掉,变成了带下划线的成员变量,还有一个区别就是属性会创建set和get方法。

static void OBJC_CLASS_SETUP_$_DDAnimal(void ) {
	OBJC_METACLASS_$_DDAnimal.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_DDAnimal.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_DDAnimal.cache = &_objc_empty_cache;
	OBJC_CLASS_$_DDAnimal.isa = &OBJC_METACLASS_$_DDAnimal;
	OBJC_CLASS_$_DDAnimal.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_DDAnimal.cache = &_objc_empty_cache;
}

在这个函数里也能看到DDAnimal的继承和isa的指向,同时也能看到METACLASS(元类),这也印证了之前探索的isa走向和继承关系。

类型编码

{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_DDAnimal_setName_}

注意到了这样一行代码,这里应该是setName方法的定义,那么"v24@0:8@16"是什么意思呢?
参考苹果官网类型编码图表

image.png 按照上图一一对比,可知:

v  : 表示返回值,返回一个void;
24 : 表示这一串所占用的内存,总共24个字节;
@  : 表示第一个传入的参数,传入一个id类型的参数,这里表示的是self;
0  : 表示从0号位置开始存第一个参数;
:  : 表示第二个传入的参数,传入一个method selector;
8  : 表示从8号位置开始存第二个参数;
@  : 表示第三个传入的参数,也是传入一个id类型的参数,这里表示的是一个NSString类型的;
16 : 表示从16号位置开始存第三个参数;

static void _I_DDAnimal_setName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), (id)name, 0, 1); }
对照方法的实现,正如我们所分析的,非常清晰,一目了然。

objc_setProperty() VS self + OBJC_IVAR_$_DDAnimal$_nickName

上面的set可以看出有的用了objc_setProperty(),有的又用的self + OBJC_IVAR_$_DDAnimal$_nickName,先来看看self + OBJC_IVAR_$_DDAnimal$_nickName是什么。

// 可以看出是通过__OFFSETOFIVAR__来赋值的,
extern "C" unsigned long int OBJC_IVAR_$_DDAnimal$_nickName __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct DDAnimal, _nickName);
// __OFFSETOFIVAR__的定义
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)

((long long) &((TYPE *)0)->MEMBER)

ANSI C标准允许值为0的常量被强制转换成任何一种类型的指针
并且转换结果是一个NULL指针,因此((type *)0)的结果就是一个类型为type *的NULL指针
如果利用这个NULL指针来访问type的成员当然是非法的,
但&( ((type *)0)->MEMBER )的意图仅仅是计算MEMBER字段的地址
聪明的编译器根本就不生成访问type的代码
而仅仅是根据type的内存布局结构体实例首址在编译期计算这个(常量)地址,
这样就完全避免了通过NULL指针访问内存的问题。
又因为首址为0,所以这个地址的值就是字段相对于结构体基址的偏移
以上方法避免了实例化一个type对象,并且求值在编译期进行,没有运行期负担。

这样一来就很好理解了,self + OBJC_IVAR_$_DDAnimal$_nickName就是根据结构体的首地址加上成员相对应首地址的便宜,找到这片内存空间,然后直接赋值,非常清晰。
那么objc_setProperty()呢?

objc_setProperty()

在objc源码里能找到方法的声明和实现,但是却没法知道为什么有的会使用objc_setProperty(),有的却不用,很明显,这个应该是在编译期决定的,既然是编译期,那么我们就要去看看LLVM了(戴上痛苦面具,看看LLVM)。
在LLVM里搜索objc_setProperty,出来一长串,痛苦开始,挨个看它是在哪里调用的。仔细一看,搜索出来的结果挺多的,但是很多都是注释,除开注释,最终锁定在了这里。

return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");

看函数名称,大致能理解成创建了一个"objc_setProperty"的runtime方法。 既然是创建,那肯定是要调用才创建,应该就是这里了。这个函数是getSetPropertyFn(),继续看看这个函数是在哪里调用的

llvm::FunctionCallee CGObjCMac::GetPropertySetFunction() {
  return ObjCTypes.getSetPropertyFn();
}

找到GetPropertySetFunction()

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 {
      setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction(); // 这里调用了
      if (!setPropertyFn) {
        CGM.ErrorUnsupported(propImpl, "Obj-C setter requiring atomic copy");
        return;
      }
    }

找到调用GetPropertySetFunction()的位置,发现是在这两个条件下会调用,接下来要去找到这些条件是在哪里赋值的。

  • PropertyImplStrategy::GetSetProperty:
  • PropertyImplStrategy::SetPropertyAndExpressionGet:
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.
  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;
    }
  }
}

找到PropertyImplStrategy的实现(代码过长,这里只截取了GetSetPropertySetPropertyAndExpressionGet相关的)。我们发现

  • copy修饰的是GetSetProperty。
  • retain修饰的:
    1. 在ARC环境下且不是atomic,if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)情况,Kind = Expression,否则Kind = SetPropertyAndExpressionGet
    1. 如果不满足ARC环境或者是atomic,如果不是atomic,Kind = SetPropertyAndExpressionGet,否则Kind = GetSetProperty

小结

至此,我们也算是找了为什么会有两种set函数了。因为SetPropertyAndExpressionGetGetSetProperty都会使用objc_setProperty(),所以copyretain修饰的属性不论是nonatomic还是atomic在底层都会使用objc_setProperty()。

Tips: 在PropertyImplStrategy的实现里还有其他修饰符的赋值,代码过多,感兴趣的可以去看看。

objc_getProperty()

同样的方法我们去查找什么时候会使用objc_getProperty()。代码过多,就只贴出关键位置。

return CGM.CreateRuntimeFunction(FTy, "objc_getProperty");
llvm::FunctionCallee CGObjCMac::GetPropertyGetFunction() {
  return ObjCTypes.getGetPropertyFn();
}
case PropertyImplStrategy::GetSetProperty: {
    llvm::FunctionCallee getPropertyFn =
        CGM.getObjCRuntime().GetPropertyGetFunction();
}

找到最终使用的地方,可以看出只有GetSetProperty类型才会使用objc_getProperty。结合上面找到的PropertyImplStrategy初始化函数,copy和(atomic, retain)修饰的kind = GetSetProperty
可是情况真的是这样吗?代码验证一下

// OC代码
@property (nonatomic, copy) NSString *name;
@property (atomic, retain) NSString *retainName;

// c++代码
static NSString * _Nonnull _I_DDAnimal_name(DDAnimal * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

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

extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);

static NSString * _Nonnull _I_DDAnimal_retainName(DDAnimal * self, SEL _cmd) { typedef NSString * _Nonnull _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _retainName), 1); }
static void _I_DDAnimal_setRetainName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull retainName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _retainName), (id)retainName, 1, 0); }

可以发现,两个属性都有objc_setProperty,但是copy修饰的却没有objc_getProperty。这是为什么呢?

case PropertyImplStrategy::GetSetProperty: {
    llvm::FunctionCallee getPropertyFn =
        CGM.getObjCRuntime().GetPropertyGetFunction();
    if (!getPropertyFn) {
      CGM.ErrorUnsupported(propImpl, "Obj-C getter requiring atomic copy");
      return;
    }
}

再看看源码,注意到下面还有个判断,判断方法是否生成成功,不成功则会提示"Obj-C getter requiring atomic copy",意思是需要atomic修饰,去修改代码试一下。

// OC代码
@property (atomic, copy) NSString *name;

// c++代码
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);

static NSString * _Nonnull _I_DDAnimal_name(DDAnimal * self, SEL _cmd) { typedef NSString * _Nonnull _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), 1); }

发现name的getter也是使用objc_getProperty了。

小结

copyretain修饰的属性必须是使用atomic,才会在底层使用objc_getProperty()。

补充

上面解释了什么情况会用objc_getProperty,但是总感觉有点牵强,继续在llvm里找了下相关代码,还真的发现了疑似代码。

unsigned Attributes = PD->getPropertyAttributes();
if (mustSynthesizeSetterGetterMethod(IMD, PD, true /*getter*/)) {
    bool GenGetProperty =
        !(Attributes & ObjCPropertyAttribute::kind_nonatomic) &&
        (Attributes & (ObjCPropertyAttribute::kind_retain |
                       ObjCPropertyAttribute::kind_copy));
    std::string Getr;
    if (GenGetProperty && !objcGetPropertyDefined) {
      objcGetPropertyDefined = true;
      // FIXME. Is this attribute correct in all cases?
      Getr = "\nextern \"C\" __declspec(dllimport) "
            "id objc_getProperty(id, SEL, long, bool);\n";
    }
    RewriteObjCMethodDecl(OID->getContainingInterface(),
                          PID->getGetterMethodDecl(), Getr);
    Getr += "{ ";
    // Synthesize an explicit cast to gain access to the ivar.
    // See objc-act.c:objc_synthesize_new_getter() for details.
    if (GenGetProperty) {
      // return objc_getProperty(self, _cmd, offsetof(ClassDecl, OID), 1)
      Getr += "typedef ";
      const FunctionType *FPRetType = nullptr;
      RewriteTypeIntoString(PID->getGetterMethodDecl()->getReturnType(), Getr,
                            FPRetType);
      Getr += " _TYPE";
      if (FPRetType) {
        Getr += ")"; // close the precedence "scope" for "*".

        // Now, emit the argument types (if any).
        if (const FunctionProtoType *FT = dyn_cast<FunctionProtoType>(FPRetType)){
          Getr += "(";
          for (unsigned i = 0, e = FT->getNumParams(); i != e; ++i) {
            if (i) Getr += ", ";
            std::string ParamStr =
                FT->getParamType(i).getAsString(Context->getPrintingPolicy());
            Getr += ParamStr;
          }
          if (FT->isVariadic()) {
            if (FT->getNumParams())
              Getr += ", ";
            Getr += "...";
          }
          Getr += ")";
        } else
          Getr += "()";
      }
      Getr += ";\n";
      Getr += "return (_TYPE)";
      Getr += "objc_getProperty(self, _cmd, ";
      RewriteIvarOffsetComputation(OID, Getr);
      Getr += ", 1)";
    }
    else
      Getr += "return " + getIvarAccessString(OID);
    Getr += "; }";
    InsertText(startGetterSetterLoc, Getr);
  }

这里看到有一个字符串拼接,拼接出来的字符串和c++代码里的极其相似。

extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), 1); }

都到这了,我们可以大胆猜测,c++里的代码就是这里拼接出来,淦。
那我们可以在这里找到刚刚的疑惑,GenGetProperty是一个判断条件,不难看出,不是nonatomic修饰,且必须是copy或者retain就会使用objc_getProperty
同样的,也可以找到objc_setProperty,大家可以自行查看。