Swift进阶 02:类、对象与属性

2021-01-27  本文已影响0人  源本平凡

本文主要对以下几点进行介绍:

SIL

在底层流程中,OC代码和Swift代码是通过不同的编译器进行编译,然后通过LLVM,生成.o可执行文件,流程如下图所示

Swift与OC编译流程图

下面是Swift中的编译流程,其中SIL(Swift Intermediate Language)是Swift编译过程中的中间代码,主要用于进一步分析和优化Swift代码。如下图所示,SIL位于在ASTLLVM IR之间

Swift编译流程

注意:Swift与OC的区别在于Swift生成了高级的SIL

我们可以通过swiftc -h终端命令,查看swiftc的所有命令

swiftc命令

如果想要详细对SIL的内容进行探索,可以参考这个视频,也可以参考这个文档

例如:在main.swift文件定义如下代码

class SunriseTeacher {
    var age: Int = 18
    var name: String = "Sunrise"
}

let sunrise = SunriseTeacher()
抽象语法树
// main
//`@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
//`%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  //`alloc_global`:创建一个`全局变量`,即代码中的`sunrise`
  alloc_global @$s4main7sunriseAA14SunriseTeacherCvp // id: %2
  //`global_addr`:获取全局变量地址,并赋值给寄存器%3
  %3 = global_addr @$s4main7sunriseAA14SunriseTeacherCvp : $*SunriseTeacher // user: %7
  //`metatype`获取`SunriseTeacher`的`MetaData`赋值给%4
  %4 = metatype $@thick SunriseTeacher.Type       // user: %6
  //将`__allocating_init`的函数地址赋值给 %5
  // function_ref SunriseTeacher.__allocating_init()
  %5 = function_ref @$s4main14SunriseTeacherCACycfC : $@convention(method) (@thick SunriseTeacher.Type) -> @owned SunriseTeacher // user: %6
  //`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
  %6 = apply %5(%4) : $@convention(method) (@thick SunriseTeacher.Type) -> @owned SunriseTeacher // user: %7
  //将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
  store %6 to %3 : $*SunriseTeacher               // id: %7
  //构建`Int`,并`return
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  return %9 : $Int32                              // id: %10
} // end sil function 'main'

注意:code命令生效是在.zshrc文件中添加如下配置:

alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'
混淆代码还原
// s4main14SunriseTeacherCACycfC 实际就是__allocating_init()
// SunriseTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main14SunriseTeacherCACycfC : $@convention(method) (@thick SunriseTeacher.Type) -> @owned SunriseTeacher {
// %0 "$metatype"
bb0(%0 : $@thick SunriseTeacher.Type):
  // 堆上分配内存空间
  %1 = alloc_ref $SunriseTeacher                  // user: %3
  // function_ref SunriseTeacher.init() 初始化当前变量
  %2 = function_ref @$s4main14SunriseTeacherCACycfc : $@convention(method) (@owned SunriseTeacher) -> @owned SunriseTeacher // user: %3
  // 返回
  %3 = apply %2(%1) : $@convention(method) (@owned SunriseTeacher) -> @owned SunriseTeacher // user: %4
  return %3 : $SunriseTeacher                     // id: %4
} // end sil function '$s4main14SunriseTeacherCACycfC'
符号断点调试
_ _allocating_init swift_allocObject

源码调试

下面我们就通过swift_allocObject来探索Swift中对象的创建过程

创建实例对象 实例化断点断住

swift_allocObject 源码分析

swift_allocObject的源码如下,主要有以下几部分:

static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask));

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata); // 初始化一个实例对象

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
  void *p;
  // This check also forces "default" alignment to use AlignedAlloc.
  if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
    p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
    p = malloc(size); // 堆中创建size大小的内存空间,用于存储实例变量
#endif
  } else {
    size_t alignment = (alignMask == ~(size_t(0)))
                           ? _swift_MinAllocationAlignment
                           : alignMask + 1;
    p = AlignedAlloc(size, alignment);
  }
  if (!p) swift::crash("Could not allocate memory.");
  return p;
}
HeapObject初始化方法 RefCounts
总结

针对上面分析,我们还遗留了两个问题:metadata是什么,40是怎么计算的?下面来继续探索

在demo中,我们可以通过Runtime方法获取类的内存大小

类对象内存

这点与在源码调试时左边LocalrequiredSize值是相等的,从HeapObject的分析中我们知道了,一个类在没有任何属性的情况下,默认占用16字节大小

对于Int、String类型,进入其底层定义,两个都是结构体类型,那么是否都是8字节呢?可以通过打印其内存大小来验证

//********* Int底层定义 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}

//********* String底层定义 *********
@frozen public struct String {...}

//********* 验证 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)

//********* 打印结果 *********
8
16

从打印的结果中可以看出,Int类型占8字节,String类型占16字节(后面文章会进行详细讲解),这点与OC中是有所区别的

所以这也解释了为什么SunriseTeacher的内存大小等于40,即 40 = metadata(8字节) + refCount(8字节)+ Int(8字节)+ String(16字节)

