面试题-Foundation相关
1.nil、NIL、NSNULL` 有什么区别?
- nil、NIL 可以说是等价的,都代表内存中一块空地址.代表空指针
- NSNULL代表一个空对象
2.如何实现一个线程安全的 NSMutableArray?
- NSMutableArray是线程不安全的,当有多个线程同时对数组进行操作的时候可能导致崩溃或数据错误
- 线程锁:使用线程锁对数组读写时进行加锁
- 派发队列:多用派发队列,少用同步锁中指出:使用“串行同步队列”,将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。
- 而通过并发队列,结合GCD的栅栏块(barrier)来不仅实现数据同步线程安全,还比串行同步队列方式更高效。
3.如何定义一台 iOS 设备的唯一性?
- UUID+keyChain存储
- 获取UUID
[[UIDevice currentDevice] identifierForVendor] UUIDString]
- 当你升级或者重装软件的时候,UUID会发生变化,这时候我们再去获取UUID时,得到的值和以前的不一样,但其实还是同一台设备
- keychain:是钥匙串的意思,是苹果公司Mac OS中的密码管理系统。keychain里保存的信息不会因App被删除而丢失,在用户重新安装App后依然有效,数据还在。所以我们可以将UUID存储到keychain里面,需要用到的时候从keychain中取,这样可以保证UUID的唯一性。
4.atomic 修饰的属性是绝对安全的吗?为什么?
- 不一定安全,所谓的安全只是局限于 Setter、Getter 的访问器方法而言的
- 方法里加入一些互斥锁,目的就是防止多(条)线程访问同一个内存地址,造成数据错误。
- 但是不能保证多线程安全,当开启两个异步线程频繁的去对ojb执行+1操作的时候,就不是线程安全的,解决方法是加锁
5.isEqual 和 hash 方法时要注意什么?
- hash
- 对关键属性的hash值进行位或运算作为hash值
- isEqual
- ==运算符判断是否是同一对象,因为同一对象必然完全相同
- 判断是否是同一类型,这样不仅可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
- 判断对象是否是nil, 做参数有效性检查
- 各个属性分别使用默认判等方法进行判断
- 返回所有属性判等的与结果
6.id 和 instanceType 有什么区别?
- 相同点
- instancetype 和 id 都是万能指针,指向对象。
- 不同点:
- id 在编译的时候不能判断对象的真实类型,instancetype 在编译的时候可以判断对象的真实类型。
- id 可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype只能作为返回值类型。
7.说一下对 Super 关键字的理解
- 使用super关键字发送消息会被编译器转化为调用objc_msgSendSuper以及相关函数
id objc_msgSendSuper(struct objc_super *super, SEL op, ...);
- 这里的super已经不再是我们调用时写的[super init]的super了,这里指代的是struct objc_super结构体指针。
8.@synthesize 和 @dynamic 分别有什么作用?
- @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
- @synthesize 的语义是如果你没有手动实现 setter 方法和 getter方法,那么编译器会自动为你加上这两个方法。
- @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。
- 假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var =someVar,由于缺setter方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
9.Objective-C中反射机制了解吗?
- iOS反射机制:运行时选择创建哪个实例,并动态选择调用哪个方法。
// 当前对象是否这个类或其子类的实例
- (BOOL)isKindOfClass:(Class)aClass;
// 当前对象是否是这个类的实例
- (BOOL)isMemberOfClass:(Class)aClass;
// 当前对象是否遵守这个协议
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 当前对象是否实现这个方法
- (BOOL)respondsToSelector:(SEL)aSelector;
- 获取Class的三种方法
// 通过字符串获取class
Class class = NSClassFromString(@"NSString");
NSLog(@"class type : %@", class);
// 直接用class 来创建对象 ,通过对象来获取class
id str = [[class alloc] init];
NSLog(@"%@", [str class]);
// 通过类来获取class
NSLog(@"%d", class==NSString.class);
- 实际应用
- 根据后台推送过来的数据,进行动态页面跳转,跳转到页面后根据返回到数据执行对应的操作。
- OC中使用反射优缺点:
- 优点:
- 松耦合,类与类之间不需要太多依赖
- 构建灵活
- 缺点:
- 不利于维护。使用反射模糊了程序内部实际发生的事情,隐藏了程序的逻辑。这种绕过源码的方式比直接代码更为复杂,增加了维护成本
- 性能较差。使用反射匹配字符串间接命中内存比直接命中内存的方式要慢。当然,这个程度取决于使用场景,如果只是作为程序中很少涉及的部分,这个性能上的影响可以忽略不计。但是,如果在性能很关键的应用核心逻辑中使用反射,性能问题就尤其重要了
10. typeof 和 __typeof,typedef 的区别?
- typeof
- 是一个一元运算,放在一个运算数之前,运算数可以是任意类型
- 可以理解为:我们根据typeof()括号里面的变量,自动识别变量类型并返回该类型。
- typeof 常见运用于Block中,避免循环引用发生的问题。
- typedef
- 定义一种类型的别名,而不只是简单的宏替换。
- typedef 常用于命名(枚举和Block)
11.self + weakSelf + strongSelf 三者如何使用?
__weak __typeof(self)weakSelf = self; //1
[self.context performBlock:^{
[weakSelf doSomething]; //2
__strong __typeof(weakSelf)strongSelf = weakSelf; //3
[strongSelf doAnotherSomething];
}];
- 1.使用__weak __typeof是在编译的时候,另外创建一个局部变量weak对象来操作self,引用计数不变。block 会将这个局部变量捕获为自己的属性,访问这个属性,从而达到访问 self的效果,因为他们的内存地址都是一样的。
- 2.因为weakSelf和self是两个变量,doSomething有可能就直接对self自身引用计数减到0了.所以在[weakSelf doSomething]的时候,你很难控制这里self是否就会被释放了.weakSelf只能看着.
- 3.__strong __typeof在编译的时候,实际是对weakSelf的强引用.指针连带关系self的引用计数会增加.但是你这个是在block里面,生命周期也只在当前block的作用域.所以,当这个block结束,strongSelf随之也就被释放了.不会影响block外部的self的生命周期.
- 4.在 Block 内如果需要访问self的方法、变量,建议使用 weakSelf。
- 5.如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。
12.如何将 Obj-C 代码改变为 C++/C 的代码?
clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.13 main.m
13.知不知道在哪里下载苹果的源代码?
16.objc_getClass()、object_getClass()、Class 这三个方法用来获取类对象有什么不同?
14.objc_getClass()、object_getClass()、Class 这三个方法用来获取类对象有什么不同?
- 全部返回 Class 类对象
- Class 方法
- Class 方法无论是类对象还是实例对象都可以调用,可以嵌套,返回永远是自身的类对象。
- object_getClass 方法
- object_getClass 和 class 同样可以嵌套,但是 object_getClass 得到的是他的 isa 指向的地址。
- objc_getClass 方法
- objc_getClass 无法嵌套,因为参数 是 char 类型,效果和 class 相同
15.NSCache优于NSDictionary的几点?
- NSCache苹果提供的一套缓存机制
- 主要作用于内存缓存的管理方面;
- 在没有引入NSCache之前,我们要管理缓存,都是使用的NSMutableDictionary来管理,如:
// 定义下载操作缓存池
@property (nonatomic, strong) NSMutableDictionary *operationCache;
// 定义图片缓存池
@property (nonatomic, strong) NSMutableDictionary *imageCache;
- 然而 NSMutableDictionary在线程方面来说是不安全,这也是苹果官方文档明确说明了的,而如果使用的是NSCache,那就不会出现这些问题.
- 相同点:
- NSCache和NSMutableDictionary功能用法基本是相同的。
- 区别:
-
NSCache是线程安全的,NSMutableDictionary线程不安全;NSCache线程是安全的,Mutable开发的类一般都是线程不安全的;
-
当内存不足时NSCache会自动释放内存(所以从缓存中取数据的时候总要判断是否为空);
-
NSCache可以指定缓存的限额,当缓存超出限额自动释放内存缓存限额:
- 缓存数量
@property NSUInteger countLimit;
- 缓存成本
@property NSUInteger totalCostLimit;
-
苹果给NSCache封装了更多的方法和属性,比NSMutableDictionary的功能要强大很多;
-
代码演示
- 先定义缓存池,并懒加载初始化:
#import "ViewController.h"
@interface ViewController () <NSCacheDelegate>
// 定义缓存池
@property (nonatomic, strong) NSCache *cache;
@end
@implementation ViewController
- (NSCache *)cache {
if (_cache == nil) {
_cache = [[NSCache alloc] init];
// 缓存中总共可以存储多少条
_cache.countLimit = 5;
// 缓存的数据总量为多少
_cache.totalCostLimit = 1024 * 5;
}
return _cache;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//添加缓存数据
for (int i = 0; i < 10; i++) {
[self.cache setObject:[NSString stringWithFormat:@"hello %d",i] forKey:[NSString stringWithFormat:@"h%d",i]];
NSLog(@"添加 %@",[NSString stringWithFormat:@"hello %d",i]);
}
//输出缓存中的数据
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[self.cache objectForKey:[NSString stringWithFormat:@"h%d",i]]);
}
}
控制台输出结果为:
通过输出结果可以看出:
-
当我们使用NSCache来创建缓存池的时候,我们可以很灵活的设置缓存的限额;
-
当程序中的个数超过我们的限额的时候,会先移除最先创建的;
-
如果已经移除了,那么当我们输出缓存中的数据的时候,就只剩下后面创建的数据了;
-
演示NSCache的代理方法
先设置代理对象:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //设置NSCache的代理 self.cache.delegate = self; 调用代理方法: 这里我仅用一个方法来演示:
//当缓存被移除的时候执行
- (void)cache:(NSCache *)cache willEvictObject:(id)obj{
NSLog(@"缓存移除 %@",obj);
}
- 通过结果可以看出: NSCache的功能要比NSMutableDictionary的功能要强大很多很多;
当遇到内存警告的时候
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
//当收到内存警告,清除内存
[self.cache removeAllObjects];
//输出缓存中的数据
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[self.cache objectForKey:[NSString stringWithFormat:@"h%d",i]]);
}
}
控制台输出结果:
- 通过结果可以看出: 当收到内存警告之后,清除数据之后,NSCache缓存池中所有的数据都会为空!
当收到内存警告,调用removeAllObjects 之后,无法再次往缓存池中添加数据
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
//当收到内存警告,调用removeAllObjects 之后,无法再次往缓存中添加数据
[self.cache removeAllObjects];
//输出缓存中的数据
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[self.cache objectForKey:[NSString stringWithFormat:@"h%d",i]]);
}
}
// 触摸事件, 以便验证添加数据
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.cache removeAllObjects];
//添加缓存数据
for (int i = 0; i < 10; i++) {
[self.cache setObject:[NSString stringWithFormat:@"hello %d",i] forKey:[NSString stringWithFormat:@"h%d",i]];
// NSLog(@"添加 %@",[NSString stringWithFormat:@"hello %d",i]);
}
//输出缓存中的数据
for (int i = 0; i < 10; i++) {
NSLog(@"%@",[self.cache objectForKey:[NSString stringWithFormat:@"h%d",i]]);
}
}
控制台输出结果为:
- 通过输出结果,我们可以看出: 当收到内存警告,而我们又调用removeAllObjects 之后,则无法再次往缓存中添加数据;
16.NSCache,NSDictionary,NSArray的区别
NSArray
- NSArray作为一个存储对象的有序集合,可能是被使用最多的集合类。
- 性能特征
- 在数组的开头和结尾插入/删除元素通常是一个O(1)操作,而随机的插入/删除通常是 O(N)的
- 有用的方法
- NSArray的大多数方法使用isEqual:来检查对象间的关系(例如containsObject:)。有一个特别的方法
indexOfObjectIdenticalTo:
- 用来检查指针相等,如果你确保在同一个集合中搜索,那么这个方法可以很大的提升搜索速度
NSDictionary
- 一个字典存储任意的对象键值对。 由于历史原因,初始化方法使用相反的对象到值的方法
[NSDictionary dictionaryWithObjectsAndKeys:object, key, nil]
- 而新的快捷语法则从key开始
@{key : value, ...}
- NSDictionary中的键是被拷贝的并且需要是恒定的。如果在一个键在被用于在字典中放入一个值后被改变,那么这个值可能就会变得无法获取了。一个有趣的细节,在NSDictionary中键是被拷贝的,而在使用一个toll-free桥接的CFDictionary时却只被retain。CoreFoundation类没有通用对象的拷贝方法,因此这时拷贝是不可能的(*)。这只适用于使用CFDictionarySetValue()的时候。如果通过setObject:forKey使用toll-free桥接的CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成立 — 转换为CFDictionary的NSDictionary对象,对其使用CFDictionarySetValue()方法会调用回setObject:forKey并拷贝键。
NSCache
- NSCache是一个非常奇怪的集合。在iOS 4/Snow Leopard中加入,默认为可变并且线程安全的。这使它很适合缓存那些创建起来代价高昂的对象。它自动对内存警告做出反应并基于可设置的成本清理自己。与NSDictionary相比,键是被retain而不是被拷贝的。
- NSCache的回收方法是不确定的,NSCache可以设置撑自动回收实现了NSDiscardableContent协议的对象。实现该属性的一个比较流行的类是同时间加入的NSPurgeableData,但是在OS X 10.9之前,是非线程安全的
- NSCache性能:
- 那么NSCache如何承受NSMutableDictionary的考验?加入的线程安全必然会带来一些消耗;
iOS 构建缓存时选 NSCache 而非NSDictionary
- 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减时间最久为被使用的对象
- NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便
- NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了
17.delegate(代理,委托),Notification(通知),KVO分别是什么?
delegate(代理,委托)
- 委托是协议的一种,顾名思义,就是委托他人帮自己去做什么事。
- 非常严格的语法。所有将听到的事件必须是在delegate协议中有清晰的定义,语法清晰,易读;
- 如果delegate中的一个方法没有实现那么就会出现编译警告/错误
- 在释放代理对象时,需要小心的将delegate改为nil。一旦设定失败,那么调用释放对象的方法将会出现内存crash
Notification(通知)
- 在iOS应用开发中有一个
Notification Center
(通知中心)的概念。它是一个单例对象,允许当事件发生时通知一些对象 - 对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单
- controller能够传递context对象(dictionary),context对象携带了关于发送通知的自定义的信息
- 在调试的时候应用的工作以及控制过程难跟踪;
KVO
- KVO提供一种机制,当指定的被观察的对像的属性被修改后,KVO会自动通知响应的观察者,KVC(键值编码)是KVO的基础,KVO基于 runtime
- 够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
- 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
18.如何选择delegate、notification、KVO?
三种模式都是一个对象传递事件给另外一个对象,并且不要他们有耦合。
- delegate. 一对一
- notification 一对多,多对多
- KVO 一对一
三者各有自己的特点:
- delegate 语法简洁,方便阅读,易于调试
- notification 灵活多变,可以跨越多个类之间进行使用
- KVO 实现属性监听,实现model和view同步
- 可以根据实际开发遇到的场景来使用不同的方式
19. 若你去设计一个通知中心,你会怎样设计?
个人理解: 参考现有的通知中心
- 1.创建通知中心单例类,并在里面有个一个保存通知的全局NSDiction
- 2.对于注册通知的类,将其注册通知名作为key, 执行的方法和类,以及一些参数做为一个数组为值
- 3.发送通知可以调用通知中心,通过字典key(通知名),找到对应的 类和方法进行执行调用传值.
20.结构体与数组有什么区别?
- 结构体可以存不同类型的元素,而数组只能存同一类型
- 结构体类型需要我们自已定义.数组是用别的类型加[元素个数]
- 结构体内存分配方式很特别,使用对齐原则,不一定是所有元素的字节数和,而数组一定是所有元素的字节数和.
- 结构体指针可以指针名->结构体元素名(取元素);数组不行.
21.NSDictionary的实现原理是什么
- 哈希表(NSDictionary 是通过hash表来实现key和value之间的映射和存储的)
22.OC与 JS交互方式有哪些?
- 通过拦截URL
- 使用MessageHandler(WKWebView)
- JavaScriptCore (UIWebView)
- 使用三方库WebViewJavascriptBridge,可提供JS调OC,以及OC调用JS
23.JS调用OC代码的方式有哪些?
方法一:拦截url
- 通过拦截url(适用于UIWebView和WKWebView)
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSString *url = request.URL.absoluteString;
if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
//跳转原生界面
return NO;
}
return YES;
}
方法二:messageHander
- 当JS端想传一些数据给iOS.那它们会调用下方方法来发送.
- window.webkit.messageHandlers.<方法名>.postMessage(<数据>)上方代码在JS端写会报错,导致网页后面业务不执行.可使用try-catch执行.
- 那么在OC中的处理方法如下.它是WKScriptMessageHandler的代理方法.name和上方JS中的方法名相对应.
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
方式三:WebViewJavascriptBridge
1. 设置 webViewBridge
_bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.webView];
[_bridge setWebViewDelegate:self];
2. 注册handler方法,需要和 前段协商好 方法名字,是供 JS调用Native 使用的。
[_bridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
// OC调用
NSString *scanResult = @"http://www.baidu.com";
// js 回调传参
responseCallback(scanResult);
}];
3. OC掉用JS
[_bridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
NSLog(@"调用完JS后的回调:%@",responseData);
}];
24.OC调用JS代码方式有哪些?
方式一:stringByEvaluatingJavaScriptFromString直接调用
// 直接运行 使用
NSString *jsStr = @"执行的JS代码";
[webView stringByEvaluatingJavaScriptFromString:jsStr];
方式二:用JavaScriptCore框架
#import <JavaScriptCore/JavaScriptCore.h>
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//获取webview中的JS内容
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *runJS = @"执行的JS代码";
//准备执行的JS代码
[context evaluateScript:runJS];
}
25.Block和Protocol的区别,Block是为了解决什么问题而使用的
- 代理和Block的共同特性是回调机制,不同的是,代理的方法比较多,比较分散,公共接口,方法较多也选择用delegate进行解耦
- 使用Block的代码比较集中统一,异步和简单的回调用Block更好 Block是为了解决什么问题而使用的? 个人认为:
- Block为了多线程之间调度产生的;
- Block 也是一个OC对象,可以当参数传递,使用方便简单,灵活,很少的代码就可以实现代码回调.比Protocol省很多代码
26.字典的工作原理 ?怎么从100w个中是怎么快速去取value?
- NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的,hash函数设计的好坏影响着数据的查找访问效率。
(void)setObject:(id)anObject forKey:(id <NSCopying>)aKey;
- Objective-C 中的字典 NSDictionary 底层其实是一个哈希表,实际上绝大多数语言中字典都通过哈希表实现
哈希表:
- 哈希表的本质是一个数组,数组中每一个元素称为一个箱子(bin),箱子中存放的是键值对。数组长度即箱子数。
- 哈希表还有一个重要的属性: 负载因子(load factor),它用来衡量哈希表的 空/满 程度,一定程度上也可以体现查询的效率,计算公式为:负载因子 = 总键值对数 / 箱子个数 负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,或者 0.75 等)时,哈希表将自动扩容。
- 参考: www.jianshu.com/p/88dfc8f40…
27. isEquel,hash,isEquelToString 方法的区别?
isEquel
- isEquel 用于比较2个对象是否相等, 与==(地址比较)不一样, * * isEquel 首先比较2个对象地址,如果相同就返回YES,如果不同,就比较对象类型,以及属性的值,一般重写 isEquel 来比较2个对象
- isEquel 比较的是2个NSObject的方法
hash
- hash 是一个类方法,任何类都可以调用这个方法,返回的结果是一个NSInteger值(如果两个对象相等,那么他们的hash值一定相等,但是,如果两个对象的哈希值相等是不能一定推出来这两个对象是相等的)
isEquelToString
- isEquelToString是比较2个字符串值是否相等
28.iOS 9 以后通知不再需要手动移除
- iOS 9通知中心持有的是注册者的
unsafe_unretained
指针 - iOS 9 以后,通知中心持有的是注册者的
weak
指针
29.如何对 NSMutableArray 进行 KVO
- 一般情况下只有通过调用 set 方法对值进行改变才会触发 KVO。但是在调用NSMutableArray的 addObject或removeObject 系列方法时,并不会触发它的 set 方法。所以为了实现NSMutableArray的 KVO,官方为我们提供了如下方法
@property (nonatomic, strong) NSMutableArray *arr;
//添加元素操作
[[self mutableArrayValueForKey:@"arr"] addObject:item];
//移除元素操作
[[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
30.函数指针和 Block区别
相同点:
- 二者都可以看成是一个代码片段。
- 函数指针类型和 Block 类型都可以作为变量和函数参数的类型
不同点:
- 函数指针只能指向预先定义好的函数代码块,函数地址是在编译链接时就已经确定好的。从内存的角度看,函数指针只不过是指向代码区的一段可执行代码
- block 本质是 OC对象,是 NSObject的子类,是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。
声明
本文面试题答案是参考各路大神收集得来,答案不一定是最全面,最合适的,如果哪里有问题,欢迎大家积极讨论。
参考文章:
转载自:https://juejin.cn/post/6982132528828121124