OC 类探索(二)

一、WWDC关于runtime的优化

WWDC2020-10163

1.1类数据结构的变化

1.1.1 class on disk

image.png
  • 类对象本身包含最常访问的信息:元类,超类和方法缓存的指针。还有指向更多数据的结构体 class_ro_t 的指针。
  • class_ro_t拥有类的名称,方法,协议,实例变量等编译期确定的信息。
  • Swift类和OC类共享这一基础结构。

ro 表示 Read Onlyrw表示Read Write

1.1.2 class in memory

当类被第一次加载进内存的时候他们的结构也是这样,一旦被使用就有了clean memorydirty memory

  • clean memory:在程序运行时会不会发生改变的内存。class_ro_t属于Clean Memory(只读)。
  • dirty memory:在程序运行时会发生更改的内存。类结构一经使用就会变成dirty memory。例如,可以在 Runtime 给类动态的添加方法。

dirty memoryclean memory昂贵的多,进程一经运行它就必须一直存在。clean memory可以进行移除从而节省更多内存空间。如果需要clean memory系统可以从磁盘中重新加载。
macOS可以选择换出dirty memoryiOS不使用swap。所以在iOSdirty memory代价很大。dirty memory是类数据被分成两部分的原因。
可以通过分离出永不更改的数据部分,将大多数类数据保留为 Clean Memory

在类加载到 Runtime 中后,才会分配用于读取/写入数据的结构体 class_rw_t,此时类的结构会变为:

image.png

  • class_ro_t:只读,存放的是编译期间就确定的字段信息。
  • class_rw_t:读写,在 Runtime 时期才创建,会先将class_ro_t的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去。

为什么这么设计?
因为 Objective-C 是动态语言,可以在运行时更改方法,属性等。并且分类可以在不改变类设计的前提下,将新方法添加到类中。

class_rw_t 会占用比 class_ro_t 更多的内存,在视频中苹果给的测试数据是大概10%的类实际会存在动态的更改行为(动态添加方法、使用 Category 方法等)。

1.1.3 变化

所以苹果将动态的部分提取出来,称为class_rw_ext_t,这个时候结构如下:

image.png

这样就将class_rw_t拆分出了部分。对于真正需要扩展数据的类会保留上面的结构,对于不需要的如下:
image.png

相当于没有class_rw_ext_t这部分数据。这样经过拆分,可以把 90% 的类优化为 Clean Memory,在系统层面节省了内存。
验证:

➜  ~ heap WeChat | egrep 'class_rw|COUNT'
   COUNT      BYTES       AVG   CLASS_NAME                                        TYPE    BINARY
    2970      95040      32.0   Class.data (class_rw_t)                           C       libobjc.A.dylib
     287      13776      48.0   Class.data.extended (class_rw_ext_t)              C       libobjc.A.dylib

2970class_rw_t但是实际上只有287个需要class_rw_ext_t

小结:经过拆分class_rw_t为两部分:class_rw_t + class_rw_ext_t 。对于需要扩展数据的类会保留class_rw_ext_t,对于不需要的不会生成class_rw_ext_t部分数据。这些数据保存在class_ro_t中。这样就将类中数据优化为了Clean Memory

1.2相对方法地址优化

1.2.1 method lists

在认知中每个类都有一个方法列表,以便Runtime查找和消息发送,结构如下:

image.png

每个方法包含三部分:

  • methodName/Selector:方法名称或选择器。
  • 方法类型编码(method type encoding):方法类型编码标识。
  • IMP:方法实现的函数指针。

64 位系统中,一个方法占用了 24 字节的空间:

image.png

这个时候寻址如下:
image.png

库的地址取决于动态链接库加载之后的位置(ASLR),而动态链接器需要修正真实的指针地址,也是一种代价。

1.2.2 relative method lists

由于方法地址仍旧在当前二进制地址空间区域内,所以方法列表并不需要使用 64 位的寻址范围空间,它们仅需要根据自身地址以及偏移量就可以找到其他方法位置。所以可以使用32位相对偏移来代替绝对 64 位地址。优化后方法与内存地址的寻址:

image.png

这么做的好处:

  • 加载后不需要进行修正指针地址:无论将库加载到内存中的任何位置,偏移量始终是相同的。
  • 可以保存在只读存储器中,更加安全。
  • 使用 32 位偏移量在 64 位平台上所需的内存量减少了一半。
