OC 类原理探索:属性的底层原理

1,085 阅读5分钟

OC 类原理探索 系列文章

一、成员变量和属性

main.m中创建SSLPerson类:

@interface SSLPerson : NSObject {
    NSString *_hobby;
    NSObject *_obj;
    int _age;
}

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

@end

成员变量和实例变量

  • 成员变量:像_hobby_obj_age这些在花括号中的被称为成员变量;
  • 实例变量:像_hobby_obj这些属于NSObject类或者继承于NSObject类的又被称为实例变量`;

属性

  • nickNamenamecareer这些被@property修饰的称为属性。

获取类的属性和成员变量

下面这个方法可以打印类的所有成员变量属性

#ifdef DEBUG
#define SSLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## __VA_ARGS__] UTF8String]);
#else
#define SSLog(format, ...);
#endif

void logObjc_copyIvar_copyProperies(Class pClass){
    
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar const ivar = ivars[i];
        //获取实例变量名
        const char*cName = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:cName];
        SSLog(@"class_copyIvarList:%@",ivarName);
    }
    free(ivars);

    unsigned int pCount = 0;
    objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
    for (unsigned int i=0; i < pCount; i++) {
        objc_property_t const property = properties[i];
        //获取属性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        //获取属性值
        SSLog(@"class_copyProperiesList:%@",propertyName);
    }
    free(properties);
}

成员变量和属性的区别

clang -rewrite-objc main.m -o main.cpp编译main.m,得到main.cpp文件:

extern "C" unsigned long OBJC_IVAR_$_SSLPerson$_nickName;
extern "C" unsigned long OBJC_IVAR_$_SSLPerson$_name;
struct SSLPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_hobby;
    NSObject *_obj;
    int _age;
    NSString *_nickName;
    NSString *_name;
    NSString *_career;
};

// @property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, strong) NSString *name;
// @property (atomic, copy) NSString *career;

static NSString * _I_SSLPerson_nickName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_SSLPerson_setNickName_(SSLPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _nickName), (id)nickName, 0, 1); }

static NSString * _I_SSLPerson_name(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_name)); }
static void _I_SSLPerson_setName_(SSLPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_name)) = name; }

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

static NSString * _I_SSLPerson_career(SSLPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _career), 1); }
static void _I_SSLPerson_setCareer_(SSLPerson * self, SEL _cmd, NSString *career) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _career), (id)career, 1, 1); }
  • 成员变量_hobby_obj_age在底层只是单纯的成员变量,没有其他;
  • 属性nickNamenamecareer在底层被编译成了带_ 的成员变量和相应的gettersetter方法。

我们还发现了一些问题:

  • nickNamecareersetter方法用到了objc_setProperty函数赋值;
  • namesetter方法却用到了self + OBJC_IVAR_$_SSLPerson$_name内存平移的方式赋值;
  • 我们还看到了一些类型编码{"nickName", "@16@0:8", _I_SSLPerson_nickName}相关的东西,它们都代表什么呢?

接下来解决这些问题。

二、编码

官方类型编码表

image.png

查找方式:cmd + shift + 0 -> documentation -> 搜索ivar_getTypeEncoding -> 点击Type Encodings -> 官方编码地址

代码获取类型编码

void sslTypes(void){
    NSLog(@"char --> %s",@encode(char));
    NSLog(@"int --> %s",@encode(int));
    NSLog(@"short --> %s",@encode(short));
    NSLog(@"long --> %s",@encode(long));
    NSLog(@"long long --> %s",@encode(long long));
    NSLog(@"unsigned char --> %s",@encode(unsigned char));
    NSLog(@"unsigned int --> %s",@encode(unsigned int));
    NSLog(@"unsigned short --> %s",@encode(unsigned short));
    NSLog(@"unsigned long --> %s",@encode(unsigned long long));
    NSLog(@"float --> %s",@encode(float));
    NSLog(@"bool --> %s",@encode(bool));
    NSLog(@"void --> %s",@encode(void));
    NSLog(@"char * --> %s",@encode(char *));
    NSLog(@"id --> %s",@encode(id));
    NSLog(@"Class --> %s",@encode(Class));
    NSLog(@"SEL --> %s",@encode(SEL));
    int array[] = {1,2,3};
    NSLog(@"int[] --> %s",@encode(typeof(array)));
    typedef struct person{
        char *name;
        int age;
    }Person;
    NSLog(@"struct --> %s",@encode(Person));
    
    typedef union union_type{
        char *name;
        int a;
    }Union;
    NSLog(@"union --> %s",@encode(Union));

    int a = 2;
    int *b = {&a};
    NSLog(@"int[] --> %s",@encode(typeof(b)));
}

类型编码 实际分析

我们打开main.cpp文件,用下面代码进行分析:

{{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_SSLPerson_nickName},
{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_SSLPerson_setNickName_},
  • @16@0:8
    1. @ : id类型的返回值;
    2. 16 : 占用内存;
    3. @ : id类型的参数;
    4. 0 : 从0号位置开始;
    5. : : SEL
    6. 8 : 从8号位置开始,反推8 + 8 = 16
  • v24@0:8@16
    1. v : void
    2. 24 : 占用内存;
    3. @ : 第一个参数为id类型;
    4. 0 : 从0号位置开始;
    5. : : SEL
    6. 8 : 从8号位置开始;
    7. @ : 第二个参数为id类型;
    8. 16: 从16号位置开始,反推16 + 8 = 24

三、LLVM 探索属性方法

LLVM 探索 objc_setProperty

我们用LLVM探索,属性赋值什么情况会使用objc_setProperty,下载LLVM源码,用Visual Studio Code打开。

我们的探索思路是用反推法,先找到objc_setProperty函数的创建 -> 再找到调用objc_setProperty函数的地方 -> 查找为什么调用或者什么条件下调用函数 -> 探索到最终答案。

VS中搜索objc_setProperty,会看到很多的相关代码,我们慢慢找,最终会找到下面这段代码:

image.png

找到了函数的创建CGM.CreateRuntimeFunction(FTy, "objc_setProperty"),接下来我们看看什么地方调用了getSetPropertyFn

搜索getSetPropertyFn,寻找调用这个函数的地方:

image.png

GetPropertySetFunction()是个中间层代码,我们用它继续搜索:

image.png

  • 我们找到了函数的调用setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction();
  • 能否调用到这段代码由strategy.getKind()来决定,也就是PropertyImplStrategy类型。

我们先看一下PropertyImplStrategy的定义:

image.png

通过注释可以发现StrategyKindGetSetPropertySetPropertyAndExpressionGet时调用objc_setProperty,那么我们去初始化方法中查找一下kind的赋值情况。

image.png image.png image.png

找到关键方法IsCopyKind = GetSetProperty;,所以属性为copy时用objc_setProperty赋值,否则用内存平移的方式赋值。

另外一种探索方式:

image.png

结论:当修饰符为copy或者retain时,使用objc_setProperty方式赋值。

下面进行验证:

定义SSLPerson类:

@interface SSLPerson : NSObject

@property (nonatomic, copy) NSString *aName;
@property (atomic, copy) NSString *bName;
@property (nonatomic, strong) NSString *cName;
@property (atomic, strong) NSString *dName;

@end

clang -rewrite-objc main.m -o main.cpp编译main.m得到main.cpp文件:

static NSString * _I_SSLPerson_aName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_aName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_SSLPerson_setAName_(SSLPerson * self, SEL _cmd, NSString *aName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _aName), (id)aName, 0, 1); }

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

static NSString * _I_SSLPerson_bName(SSLPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _bName), 1); }
static void _I_SSLPerson_setBName_(SSLPerson * self, SEL _cmd, NSString *bName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _bName), (id)bName, 1, 1); }

static NSString * _I_SSLPerson_cName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_cName)); }
static void _I_SSLPerson_setCName_(SSLPerson * self, SEL _cmd, NSString *cName) { (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_cName)) = cName; }

static NSString * _I_SSLPerson_dName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_dName)); }
static void _I_SSLPerson_setDName_(SSLPerson * self, SEL _cmd, NSString *dName) { (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_dName)) = dName; }
  • 得到结果
    • aName使用nonatomiccopy修饰,使用objc_setProperty赋值。
    • bName使用atomiccopy修饰,使用objc_setProperty赋值。
    • cNamedName没有copy修饰,使用的内存平移赋值。
  • 结果符合我们的结论。

LLVM 探索 objc_getProperty

objc_getProperty的探索还是用到LLVM,用第二种方式进行探索。

在搜索objc_getProperty时发现了这种实现方式:

image.png

这种是拼接的方式,类似于我们编译的c++代码:

image.png

完整代码:

void RewriteModernObjC::RewritePropertyImplDecl(ObjCPropertyImplDecl *PID,
                                          ObjCImplementationDecl *IMD,
                                          ObjCCategoryImplDecl *CID) {
  // ...
  unsigned Attributes = PD->getPropertyAttributes();
  if (mustSynthesizeSetterGetterMethod(IMD, PD, true /*getter*/)) {
    // 判断是否生成objc_getProperty(
    bool GenGetProperty =
    // 不是nonatomic 且 用retain或copy修饰
        !(Attributes & ObjCPropertyAttribute::kind_nonatomic) &&
        (Attributes & (ObjCPropertyAttribute::kind_retain |
                       ObjCPropertyAttribute::kind_copy));
    std::string Getr;
    // ...
    Getr += "{ ";
    // Synthesize an explicit cast to gain access to the ivar.
    // See objc-act.c:objc_synthesize_new_getter() for details.
    if (GenGetProperty) { // 使用objc_getProperty
      // return objc_getProperty(self, _cmd, offsetof(ClassDecl, OID), 1)
      // 这里省略了返回类型_TYPE的处理...
      Getr += "return (_TYPE)";
      Getr += "objc_getProperty(self, _cmd, ";
      RewriteIvarOffsetComputation(OID, Getr);
      Getr += ", 1)";
    }
    else // 使用内存平移的方式取值
      Getr += "return " + getIvarAccessString(OID);
    Getr += "; }";
    InsertText(startGetterSetterLoc, Getr);
  }

  if (PD->isReadOnly() ||
      !mustSynthesizeSetterGetterMethod(IMD, PD, false /*setter*/))
    return;

  // Generate the 'setter' function.
  std::string Setr;
  // 判断是否生成setProperty
  bool GenSetProperty = 
  // 用retain或copy修饰
  Attributes & (ObjCPropertyAttribute::kind_retain |
                                      ObjCPropertyAttribute::kind_copy);
  // ...
  Setr += "{ ";
  // Synthesize an explicit cast to initialize the ivar.
  // See objc-act.c:objc_synthesize_new_setter() for details.
  if (GenSetProperty) { // 使用objc_setProperty
    Setr += "objc_setProperty (self, _cmd, ";
    RewriteIvarOffsetComputation(OID, Setr);
    Setr += ", (id)";
    Setr += PD->getName();
    Setr += ", ";
    if (Attributes & ObjCPropertyAttribute::kind_nonatomic) // 是否是nonatomic
      Setr += "0, ";
    else
      Setr += "1, ";
    if (Attributes & ObjCPropertyAttribute::kind_copy) // 是否是copy
      Setr += "1)";
    else
      Setr += "0)";
  }
  else { // 使用内存平移方式赋值
    Setr += getIvarAccessString(OID) + " = ";
    Setr += PD->getName();
  }
  Setr += "; }\n";
  InsertText(startGetterSetterLoc, Setr);
}

看关键代码:

image.png

得出结论,不是nonatomic且用retaincopy修饰时使用objc_getProperty

下面进行验证:

定义SSLPerson类:

@interface SSLPerson : NSObject

@property (nonatomic, copy) NSString *aName;
@property (atomic, copy) NSString *bName;
@property (nonatomic, strong) NSString *cName;
@property (atomic, strong) NSString *dName;

@end

clang -rewrite-objc main.m -o main.cpp编译main.m得到main.cpp文件:

static NSString * _I_SSLPerson_aName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_aName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_SSLPerson_setAName_(SSLPerson * self, SEL _cmd, NSString *aName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _aName), (id)aName, 0, 1); }

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

static NSString * _I_SSLPerson_bName(SSLPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _bName), 1); }
static void _I_SSLPerson_setBName_(SSLPerson * self, SEL _cmd, NSString *bName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct SSLPerson, _bName), (id)bName, 1, 1); }

static NSString * _I_SSLPerson_cName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_cName)); }
static void _I_SSLPerson_setCName_(SSLPerson * self, SEL _cmd, NSString *cName) { (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_cName)) = cName; }

static NSString * _I_SSLPerson_dName(SSLPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_dName)); }
static void _I_SSLPerson_setDName_(SSLPerson * self, SEL _cmd, NSString *dName) { (*(NSString **)((char *)self + OBJC_IVAR_$_SSLPerson$_dName)) = dName; }
  • 得到结果
    • bName使用atomiccopy修饰,使用objc_getProperty取值。
    • aNamecNamedName没有同时使用atomiccopy修饰,使用的内存平移取值。
  • 结果符合我们的结论。

总结

  • 使用retaincopy修饰属性时,用objc_setProperty方式赋值,否则使用内存平移的方式进行赋值。
  • 使用非nonatomic修饰,且用retaincopy修饰属性时,用objc_getProperty取值,否则使用内存平移的方式取值。