IOS底层探索运行时类的变化

250 阅读6分钟

类的数据结构在运行时中的变化

WWDC 2020视频中有详细介绍

Class on disk

图片.png 在磁盘上,App二进制文件中类如图中MYClass一样,包含指向元类、超类、方法缓存的指针以及指向存储额外信息的指针class_ro_tro代表只读,它包括像类的名称、方法、协议、实例变量的信息,SwiftObjective-C共享这一数据结构。

Class in memory (Clean Memory & Dirty Memory)

图片.png

  • Clean Memory是指加载后不会发生更改的内存,class_ro_t就是Clean Memory,它是只读的。
  • Dirty Memory是指在进程运行时发生更改的内存,类结构一经使用就会变成Dirty Memory,因为运行时会向它写入新的数据,例如创建一个新的方法缓存并从类中指向它。
  • Dirty MemoryClean Memory要昂贵得多,只要进程在运行,它就必须一直存在。
  • Clean Memory可以进行移除从而节省更多的内存空间,因为如果你需要Clean Memory系统可以从磁盘中重新加载。
  • MacOS可以选择换出Dirty Memory 但因为iOS不使用swap所以Dirty MemoryiOS中的代价很大。

当一个类首次被使用,运行时会为他分配额外的存储容量,这个运行时分配的存储容量是class_rw_t用于读取-编写数据。 图片.png 例如所有的类都会链接生成一个树状结构,这是通过使用First SubclassNext Sibling Class 指针实现的,这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。

为什么方法和属性在只读数据class_ro_t中,class_rw_t还要方法和属性呢?

因为它们可以在运行时更改。当Category被加载时 他可以向类中添加新的方法,因为class_ro_t是只读的,我们需要在class_rw_t中追踪这些东西。

但是class_rw_t占用的是Dirty Memory,这样做的话会占用相当多的内存。如何缩小这些结构呢?

可以拆掉那些平时不用的部分,这样class_rw_t就减少了一半。 图片.png 对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它划到类中供其使用。大约90%的类从来不需要这些扩展数据。

图片.png 注:只有Swift类会使用这个Demangled Name字段,并且Swift类并不需要这一字段,除非有东西询问他们的Objective-C名称时才需要。

setter方法的两种方式

@interface ABPerson : NSObject
{
    //成员变量
    NSString *hobby;
    //实例变量(特殊的成员变量)
    NSObject *objc;
}
@property (nonatomic,copy) NSString *nickName;
@property (nonatomic,strong) NSString *name;
@property (atomic,strong) NSString *aname;
@end

@implementation ABPerson

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        ABPerson *p = [ABPerson alloc];
        NSLog(@"Hello, World!");
    }
    return 0;
}

编译成.cpp文件命令:

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

ABPerson结构体

struct ABPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; //ISA
	NSString *hobby;
	NSObject *objc;
	NSString *_nickName;
	NSString *_name;
	NSString *_aname;
};

在结构体中,属性前面被加上了下划线,成员变量没有变。

nickNamesetter方法:

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

namesetter方法:

static void _I_ABPerson_setName_(ABPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_ABPerson$_name)) = name; }

对比两个属性的setter方法,nickNamesetter方法会调用objc_setProperty,而namesetter是直接通过内存平移赋值。

objc_setProperty

objc_setProperty相当于一个封装,将寻找属性的代码封装起来,再调用具体的底层实现。

那么当调用setter方法为什么会定位到objc_setProperty呢?这可能是在编译阶段就完成的工作。

打开LLVM工程搜索objc_setProperty

图片.png 往上依次找方法的调用 图片.png 查看GetSetPropertyFn的实现 图片.png 返回了setPropertyFn

图片.png 在这里被调用,那么调用的条件是什么呢?

void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
                                        const ObjCPropertyImplDecl *propImpl,
                                        llvm::Constant *AtomicHelperFn) {
  省略部分代码
  PropertyImplStrategy strategy(CGM, propImpl);//获取策略
  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
 }

条件是找到匹配的策略调用相应的case

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(CodeGenModule &CGM,
                                     const ObjCPropertyImplDecl *propImpl) {
 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;
 //当是copy是使用GetSetProperty策略
  if (IsCopy) {
    Kind = GetSetProperty;
    return;
  }
 省略部分代码

当修饰属性为copy时会调用objc_setProperty

方法编码

main.cpp文件中,我们看到了:

图片.png@16@0:8为例,符号依次介绍: @:是id类型,是返回值类型 16:这个编码所占用内存数 @:方法参数,指方法的默认参数是id self 0:代表self0号位置开始 ::代表SEL 8:代表SEL8号位置开始

不清楚对应符号什么意思可以查询Objective-C type encodings

图片.png

面试题isKindOfClass & isMemberOfClass

        BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       
        BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     
        BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       
        BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];     
        NSLog(@"\n 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)[LGPerson alloc] isKindOfClass:[LGPerson class]];       
        BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     
        NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

isKindOfClass会调用objc_opt_isKindOfClass 图片.png

//x为真得可能性比较大
#define fastpath(x) (__builtin_expect(bool(x), 1)) 
//x为假的可能性比较大
#define slowpath(x) (__builtin_expect(bool(x), 0))

BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
//objc2.0使用下面实现
#if __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
//不满足调整,调用isKindOfClass
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

分析:re1

 BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
 obj是NSObject的类对象
 cls是NSObject的元类,即根元类
 tcls根元类的父类就是NSObject
 otherClass是NSObject
 所以 tcls == otherClass 为YES
  

分析:re3

 BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; 
 obj是LGPerson的类对象
 cls是LGPerson的元类
 tcls是元类的父类就是NSObject的元类,即根元类
 otherClass是LGPerson
 所以 tcls == otherClass 为NO
  

分析:re5

 BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
 obj是NSObject的实例对象
 cls是NSObject的类对象
 tcls=cls即NSObject的类对象
 otherClass是NSObject
 所以 tcls == otherClass 为YES
  

分析:re7

 BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];
 obj是LGPerson的实例对象
 cls是LGPerson的类对象
 tcls=cls即LGPerson的类对象
 otherClass是LGPerson
 所以 tcls == otherClass 为YES
  

分析:re2、 re4

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

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
 BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; 
 NSObject的元类是根元类和NSObject不是一个东西,返回NO
 同理:
 re4也是NO

分析:re6

 BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; 
 NSObject实例对象的类对象就是NSObject
 所以re6为YES

分析:re8

 re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];
 LGPerson实例对象的类对象就是LGPerson
 所以re8为YES

打印结果:

图片.png

总结 BOOL

  • 类在被首次使用时,运行时会给其分配存储容量class_rw_t,用于读取编写数据,通过拆分class_rw_t,将不常用的放到class_rw_ext_t,当需要的时候加进去,从而达到减少内存的作用
  • setter方法有两种方式,一种是通过内存平移赋值,一种是通过调用objc_setProperty
  • 当修饰属性为copy时会调用objc_setProperty