image.png

1.2.3 Swizzling relative method lists

⚠️:相对方法地址会存在另外一个问题, Method Swizzling 如何处理呢?
Method Swizzling 替换的是 2 个函数指针的指向。函数可以在任意地方实现,但使用了上述的相对寻址优化之后,Method Swizzling就无法工作了。
官方给出的解决方案是全局映射表,在映射表中维护 Swizzles 方法对应的实现函数指针地址。由于 Method Swizzling 的操作并不常见,所以这个表不会变得很大,新的 Method Swizzling 如下:

image.png

macOS Big SuriOS14tvOS14watchOS7

这个时候如果直接访问底层数据会将两个32位地址作为一个64位地址访问,还是需要通过API去访问。

image.png

小结:

  • 相对方法地址加载后不需要进行指针修正,更加安全,占用内存量减半。
  • 对于Method Swizzling提供了全局映射表解决地址交换问题。

1.3 Tagged Pointer Format Changes

1.3.1Tagged Pointer是什么?

Tagged Pointer 是一种特殊标记的对象,通过在其最后一个 bit 位设置为特殊标记位,并将数据直接保存在指针自身中。
64 位系统中,有 64 位空间可以表示一个对象指针。由于内存对齐,通常没有真正使用到所有这些位。对象必须位于指针大小倍数的地址中,低位和高位均被 0 填充,因此只用到了中间部分的位,出现了大量的内存浪费

image.png

基于上面的问题,按照 Tagged Pointer 的思路,可以将低位设置为 1 加以区分。
image.png

并且可以在最低位之后的 3 位,赋予类型意义(在剩余的字段中,记录所包含的数据)。3 位可以表示 7 种数据类型:

OBJC_TAG_NSAtom = 0, 
OBJC_TAG_1 = 1, 
OBJC_TAG_NSString = 2, 
OBJC_TAG_NSNumber = 3, 
OBJC_TAG_NSIndexPath = 4, 
OBJC_TAG_NSManagedObjectID = 5, 
OBJC_TAG_NSDate = 6, 
OBJC_TAG_7 = 7

Intel x86架构中,Tagged Pointer 对象的表示如下:

image.png

OBJC_TAG_7类型的Tagged Pointer是个例外,它可以将后 8 位作为扩展字段。基于此可以多支持 256 种类型的 Tagged Pointer,如 UIColorsNSIndexSets之类的对象。

image.png
// Tagged pointer layout and usage is subject to change on different OS versions.

// Tag indexes 0..<7 have a 60-bit payload.
// Tag index 7 is reserved.
// Tag indexes 8..<264 have a 52-bit payload.
// Tag index 264 is reserved.

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,
    OBJC_TAG_NSMethodSignature = 20,
    OBJC_TAG_UTTypeRecord      = 21,

    // When using the split tagged pointer representation
    // (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where
    // the tag and payload are unobfuscated. All tags from here to
    // OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache
    // builder is able to construct these as long as the low bit is
    // not set (i.e. even-numbered tags).
    OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set

    OBJC_TAG_Constant_CFString = 136,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263,

    OBJC_TAG_RESERVED_264      = 264
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

arm64中结构如下:

image.png

当为OBJC_TAG_7类型时:
image.png

arm64中:最高位代表 Tagged Pointer 标识位,次 3 位标识 Tagged Pointer 的类型,接下去的位来表示包含的数据(可能还有扩展类型字段)(iOS13中是这样的)

ARM64 中,为什么要用最高位代表的 Tagged Pointer 标记,而不与 Intel x86 一样使用低位标记?
实际是对 objc_msgSend 的微小优化,在对objc_msgSend查找指针时候的一个case。在最高位能一次排查Tagged Pointer 指针nil两种类型,节省了一个case的逻辑。

具体内容可以查看:WWDC2013 - Advances in Objective-C

1.3.2Tagged Pointer 优化点

iOS14中最高位仍然是标志位(msg_send),将tag位挪到了最后面:

image.png

why?
对于普通指针动态链接会忽略指针的前8位。这是由于Top Byte IgnoreARM特性。对于一个对齐指针,底部3位总是0,那么为添加7(扩展标签)以将低位设置为1
这意味着实际上可将下面的指针放入一个扩展标签指针的Paylaod中:

