iOS5 类结构优化

297 阅读7分钟

一,WWDC2020对runtime的优化

  • 视频的观看地址:developer.apple.com/videos/play… (最好用Safari浏览器打开)

  • LLVM源码地址:github.com/apple/llvm-…
    看完视频后总结:本次改动不需要改动任何代码,也不用学习新的API,这次主要是runtime关于内存的优化。在这种环境下我们不用改APP也会运行得比之前更快更高效。

  • 类结构

  1. Metaclass
  2. Superclass
  3. Flags
  4. Method cache

二, Clean Memory 和 Dirty Memory

  • Clean Memory

  • clean memory 加载后不会发生改变的内存

  • class_ro_t 就属于clean memory,因为它是只读的不会,不会对齐内存进行修改

  • clean memory 是可以进行移除的,从而节省更多的内存空间,因为如果你有需要clean memory ,系统可以从磁盘中重新加载

  • class_ro_t 内存结构

  1. Flags
  2. Size
  3. Name
  4. Methods
  5. Protocols
  6. Ivars
  7. Properties
  • Dirty Memory

  • Dirty Memory指的是在程序运行中会发生改变的内存。

  • 类的结构一经使用就会变成Dirty Memory,因为运行时会向它写入新的数据。例如往类中添加方法又或者加载类的子类父类

  • 这里指的是class_rw_t

class_rw_t 结构

  1. Flags
  2. First Subclass
  3. Next Sibling Class
  4. Methods
  5. Properties
  6. Protocols
  7. Demangled Name
  • First Subclass、Next Subling Class:包含了运行时才会生成的信息First Subclass、Next Subling Class,所有的类都会变成一个树状结构,就是通过First SubclassNext Subling Class指针实现的,它允许运行时遍历当前使用的所有类
  • Demangled Name:这个字段使用的频率是比较少的,swift中才会使用。

总结

dirty memory要比clean memory更有价值而且要多,只要进行运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory,这样才能不断的提高程序的性能。

class_rw_t 优化

当一个类首次被使用时,runtime会为它分配额外的存储容量,运行时分配的存储容量就是class_rw_tclass_rw_t用于读取-编写数据,在这个数据结构中存储的是只有运行时才会生成的新数据 图解如下

问题:为什么方法属性class_ro_t中时,class_rw_t还要有方法属性呢?
  • 因为它们可以在运行时进行更改
  • category被加载时,它可以向类中添加新的方法
  • 通过runtime API手动向类中添加属性方法
  • class_ro_t 是只读的,所以我们需要在class_rw_t中来跟踪这些东西
拆分class_rw_t,提取其中的clean memory

在读取-编写属性和方法的时候,只有10%的类都需要修改或者添加的,那么90%类可以说是不被修改的,那么就可以对class_rw_t进行拆分 拆分如图

25800336-95529f19fe1f9f81.webp

结果:这样class_rw_t的大小会减少一半

对那些需要修改内存的,需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。图解如下

25800336-e3b654b98b289cd2.webp

总结

  • 当有类使用了category的时候,那么此时的类就有了class_rw_t的结构,如果未使用分类,那么类就是一个单纯的class_ro_t的结构。
  • 类结构的优化其实最要是分离出class_ro_tclass_rw_t优化,其实就是对class_rw_t不常用的部分进行了剥离。如果需要用到这部分就从扩展记录中分配一个,滑到类中供其使用

二,变量

成员变量和实例变量

  • Objective-C中写在类声明的大括号中的变量称之为成员变量,例如int aNSObject *obj
  • 成员变量用于类内部,无需与外界接触的变量

实例变量

  • 变量的数据类型不是基本数据类型且是一个则称这个变量为实例变量,例如 NSObject *obj
  • 成员变量包含实例变量

成员变量和属性的区别

  • 成员变量:在底层没有其他操作只是变量的声明
  • 属性:系统会自动在底层添加了_属性名变量,同时生成settergetter方法

属性存放

  • 探索源码
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;

三,编码

SELIMP关系

  • SEL:方法编号
  • IMP:函数指针地址

官方类型编码

a7cd79b382f5454e845b9b746a0d913b_tplv-k3u1fbpfcp-watermark.png

  • Objective-C 不支持long double类型,@encode(long double)返回d,和double类型的编码值一样

  • 类型编码图的获取途径:打开xcode--> command+shift+0--> 搜索ivar_getTypeEncoding--> 点击Type Encodings

2021-07-31 22:40:39.479316+0800 alloc_探索[3177:69006] char --> c
2021-07-31 22:40:39.479534+0800 alloc_探索[3177:69006] int --> i
2021-07-31 22:40:39.479708+0800 alloc_探索[3177:69006] short --> s
2021-07-31 22:40:39.479851+0800 alloc_探索[3177:69006] long --> q
2021-07-31 22:40:39.480077+0800 alloc_探索[3177:69006] long long --> q
2021-07-31 22:40:39.480296+0800 alloc_探索[3177:69006] float --> f
2021-07-31 22:40:39.480427+0800 alloc_探索[3177:69006] double --> d

  • 对比表中和返回值和编码类型一致

