iOS底层-类的探究分析(中)

358 阅读6分钟

前言

在上一篇文章类的探究分析(上)中,我们对类进行了初步的探究,得到了isa的走位图,也拿到了类的属性方法。在isa走位图中我们提到元类,那么为什么要有元类呢,属性实例变量的区别是什么呢?我们拿到的属性中有@v:等字符又是什么意思呢?本文将从这几个方面继续对类进行探究。

一、WWDC2020

  • WWDC2020中,我们对类进行了新的认知:

从内存加载类的变化:

从内存中加载流程如图所示: 截屏2021-06-22 22.10.03.png

clean MemoryDirty Memory

  • clean Memory是加载后不会发生更改的内存。clean Memory可以进行移除,从而节省更多空间。因为如果需要clean Memory数据,可以从内存中去加载。
  • Dirty Memory是运行时会发生更改后的内存。类一经使用就变成了Dirty Memory,因为运行时会向它写入新数据。例如创建方法缓存并从类中指向它Dirty Memory要比clean Memory昂贵的多,只要进程在运行,就一直存在。

二、属性成员变量

  • 我们先在main.m定义一个WSPerson的类,然后再用clang编译出一个main.cpp文件:
// main.m
@interface WSPerson : NSObject {
// 成员变量
    NSString *wsSubject;
    int wsAge;
    // WSTeacher *teacher; 实例变量
}
// 属性
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;

@end
@implementation WSPerson

// main.cpp
struct WSPerson_IMPL {
     struct NSObject_IMPL NSObject_IVARS;
     NSString *wsSubject;
     int wsAge;
     NSString *_name;
     NSString *_nickName;
};

// name get
static NSString * _I_WSPerson_name(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// name set
static void _I_WSPerson_setName_(WSPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WSPerson, _name), (id)name, 0, 1); }

// nickName get
static NSString * _I_WSPerson_nickName(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_nickName)); }
// nickName get
static void _I_WSPerson_setNickName_(WSPerson * self, SEL _cmd, NSString *nickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_nickName)) = nickName; }

// {(struct objc_selector *)"name", "@16@0:8", (void *)_I_WSPerson_name}
// (struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_WSPerson_setName_}
@end

在底层,我们能看到定义的属性以成员变量的形式在结构体WSPerson_IMPL中,然后生成对应的settergetter方法,而类的成员变量只是以成员变量存在结构体中。

  • 结论
      1. 属性 = 下划线成员变量 + setter + getter
      1. 实例变量 = 特殊的成员变量(类的实例化)。 但这里发现个问题:

1.nickNameset方法是根据首地址平移赋值,但name就变成了objc_setProperty,这是什么,它和内存平移有什么区别?
2. 类似@16@0:8这种格式的数据是什么?

编码

类似@16@0:8这种的编码,我们可以 cmd+shift+0,打开苹果的开发文档,然后搜索ivar_getTypeEncoding,再点击Discussion中的 Type Encodings ,这里介绍了编码中相关字符的作用。

  • 我们举个例子@16@0:8v24@0:8@16截屏2021-06-22 13.22.16.png
    从图上我们更能清晰的了解编码的含义。

objc_setProperty

LLVM分析

  • 先下载并编好llvm,然后再里面搜索objc_setProperty,在CGObjCMac.cpp文件中查到了创建objc_setProperty
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");
  }
  • 这里返回的是创建objc_setProperty的方法,那么我们反推,这里创建,肯定有地方在调用,再去搜索getSetPropertyFn方法,发现GetPropertySetFunction这个方法的返回是getSetPropertyFn类型,我们再继续查找,找到个这样的枚举:
switch (strategy.getKind()) {
  case PropertyImplStrategy::Native: {
    // size为0,返回
    if (strategy.getIvarSize().isZero())
      return;
    Address argAddr = GetAddrOfLocalVar(*setterMethod->param_begin());
    LValue ivarLValue =
      EmitLValueForIvar(TypeOfSelfObject(), LoadObjCSelf(), ivar, /*quals*/ 0);
    Address ivarAddr = ivarLValue.getAddress(*this);
    llvm::Type *bitcastType =
    llvm::Type::getIntNTy(getLLVMContext(),
                            getContext().toBits(strategy.getIvarSize()));
    argAddr = Builder.CreateElementBitCast(argAddr, bitcastType);
    ivarAddr = Builder.CreateElementBitCast(ivarAddr, bitcastType);
    llvm::Value *load = Builder.CreateLoad(argAddr);
requirements.
    llvm::StoreInst *store = Builder.CreateStore(load, ivarAddr);
    store->setAtomic(llvm::AtomicOrdering::Unordered);
    return;
  }

  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;
      }
    }
  • 从枚举内容都在PropertyImplStrategy类中,当值为GetSetPropertySetPropertyAndExpressionGet时,会调用GetPropertySetFunction,然后我们去查找这个类,里面有一个枚举StrategyKind,在里面我们找到类GetSetPropertySetPropertyAndExpressionGet
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
    };
    ...
    ...
    PropertyImplStrategy(CodeGenModule &CGM,
                         const ObjCPropertyImplDecl *propImpl);
  • PropertyImplStrategy中,有一个赋值方法PropertyImplStrategy,我们再来查看下:
  if (IsCopy) {
    Kind = GetSetProperty;
    return;
  }
  
  if (setterKind == ObjCPropertyDecl::Retain) {
    if (CGM.getLangOpts().getGC() == LangOptions::GCOnly) {
    } else if (CGM.getLangOpts().ObjCAutoRefCount && !IsAtomic) {
      if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)
        Kind = Expression;
      else
        Kind = SetPropertyAndExpressionGet;
      return;
    } else if (!IsAtomic) {
      Kind = SetPropertyAndExpressionGet;
      return;
    } else {
      Kind = GetSetProperty;
      return;
    }
  }
  • objc_setProperty的整个查找流程如下: 截屏2021-06-22 18.35.07.png