image.png

image.png

这样就开启了tagged pointer的引用二进制文件中的常量数据的能力。例如字符串或其他数据结构,这样就不占用dirty memory内存了。

⚠️:使用苹果提供的API访问底层数据结构。

小结:

  • tag标记位从高位挪到了低位。
  • flagExtended不变,仍然在高位。flag在高位是为了优化objc_msgSend查找效率(一次同时排查Target pointer与nil
  • tag放入低位是因为对于一个对齐指针低3位总是0,这样做了后可以将真正的指针放入Payload中,这样就不占用dirty memory内存空间了。

二、Objective-C type encodings

2.1 Objective-C type encodings

Objective-C type encodings官网

Code Meaning
c A char
i An int
s A short
l A long
l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type...} A structure
(name=type...) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)

⚠️Objective-C does not support the long double type. @encode(long double) returns d, which is the same encoding as for double.

2.2 Objective-C method encodings

runtime系统为类型限定符提供了另外的编码:

Code Meaning
r const
n in
N inout
o out
O bycopy
R byref
V oneway

可以通过API获取编码:

ivar_getTypeEncoding(Ivar  _Nonnull)
method_getTypeEncoding(<#Method  _Nonnull m#>)

2.3代码验证

苹果还提供了一个指令@encode,可以将具体的类型表示成字符串编码:

void HPEncodeTypes(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 HPStruct{
        char *name;
        int age;
    }hpStruct;
    NSLog(@"struct --> %s",@encode(hpStruct));
    
    typedef union HPUnion{
        char *name;
        int a;
    }hpUnion;
    NSLog(@"union --> %s",@encode(hpUnion));

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

输出:

char --> c
int --> i
short --> s
long --> q
long long --> q
unsigned char --> C
unsigned int --> I
unsigned short --> S
unsigned long --> Q
float --> f
bool --> B
void --> v
char * --> *
id --> @
Class --> #
SEL --> :
int[] --> [3i]
struct --> {HPStruct=*i}
union --> (HPUnion=*i)
int[] --> ^i

在方法编码中不仅有编码还有数字,那么他们代表什么意思呢?

  • @16@0:8@代表id类型返回值,16代表所占用的总内存,@默认参数id self0代表self0号位置开始,:代表SEL 8代表SEL8号位置开始。
  • v24@0:8@16v代表返回值void24代表所占总内存,@默认参数id self0代表self0号位置开始,:代表SEL 8代表SEL8号位置开始。@代表第三个参数id类型,16代表从16号位置开始。

三、成员变量和属性

上篇文章分析了属性存储在class_rw_tproperties()中,成员变量存储在ro()中。

  • 成员变量:包含在类的{}中的变量。
  • 实例变量:实例变量是特殊的成员变量,对象类型就是实例变量。普通的成员变量就指基本数据类型了。NSString修饰的是成员变量不是实例变量因为本质上它是字符串,底层不是结构体。
  • 属性:用@property修饰的。

3.1 成员变量、属性、实例变量

那么他们之间有什么关系呢?这就需要我们用clang查看下底层结构了。
原代码如下:

#import 

@interface HPObject : NSObject {
    NSString *sex;
    NSObject *obj;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *name1;
@property (atomic, strong) NSString *name2;
@property (atomic, copy) NSString *name3;

@end


@implementation HPObject

@end

转换后如下:

#ifndef _REWRITER_typedef_HPObject
#define _REWRITER_typedef_HPObject
typedef struct objc_object HPObject;
typedef struct {} _objc_exc_HPObject;
#endif

extern "C" unsigned long OBJC_IVAR_$_HPObject$_name;
extern "C" unsigned long OBJC_IVAR_$_HPObject$_name1;
extern "C" unsigned long OBJC_IVAR_$_HPObject$_name2;
struct HPObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *sex;
    NSObject *obj;
    NSString *_name;
    NSString *_name1;
    NSString *_name2;
    NSString *_name3;
};


// @property (nonatomic, copy) NSString *name;
// @property (nonatomic, strong) NSString *name1;
// @property (atomic, strong) NSString *name2;
// @property (atomic, copy) NSString *name3;
  • 底层已经没有属性了,生成了对应带下划线的成员变量。
  • HPObject继承了NSObjectisa

继续往下看可以看到:

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

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

static NSString * _I_HPObject_name1(HPObject * self, SEL _cmd) {
    return (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name1));
}
static void _I_HPObject_setName1_(HPObject * self, SEL _cmd, NSString *name1) {
    (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name1)) = name1;
}

