iOS底层 – 类的结构(下)

上一篇文章中我们探索了类的结构,留下了一个疑问就是class_ro_tclass_rw_t的区别,我们从这个区别开始。

class_ro_tclass_rw_t的区别

class_ro_t

class_ro_t存储了当前类在编译期就已经确定的属性方法和遵循的协议,里面没有category的方法。运行时添加的方法存储在运行时生成的class_rw_t中。

ro标示的是read only,是无法进行修改的,我们看一下它的定义:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };
    explicit_atomic<const char *> name;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;//方法列表的获取方法
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;//成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;//属性列表

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
    _objc_swiftMetadataInitializer swiftMetadataInitializer() const {
        if (flags & RO_HAS_SWIFT_INITIALIZER) {
            return _swiftMetadataInitializer_NEVER_USE[0];
        } else {
            return nil;
        }
    }
    const char *getName() const {
        return name.load(std::memory_order_acquire);
    }
    static const uint16_t methodListPointerDiscriminator = 0xC310;
#if 0 // FIXME: enable this when we get a non-empty definition of __ptrauth_objc_method_list_pointer from ptrauth.h.
        static_assert(std::is_same<
                      void * __ptrauth_objc_method_list_pointer *,
                      void * __ptrauth(ptrauth_key_method_list_pointer, 1, methodListPointerDiscriminator) *>::value,
                      "Method list pointer signing discriminator must match ptrauth.h");
#endif
    method_list_t *baseMethods() const {
#if __has_feature(ptrauth_calls)
        method_list_t *ptr = ptrauth_strip((method_list_t *)baseMethodList, ptrauth_key_method_list_pointer);
        if (ptr == nullptr)
            return nullptr;
        // Don't auth if the class_ro and the method list are both in the shared cache.
        // This is secure since they'll be read-only, and this allows the shared cache
        // to cut down on the number of signed pointers it has.
        bool roInSharedCache = objc::inSharedCache((uintptr_t)this);
        bool listInSharedCache = objc::inSharedCache((uintptr_t)ptr);
        if (roInSharedCache && listInSharedCache)
            return ptr;

        // Auth all other small lists.
        if (ptr->isSmallList())
            ptr = ptrauth_auth_data((method_list_t *)baseMethodList,
                                    ptrauth_key_method_list_pointer,
                                    ptrauth_blend_discriminator(&baseMethodList,
                                                                methodListPointerDiscriminator));
        return ptr;
#else
        return (method_list_t *)baseMethodList;
#endif
    }
	///省略代码
};
复制代码

class_rw_t

类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t中,它是可读可写的:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    Class firstSubclass;
    Class nextSiblingClass;
		///省略代码
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }
    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
};

复制代码

class_rw_t生成在运行时,在编译期间,class_ro_t结构体就已经确定,objc_class中的bitsdata部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtimerealizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

WWDC2020中类结构优化

二进制类在磁盘中的表现是:

截屏2021-06-21 上午9.49.58.png

类对象本身,包含最常访问的信息:指向元类,超类和方法缓存的指针,在类结构之中有指向包含更多数据的结构体class_ro_t的指针,包含了类的名称,方法,协议,实例变量等等编译期确定的信息。其中 ro 表示 read only 的意思。

当类被Runtime加载之后,类的结构会发生一些变化在了解这些变化之前,我们需要知道2个概念:

**Clean Memory:**加载后不会发生更改的内存块,class_ro_t属于Clean Memory,因为它是只读的。
**Dirty Memory:**运行时会进行更改的内存块,类一旦被加载,就会变成Dirty Memory,例如,我们可以在 Runtime 给类动态的添加方法。

Dirty MemoryClean Memory要昂贵的多,因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它。对于我们来说,越多的Clean Memory显然是更好的,因为它可以节约更多的内存。我们可以通过分离出永不更改的数据部分,将大多数类数据保留为Clean Memory,应该怎么做呢?

我们先看一下,类加载后的结构

截屏2021-06-21 上午9.50.15.png

在类加载到 Runtime 中后会被分配用于读取/写入数据的结构体class_rw_t

事实证明,class_rw_t会占用比class_ro_t占用更多的内存,在 iPhone 中,我们在系统测量了大约 30MB 的这些class_rw_t结构。应该如何优化这些内存呢?通过测量实际设备上的使用情况,我们发现大约 10% 的类实际会存在动态的更改行为,如动态添加方法,使用 Category 方法等。因此,我们能可以把这部分动态的部分提取出来,我们称之为class_rw_ext_t,所以,结构会变成这个样子。

截屏2021-06-21 上午9.50.41.png

