UIView
和CALayer
是什么关系UIView
继承自UIResponder
类,可以响应事件CALayer
直接继承自NSObject
类,不可以响应事件UIView
是CALayer
的delegate
(CALayerDelegate
)UIView
主要处理事件,CALayer
负责绘制- 每个
UIView
内部都有一个CALayer
在背后提供内容的绘制和显示,并且UIView
的尺寸样式都由内部的Layer
所提供。两者都有树状层级结构,Layer
内部有SubLayers
,View
内部有SubViews
,但是Layer
比View
多了个AnchorPoint
NSCache
和NSMutableDictionary
的相同点与区别相同点:
NSCache
和NSMutableDictionary
功能用法基本是相同的
区别:NSCache
是线程安全的,NSMutableDictionary
线程不安全,Mutable开发的类
一般都是线程不安全的
当内存不足时NSCache
会自动释放内存(所以从缓存中取数据的时候总要判断是否为空)NSCache
可以指定缓存的限额,当缓存超出限额自动释放内存NSCache
的Key
只是对对象进行了Strong
引用,而非拷贝,所以不需要实现NSCopying
协议atomic
的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)atomic
会对属性的setter/getter
方法进行加锁,这仅仅只能保证在操作setter/getter
方法是安全的。不能保证其他线程的安全- 例如:线程1调用了某一属性的
setter
方法并进行到了一半,线程2调用其getter
方法,那么会执行完setter
操作后,再执行getter
操作,线程2会获取到线程1setter
后的完整的值;当几个线程同时调用同一属性的setter、getter
方法时,会获取到一个完整的值,但获取到的值不可控
iOS 中内省的几个方法
对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现
OC运行时内省的4个方法:
- 判断对象类型:
1
2-(BOOL) isKindOfClass: // 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: // 判断是否是这个类的实例 - 判断对象/类是否有这个方法
1
2-(BOOL) respondsToSelector: // 判断实例是否有这样方法
+(BOOL) instancesRespondToSelector: // 判断类是否有这个方法
objc
在向一个对象发送消息时,发生了什么根据对象的isa指针找到该对象所属的类,去objc的对应的类中找方法
1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行
2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
3.如果没找到,去父类指针所指向的对象中执行1,2.
4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制
5.如果没有重写拦截调用的方法,程序报错你是否接触过OC中的反射机制?简单聊一下概念和使用
class
反射- 通过类名的字符串形式实例化对象
1
2Class class = NSClassFromString(@"student");
Student *stu = [[class alloc] init]; - 将类名变为字符串
1
2Class class = [Student class];
NSString *className = NSStringFromClass(class); SEL
的反射- 通过方法的字符串形式实例化方法
1
2SEL selector = NSSelectorFromString(@"setName");
[stu performSelector:selector withObject:@"Mike"]; - 将方法变成字符串
NSStringFromSelector(@selector(setName:));
这个写法会出什么问题
@property (nonatomic, copy) NSMutableArray *arr;
添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。原因:是因为
copy
就是复制一个不可变NSArray
的对象,不能对NSArray
对象进行添加/修改如何让自己的类用
copy
修饰符若想令自己所写的对象具有拷贝功能,则需实现
NSCopying
协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying
与NSMutableCopying
协议。
具体步骤:
1.需声明该类遵从NSCopying
协议
2.实现NSCopying
协议的方法,具体区别戳这里
NSCopying
协议方法为:1
2
3
4
5- (id)copyWithZone:(NSZone *)zone {
MyObject *copy = [[[self class] allocWithZone: zone] init];
copy.username = self.username;
return copy;
}
为什么
assign
不能用于修饰对象首先我们需要明确,对象的内存一般被分配到堆上,基本数据类型和oc数据类型的内存一般被分配在栈上
如果用assign
修饰对象,当对象被释放后,指针的地址还是存在的,也就是说指针并没有被置为nil
,从而造成了野指针。因为对象是分配在堆上的,堆上的内存由程序员分配释放。而因为指针没有被置为nil
,如果后续的内存分配中,刚好分配到了这块内存,就会造成崩溃
而assign
修饰基本数据类型或oc数据类型,因为基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针请写出以下代码输出
1
2
3int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d, %d", *(a + 1), *(ptr + 1));参考答案:2,随机值
分析:a
代表有5个元素的数组的首地址,a[5]
的元素分别是1,2,3,4,5。接下来,a + 1
表示数据首地址加1,那么就是a[1]
,也就是对应于值为2,但是,这里是&a + 1
,因为a
代表的是整个数组,它的空间大小为5 * sizeof(int)
,因此&a + 1
就是a + 5
。a
是个常量指针,指向当前数组的首地址,指针+1就是移动sizeof(int)
个字节
因此,ptr
是指向int *
类型的指针,而ptr
指向的就是a + 5
,那么ptr + 1
也相当于a + 6
,所以最后的*(ptr + 1)
就是一个随机值了。而*(ptr – 1)
就相当于a + 4
,对应的值就是5一个
view
已经初始化完毕,view
上面添加了n个button
(可能使用循环创建),除用view
的tag
之外,还可以采用什么办法来找到自己想要的button
来修改Button
的值第一种:如果是点击某个按钮后,才会刷新它的值,其它不用修改,那么不用引用任何按钮,直接在回调时,就已经将接收响应的按钮给传过来了,直接通过它修改即可
第二种:点击某个按钮后,所有与之同类型的按钮都要修改值,那么可以通过在创建按钮时将按钮存入到数组中,在需要的时候遍历查找UIViewController
的viewDidUnload、viewDidLoad
和loadView
分别什么时候调用?UIView
的drawRect
和layoutSubviews
分别起什么作用第一个问题:
在控制器被销毁前会调用viewDidUnload
(MRC
下才会调用)
在控制器没有任何view
时,会调用loadView
在view
加载完成时,会调用viewDidLoad
第二个问题:
在调用setNeedsDisplay
后,会调用drawRect
方法,我们通过在此方法中可以获取到context
(设置上下文),就可以实现绘图
在调用setNeedsLayout
后,会调用layoutSubviews
方法,我们可以通过在此方法去调整UI。当然能引起layoutSubviews
调用的方式有很多种的,比如添加子视图、滚动scrollview
、修改视图的frame
等自动释放池工作原理
自动释放池是
NSAutorelease
类的一个实例,当向一个对象发送autorelease
消息时,该对象会自动入池,待池销毁时,将会向池中所有对象发送一条release
消息,释放对象[pool release]、[pool drain]
表示的是池本身不会销毁,而是池子中的临时对象都被发送release
,从而将对象销毁苹果是如何实现
autoreleasepool
的autoreleasepool
是由AutoreleasePoolPage
以双向链表的方式实现的,主要通过下列三个函数完成:- 由
objc_autoreleasePoolPush
作为自动释放池作用域的第一个函数 - 使用
objc_autorelease
将对象加入自动释放池 - 由
objc_autoreleasePoolPop
作为自动释放池作用域的最后一个函数
- 由
autorelease
的对象何时被释放RunLoop
在每个事件循环结束后会去自动释放池将所有自动释放对象的引用计数减一,若引用计数变成了0,则会将对象真正销毁掉,回收内存。
在没有手动添加Autorelease Pool
的情况下,autorelease
的对象是在每个事件循环结束后,自动释放池才会对所有自动释放的对象的引用计数减一,若引用计数变成了0,则释放对象,回收内存。因此,若想要早一点释放掉autorelease
对象,那么我们可以在对象外加一个自动释放池。比如,在循环处理数据时,临时变量要快速释放,就应该采用这种方式:1
2
3
4
5
6
7
8
9
10// 通过alloc创建的对象,直接加入@autoreleasepool没有作用,需在创建对象后面显式添加autorelease
// 通过类方法创建的对象不需要显式添加autorelease,原因是类方法创建的对象系统会自动添加autorelease
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = @"Abc";
str = [str lowercaseString];
str = [str stringByAppendingString:@"xyz"];
NSLog(@"%@", str);
} // 出了这里,就会去遍历该自动释放池了
}简述内存管理基本原则
OC内存管理遵循
谁创建,谁释放,谁引用,谁管理
的机制,当使用alloc、copy(mutableCopy)或者retian
一个对象时,你就有义务向它发送一条release或者autorelease
消息释放该对象,其他方法创建的对象,不需要由你来管理内存,当对象引用计数为0时,系统将释放该对象,这是OC的手动管理机制(MRC
)
向一个对象发送一条autorelease
消息,这个对象并不会立即销毁,而是将这个对象放入了自动释放池,待池子释放时,它会向池中每一个对象发送一条release
消息,以此来释放对象
向一个对象发送release
消息,并不意味着这个对象被销毁了,而是当这个对象的引用计数为0时,系统才会调用dealloc
方法释放该对象和对象本身所拥有的实例sizeof
关键字sizeof
是在编译阶段处理,且不能被编译为机器码。sizeof
的结果等于对象或类型所占的内存字节数。sizeof
的返回值类型为size_t
变量:int a; sizeof(a)
为4;
指针:int *p; sizeof(p)
为4;
数组:int b[10]; sizeof(b)
为数组的大小410;int c[0]; sizeof(c)
等于0sizeof(void)
等于1
`sizeof(void )`等于4什么是离屏渲染?什么情况下会触发?离屏渲染消耗性能的原因
离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作
离屏渲染触发的场景有以下:- 圆角(同时设置
layer.masksToBounds = YES、layer.cornerRadius
大于0) - 图层蒙版
- 阴影,
layer.shadowXXX
,如果设置了layer.shadowPath
就不会产生离屏渲染 - 遮罩,
layer.mask
- 光栅化,
layer.shouldRasterize = YES
- 圆角(同时设置
离屏渲染消耗性能的原因
需要创建新的缓冲区,离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen
)切换到离屏(Off-Screen
)等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
ARC 下,不显式指定任何属性关键字时,默认的关键字都有哪些
基本数据类型默认关键字是:
atomic, readwrite, assign
普通Objective-C
对象默认关键字是:atomic, readwrite, strong
OC中的类方法和实例方法有什么本质区别和联系
类方法:
- 类方法是属于类对象的
- 类方法只能通过类对象调用
- 类方法中的 self 是类对象
- 类方法可以调用其他的类方法
- 类方法中不能访问成员变量
- 类方法中不能直接调用对象方法
实例方法:
- 实例方法是属于实例对象的
- 实例方法只能通过实例对象调用
- 实例方法中的 self 是实例对象
- 实例方法中可以访问成员变量
- 实例方法中直接调用实例方法
- 实例方法中也可以调用类方法(通过类名)
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 不能向编译后得到的类中增加实例变量
- 能向运行时创建的类中添加实例变量
- 因为编译后的类已经注册在
runtime
中,类结构体中的objc_ivar_list
实例变量的链表和instance_size
实例变量的内存大小已经确定,同时runtime
会调用class_setIvarLayout
或class_setWeakIvarLayout
来处理strong weak
引用,所以不能向存在的类中添加实例变量
运行时创建的类是可以添加实例变量,调用class_addIvar
函数。但是得在调用objc_allocateClassPair
之后,objc_registerClassPair
之前,原因同上
runtime
如何通过selector
找到对应的IMP
地址(分别考虑实例方法和类方法)Selector、Method 和 IMP
的有什么区别与联系对于实例方法,每个实例的
isa
指针指向着对应类对象,而每一个类对象中都有一个对象方法列表。对于类方法,每个类对象的isa
指针都指向着对应的元类对象,而每一个元类对象中都有一个类方法列表。方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector
本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现Selector、Method 和 IMP
的关系可以这样描述:在运行期分发消息,方法列表中的每一个实体都是一个方法(Method
)它的名字叫做选择器(SEL
)对应着一种方法实现(IMP
)objc_msgSend、_objc_msgForward
都是做什么的?OC 中的消息调用流程是怎样的objc_msgSend
是用来做消息发送的。在OC
中,对方法的调用都会被转换成内部的消息发送执行_objc_msgForward
是IMP
类型(函数指针)用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发- 在消息调用的过程中,
objc_msgSend
的动作比较清晰:首先在Class
中的缓存查找IMP
(没缓存则初始化缓存)如果没找到,则向父类的Class
查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward
函数指针代替IMP
。最后,执行这个IMP
。当调用一个NSObject
对象不存在的方法时,并不会马上抛出异常,而是会经过多层转发,层层调用对象的-resolveInstanceMethod:、-forwardingTargetForSelector:、-methodSignatureForSelector:、-forwardInvocation:
等方法。其中最后-forwardInvocation:
是会有一个NSInvocation
对象,这个NSInvocation
对象保存了这个方法调用的所有信息,包括Selector名,参数和返回值类型
,可以从这个NSInvocation
对象里拿到调用的所有参数值
class
方法和objc_getClass
方法有什么区别object_getClass(obj)
返回的是obj
中的isa
指针,即指向类对象的指针;而[obj class]
则分两种情况:一是当obj
为实例对象时,[obj class]
中class
是实例方法,返回的是obj
对象中的isa
指针;二是当obj
为类对象(包括元类和根类以及根元类)时,调用的是类方法,返回的结果为其本身OC中向一个
nil
对象发送消息将会发生什么在
OC
中向nil
发送消息是完全有效的,只是在运行时不会有任何作用;向一个nil
对象发送消息,首先在寻找对象的isa
指针时就是0地址
返回了,所以不会出现任何错误,也不会崩溃_objc_msgForward
函数是做什么的?直接调用它将会发生什么_objc_msgForward
是一个函数指针(和IMP
的类型一样)用于消息转发;当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发objc_msgSend
在消息传递
中的作用。在消息传递
过程中,objc_msgSend
的动作比较清晰:首先在Class
中的缓存查找IMP
(没有缓存则初始化缓存
)如果没找到,则向父类的Class
查找。如果一直查找到根类
仍旧没有实现,则用_objc_msgForward
函数指针代替IMP
,最后执行这个IMP
一旦调用了_objc_msgForward
,将跳过查找IMP
的过程,直接触发消息转发
,如果调用了_objc_msgForward
,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend
,我没有在这个对象里找到这个方法的实现,如果用不好会直接导致程序Crash
什么时候会报
unrecognized selector
的异常
- 当调用该对象上某个方法,而该对象上没有实现这个方法的时候。可以通过
消息转发
进行解决,流程见下图 OC
在向一个对象发送消息时,runtime
库会根据对象的isa
指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
但是在这之前,OC的运行时会给出三次拯救程序崩溃的机会- Method resolution(消息动态解析)
OC
运行时会调用+resolveInstanceMethod:
或者+resolveClassMethod:
,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移到下一步,消息转发(Message Forwarding
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果是执行 run 函数,就动态解析,指定新的 IMP
if (sel == NSSelectorFromString(@"run:")) {
// class: 给哪个类添加方法
// SEL: 添加哪个方法
// IMP: 方法实现 => 函数 => 函数入口 => 函数名
// type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
class_addMethod(self, sel, (IMP)runMethod, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//新的 run 函数
void runMethod(id self, SEL _cmd, NSNumber *meter) {
NSLog(@"跑了%@", meter);
} - Fast forwarding(消息接受者重定向)
如果目标对象实现了-forwardingTargetForSelector:
,Runtime
这时就会调用这个方法,给你把这个消息转发给其他对象的机会。只要这个方法返回的不是nil
和self
,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding
。 这里叫Fast
,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation
对象,所以相对更快点1
2
3
4
5
6
7
8// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(run:)) {
return [[Person alloc] init];
// 返回 Person 对象,让 Person 对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
} - Normal forwarding(消息重定向)
这一步是Runtime
最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil
,Runtime
则会发出-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送-forwardInvocation:
消息给目标对象既然1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 从 anInvocation 中获取消息
SEL sel = anInvocation.selector;
if (sel == NSSelectorFromString(@"run:")) {
// 1. 指定当前类的一个方法作为IMP
// anInvocation.selector = @selector(readBook:);
// [anInvocation invoke];
// 2. 指定其他类来执行这个IMP
Person *p = [[Person alloc] init];
// 判断 Person 对象方法是否可以响应 sel
if([p respondsToSelector:sel]) {
// 若可以响应,则将消息转发给其他对象处理
[anInvocation invokeWithTarget:p];
} else {
// 若仍然无法响应,则报错:找不到响应方法
[self doesNotRecognizeSelector:sel];
}
}else{
[super forwardInvocation:anInvocation];
}
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}-forwardingTargetForSelector:
和-forwardInvocation:
都可以将消息转发给其他对象处理,那么两者的区别在哪?
区别就在于-forwardingTargetForSelector:
只能将消息转发给一个对象。而-forwardInvocation:
可以把消息存储,在你觉得合适的时机转发出去,或者不处理这个消息。修改消息的target,selector,参数等。将消息转发给多个对象
iOS layoutSubviews
什么时候会被调用init
方法不会调用layoutSubviews
,但是是用initWithFrame
进行初始化时,当rect
的值不为CGRectZero
时,会触发addSubview
会触发layoutSubviews
方法setFrame
只有当设置的frame
的参数的size
与原来的size
不同,才会触发其view
的layoutSubviews
方法- 滑动
UIScrollView
会调用scrollview
及scrollview
上的view
的layoutSubviews
方法 - 旋转设备只会调用
VC
的view
的layoutSubviews
方法 - 直接调用
[self setNeedsLayout];
(这个在上面苹果官方文档里有说明)-layoutSubviews
方法:这个方法默认没有做任何事情,需要子类进行重写-setNeedsLayout
方法:标记为需要重新布局,异步调用layoutIfNeeded
刷新布局,不立即刷新,但layoutSubviews
一定会被调用-layoutIfNeeded
方法:如果有需要刷新的标记,立即调用layoutSubviews
进行布局(如果没有标记,不会调用layoutSubviews
)
如果要立即刷新,要先调用[view setNeedsLayout]
,把标记设为需要布局,然后马上调用[view layoutIfNeeded]
,实现布局
在视图第一次显示之前,标记总是需要刷新
的,可以直接调用[view layoutIfNeeded]
下面代码会发生什么问题
1
2
3
4
5
6
7
8@property (nonatomic, strong) NSString *str;
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
self.str = [NSString stringWithFormat:@"changzifuchaung:%d",i];
});
}会
crash
。因为在并行队列DISPATCH_QUEUE_CONCURRENT
中异步dispatch_async
对str
属性进行赋值,就会导致str
已经被release
了,还会执行release
。这就是向已释放内存的对象发送消息而发生crash
详细解析:对str
属性strong
修饰进行赋值,相当与MRC
中的1
2
3
4
5
6
7- (void)setStr:(NSString *)str{
if (str == _str) return;
id pre = _str;
[str retain];//1.先保留新值
_str = str;//2.再进行赋值
[pre release];//3.释放旧值
}那么假如
并发队列
里调度的线程A
执行到步骤1
,还没到步骤2
时,线程B
执行到步骤3
,那么当线程A
再执行步骤3
时,旧值就会被过度释放
,导致向已释放内存的对象发送消息而崩溃
追问:怎么修改这段代码变为不崩溃呢
1、使用串行队列
将set
方法改成在串行队列中执行就行,这样即使异步,但所有block
操作追加在队列最后依次执行
2、使用atomic
atomic
关键字相当于在setter
方法加锁,这样每次执行setter
都是线程安全的,但这只是单独针对setter
方法而言的狭义的线程安全
3、使用weak
关键字weak
的setter
没有保留新值
的操作,所以不会引发重复释放。当然这个时候要看具体情况能否使用weak
,可能值并不是所需要的值
4、使用互斥锁,保证数据访问的唯一性@synchronized (self) {self.str = [NSString stringWithFormat:@"changzifuchaung:%d",i];}
5、使用Tagged Pointer
Tagged Pointer
是苹果在64位系统引入的内存技术。简单来说就是对于NSString
(内存小于60位的字符串)或NSNumber
(小于2^31),64位的指针有8个字节,完全可以直接用这个空间来直接表示值,这样的话其实会将NSString
和NSNumber
对象由一个指针
转换成一个值类型
,而值类型的setter和getter
又是原子的,从而线程安全发散:下面代码会
crash
吗1
2
3
4
5
6
7
8
9
10@property (nonatomic, strong) NSString *str;
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
dispatch_async(queue, ^{
// 相比上面,仅字符串变短了
self.str = [NSString stringWithFormat:@"%d",i];
NSLog(@"%d, %s, %p", i, object_getClassName(self.str), self.str);
});
}不会
crash
。而且发现str
这个字符串类型是NSTaggedPointerString
Tagged Pointer
是一个能够提升性能、节省内存的有趣的技术Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
(后来可以存储小字符串)Tagged Pointer指针的值
不再是地址
了,而是真正的值
。所以,实际上它不再是一个对象
了,它只是一个披着对象皮的普通变量
而已
它的内存并不存储在堆
中,也不需要malloc和free
,所以拥有极快的读取和创建速度