static NSString * _I_HPObject_name2(HPObject * self, SEL _cmd) {
    return (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name2));
}
static void _I_HPObject_setName2_(HPObject * self, SEL _cmd, NSString *name2) {
    (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name2)) = name2;
}

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

static NSString * _I_HPObject_name3(HPObject * self, SEL _cmd) {
    typedef NSString * _TYPE;
    return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct HPObject, _name3), 1);
}
static void _I_HPObject_setName3_(HPObject * self, SEL _cmd, NSString *name3) {
    objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct HPObject, _name3), (id)name3, 1, 1);
}
  • 属性还生成了对应的gettersetter方法。
  • _I_HPObject_setName__I_HPObject_setName3_是通过objc_setProperty去设置值的,而_I_HPObject_setName1__I_HPObject_setName2_是通过内存平移进行赋值的。

所以这里为什么会有objc_setProperty与内存平移赋值两种形式?在后面会具体分析。

⚠️:属性 = 带下划线成员变量 + setter + getter 方法

3.2 代码验证属性与成员变量

可以通过runtimeAPI获取属性和成员变量进行验证:

void HPObjc_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];
        printf("\n%s",[ivarName UTF8String]);
    }
    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)];
        printf("\n%s",[propertyName UTF8String]);
    }
    free(properties);
}

结果:

sex
obj
_name
_name1
_name2
_name3
name
name1
name2
name3

与查看底层结构结果一致。

四、setter&getter的底层原理

上面分析道setter方法有objc_setProperty与内存平移赋值两种形式?
setter方法的本质是对内存区域赋值,所有的上层调用调用到底层都是同样的原理。显示底层对每一个setter都实现一次不现实,所以就有了个中间层objc_setProperty。在ivar入口处会对sel->imp重定向到objc_setProperty。在编译时期就已经处理完毕。

4.1 LLVM源码分析

4.1.1 objc_setProperty

llvm源码中直接搜索objc_setProperty,在CGObjCMac.cpp中会搜索到类似下面的代码:

    const char *name;
    if (atomic && copy)
      name = "objc_setProperty_atomic_copy";
    else if (atomic && !copy)
      name = "objc_setProperty_atomic";
    else if (!atomic && copy)
      name = "objc_setProperty_nonatomic_copy";
    else
      name = "objc_setProperty_nonatomic";

llvm::FunctionCallee getSetPropertyFn()方法中最终返回了:

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

继续搜索getSetPropertyFn的调用方:

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

GetPropertySetFunction是一个中间层,继续查找发现在CGObjC.cppCodeGenFunction::generateObjCSetterBody中发现调用了:

image.png

是否调用是根据PropertyImplStrategy类型来决定的。类型定义如下:

    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是什么时候赋值的呢?在PropertyImplStrategy中有如下代码:

PropertyImplStrategy(CodeGenModule &CGM,
                         const ObjCPropertyImplDecl *propImpl);

查看它的实现:


image.png
  • copy
  • retain + nonatomic
  • retain + atomic

结论:当属性使用copy/retain修饰的时候底层会调用objc_setProperty,默认是走的native分支(内存平移)

验证:

//objc_setProperty
@property (nonatomic,copy) NSString *name3;
@property (nonatomic,retain) NSString *name4;
@property (atomic,copy) NSString *name5;
@property (atomic,retain) NSString *name6;

//不能有objc_setProperty
@property (atomic,strong) NSString *name7;
@property (nonatomic,strong) NSString *name8;
image.png

符合预期。

4.1.2 objc_getProperty

既然有set那么对应的就有get

  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");
  }

最终定位到void CodeGenFunction::generateObjCGetterBody方法中:

case PropertyImplStrategy::GetSetProperty: {
    llvm::FunctionCallee getPropertyFn =
        CGM.getObjCRuntime().GetPropertyGetFunction()

也是GetSetProperty分支。

image.png

  • copy
  • retain + atomic

结论:copy/retain + atomic修饰的情况下会调用objc_getProperty,否则内存平移。(真的是这样么?)
那么直接验证下:

@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *name1;
@property (nonatomic,retain) NSString *name2;
@property (atomic,retain) NSString *name3;
@property (nonatomic, strong) NSString *name4;
@property (atomic, strong) NSString *name5;

@end

@implementation HPObject

@end
image.png

验证发现只有atomic + copy/atomic + retain修饰的属性才走objc_getProperty逻辑,否则是内存平移。为什么与分析的源码结果不一致?

image.png

在源码中有如下错误信息说明需要atomic + copy,条件是getPropertyFn为空。再继续看类型赋值的逻辑有如下代码:
image.png

这样就说明对于这块是有特殊逻辑的。
之后又在源码中发现了以下代码:
image.png

说明atomic + copy/retain就是objc_getProperty。这就与验证结果一致了。

同样的set:

image.png

同样的set需呀copy/retain修饰才调用objc_setProperty,否则内存平移,这里也就验证了。

@interface HPSubObject : HPObject

//objc_getProperty
@property (atomic,copy) NSString *name1;
@property (atomic,retain) NSString *name2;

//objc_setProperty
@property (nonatomic,copy) NSString *name3;
@property (nonatomic,retain) NSString *name4;
@property (atomic,copy) NSString *name5;
@property (atomic,retain) NSString *name6;

//不能有objc_setProperty
@property (atomic,strong) NSString *name7;
@property (nonatomic,strong) NSString *name8;

//不能有objc_getProperty
@property (nonatomic,copy) NSString *name9;
@property (nonatomic,retain) NSString *name10;

@end

clang验证:

image.png

验证通过。

小结:

  • 对于set方法来说,copy/retain修饰的属性会重定向到objc_setProperty的实现,否则是内存平移。
  • 对于get方法来说,atomic + copy/atomic +retain修饰的属性会重定向到objc_getProperty的逻辑,否则是内存平移。

4.1.3 真机验证

上面是通过clang编译来验证的,那么在真机情况下是否也这样呢?

@interface HPObject : NSObject

//objc_getProperty + objc_getProperty
@property (atomic,copy) NSString *name1;
@property (atomic,retain) NSString *name2;

//有objc_setProperty,不能有objc_getProperty
@property (nonatomic,copy) NSString *name3;
@property (nonatomic,retain) NSString *name4;

//不能有objc_setProperty以及objc_getProperty
@property (atomic,strong) NSString *name5;
@property (nonatomic,strong) NSString *name6;

@end

使用手机运行,然后打符号断点。要打以下符号断点:

objc_setProperty_atomic_copy
objc_setProperty_atomic
objc_setProperty_nonatomic_copy
objc_setProperty_nonatomic
objc_setProperty
objc_getProperty

验证结果:

image.png

验证结果与clang验证差异点:

  • nonatomic + copy会走get
  • nonatomic + retain会走set
  • atomic + strong会走set + get

代码验证结论:atomic/copy修饰的属性会走objc_setProperty + objc_getProperty,否则是内存平移。

4.1.4 为什么clang与真机验证结果不一致?

首先需要明白一点的是clang验证的结论是通过llvm源码分析得出来的,而clang就是llvm编译后的产物,所以这两者肯定能对的上。

image.png

StrategyKindllvm中涉及到类型赋值如上图所示4部分,得到的确定结论是copy/atomic会生成get + set
那么retain/strong nonatomic的逻辑set是怎么被过滤掉的。

源码中有以下代码:


image.png

意味着要调用objc_getProperty就要先进行声明,但是clang的调用在声明之前:

image.png

猜测是执行clang代码生成器的时候导致的。

总结

  • clang
    • copy/retain会调用objc_setProperty,否则内存平移。
    • copy/retain + atomic会调用objc_getProperty,否则内存平移。
  • 真机:atomic/copy会调用objc_setProperty + objc_getProperty,否则内存平移。(以真机为准)。

4.2 objc源码分析

通过llvm分析已经知道了内存平移和set/getProperty的区别,那么为什么要这么做呢?

4.2.1 objc_setProperty

objc_setProperty

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

底层调用的是copyWithZone,所以copy方法是需要拷贝内存的。所以这块
才做了区分。如果直接内存平移的话就是对原始值的修改,而copy是需要开辟新内存的。

4.2.2 objc_getProperty

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

可以看到内部有加锁操作。所以要求atomic修饰。

五、API方式解析类方法存储

5.1 class_copyMethodList 获取类的方法列表

void HP_MethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        printf("\nMethod Name:%s",[key UTF8String]);
    }
    free(methods);
}