经过拆分,可以把 90% 的类优化为Clean Memory,在系统层面,取得效果是节省了大约 14MB 的内存,使内存可用于更有效的用途。

更多内容可以查看苹果方法视频

小结

class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。

Type Encodings

在前面我们执行clangmain.m文件转换成main.cpp文件的时候,我们发现有一些编码,例如

{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_JSPerson_setNickName_},中的v24@0:8@16,这其实就是运行时中的encode编码,它的编码表如下:

截屏2021-06-20 20.05.54.png

苹果官方链接地址

v24@0:8@16的含义也就是:

  • v:void
  • 24:占用的内存
  • @: 对象类型参数self
  • 0:上面参数从0位置开始
  • :: SEL
  • 8:SEL8位置开始
  • @:对象类型,实际传入的第一个参数
  • 16:从16位置开始

setter方法底层

我们首先定义一个JSPerson类:

@interface JSPerson : NSObject
{
    NSString *hobby; // 
    int a;
    NSObject *objc;  // 
}

@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;

@property (nonatomic, strong) NSString *name;
@property (atomic, strong) NSString *aname;

@end

@implementation JSPerson


@end
复制代码

通过clang命令将其转换成转换成c++代码:

clang -rewrite-objc main.m -o main.cpp

我们在main.cpp全局搜索JSPerson,定位到类的方法的代码:

// @implementation JSPerson
static NSString * _I_JSPerson_nickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

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

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

static NSString * _I_JSPerson_acnickName(JSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), 1); }
static void _I_JSPerson_setAcnickName_(JSPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), (id)acnickName, 1, 1); }

static NSString * _I_JSPerson_nnickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)); }
static void _I_JSPerson_setNnickName_(JSPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)) = nnickName; }

static NSString * _I_JSPerson_anickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)); }
static void _I_JSPerson_setAnickName_(JSPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)) = anickName; }

static NSString * _I_JSPerson_name(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)); }
static void _I_JSPerson_setName_(JSPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)) = name; }

static NSString * _I_JSPerson_aname(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_aname)); }
static void _I_JSPerson_setAname_(JSPerson * self, SEL _cmd, NSString *aname) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_aname)) = aname; }
// @end
复制代码

可以看到,各个属性的getset方法如上,可以发现,不同属性的set方法执行的方法不一定相同,比如:

  • nameset方法:static void _I_JSPerson_setName_(JSPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)) = name; }
  • nickNameset方法:static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }

nameset方法内存平移nickNameset方法调用的是objc_setProperty ,这两种方式的是怎么确定的呢,什么情况下属性的set方法调用objc_setProperty方法呢?探究这个问题我们就需要使用LLVM了,我们从github上下载LLVM的源码,下载地址

LLVM源码里我们全局搜索objc_setProperty ,查找调用这个方法的地方,发现了getSetPropertyFn方法,它的返回值是CGM.CreateRuntimeFunction(FTy, "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");
  }
复制代码

接下来搜索关键字getSetPropertyFn(),同样是查找调用的地方:

  llvm::FunctionCallee GetPropertySetFunction() override {
    return ObjCTypes.getSetPropertyFn();
  }
复制代码

这里只是一个中间调用的方法,我们继续搜索关键字GetPropertySetFunction(),找调用的地方:

void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
                                        const ObjCPropertyImplDecl *propImpl,
                                        llvm::Constant *AtomicHelperFn) {
  //省略代码
  switch (strategy.getKind()) {
  case PropertyImplStrategy::Native: {
    // We don't need to do anything for a zero-size struct.
    if (strategy.getIvarSize().isZero())
      return;

    Address argAddr = GetAddrOfLocalVar(*setterMethod->param_begin());

    LValue ivarLValue =
      EmitLValueForIvar(TypeOfSelfObject(), LoadObjCSelf(), ivar, /*quals*/ 0);
    Address ivarAddr = ivarLValue.getAddress(*this);

    // Currently, all atomic accesses have to be through integer
    // types, so there's no point in trying to pick a prettier type.
    llvm::Type *bitcastType =
      llvm::Type::getIntNTy(getLLVMContext(),
                            getContext().toBits(strategy.getIvarSize()));

    // Cast both arguments to the chosen operation type.
    argAddr = Builder.CreateElementBitCast(argAddr, bitcastType);
    ivarAddr = Builder.CreateElementBitCast(ivarAddr, bitcastType);

    // This bitcast load is likely to cause some nasty IR.
    llvm::Value *load = Builder.CreateLoad(argAddr);

    // Perform an atomic store.  There are no memory ordering 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;
      }
    }
  }
复制代码