四,objc_setProperty 和 内存偏移

  • 定义NBPerson类
@interface NBPerson : NSObject
{
    NSString * newName;
    NSObject * objc;
}

@property(nonatomic,  copy)NSString * name;
@property(nonatomic,strong)NSString * nickName;
@property(nonatomic,assign)NSInteger age;
@end

@implementation NBPerson

@end
  • 生成.cpp文件,并查找。
// @implementation NBPerson


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

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

static NSString * _I_NBPerson_nickName(NBPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_NBPerson$_nickName)); }
static void _I_NBPerson_setNickName_(NBPerson * self, SEL _cmd, NSString *nickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_NBPerson$_nickName)) = nickName; }

static NSInteger _I_NBPerson_age(NBPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_NBPerson$_age)); }
static void _I_NBPerson_setAge_(NBPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_NBPerson$_age)) = age; }
// @end

  • LWName属性底层是通过objc_setProperty实现的,LWNicknameage是通过内存偏移实现的 查找 llvm源码 llvm-project-next/clang/lib/CodeGen/CGObjCMac.cpp找到代码
  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");
  }
  • CGM.CreateRuntimeFunction(FTy, "objc_setProperty"),创建了objc_setProperty方法。由下层往上层推理,全局搜索getSetPropertyFn()找到源码
  llvm::FunctionCallee GetPropertySetFunction() override {
    return ObjCTypes.getSetPropertyFn();
  }
  • 在搜索GetPropertySetFunction()在CGObject.cpp中找到
PropertyImplStrategy strategy(CGM, propImpl);
  switch (strategy.getKind()) {
  case PropertyImplStrategy::Native: {//代码省略---
  }

  case PropertyImplStrategy::GetSetProperty:
  case PropertyImplStrategy::SetPropertyAndExpressionGet: {

    llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
    llvm::FunctionCallee setPropertyFn = nullptr;
    if (UseOptimizedSetter(CGM)) {//代码省略---
    }
    else {
      setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction();
      if (!setPropertyFn) {
        CGM.ErrorUnsupported(propImpl, "Obj-C setter requiring atomic copy");
        return;
      }
    }
  • 根据switch条件PropertyImplStrategy类型调用GetPropertySetFunction()
  • PropertyImplStrategy类型有两种GetSetProperty或者SetPropertyAndExpressionGet,下一步只要知道什么时候给策略赋值
/// Pick an implementation strategy for the given property synthesis.
PropertyImplStrategy::PropertyImplStrategy(CodeGenModule &CGM,
                                     const ObjCPropertyImplDecl *propImpl) {
  const ObjCPropertyDecl *prop = propImpl->getPropertyDecl();
  ObjCPropertyDecl::SetterKind setterKind = prop->getSetterKind();

  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;

  // If we have a copy property, we always have to use setProperty.
  // If the property is atomic we need to use getProperty, but in
  // the nonatomic case we can just use expression.
  if (IsCopy) {
    Kind = IsAtomic ? GetSetProperty : SetPropertyAndExpressionGet;
    return;
  }
// Handle retain.
  if (setterKind == ObjCPropertyDecl::Retain) {
    // In GC-only, there's nothing special that needs to be done.
    if (CGM.getLangOpts().getGC() == LangOptions::GCOnly) {
      // fallthrough

    // In ARC, if the property is non-atomic, use expression emission,
    // which translates to objc_storeStrong.  This isn't required, but
    // it's slightly nicer.
    } else if (CGM.getLangOpts().ObjCAutoRefCount && !IsAtomic) {
      // Using standard expression emission for the setter is only
      // acceptable if the ivar is __strong, which won't be true if
      // the property is annotated with __attribute__((NSObject)).
      // TODO: falling all the way back to objc_setProperty here is
      // just laziness, though;  we could still use objc_storeStrong
      // if we hacked it right.
      if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)
        Kind = Expression;
      else
        Kind = SetPropertyAndExpressionGet;
      return;

    // Otherwise, we need to at least use setProperty.  However, if
    // the property isn't atomic, we can use normal expression
    // emission for the getter.
    } else if (!IsAtomic) {
      Kind = SetPropertyAndExpressionGet;
      return;

    // Otherwise, we have to use both setProperty and getProperty.
    } else {
      Kind = GetSetProperty;
      return;
    }
  }
  • LLVM源码流程:objc_setProperty -> getSetPropertyFn -> GetPropertySetFunction -> PropertyImplStrategy -> IsCopy(判断copy关键字)
  • 结论:无论属性是否是原子性还是非原子性的,用到copy关键字修饰的属性setter方法底层都用objc_setProperty实现,strong关键字无法通过最后得判断,需要通过首地址+内存偏移的方式实现。