调用:

printf("\n类的方法:");
HP_MethodList(HPObject.class);
printf("\n元类的方法:");
HP_MethodList(object_getClass(HPObject.class));

输出:

类的方法:
Method Name:additions1InstanceMethod
Method Name:instanceMethod
Method Name:additions2InstanceMethod
Method Name:name
Method Name:.cxx_destruct
Method Name:setName:
Method Name:age
Method Name:setAge:
元类的方法:
Method Name:additions1ClassMethod
Method Name:additions2ClassMethod
Method Name:classMethod

5.2 方法验证

5.2.1 实例方法验证

void HP_InstanceMethodVerify(Class pClass) {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    Method method1 = class_getInstanceMethod(pClass, @selector(instanceMethod));
    Method method2 = class_getInstanceMethod(metaClass, @selector(instanceMethod));

    Method method3 = class_getInstanceMethod(pClass, @selector(classMethod));
    Method method4 = class_getInstanceMethod(metaClass, @selector(classMethod));
    printf("%p-%p-%p-%p",method1,method2,method3,method4);
}

调用:

HP_InstanceMethodVerify(HPObject.class);

输出:

0x1000080e0-0x0-0x0-0x1000080c0

说明类有instanceMethod方法,元类有classMethod方法。

5.2.2 类方法验证

void HP_ClassMethodVerify(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(instanceMethod));
    Method method2 = class_getClassMethod(metaClass, @selector(instanceMethod));
    Method method3 = class_getClassMethod(pClass, @selector(classMethod));
    Method method4 = class_getClassMethod(metaClass, @selector(classMethod));
    
    printf("%p-%p-%p-%p",method1,method2,method3,method4);
}

调用:

HP_ClassMethodVerify(HPObject.class);

输出:

0x0-0x0-0x1000080c8-0x1000080c8

这个结果的第4个为什么也有呢?按照前面的验证方式元类类方法在元类中应该是实例方法,那么它就不应该存在类方法classMethod

元类为什么有类方法呢?
既然结果不符合预期那就查看下源码:

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

Class getMeta() {
    if (isMetaClassMaybeUnrealized()) return (Class)this;
    else return this->ISA();
}

可以看到class_getClassMethod的底层调用的是class_getInstanceMethod。如果cls本身是元类则使用cls本身调用,否则就找cls的元类。

⚠️:在底层没有类方法,都是实例方法。所谓的加减号只是为了上层区分而已。

实例对象之间没有关系,类才有继承关系。对象没有。

5.3 IMP验证

void HP_IMPVerify(Class pClass) {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);

    IMP imp1 = class_getMethodImplementation(pClass, @selector(instanceMethod));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethod));// 0
    // sel -> imp 方法的查找流程
    IMP imp3 = class_getMethodImplementation(pClass, @selector(classMethod)); // 0
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethod));
    printf("%p-%p-%p-%p",imp1,imp2,imp3,imp4);
}

调用:

HP_IMPVerify(HPObject.class);

输出:

0x100003be0-0x1002eb640-0x1002eb640-0x100003bd0

按理解imp2imp3是不应该存在的,为什么这里存在并且他们还相同。这里其实就涉及到消息转发了,返回的是_objc_msgForwardimp