验证

  • 从上述分析:Kind = GetSetProperty或者Kind=SetPropertyAndExpressionGet,系统会调用objc_setProperty,我们再来测试下:
// OC main.m
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;
@property (atomic, copy) NSString *hobby;
@property (atomic, strong) NSString *job;

@property (nonatomic, retain) NSString *rname;
@property (atomic, retain) NSString *rnickName;

@property (nonatomic) NSString *nonName;
@property (atomic) NSString *aName;

WSPerson定义这样的属性,然后编译成C++查看:


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

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

static NSString * _I_WSPerson_nickName(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_nickName)); }
static void _I_WSPerson_setNickName_(WSPerson * self, SEL _cmd, NSString *nickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_nickName)) = nickName; }

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

static NSString * _I_WSPerson_hobby(WSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct WSPerson, _hobby), 1); }
static void _I_WSPerson_setHobby_(WSPerson * self, SEL _cmd, NSString *hobby) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WSPerson, _hobby), (id)hobby, 1, 1); }

static NSString * _I_WSPerson_job(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_job)); }
static void _I_WSPerson_setJob_(WSPerson * self, SEL _cmd, NSString *job) { (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_job)) = job; }

static NSString * _I_WSPerson_rname(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_rname)); }
static void _I_WSPerson_setRname_(WSPerson * self, SEL _cmd, NSString *rname) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WSPerson, _rname), (id)rname, 0, 0); }

static NSString * _I_WSPerson_rnickName(WSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct WSPerson, _rnickName), 1); }
static void _I_WSPerson_setRnickName_(WSPerson * self, SEL _cmd, NSString *rnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct WSPerson, _rnickName), (id)rnickName, 1, 0); }

static NSString * _I_WSPerson_nonName(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_nonName)); }
static void _I_WSPerson_setNonName_(WSPerson * self, SEL _cmd, NSString *nonName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_nonName)) = nonName; }

static NSString * _I_WSPerson_aName(WSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_aName)); }
static void _I_WSPerson_setAName_(WSPerson * self, SEL _cmd, NSString *aName) { (*(NSString **)((char *)self + OBJC_IVAR_$_WSPerson$_aName)) = aName; }

也就是:

// 得到的结果
@property (nonatomic, copy) NSString *name; // objc_setProperty
@property (nonatomic, strong) NSString *nickName;
@property (atomic, copy) NSString *hobby; // objc_getProperty, objc_setProperty
@property (atomic, strong) NSString *job;

@property (nonatomic, retain) NSString *rname; // objc_setProperty
@property (atomic, retain) NSString *rnickName; // objc_getProperty, objc_setProperty
@property (atomic, copy) NSString *rhobby; // objc_getProperty objc_setProperty
@property (atomic, strong) NSString *rjob;

@property (nonatomic) NSString *nonName;
@property (atomic) NSString *aName;

结论:当属性用copyretain修饰时,会调用objc_setProperty

我们在刚才的打印中,发现有个objc_getProperty,这个的出现又和什么有关呢,我们来继续探究下

objc_getProperty

通过上面的经验,我们再次在llvm中查找:

  • 1.找到创建objc_getProperty处:
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 = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
    CanQualType Params[] = {
        IdType, SelType,
        Ctx.getPointerDiffType()->getCanonicalTypeUnqualified(), Ctx.BoolTy};
    llvm::FunctionType *FTy =
        Types.GetFunctionType(
          Types.arrangeBuiltinFunctionDeclaration(IdType, Params));
    return CGM.CreateRuntimeFunction(FTy, "objc_getProperty");
  }
  • 2.查找getGetPropertyFn
llvm::FunctionCallee GetPropertyGetFunction() override {
    return ObjCTypes.getGetPropertyFn();
  }
  • 3.再查找GetPropertyGetFunction,找到枚举:
case PropertyImplStrategy::GetSetProperty: {
    llvm::FunctionCallee getPropertyFn =
        CGM.getObjCRuntime().GetPropertyGetFunction();
        ...
}
  • 4.然后我们再去查看PropertyImplStrategykind赋值处kindGetSetProperty的地方和上面一致。

  • 5.整体流程:

截屏2021-06-22 18.59.11.png 根据objc_setProperty的测试,我们得出结论:

结论:当属性元子特性为atomic时,无论是copy还是retain修饰时,都会调用objc_setProperty

  • 总结如下图:

截屏2021-06-22 19.22.06.png

为什么要有元类

  • 在上一篇文章中,我们在实例方法里没有找到类方法,最后在元类实例方法找到了这个类方法。那么为什么会有元类呢?,我们来举个例子:
@interface WSPerson : NSObject {
    NSString *subject;
}

@property (nonatomic,strong) NSString *name;
@property (nonatomic,strong) NSString *nickName;

- (void)sayNB;
+ (void)sayNB;

@end

然后我们用lldb去一步步拿methods,得到结果:

(lldb) p $6.get(0).big()
(method_t::big) $7 = {
  name = "sayNB"
  types = 0x0000000100003f79 "v16@0:8"
  imp = 0x0000000100003cd0 (KCObjcBuild`-[WSPerson sayNB])
}

问题来了:sayNB到底是类方法还是实例方法?所以苹果为了解决这个坑,就创造了元类

  • 类方法其实也是实例方法,是元类的实例方法