这里验证了40的来源,但是metadata是什么还不知道,继续往下分析

HeapMetadata类型分析

下面就来分析metadata,看看它到底是什么?

using HeapMetadata = TargetHeapMetaData<Inprocess>;
//模板类型
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  //初始化方法
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
//******** TargetMetaData 定义 ********
struct TargetMetaData{
  using StoredPointer = typename Runtime: StoredPointer;
   ...
   
   StoredPointer kind;
}

//******** Inprocess 定义 ********
struct Inprocess{
   ...
   using StoredPointer = uintptr_t;
   ...
}

//******** uintptr_t 定义 ********
typedef unsigned long uintptr_t;

TargetHeapMetadata、TargetMetaData定义中,均可以看出初始化方法中参数kind的类型是MetadataKind

name value
Class 0x0
Struct 0x200
Enum 0x201
Optional 0x202
ForeignClass 0x203
Opaque 0x300
Tuple 0x301
Function 0x302
Existential 0x303
Metatype 0x304
ObjCClassWrapper 0x305
ExistentialMetatype 0x306
HeapLocalVariable 0x400
HeapGenericLocalVariable 0x500
ErrorObject 0x501
LastEnumerated 0x7FF
const TargetClassMetadata<Runtime> *getClassObject() const;
 
//******** 具体实现 ********
template<> inline const ClassMetadata *
  Metadata::getClassObject() const {
    //匹配kind
    switch (getKind()) {
      //如果kind是class
    case MetadataKind::Class: {
      // Native Swift class metadata is also the class object.
      //将当前指针强转为ClassMetadata类型
      return static_cast<const ClassMetadata *>(this);
    }
    case MetadataKind::ObjCClassWrapper: {
      // Objective-C class objects are referenced by their Swift metadata wrapper.
      auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
      return wrapper->Class;
    }
    // Other kinds of types don't have class objects.
    default:
      return nullptr;
    }
  }

这一点,我们可以通过lldb来验证

metadata数据信息

所以,TargetMetadataTargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是TargetClassMetadata

template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
   ...
   // swift特有的标志
   ClassFlags Flags;
   // 实例对象内存大小
   uint32_t InstanceSize;
   // 实例对象内存对齐方式
   uint16_t InstanceAlignMask;
   // 运行时保留字段
   uint16_t Reserved;
   // 类的内存大小
   uint32_t ClassSize;
   // 类的内存首地址
   uint32_t ClassAddressPoint;
 ...
}
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    ...
    ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
    TargetPointer<Runtime, void> CacheData[2];
    StoredSize Data;
    ...
}

总结

综上所述,当metadatakindClass时,有如下继承链:

类结构继承链
struct swift_class_t: NSObject{
   void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
   void *superClass;
   void *cacheData;
   void *data;
   uint32_t flags; //4字节
   uint32_t instanceAddressOffset;//4字节
   uint32_t instanceSize;//4字节
   uint16_t instanceAlignMask;//2字节
   uint16_t reserved;//2字节
   
   uint32_t classSize;//4字节
   uint32_t classAddressOffset;//4字节
   void *description;
   ...
}
与OC对比

Swift属性

在swift中,属性主要分为以下几种

存储属性

存储属性,又分两种:

定义如下代码

class SunriseTeacher {
    var age: Int = 18
    var name: String = "Sunrise"
}

let sunrise = SunriseTeacher()

其中代码中的age、name来说,都是变量存储属性,这一点可以在SIL中体现

class SunriseTeacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

存储属性特征:会占用分配实例对象的内存空间

下面我们同断点调试来验证

属性内存分布 HeapObject

计算属性

计算属性:是指不占用内存空间,本质是set/get方法的属性

我们通过一个demo来说明,以下写法正确吗?

class SunriseTeacher {
    var age: Int{
        get{
            return 18
        }
        set{
            age = newValue
        }
    }
}

在实际编程中,编译器会报以下警告,其意思是在age的set方法中又调用了age.set

计算属性-1

然后运行发现崩溃了,原因是age的set方法中调用age.set导致了循环引用,即递归

计算属性-2
验证:不占内存

对于其不占用内存空间这一特征,我们可以通过以下案例来验证,打印以下类的内存大小