这里是个switch语句,调用的条件取决于strategy.getKind(),我们接下来就搜索PropertyImplStrategy找一下类型是什么时候设置的,在PropertyImplStrategy的定义中我们找到了答案:copy修饰的属性会有Kind = GetSetProperty,也就是set方法会调用objc_setProperty

  // 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;
  }
复制代码

我们写代码验证一下:

@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;
复制代码

我们定义四个属性,然后用clang将其转换成c++代码:

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

static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }//copy

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

static NSString * _I_JSPerson_acnickName(JSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), 1); }
static void _I_JSPerson_setAcnickName_(JSPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), (id)acnickName, 1, 1); }//copy

static NSString * _I_JSPerson_nnickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)); }
static void _I_JSPerson_setNnickName_(JSPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)) = nnickName; }//strong

static NSString * _I_JSPerson_anickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)); }
static void _I_JSPerson_setAnickName_(JSPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)) = anickName; }//strong
复制代码

小结

  • set方法在底层并不是定义了很多set方法调用,而是采用内存平移或调用objc_setProperty方法。
  • 使用copy修饰的属性的set方法调用的是objc_setProperty方法。
  • 没有copy修饰的属性的set方法是内存平移

isKindOfClassvsisMemberOfClass

我们平时开发经常会使用isKindOfClassisMemberOfClass方法来判断对象的类型,从一个例子开始。首先定义两个继承关系的类:

@interface JSPerson : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end
@implementation JSPerson
@end
@interface JSTeacher : JSPerson
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end
@implementation JSTeacher
- (void)teacherSay{
    NSLog(@"%s",__func__);
}
@end
复制代码

我们定义一个方法,用来打印方法的结果:

void jsKindofDemo(void){
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
    BOOL re3 = [(id)[JSPerson class] isKindOfClass:[JSPerson class]];       //
    BOOL re4 = [(id)[JSPerson class] isMemberOfClass:[JSPerson class]];     //
    NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
    BOOL re7 = [(id)[JSPerson alloc] isKindOfClass:[JSPerson class]];       //
    BOOL re8 = [(id)[JSPerson alloc] isMemberOfClass:[JSPerson class]];     //
    NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}
复制代码

打印结果如下:

 re1 :1
 re2 :0
 re3 :0
 re4 :0
 re5 :1
 re6 :1
 re7 :1
 re8 :1
复制代码

re5re8符合我们平时使用的思想,对象判断是否是类,re1re4是什么情况呢,我们看isKindOfClass底层源码:

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__//我们只看这里 objc2
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();//获取对象(类对象)的类(元类)
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {//遍历父类
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
复制代码

通过源码和上一篇中isa的走位和继承链,我们可以看出

  • 实例对象调用:1、获取对象的类。2、依次查找对象的类和父类,如果和传入的类相等返回YES,遍历结束未找到返回NO

    即查找顺序:对象的类->父类->根类(NSObject)->nil

  • 类对象:1、获取到类的元类。2、依次查找元类及元类的父类,如果和传入的类相等返回YES,遍历结束未找到返回NO

    即查找顺序:元类->元类父类->根元类->根类(NSObject)->nil

通过上面的结论,我们看上面的例子:

  • re1:传入是NSObject的类对象,首先找它的元类即根元类,根元类的父类是NSObject=传入的第二个参数,所以re1=1
  • re3:传入是JSPerson的类对象,首先找它的元类,依次找元类的父类到根元类,最后到根类,没有类=[JSPerson class],所以re3=0
  • re5:传入是NSObject的实例对象,找它的类就是NSObject=[NSObject class],所以re5=1
  • re7:传入是JSPerson的实例对象,找它的类就是JSPerson=[JSPerson class],所以re7=1

我们继续看isMemberOfClass的源码

//类方法
+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}
//实例方法
- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
复制代码

可以看出:

  • 实例对象:判断实例对象的类和传入的类是否相等,即是否相等
  • 类对象:判断类的元类是否和传入的类相等,即看元类

通过上面结论我们继续看例子:

  • re2:传入是NSObject的类对象,首先找它的元类即根元类,根元类不是NSObject类,所以re2=0
  • re4:传入是JSPerson的类对象,首先找它的元类,元类!=[JSPerson class],所以re4=0
  • re6:传入是NSObject的实例对象,找它的类就是NSObject=[NSObject class],所以re6=1
  • re8:传入是JSPerson的实例对象,找它的类就是JSPerson=[JSPerson class],所以re8=1

本文我们主要研究了类的rorw的区别,属性的set方法,以及isKindOfClassvsisMemberOfClass的源码,类的探究就到这里了,有遗漏的话后面会补充。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享