(lldb) p _objc_msgForward
(void (*)()) $23 = 0x00000001002eb640 (libobjc.A.dylib`_objc_msgForward)

class_getMethodImplementation源码实现如下:

__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    lockdebug_assert_no_locks_locked_except({ &loadMethodLock });

    imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}
  • imp没有实现的时候就直接返回了_objc_msgForward。会进行消息转发。

六、 isKindOf

有一道经典面试题如下:

void KindOfTest(void) {
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL re3 = [(id)[HPObject class] isKindOfClass:[HPObject class]];
    BOOL re4 = [(id)[HPObject class] isMemberOfClass:[HPObject class]];
    NSLog(@" re1 :%d\n re2 :%d\n re3 :%d\n re4 :%d\n",re1,re2,re3,re4);

    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
    BOOL re7 = [(id)[HPObject alloc] isKindOfClass:[HPObject class]];
    BOOL re8 = [(id)[HPObject alloc] isMemberOfClass:[HPObject class]];
    NSLog(@" re5 :%d\n re6 :%d\n re7 :%d\n re8 :%d\n",re5,re6,re7,re8);
}

要解答这道题,首先要清楚isKindOfClassisMemberOfClass的实现。
源码如下:
isKindOfClass

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isMemberOfClass

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

class

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

  • - isKindOfClass:获取类对象,循环获取父类判断是否与传进来的参数是否相同。
    ret5 NSObject实例对象的class返回NSObject类对象,类对象cls相同,返回YES
    ret7 HPObject实例对象的class返回HPObject类对象,类对象cls相同,返回YES

  • + isKindOfClass:获取类对象的isa,循环判断父类是否与传进来的参数相同。
    ret1 [NSObject class]返回的是NSObject类对象本身。cls参数也是类对象本身。tcls是元类。元类一直找到根元类,最终找到NSObject类(跟元类继承自NSObject)。所以返回YES
    ret3 cls参数是HPObject类。HPObject元类一直找到根元类的父类NSObject也与cls不等。返回NO

  • - isMemberOfClass:判断类对象是否和参数相同。
    ret6 实例对象class返回NSObject类对象,与cls相同,返回YES
    ret8 实例对象class返回HPObject类对象,与cls相同,返回YES

  • + isMemberOfClass:判断类对象的isa是否和参数相同。
    ret2 NSObject元类 不等于 NSObject类,返回NO
    ret4 HPObject元类 不等于HPObject类,返回NO

所以输出应该为:YES、NO、NO、NO、YES、YES、YES、YES
验证:

 re1 :1
 re2 :0
 re3 :0
 re4 :0
 re5 :1
 re6 :1
 re7 :1
 re8 :1

结果与分析的一致,那么逻辑真的是这样的么?源码调试一下。
发现isKindOfClass调用的是objc_opt_isKindOfClass

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    //元类
    Class cls = obj->getIsa();
    //// class or superclass has default new/self/class/respondsToSelector/isKindOfClass
    if (fastpath(!cls->hasCustomCore())) {//cache中有缓存
        //这里与+ isKindOfClass 相同
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    //objc1走objc_msgSend
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
  • + - isKindOfClass底层调用的都是objc_opt_isKindOfClass
  • 获取调用方的isa(循环获取父类一直到nil)与参数比较是否相等。对象返回类与参数比较,类返回元类与参数比较。
  • 这里会判断缓存,做了优化。

class实际调用的是objc_opt_class:

// Calls [obj class]
Class
objc_opt_class(id obj)
{
#if __OBJC2__
    if (slowpath(!obj)) return nil;
   //获取isa
    Class cls = obj->getIsa();
    if (fastpath(!cls->hasCustomCore())) {//cache中有缓存
   //是元类返回obj,否则返回cls
        return cls->isMetaClass() ? obj : cls;
    }
#endif
    return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}
  • + - class底层调用的都是objc_opt_class
  • 获取调用方的isa判断是否是元类,是元类返回调用方,否则返回cls。也就是说对象调用返回的是类,类调用返回的也是类。这也就是class方法的本质返回类。
  • 这里会判断缓存,做了优化。

isMemberOfClass的调用仍然是+ isKindOfClass- isKindOfClass

⚠️ classisKindOfClass的底层实现再次说明:在底层没有类方法,都是实例方法。

小节:

  • + - isKindOfClass底层实现是objc_opt_isKindOfClass,对象调用时是类与参数比较,类调用时元类与参数比较。(在这个过程中会找父类直到->NSObject->nil
  • + - class底层调用的都是objc_opt_class,无论谁调用都是返回类
  • + - isMemberOfClass调用的仍然是+ - isKindOfClass,对象调用是类与参数比较,类调用是元类与参数的比较。由于isMemberOfClass只是进行值比较不需要进行优化,所以底层没有重绑定
  • 在底层没有类方法,都是实例方法。

你可能感兴趣的:(OC 类探索(二))