class Square{
    var width: Double = 8.0
    var area: Double{
        get{
            //这里的return可以省略,编译器会自动推导
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

print(class_getInstanceSize(Square.self))

//********* 打印结果 *********
24

从结果可以看出类Square的内存大小是24,等于 (metadata + refCounts)类自带16字节 + width(8字节) = 24,是没有加上area的。从这里可以证明area属性没有占有内存空间

验证:本质是set/get方法
class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}
计算属性-3

属性观察者(didSet、willSet)

验证
class SunriseTeacher {
    var name: String = "旧值-wrs"{
        // 新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}

let sunrise = SunriseTeacher()
sunrise.name = "新值-sun"

//**********打印结果*********
willSet newValue 新值-sun
didSet oldValue 旧值-wrs
属性观察者-1
问题1:init方法中是否会触发属性观察者?

以下代码中,init方法中设置name,是否会触发属性观察者?

class SunriseTeacher {
    var name: String = "旧值-wrs"{
        // 新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    init() {
        self.name = "sunrise"
    }
}

运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:

总结:初始化器(即init方法设置)和定义时设置默认值(即在didSet中调用其他属性值)都不会触发

问题2:哪里可以添加属性观察者?

主要有以下三个地方可以添加:

class SunriseMediumTeacher: SunriseTeacher{
    override var age: Int{
        // 新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
class SunriseTeacher {
    var age: Int = 18
    var age2: Int{
        get{
            18
        }
        set{
            self.age = newValue
        }
    }
}

let sunrise = SunriseTeacher()

class SunriseMediumTeacher: SunriseTeacher{
    override var age: Int{
        // 新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    override var age2: Int{
        // 新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
问题3:子类和父类的计算属性同时存在didset、willset时,其调用顺序是什么?

有以下代码,其调用顺序是什么?

class SunriseTeacher {
    var age: Int = 18{
        // 新值存储之前调用
        willSet{
            print("父类 willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("父类 didSet oldValue \(oldValue)")
        }
    }
    
    var age2: Int{
        get{
            18
        }
        set{
            self.age = newValue
        }
    }
     
}

class SunriseMediumTeacher: SunriseTeacher{
    override var age: Int{
        // 新值存储之前调用
        willSet{
            print("子类 willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
    override var age2: Int{
        // 新值存储之前调用
        willSet{
            print("子类 willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
}
 
let sunrise = SunriseMediumTeacher()
sunrise.age = 20

//**********打印结果*********
子类 willSet newValue 20
父类 willSet newValue 20
父类 didSet oldValue 18
子类 didSet oldValue 18

总结:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,在父类didset, 子类的didset,即:子父 父子

问题4:子类调用了父类的init,是否会触发观察属性?

在问题3的基础,修改SunriseMediumTeacher类

class SunriseMediumTeacher: SunriseTeacher{
    override var age: Int{
        // 新值存储之前调用
        willSet{
            print("子类 willSet newValue \(newValue)")
        }
        // 新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
    override init() {
        
        super.init()
        self.age = 19
    }
}

//**********打印结果*********
子类 willSet newValue 19
父类 willSet newValue 19
父类 didSet oldValue 18
子类 didSet oldValue 18

从打印结果发现,会触发属性观察者,主要是因为子类调用了父类init,已经初始化过了,而初始化流程保证了所有属性都有值(即super.init确保变量初始化完成了),所以可以观察属性了

延迟属性

延迟属性主要有以下几点说明:

1、使用lazy修饰的存储属性
class SunriseTeacher{
    lazy var age: Int = 18
}
2、延迟属性必须有一个默认的初始值

如果定义为可选类型,则会报错,如下所示

延迟属性-1
3、延迟存储在第一次访问的时候才被赋值

可以通过调试,来查看实例变量的内存变化

延迟属性-2 延迟属性-3

从而可以验证,懒加载存储属性只有在第一次访问时才会被赋值

我们也可以通过sil文件来查看,这里可以在生成sil文件时,加上还原swift中混淆名称的命令(即xcrun swift-demangle):swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && code main.sil,demo代码如下

class SunriseTeacher{
    lazy var age: Int = 18
}

var sunrise = SunriseTeacher()

sunrise.age = 20
延迟属性-4 延迟属性-5

通过sil,有以下两点说明:

print(MemoryLayout<Optional<Int>>.stride)
print(MemoryLayout<Optional<Int>>.size)

//*********** 打印结果 ***********
16
9

为什么实际大小是9?Optional其本质是一个enum,其中Int8字节,另一个字节主要用于存储case值(这个后续会详细讲解)

4、延迟存储属性并不能保证线程安全

继续分析3中sil文件,主要是查看age的getter方法,如果此时有两个线程:

5、延迟存储属性对实例对象大小的影响

下面来继续看下不使用lazy的内存与使用lazy的内存是否有变化?

延迟属性-6 延迟属性-7

从而可以证明,使用lazy和不使用lazy,其实例对象的内存大小是不一样的

类型属性

类型属性,主要有以下几点说明:

1、使用关键字static修饰
class SunriseTeacher{
    static var age: Int = 18
}

// **** 使用 ****
var age = SunriseTeacher.age

生成SIL文件

类型属性-1 类型属性-2 类型属性-3 类型属性-4 类型属性-5
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
2、类型属性必须有一个默认的初始值

如下图所示,如果没有给默认的初始值,会报错

类型属性-6

所以对于类型属性来说,一是全局变量,只初始化一次,二是线程安全

单例的创建

//****** Swift单例 ******
class SunriseTeacher{
    //1、使用 static + let 创建声明一个实例对象
    static let shareInstance = SunriseTeacher.init()
    //2、给当前init添加private访问权限
    private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = SunriseTeacher.shareInstance

//****** OC单例 ******
@implementation SunriseTeacher
+ (instancetype)shareInstance{
    static SunriseTeacher *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[SunriseTeacher alloc] init];
    });
    return shareInstance;
}
@end

总结

上一篇 下一篇

猜你喜欢

热点阅读