一.存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由var
关键字引入)要么是常量存储属性(由let
关键字引入)。
class LGTeacher {
var age = 10
let name = ""
}
这里的age
和name
就是我们所说的存储属性,我们这里需要加以区别的是let
和var
的区别。
从定义上:let
用来声明常量,常量的值一旦设置好便不能再被更改。var
用来声明变量,变量的值可以在将来设置为不同的值。
1.汇编角度分析let和var
在main.swift中添加代码,使用真机测试
var age = 18
let x = 16
projectTest`main:
0x1049c8d90 <+0>: adrp x9, 10
0x1049c8d94 <+4>: mov w8, #0x12 //将18复制到w8寄存器上
0x1049c8d98 <+8>: str x8, [x9, #0x160] //将x8存到x9+0x160所对应的内存地址上
0x1049c8d9c <+12>: adrp x9, 10
0x1049c8da0 <+16>: mov w8, #0x10 //将16复制到w8寄存器上
-> 0x1049c8da4 <+20>: str x8, [x9, #0x168] //将x8存到x9+0x168所对应的内存地址上(刚好与age相差8字节)
0x1049c8da8 <+24>: mov w0, #0x0
0x1049c8dac <+28>: ret
- 因此从汇编角度上看,
let
与var
并没有什么区别,都是将值存入到内存中去
2.SIL角度分析let和var
import Foundation
//都是存储属性, var有set方法,let没有生成set方法
//本质上let和var其实本质上也是一种语法,只是let没有set方法。所以不能被修改
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue let x: Int { get }
二.计算属性
存储属性是最常见的,除了存储属性,类、结构体和枚举也能定义计算属性,计算属性并不存储值,他们提供getter
和setter
来获取和修改值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量
。与此同时我们书写计算属性时候必须包含类型
,因为编译器需要知道期望返回值是什么。
struct square{
//实例占据内存,8字节
var width: Double
//本质就是方法
var area: Double{
get{
//get中,也可以省略return。编译器会自动推导
width * width
}
set{
self.width = newValue
}
//newValue,编译器帮我们生成的。如果想要修改,使用set(xxxx){}
//SIL中对newValue的介绍
//debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
}
//将set方法私有化,只能在当前square使用set方法
private(set) var height: Double
}
三.属性观察者
属性观察者用来观察属性值的变化,一个willSet
当属性将被改变调用,即使这个值与原有值相同,而didSet
在属性已经改变之后调用。它们的语法类似于getter
和setter
class SubjectName{
var subjectName = ""{
willSet{
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been changed \(oldValue)")
}
}
}
let s = SubjectName()
s.subjectName = "Swift"
执行
subjectName will set value Swift
subjectName has been changed
SIL中willSet
// SubjectName.subjectName.setter
sil hidden @$s4main11SubjectNameC07subjectC0SSvs : $@convention(method) (@owned String, @guaranteed SubjectName) -> () {
// %0 "value" // users: %22, %16, %12, %11, %2
// %1 "self" // users: %20, %13, %11, %4, %3
bb0(%0 : $String, %1 : $SubjectName):
debug_value %0 : $String, let, name "value", argno 1 // id: %2
debug_value %1 : $SubjectName, let, name "self", argno 2 // id: %3
%4 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %5
%5 = begin_access [read] [dynamic] %4 : $*String // users: %6, %8
%6 = load %5 : $*String // users: %21, %9, %20, %7
retain_value %6 : $String // id: %7
end_access %5 : $*String // id: %8
debug_value %6 : $String, let, name "tmp" // id: %9
// function_ref SubjectName.subjectName.willset
%10 = function_ref @$s4main11SubjectNameC07subjectC0SSvw : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
%11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
retain_value %0 : $String // id: %12
%13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
%14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
%15 = load %14 : $*String // user: %17
store %0 to %14 : $*String // id: %16
release_value %15 : $String // id: %17
end_access %14 : $*String // id: %18
// function_ref SubjectName.subjectName.didset
%19 = function_ref @$s4main11SubjectNameC07subjectC0SSvW : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
%20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
release_value %6 : $String // id: %21
release_value %0 : $String // id: %22
%23 = tuple () // user: %24
return %23 : $() // id: %24
} // end sil function '$s4main11SubjectNameC07subjectC0SSvs'
- 在subjectName的
set
方法中,赋值前会触发willSet
,赋值后会触发didSet
1.初始化过程中不会执行willSet
class SubjectName{
var subjectName = ""{
willSet{
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been changed \(oldValue)")
}
}
init(subjectName: String) {
self.subjectName = subjectName
}
}
let s = SubjectName(subjectName: "Swift")
SIL中的init(subjectName: String)
// SubjectName.init(subjectName:)
sil hidden @$s4main11SubjectNameC07subjectC0ACSS_tcfc : $@convention(method) (@owned String, @owned SubjectName) -> @owned SubjectName {
// %0 "subjectName" // users: %19, %16, %12, %2
// %1 "self" // users: %13, %4, %20, %3
bb0(%0 : $String, %1 : $SubjectName):
debug_value %0 : $String, let, name "subjectName", argno 1 // id: %2
debug_value %1 : $SubjectName, let, name "self", argno 2 // id: %3
%4 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %11
%5 = string_literal utf8 "" // user: %10
%6 = integer_literal $Builtin.Word, 0 // user: %10
%7 = integer_literal $Builtin.Int1, -1 // user: %10
%8 = metatype $@thin String.Type // user: %10
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%9 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %10
%10 = apply %9(%5, %6, %7, %8) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %11
store %10 to %4 : $*String // id: %11
retain_value %0 : $String // id: %12
%13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
%14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
%15 = load %14 : $*String // user: %17
store %0 to %14 : $*String // id: %16
release_value %15 : $String // id: %17
end_access %14 : $*String // id: %18
release_value %0 : $String // id: %19
return %1 : $SubjectName // id: %20
} // end sil function '$s4main11SubjectNameC07subjectC0ACSS_tcfc'
-
init
是作为初始化操作,本质是不会调用get
或set
方法 - 在SIL中直接是执行
store %0 to %14 : $*String
将%0存入%14 - 换句话说,如果在初始化的过程中能够访问
willSet
或didSet
,有些属性没有被初始化完成,也会操作内存泄露
2.继承属性下的观察者
class Teacher {
var age: Int {
willSet {
print("age will set value \(newValue)")
}
didSet {
print("age has been changed \(oldValue)")
}
}
init(age: Int) {
self.age = age
}
}
class PartTeacher: Teacher {
override var age: Int {
willSet {
print("override age will set value \(newValue)")
}
didSet {
print("override age has been changed \(oldValue)")
}
}
override init(age: Int) {
super.init(age: age)
}
}
let t = PartTeacher(age: 18)
t.age = 20
执行结果
override age will set value 20
age will set value 20
age has been changed 18
override age has been changed 18
- 执行顺序
子类willSet
->父类willSet
->父类didSet
->子类didSet
通过SIL来理解,找到age
的set
方法
// PartTeacher.age.setter
sil hidden @$s4main11PartTeacherC3ageSivs : $@convention(method) (Int, @guaranteed PartTeacher) -> () {
// %0 "value" // users: %17, %13, %2
// %1 "self" // users: %15, %14, %5, %4, %20, %13, %3
bb0(%0 : $Int, %1 : $PartTeacher):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $PartTeacher, let, name "self", argno 2 // id: %3
strong_retain %1 : $PartTeacher // id: %4
%5 = upcast %1 : $PartTeacher to $Teacher // users: %11, %6
%6 = ref_element_addr %5 : $Teacher, #Teacher.age // user: %7
%7 = begin_access [read] [dynamic] %6 : $*Int // users: %8, %9
%8 = load %7 : $*Int // users: %10, %20
end_access %7 : $*Int // id: %9
debug_value %8 : $Int, let, name "tmp" // id: %10
strong_release %5 : $Teacher // id: %11
// function_ref PartTeacher.age.willset
%12 = function_ref @$s4main11PartTeacherC3ageSivw : $@convention(method) (Int, @guaranteed PartTeacher) -> () // user: %13
%13 = apply %12(%0, %1) : $@convention(method) (Int, @guaranteed PartTeacher) -> ()
strong_retain %1 : $PartTeacher // id: %14
%15 = upcast %1 : $PartTeacher to $Teacher // users: %18, %17
// function_ref Teacher.age.setter
%16 = function_ref @$s4main7TeacherC3ageSivs : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %17
%17 = apply %16(%0, %15) : $@convention(method) (Int, @guaranteed Teacher) -> ()
strong_release %15 : $Teacher // id: %18
// function_ref PartTeacher.age.didset
%19 = function_ref @$s4main11PartTeacherC3ageSivW : $@convention(method) (Int, @guaranteed PartTeacher) -> () // user: %20
%20 = apply %19(%8, %1) : $@convention(method) (Int, @guaranteed PartTeacher) -> ()
%21 = tuple () // user: %22
return %21 : $() // id: %22
} // end sil function '$s4main11PartTeacherC3ageSivs'
- 1.
PartTeacher.age.willset
执行子类的willSet
- 2.
Teacher.age.setter
执行父类的setter
,在我们之前的讲解中setter
会触发willSet
和didSet
。因此会,调用父类的willSet
和父类的didSet
- 3.
PartTeacher.age.didset
执行子类的didSet
四.延迟存储属性
延迟存储实现的初始值在其第一次使用时才进行计算(懒加载),使用关键字lazy
class Subject {
lazy var age: Int = 18
}
var s = Subject()
print(s.age)
print("end")
1.使用LLDB调试分析
在print(s.age)和print("end")分别打一个断点
执行到print(s.age)断点时
(lldb) po s
<Subject: 0x10b410270>
(lldb) x/8g 0x10b410270
0x10b410270: 0x0000000100008160 0x0000000200000003
0x10b410280: 0x0000000000000000 0x000000010b410401
0x10b410290: 0x0000000000000000 0x0000000000000000
0x10b4102a0: 0x000000010b410006 0x0000000100000001
(lldb)
除去16字节的metadata,下一个8字节就是age,此时为0。此时并没有存储值
放开断点进入print("end")断点
(lldb) x/8g 0x10b410270
0x10b410270: 0x0000000100008160 0x0000000200000003
0x10b410280: 0x0000000000000012 0x000000010b410400
0x10b410290: 0x0000000000000000 0x0000000000000000
0x10b4102a0: 0x000000010b410006 0x0000000100000001
(lldb)
此时已经存上age的值了
2.SIL文件分析
//此时age是一个Int?,可选值。因此懒加载的本质就是一个可选值
class Subject {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
// Subject.init()
sil hidden @$s4main7SubjectCACycfc : $@convention(method) (@owned Subject) -> @owned Subject {
// %0 "self" // users: %2, %5, %1
bb0(%0 : $Subject):
debug_value %0 : $Subject, let, name "self", argno 1 // id: %1
//初始化了一个lazy_age,返回了addr存入寄存器%2
%2 = ref_element_addr %0 : $Subject, #Subject.$__lazy_storage_$_age // user: %4
//声明了一个为Optinal.none的枚举类型存入寄存器%3
%3 = enum $Optional<Int>, #Optional.none!enumelt // user: %4
//将%3寄存器的值存入寄存器%2,意思也就是将age置为Optional.none。也就是nil
store %3 to %2 : $*Optional<Int> // id: %4
return %0 : $Subject // id: %5
} // end sil function '$s4main7SubjectCACycfc'
// Subject.age.getter
sil hidden [lazy_getter] [noinline] @$s4main7SubjectC3ageSivg : $@convention(method) (@guaranteed Subject) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $Subject):
debug_value %0 : $Subject, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $Subject, #Subject.$__lazy_storage_$_age // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
%4 = load %3 : $*Optional<Int> // user: %6
end_access %3 : $*Optional<Int> // id: %5
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
// %7 // users: %9, %8
bb1(%7 : $Int): // Preds: bb0
debug_value %7 : $Int, let, name "tmp1" // id: %8
br bb3(%7 : $Int) // id: %9
bb2: // Preds: bb0
%10 = integer_literal $Builtin.Int64, 18 // user: %11
%11 = struct $Int (%10 : $Builtin.Int64) // users: %18, %13, %12
debug_value %11 : $Int, let, name "tmp2" // id: %12
%13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
%14 = ref_element_addr %0 : $Subject, #Subject.$__lazy_storage_$_age // user: %15
%15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
store %13 to %15 : $*Optional<Int> // id: %16
end_access %15 : $*Optional<Int> // id: %17
br bb3(%11 : $Int) // id: %18
// %19 // user: %20
bb3(%19 : $Int): // Preds: bb2 bb1
return %19 : $Int // id: %20
}
分析Subject.age.getter
1. %2 = ref_element_addr %0 : $Subject, #Subject.$__lazy_storage_$_age // user: %3
读取我们的lay_storage_age到%2
2.%4 = load %3 : $*Optional<Int>
将Optional<Int> 值给到%4
3.switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
枚举的判断,如果有值(Optional.some)就走bb1的代码块,如果没值(Optional.none)就走bb2的代码块
4.bb2,把当前Int类型的值(18)构建出来给到我们的枚举(Optional<Int>),然后执行存储操作(store %13 to %15 : $*Optional<Int> )
5.bb1,把原有的Int值直接返回回去
-
begin_access
和end_access
叫做内存独占
问题引入:延迟存储属性是否能保证bb2只被访问一次?
- 当然是不能,因为当多线程时,多条线程同时执行到某个延迟存储属性,此时都会执行bbl2代码块,因此会多次进行赋值操作。因此,
延迟存储属性并不是线程安全的
五.类型属性
类型属性
其实就是一个只会被初始化一次的全局变量
class LGTeacher {
static var age = 18
}
LGTeacher.age = 30
SIL分析
class LGTeacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
// one-time initialization token for age
sil_global private @$s4main9LGTeacherC3age_Wz : $Builtin.Word
// static LGTeacher.age
sil_global hidden @$s4main9LGTeacherC3ageSivpZ : $Int
//age变成了一个全局变量
//statc本质上就是全局变量
探究如何被初始化的
在@main中,有这么一个方法。age的可变地址,可能是对地址的访问
// function_ref LGTeacher.age.unsafeMutableAddressor
%3 = function_ref @$s4main9LGTeacherC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
查找关于s4main9LGTeacherC3ageSivau
的实现
// LGTeacher.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main9LGTeacherC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
//拿到内存地址,其实这里就是token的内存地址(对应的sil_global private @$s4main9LGTeacherC3age_Wz : $Builtin.Word)
%0 = global_addr @$s4main9LGTeacherC3age_Wz : $*Builtin.Word // user: %1
//把我们当前的指针%0转化为RawPointer
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref one-time initialization function for age
//%2为token函数的内存地址
%2 = function_ref @$s4main9LGTeacherC3age_WZ : $@convention(c) () -> () // user: %3
//执行builtin "once"(应该是执行一次的意思),一个参数为token的内存地址,一个是token函数的内存地址
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
//拿到全局变量的内存地址
%4 = global_addr @$s4main9LGTeacherC3ageSivpZ : $*Int // user: %5
//将全局变量的指针转化为RawPointer
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
//返回RawPointer,实际上把全局变量返回回去了
return %5 : $Builtin.RawPointer // id: %6
} // end sil function '$s4main9LGTeacherC3ageSivau'
- 这里的大概意思就是,根据token创建一次全局变量,并把这个全局变量地址返回回去
探究token相关函数s4main9LGTeacherC3age_WZ
//注释的意思也相当明确,age的一次性初始化方法
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main9LGTeacherC3age_WZ : $@convention(c) () -> () {
bb0:
//创建一个全局变量给到%0(s4main9LGTeacherC3ageSivpZ -> 对应的就是static LGTeacher.age(sil_global hidden @$s4main9LGTeacherC3ageSivpZ : $Int,第一块代码里可以找到))
alloc_global @$s4main9LGTeacherC3ageSivpZ // id: %0
//拿到age全局变量的内存地址
%1 = global_addr @$s4main9LGTeacherC3ageSivpZ : $*Int // user: %4
//构建Int,并赋值18(Int在当前是结构体)
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
//将这个Int结构体存入到%1(全局变量age的内存地址),初始化age变量
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$s4main9LGTeacherC3age_WZ'
探究builtin "once"
将代码降级到IR
//$s4main9LGTeacherC3age_Wz对应的就是全局变量的初始化方法
//此时调用了一个@swift_once
once_not_done: ; preds = %entry
call void @swift_once(i64* @"$s4main9LGTeacherC3age_Wz", i8* bitcast (void ()* @"$s4main9LGTeacherC3age_WZ" to i8*), i8* undef)
br label %once_done
}
源码探究@swift_once
进入Once.cpp
执行了dispatch_once_f,GCD单例写法,保证只会执行一次
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
if (! *predicate) {
*predicate = true;
fn(context);
}
#elif 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
}
- 执行了
dispatch_once_f
,本质上还是通过GCD单例写法来保证全局变量只会被初始化一次
至此也就证明了类型属性就是全局变量并且只会被初始化一次,并且线程也是安全的
拓展:如何使用类型属性写一个Swift单例
class LGTeacher {
static let sharedInstance = LGTeacher()
private init() {}
}
六.属性在MachO文件的位置信息
1.源码分析
回顾之前总结的TargetClassDescriptor
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
//对应上面添加的size =》B.addInt32(VTableEntries.size());
var size: UInt32
//V-Table
}
- 属性存放的位置就是
fieldDescriptor
1.在源码中找到fieldDescriptor
/// A pointer to the field descriptor for the type, if any.
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor,
/*nullable*/ true> Fields;
2.进入reflection
namespace reflection {
class FieldDescriptor;
}
- 发现是一个
FieldDescriptor
3.进入FieldDescriptor
class FieldDescriptor {
const FieldRecord *getFieldRecordBuffer() const {
return reinterpret_cast<const FieldRecord *>(this + 1);
}
public:
const RelativeDirectPointer<const char> MangledTypeName;
const RelativeDirectPointer<const char> Superclass;
FieldDescriptor() = delete;
const FieldDescriptorKind Kind;
const uint16_t FieldRecordSize;
const uint32_t NumFields;
...
using const_iterator = FieldRecordIterator;
const_iterator begin() const {
auto Begin = getFieldRecordBuffer();
auto End = Begin + NumFields;
return const_iterator { Begin, End };
}
const_iterator end() const {
auto Begin = getFieldRecordBuffer();
auto End = Begin + NumFields;
return const_iterator { End, End };
}
llvm::ArrayRef<FieldRecord> getFields() const {
return {getFieldRecordBuffer(), NumFields};
}
...
};
4.通过上述源码总结出FieldDescriptor
struct FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
//当前有多少个属性
NumFields uint32
//记录每个属性的信息
FieldRecords [FieldRecord]
}
5.进入FileRecord
class FieldRecord {
const FieldRecordFlags Flags;
public:
const RelativeDirectPointer<const char> MangledTypeName;
const RelativeDirectPointer<const char> FieldName;
...
};
通过源码得出FieldRecords
struct FieldRecord{
Flags uint32
MangledTypeName int32
FieldName int32
}
-
MangledTypeName
混写属性类型名称 -
FieldName
属性名称
2.Mach-o分析
Swift代码
class LGTeacher{
var age = 18
var name = "Kody"
}
Mach-o文件
mach-o
1.得出TargetClassDescriptor
0xFFFFFF54 + 0x3F48 - 0x100000000(虚拟内存地址) = 0x3E9C
2.在_TEXT_const
找到0x3E9C
- 标注部分就是
TargetClassDescriptor
起止位置,偏移4个4字节就是FiledDescriptor
- 此时此刻的
0x74
其实是存放的偏移信息
0x3EAC + 0x74 = 0x3F20
3.去_TEXT__swift5_fieldmd
找到0x3F20
- 这里就是
FieldDescriptor
的信息
4.找到FieldRecord
,偏移4个字节
- 后面的连续内存空间就是结构体
[FieldRecord]
信息 -
0x2
表示Flags
-
0xFFFFFFDC
表示MangledTypeName
-
0xFFFFFFDF
表示FieldName
,这里也是偏移信息
0x3F38 + 0xFFFFFFDF - 0x100000000 = 0x3F17
5.进入TEXT__swift5_refstr
,找到0x3F17
-
0x61
的ASCII码对应a
-
0x67
的ASCII码对应g
-
0x65
的ASCII码对应e
-
00
标志结束
网友评论