Swift-02:类、对象、属性

2021-03-04  本文已影响0人  恍然如梦_b700

本文主要介绍以下几点

SIL

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

image

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

image

注意:这里需要说明一下,Swift与OC的区别在于 Swift生成了高级的SIL

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

image

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

class CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

var t = CJLTeacher()

// 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`:创建一个`全局变量`,即代码中的`t`
  alloc_global @$s4main1tAA10CJLTeacherCvp        // id: %2
//`global_addr`:获取全局变量地址,并赋值给寄存器%3
  %3 = global_addr @$s4main1tAA10CJLTeacherCvp : $*CJLTeacher // user: %7
//`metatype`获取`CJLTeacher`的`MetaData`赋值给%4
  %4 = metatype $@thick CJLTeacher.Type           // user: %6
//将`__allocating_init`的函数地址赋值给 %5
  // function_ref CJLTeacher.__allocating_init()
  %5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %6
//`apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
  %6 = apply %5(%4) : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher // user: %7
//将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
  store %6 to %3 : $*CJLTeacher                   // 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中做了如下配置,可以在终端中指定软件打开相应文件

$ open .zshrc
//****** 添加以下别名
alias subl='/Applications/SublimeText.app/Contents/SharedSupport/bin/subl'
alias code='/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code'

//****** 使用
$ code main.sil

//如果想SIL文件高亮,需要安装插件:VSCode SIL

//********* main入口函数中代码 *********
%5 = function_ref @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher 

// s4main10CJLTeacherCACycfC 实际就是__allocating_init()
// CJLTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main10CJLTeacherCACycfC : $@convention(method) (@thick CJLTeacher.Type) -> @owned CJLTeacher {
// %0 "$metatype"
bb0(%0 : $@thick CJLTeacher.Type):
// 堆上分配内存空间
  %1 = alloc_ref $CJLTeacher                      // user: %3
  // function_ref CJLTeacher.init() 初始化当前变量
  %2 = function_ref @$s4main10CJLTeacherCACycfc : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %3
  // 返回
  %3 = apply %2(%1) : $@convention(method) (@owned CJLTeacher) -> @owned CJLTeacher // user: %4
  return %3 : $CJLTeacher                         // id: %4
} // end sil function '$s4main10CJLTeacherCACycfC'

SIL语言对于Swift源码的分析是非常重要的,关于其更多的语法信息,可以在这个网站进行查询

符号断点调试

源码调试

下面我们就通过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;
}

总结

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

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

image

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

对于IntString类型,进入其底层定义,两个都是结构体类型,那么是否都是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中是有所区别的

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

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

探索Swift中类的结构

在OC中类是从objc_class模板继承过来的,具体的参考这篇文章iOS-底层原理 08:类 & 类结构分析

而在Swift中,类的结构在底层是HeapObject,其中有 metadata + refCounts

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来验证

所以,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;
    ...
}

总结

综上所述,当metadatakind为Class时,有如下继承链:

image
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 CJLTeacher{
    var age: Int = 18
    var name: String = "CJL"
}

let t = CJLTeacher()

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

class CJLTeacher {
    //_hasStorage 表示是存储属性
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

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

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

计算属性

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

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

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

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

image

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

image

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

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()
}

属性观察者(didSet、willSet)

验证

class CJLTeacher{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}
var t = CJLTeacher()
t.name = "CJL"

//**********打印结果*********
willSet newValue CJL
didSet oldValue 测试

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

class CJLTeacher{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }

    init() {
        self.name = "CJL"
    }
}

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

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

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

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

class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
}

class CJLTeacher{
    var age: Int = 18

    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}
var t = CJLTeacher()

class CJLMediumTeacher: CJLTeacher{
    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 CJLTeacher{
    var age: Int = 18{
        //新值存储之前调用
        willSet{
            print("父类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("父类 didSet oldValue \(oldValue)")
        }
    }

    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}

class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }

}

var t = CJLMediumTeacher()
t.age = 20

运行结果如下:

image

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

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

在问题3的基础,修改CJLMediumTeacher

class CJLMediumTeacher: CJLTeacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }

    override init() {
        super.init()
        self.age = 20
    }
}

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

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

延迟属性

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

下面来一一进行分析

1、使用lazy修饰的存储属性

class CJLTeacher{
    lazy var age: Int = 18
}

2、延迟属性必须有一个默认的初始值

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

image

3、延迟存储在第一次访问的时候才被赋值
可以通过调试,来查看实例变量的内存变化

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

class CJLTeacher{
    lazy var age: Int = 18
}

var t = CJLTeacher()
t.age = 30

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

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

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

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

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

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

5、延迟存储属性对实例对象大小的影响
下面来继续看下不使用lazy的内存与使用lazy的内存是否有变化?

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

类型属性

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

1、使用关键字static修饰

class CJLTeacher{
    static var age: Int = 18
}

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

生成SIL文件

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、类型属性必须有一个默认的初始值

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

image

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

单例的创建

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

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

总结

上一篇下一篇

猜你喜欢

热点阅读