2023 iOS开发面试题
一 常见的设计模式
1.1 单例
用于创建一个全局可访问的对象实例。
/// 下面这段代码需要可以默写下来 在纸上写
#import "XZUserManager.h"
@implementation XZUserManager
+ (instancetype)sharedManager
{
return [[self alloc] init];
}
static XZUserManager *manager;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [super allocWithZone:zone];
});
return manager;
}
@end
优点:只创建一个对象,外界访问起来方便。
缺点:一个类只有一个对象,造成责任过重,在一定程度上违背了“单一职责原则”。单例模式中无抽象层,不易于扩展。由于单例的生命周期与应用程序的生命周期相同,不能过多的创建单例,过多的创建单例会影响性能,造成系统资源浪费。
1.2 委托/代理
可以实现类与类之间一对一的通信。将某个对象的任务委托给其他对象进行处理。
1.2.1 委托能否实现一对多的通信?
可以。采用多播委托的方式来实现。
1.2.2 代理为什么使用weak?
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class ZYHomeCell;
@protocol ZYHomeCellDelegate <NSObject>
/// 代理方法如果不写 @required 默认都是 @required (必须实现的)
@required
- (void)homeCellDidClickedFollow:(ZYHomeCell *)homeCell;
/// @optional 表示代理方法可以实现也可以不实现(是可选的)
@optional
- (void)homeCellDidClickedChat:(ZYHomeCell *)homeCell;
@end
@interface ZYHomeCell : UICollectionViewCell
/// 使用 weak 是为了防止循环引用造成内存泄漏
@property (nonatomic, weak) id <ZYHomeCellDelegate> delegate;
@end
NS_ASSUME_NONNULL_END
1.3 观察者模式
1.3.1 NSNotificationCenter
NSNotificationCenter是一种发布-订阅模式的通知机制,允许不同对象之间进行消息传递。
1.3.1.1 重复注册通知会有问题吗?
不会有问题。但会出现多次响应的效果。
1.3.1.2 在子线程发送通知有没有问题?
在子线程发送通知,接收通知也会在对应的子线程,此时需要注意返回到主线程执行UI刷新操作。
1.3.2 KVO
允许一个对象监听另一个对象属性值的变化。
优点:是一种无侵入的监听实现,在被监听的对象中不需要额外的添加代码。
缺点:由于会动态的生成新的类,过多使用会影响性能。而且使用上需要注意:多次添加或移除同一个观察者消息;或者在添加之前移除;或者发送回调消息到已释放的观察者都可能会引起崩溃。
1.3.2.1 KVO的实现原理?
当对一个对象添加监听的时候,iOS会修改对象的isa指针(由对象所属的类变为runtime动态生成的子类NSKVONotifying_)。子类重写的set方法,内部顺序调用:
-
willChangeValueForKey:
在被观察属性发生改变之前调用,记录旧值。 -
[super set]
方法 -
didChangeValueForKey:
(并且会在didChangeValueForKey:
方法中调用KVO的回调方法observeValueForKeyPath:ofObject:change:context:
)
1.3.2.2 直接修改成员变量会触发KVO吗?
不会触发KVO。 因为其内部是重写set方法来达到监听的。
1.3.2.3 通过KVC修改属性会触发KVO吗?
会触发KVO。 KVC内部会触发set方法。
1.3.2.4 如何KVO一个数组内元素的变化?
todo
1.3.2.5 如何手动触发KVO?
手动调用willChangeValueForKey:
和didChangeValueForKey:
1.3.2.6 使用KVOController
KVOController优势:简单/易用/安全
提供了更加安全的接口/更加易用的接口/线程安全的接口
[self.KVOController observe:_collectionView keyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"object == %@ change == %@", object, change);
}];
1.4 MVC & MVVM
MVC是iOS开发中一个经典的设计模式。MVC其用意就是将数据和视图分离开来。view上显示什么取决于model,只要model改变了,相应的view显示也跟着改变。controller负责发送网络请求,处理服务器返回来的数据,解析数据成view可以直接显示。controller还负责处理业务逻辑,所以MVC的一个痛点就是会造成臃肿的controller,代码难以维护。
MVVM很好的解决了这个问题,在MVC的模式基础上增加了view model层,把网络请求和业务逻辑交给了vm层,很好的将业务逻辑和视图显示分离开来。
MVVM的中心思想是数据实现双向绑定,业务逻辑与视图分离。
MVVM是一个很好的替代MVC的方案,解决了控制器臃肿的问题。
1.5 工厂模式
二 内存管理
iOS使用自动引用计数(Automatic Reference Counting,简称 ARC)作为其主要的内存管理机制。
2.1 属性关键字
atomic
:默认。多线程同时访问set方法
/get方法
时是按照顺序执行的,set方法
/get方法
内部 加锁来保证读写安全(这里的锁是自旋锁)。只能保证属性的读写安全,不是绝对的线程安全(比如操作数组,增加或移除,这种情况可以使用互斥锁来保证线程安全)。速度慢。需要消耗大量系统资源来为属性加锁。
nonatomic
:访问速度更快。如果多线程同时访问,会出现无法预料的结果。需要避免多线程同时操作属性。UIKit框架是线程不安全的框架,大多数的类属性都被声明成为nonatomic
,所以我们要保证在主线程来刷新UI。
readwrite
:默认。读写属性,系统会自动生成属性set方法
和get方法
。
readonly
:只读属性,仅生成get方法
,不会生成set方法
方法。用于禁止手动修改的属性,比如nsstring的length属性。
assign
:可用于修饰基本数据类型。为什么?基本数据类型是分配在栈上的,栈空间由系统自动处理回收,不会造成野指针。这也是为什么assign不能用来修饰对象的原因,对象是分配到堆空间的。对象释放之后指针依然存在,访问野指针会造成奔溃。
strong
:UI成员变量可以使用strong来修饰(通常)。
weak
:UI成员变量可以使用weak来修饰。
在使用storyboard或者xib拖控件的时候,默认生成的属性就是使用weak修饰的
@property(nonatomic,weak) IBOOutlet UIButton *btn;
代理属性需要使用weak来修饰,主要用来避免循环引用的问题。
weak修饰的属性在被释放后会被置为nil,避免野指针;
copy
: NSString/NSArray/NSDictionary 一般使用copy 而不使用strong,因为不管赋值的是可变类型还是不可变类型,复制出来的对象和源对象相互独立,互不影响。
block也经常使用copy。为什么?
block在没有使用外部变量时,内存存在全局区。
block在使用外部变量的时候,内存是存在于栈区。
当block copy之后,是存在堆区的。
存在于栈区的特点是对象随时有可能被销毁,一旦销毁在调用的时候,就会造成系统的崩溃。所以block要用copy关键字。
getter=
: 可以用来修改系统自动生成的get方法
名字,一般用在bool类型的get方法,前面会加一个is
setter=
: 可以用来修改系统自动生成的set方法
名字, 不经常使用。
2.2 weak修饰的属性,如何自动置为nil的?
Runtime维护了一个Weak表,用于存储指向某个对象的所有Weak指针。Weak表其实是一个哈希表,Key是所指对象的地址,Value是Weak指针的地址(这个地址的值是所指对象的地址)的数组。在对象被回收的时候,经过一层层调用,会最终触发clearDeallocating方法
将所有Weak指针的值设为nil。
weak 的实现原理可以概括为以下三步:
- 初始化时:runtime会调用
objc_initWeak函数
,初始化一个新的weak指针指向对象的地址。 - 添加引用时:
objc_initWeak函数
会调用objc_storeWeak() 函数
,objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表。 - 释放时,调用
clearDeallocating函数
。clearDeallocating函数
首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
2.3 @autoreleasepool
自动释放池
用于管理对象的释放, 当一个对象被加入到autoreleasepool中时,它的引用计数不变,但会在autoreleasepool被释放时自动减少引用计数。@autoreleasepool
在多数情况下由编译器自动生成,但在特定情况下可以显示的调用:
- 长时间循环
- 大块临时对象创建
目的是即时释放这些临时对象,避免这些对象占用大量的内存。
for (int i = 0; i < 10000000; i ++) {
@autoreleasepool {
///创建对象
}
}
三 Runtime
Runtime是指OC的运行时系统,允许动态的创建类和方法。核心主要是消息传递,如果消息在对象中找不到就进行转发。
3.1 消息传递的流程
- 首先根据对象的
isa指针
找到它的class
- 在
class
的cache
中找 - 在
class
的method list
找 - 在
super class
的method list
找 - 找到就执行,没有找到就进入消息转发的流程
3.2 消息转发的流程
消息转发的流程示意图- 动态方法解析
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
///并没有这个方法
[self performSelector:@selector(foo:)];
}
///动态的方法解析 运行时动态的添加一个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(foo:)) {
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
- 备用接收者
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
///并没有这个方法
[self performSelector:@selector(foo:)];
}
///动态的方法解析 运行时动态的添加一个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(foo:)) {
///这里return NO 就进行到下一步
return NO;
}
return [super resolveInstanceMethod:sel];
}
///备用接收者 把方法交给其他对象来处理
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(foo)) {
return [CustomParentView new];//返回CustomParentView对象,让CustomParentView对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
- 完整的消息转发
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
///并没有这个方法
[self performSelector:@selector(foo)];
}
///动态的方法解析 运行时动态的添加一个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(foo)) {
///这里return NO 就进行到下一步
return NO;
}
return [super resolveInstanceMethod:sel];
}
///备用接收者 把方法交给其他对象来处理
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(foo)) {
///这里return nil 就进行到下一步
return nil;
}
return [super forwardingTargetForSelector:aSelector];
}
///这个方法返回一个函数签名 如果返回nil 则直接发送-doesNotRecognizeSelector:消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL sel = anInvocation.selector;
CustomParentView *p = [CustomParentView new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
3.3 相关的结构体
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
3.4 元类
objc_class
结构体的第一个成员也是isa指针
通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object
结构体实例它的isa指针
指向类对象,
类对象的isa指针
指向了元类,super_class指针
指向了父类的类对象,
而元类的super_class
指针指向了父类的元类,那元类的isa指针
又指向了自己。
元类(Meta Class)是一个类对象的类。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针
必须指向一个包含这些类方法的一个objc_class
结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针
是指向它自己。
3.5 Runtime的应用
3.5.1 关联对象(给分类增加属性)
-------------------------------------------- .h --------------------------------------------
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (LY)
@property (nonatomic, copy) NSString *tcl;
@end
NS_ASSUME_NONNULL_END
-------------------------------------------- .h --------------------------------------------
#import "UIViewController+LY.h"
#import <objc/runtime.h>
@implementation UIViewController (LY)
@dynamic tcl;
static char kTclKey;
- (void)setTcl:(NSString *)tcl
{
objc_setAssociatedObject(self, &kTclKey, tcl, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tcl
{
return objc_getAssociatedObject(self, &kTclKey);
}
@end
3.5.2 方法交换
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(ly_ViewDidLoad);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)ly_ViewDidLoad {
// 在这里可以添加自定义的逻辑或功能
// ...
// NSLog(@"UIViewController执行viewDidLoad方法之前执行这行");
// 调用原始的viewDidLoad方法
[self ly_ViewDidLoad];
}
3.5.3 KVO的实现原理
四 RunLoop
Runloop就是来管理线程的,当线程的Runloop被开启后,线程会在执行完任务后进入休眠状态(而不是退出)有了任务就会被唤醒去执行任务。主线程的Runloop默认是开启的,其他线程的Runloop是默认关闭的,需要手动开启。
4.1 Runloop与线程
- 每条线程都有唯一的一个与之对应的Runloop对象。
- Runloop保存在一个全局的字典里,线程作为key,Runloop作为value。
- 线程刚创建时并没有Runloop对象,Runloop会在第一次获取它时创建
Runloop会在线程结束时销毁。 - 主线程的Runloop已经自动获取(创建),子线程默认没有开启Runloop。
4.2 Runloop的Mode
4.3 source0 & source1
五 多线程 & GCD & NSOperationQueue & 锁
5.1 多线程开发需要注意的问题:
5.1.1 数据竞争
5.1.2 死锁
5.1.3 过多线程消耗内存
5.2 GCD
GCD是苹果开发的一个多核编程的解决方案。
5.2.1 GCD的优势?
- GCD会自动利用更多的CPU内核。
- 自动管理线程的生命周期。程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
5.2.2 GCD的使用?
- 创建一个队列(串行队列、并发队列)
- 将任务追加到任务的等待队列中,然后系统就会根据指定的执行任务的方式来执行任务(同步执行或异步执行)。
5.2.3 GCD的使用注意?
- 在使用GCD的时候有一种情况会造成死锁:在主线程中把一个任务放到主队列中同步执行。
5.2.4 串行队列 & 并发队列
队列是GCD中的基本概念,它是一种数据结构,用于存储任务。GCD中有两种类型的队列:串行队列(Serial Queue)和并发队列(Concurrent Queue)。
两者的主要区别是执行任务的顺序以及开启新线程的个数。
5.2.5 同步 & 异步
执行任务有两种方式:同步执行(sync)异步执行(async)两者的主要区别是:
- 是否等待队列中的任务执行结束
同步:任务在当前线程执行,会阻塞当前线程。
异步:任务会开辟新的线程,不会阻塞当前线程。 -
是否具备开启新线程的能力。
同步:没有开启新线程的能力。
异步:有开启新线程的能力 但不一定用(主队列异步执行就没有开启新线程)。
image.png
5.2.3 GCD常用方法
-
dispatch_apply
快速迭代 -
dispatch_once
执行一次 -
dispatch_after
延迟执行 -
dispatch_group
队列组 -
dispatch_semaphore
信号量 -
dispatch_barrier
栅栏函数 -
dispatch_source_t
定时器
5.3 NSOperation & NSOperationQueue
5.4 锁
锁的作用:保证同一时间只有一个线程可以访问共享资源。保证线程安全,解决资源竞争的问题。
5.4.1 自旋锁自旋锁会忙等
自旋锁是一种基于忙等待的锁,当多个线程访问共享资源时,自旋锁会不停地进行循环检查,直到获取到锁为止。自旋锁的好处在于它避免了线程切换和上下文切换的开销,在多核 CPU 上,自旋锁可以充分利用 CPU 时间片,因此在锁竞争不激烈的情况下,自旋锁的性能比互斥锁好。
自旋锁:atomic
OSSpinLock
OSSpinLock
iOS 10 弃用 使用os_unfair_lock()
5.4.2 互斥锁互斥锁会休眠
与自旋锁不同的是,互斥锁会将未获得锁的线程挂起,等待锁的释放。这种操作需要进行上下文切换,开销较大,因此互斥锁的性能不如自旋锁。
互斥锁:
pthread_mutex
@synchronized
(常用:使用简单,不需要显式的创建锁对象。性能也是最差)
NSLock
(互斥锁:lock/unlock必须在同一个线程操作)
NSConditionLock
NSCondition
(条件锁)
NSRecursiveLock
(递归锁:可以被同一个线程多次获得而不会造成死锁,内部会统计加锁/解锁次数,两者需要相等的时候才会成功的释放锁)
5.4.3 信号量
dispatch_semaphore
信号量是一种常见的并发控制机制,用于控制对共享资源的访问。当多个线程访问共享资源时,信号量可以用来限制并发访问的数量。信号量有一个计数器,每个线程访问共享资源前需要获取信号量,如果计数器为0,则线程需要等待,直到其他线程释放信号量为止。如果计数器不为0,则线程可以访问共享资源,并将计数器减1。当线程访问完共享资源后,需要释放信号量,使计数器加1,以便其他线程可以访问共享资源。
///售票
- (BOOL)saleTicket
{
///信号量 -=1 信号量<=0 暂停其他线程访问共享资源 等待信号释放
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
if (self.ticketsArray.count) {
///还有余票
NSString *firstTicket = self.ticketsArray.firstObject;
NSLog(@"成功售出【%@】-- %@",firstTicket,[NSThread currentThread]);
[self.ticketsArray removeObject:firstTicket];
///释放信号 信号量+=1 信号量>=1 允许其他线程访问共享资源
dispatch_semaphore_signal(self.semaphore);
return YES;
}else {
///售罄
NSLog(@"售罄");
dispatch_semaphore_signal(self.semaphore);
return NO;
}
}
5.4.4 优先级反转问题?
优先级反转指的是高优先级的线程因等待低优先级线程所持有的锁而被阻塞的情况。在这种情
况下,高优先级的线程可能一直等待,直到低优先级的线程释放锁。并且高优先级的线程会抢
占CPU资源,低优先级的线程得不到CPU调度,进而延长了等待时间,导致高优先级的任务被延迟执行。
5.5 NSOprationQueue VS GCD
- GCD底层是C语言构成的API。NSOperationQueue及相关对象是Objc对象。在GCD中,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构。而NSOperation作为一个对象,为我们提供了更多的选择。(NSOperation 操作 是一个抽象类 使用其子类NSBlockOperation / NSInvocationOperation / 自定义的子类)。
- 在NSOperationQueue中,取消任务非常方便(NSOperation有
cancel方法
),而GCD没法停止已经加入queue的block。 - NSOperation能够方便的设置依赖关系(
addDependency:方法
),还能设置NSOperation的priority优先级(queuePriority属性
),能够使同一个并行队列中的任务区分先后地执行(设置操作的优先级)。在GCD中,我们只能区分不同任务队列的优先级,如果要区分block任务优先级也需要大量复杂代码。NSOperationQueue还可以设置最大并发数(maxConcurrentOperationCount属性
),GCD则需要自己实现。 - NSOperation任务状态属性支持KVO,可以通过KVO来监听operation的就绪(
ready属性
)、取消(cancelled属性
)、执行中(executing属性
)、执行完成(finished属性
)等状态。GCD则无法判断当前任务执行状态。
六 IAP
- 引入
#import <StoreKit/StoreKit.h>
- 通过productId获取产品信息,使用SKProductsRequest
- (void)requestProductWithProductIdentifier:(NSString *)productIdentifier
{
if (!productIdentifier.length){
return;
}
NSSet *identifiers = [NSSet setWithObjects:productIdentifier, nil];
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers];
productsRequest.delegate = self;
[productsRequest start];
}
- SKProductsRequestDelegate协议方法中返回产品信息
#pragma mark - SKProductsRequestDelegate
/// 获取商品信息代理回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
NSArray *products = [NSArray arrayWithArray:response.products];
NSArray *invalidProductIdentifiers = [NSArray arrayWithArray:response.invalidProductIdentifiers];
/// 获取可购买产品id
if (products.count > 0) {
for (SKProduct *product in products) {
NSLog(@"可购买产品id: %@", product.productIdentifier);
[self addPayment:product];
}
}
/// 无效的产品id
[invalidProductIdentifiers enumerateObjectsUsingBlock:^(NSString *invalid, NSUInteger idx, BOOL *stop) {
NSLog(@"无效的产品id: %@", invalid);
}];
}
- (void)requestDidFinish:(SKRequest *)request
{
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
}
- 获取到产品信息后,添加产品到购买队列,调起支付。使用SKPaymentQueue
/// 添加到购买队列-调起支付
- (void)addPayment:(SKProduct *)product {
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- 添加观察者 SKPaymentTransactionObserver,监听后续的支付结果
#pragma mark - **************** SKPaymentTransactionObserver Delegate
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing: //商品添加进列表 myProduct.localizedTitle
break;
case SKPaymentTransactionStatePurchased://交易成功
[self completeTransaction:transaction];
break;
case SKPaymentTransactionStateFailed://交易失败
[self failedTransaction:transaction];
break;
case SKPaymentTransactionStateRestored://已购买过该商品
break;
case SKPaymentTransactionStateDeferred://交易延迟
[self failedTransaction:transaction];
break;
default:
break;
}
}
}
- 交易成功之后,将获取的交易凭证(SKPaymentTransaction)发送给后端,让后端去苹果服务器进行校验
- 不管交易成功还是失败,处理完逻辑一定要调用
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
告诉苹果已完成当前操作,不然之后再购买就会失败!
如果创建的是【非消耗型项目】,则要求必须提供【恢复购买】的功能
七 定时器
# 6.1 CADisplayLink
# 6.2 NSTimer
# 6.3 dispatch_source_t
八 数据持久化 & 沙盒
通常一个缓存是由内存缓存
和磁盘缓存
组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。
- 对于一些用户的偏好设置,系统配置等轻量级的数据可以使用NSUserDefaults来进行存储,数据是以plist文件的形式存储在
沙盒/Library/Preferences
文件夹下。NSUserDefaults只能存储NSString/NSInteger/NSData等系统定义的数据类型。 - 如果要存储自定义的类对象则需要先使用
NSKeyedArchiver
将对象进行归档(也叫序列化)转化成NSData,然后再进行存储。解析时使用NSKeyedUnarchiver
进行解档(也叫反序列化),需要进行归档操作的类需要实现NSSecureCoding协议
的两个方法,encodeWithCoder:
和initWithCoder:
和supportsSecureCoding
。(iOS13之前是NSCoding协议
)
-------------------------------------------- .h --------------------------------------------
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LYBook : NSObject <NSSecureCoding>
@property (nonatomic, copy) NSString *bookId;
@property (nonatomic, assign) NSInteger bookNumber;
@end
NS_ASSUME_NONNULL_END
-------------------------------------------- .h --------------------------------------------
#import "LYBook.h"
@implementation LYBook
- (instancetype)initWithCoder:(NSCoder *)coder
{
if (self = [super init]) {
_bookId = [coder decodeObjectForKey:@"bookId"];
_bookNumber = [coder decodeIntegerForKey:@"bookNumber"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_bookId forKey:@"bookId"];
[coder encodeInteger:_bookNumber forKey:@"bookNumber"];
}
+ (BOOL)supportsSecureCoding
{
return YES;
}
@end
NSKeyedArchiver缺点就是也只适用于存储比较轻量级的数据,而且一次只能归档一个对象,如果需要归档多个对象,需要进行多次操作,繁琐耗时。比如我们需要存储一个大的列表以便用户在没有网络的情况下也可以浏览之前浏览过的内容。
8.1 使用 YYModel
///自动接档
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
return [self yy_modelInitWithCoder:aDecoder];
}
///自动归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
[self yy_modelEncodeWithCoder:aCoder];
}
+ (BOOL)supportsSecureCoding
{
return YES;
}
- 可以使用数据库存储,
FMDB
等
创建数据库对象
FMDatabase *db = [FMDatabase databaseWithPath:path];
8.2 沙盒文件目录
/*保存持久化数据,会备份。一般用来存储需要持久化的数据。
一般我们在项目中,我们会把一些用户的登录信息以及搜索历史记录等一些关键数据存储到这里。*/
Documents
Library
- Caches
- Preferences
Tmp
8.3 获取目录路径
///获取应用沙盒根目录
NSString *homeDirectory = NSHomeDirectory();
///获取Documents目录路径
NSString *Document = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
///获取Library目录路径
NSString *Library = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
///获取Caches目录路径
NSString *Caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
///获取tmp目录路径
NSString *Temp = NSTemporaryDirectory();
8.4 NSFileManager
九 应用调优
9.1 包大小瘦身
9.2 卡顿检测
9.3 发热
9.4 耗电
可能引发应用耗电的场景?
- 是不是开启了定位?
- 网络请求是不是太频繁了?
- 定时任务时间是不是间隔过小?
9.5 耗流量
9.6 内存泄漏
是指申请的内存空间使用完毕之后未回收。
9.6.1 内存泄漏排查方法
9.6.1.1 静态分析(Analyze)
静态分析可以检查出:
- 内存泄露检查 Memory Error
- 逻辑错误检查 Logic Error
- 声明错误检查 Dead Store
- API调用错误检查 API Misuse
9.6.1.2 动态分析(Instrument工具里的Leaks)
9.7 崩溃问题
可能会产生崩溃的情况:
- 数组越界,在取数组索引时。
- 给数组添加nil时。
- 在子线程中进行UI更新。
- 主线程无响应:如果主线程超过系统规定的时间无响应,就会被watchDog杀掉。
- 野指针:指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃。
监控崩溃:Bugly
防崩溃处理:hook 或 安全接口
X 其他问题
x.1 + (void)load
& + (void)initialize
?
+load方法
在main函数
之前执行的。
-
+load方法
在类被加载到内存中时被自动调用。类别(category)被加载到内存中时也会调用该类别的+load方法
。 -
不需要显示的调用父类的+load方法,系统会自动调用。
-
按照继承关系按序调用。父类/子类/父类分类/子类分类
+load方法调用顺序 -
+ (void)initialize
会在类第一次使用的时候调用。 -
+ (void)initialize
方法是阻塞安全的,所以+ (void)initialize
方法中应该做一些简单的初始化操作。 -
如果子类也实现了
+ (void)initialize
方法,会跟+ (void)load
方法调用的顺序一样,先调用父类的+ (void)initialize
方法,之后调用子类的+ (void)initialize
方法。 -
如果子类没有实现
+ (void)initialize
,会调用父类的+ (void)initialize
方法(所以父类的+ (void)initialize
有可能调用多次,可以理解为子类没有实现+ (void)initialize
方法时,会拷贝一份父类的+ (void)initialize
方法,而后在依次调用)。可以像下面这样做防止父类的+ (void)initialize
方法多次调用。
+ (void)initialize
{
if (self == [SHBaseViewController self]) {
NSLog(@"%s 被调用", __func__);
}
}
- 如果分类重写了
+ (void)initialize
方法,会覆盖类本身的+ (void)initialize
方法。 - 由于是系统自动调用,也不需要再调用
[super initialize]
,否则父类的+ (void)initialize
会被多次执行。子类会从继承关系依次调用。 - 当有多个Category都实现了
+ (void)initialize
方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的+ (void)initialize
方法)。
- (void)layoutSubviews
方法:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
[self setNeedsLayout];
这个会标记视图,使得runloop的下一个周期调用layoutSubviews
。
[self layoutIfNeeded];
如果这个视图有被setNeedsLayout
方法标记的,那么会立即执行layoutSubviews
方法。
x.2 KVC
键值编码:允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。
(1)按照命名规范,先查找相关的方法,如果没有找到;
(2)按照命名规范,找相关的成员变量,如果没有找到;
(3)调用setValue:forUndefinedKey;
(4)如果都没有找到相关的方法实现就抛出异常。
x.2.1 KVC在内部是按什么样的顺序来寻找key的?
///设置值
[obj setValue:@“vlaue” forKey:@“key”];
- 首先调用obj的setKey:方法
- KVC的机制会首先检查
+ (BOOL)accessInstanceVariablesDirectly
这个方法的返回值,这个方法默认会返回YES.如果返回NO,则KVC会执行setValue:forUndefinedKey:
- 如果上述方法返回YES,KVC就会按照
_key
,_isKey
,key
,isKey
的顺序搜索成员 - 以上都没有找到,执行
setValue:forUndefinedKey:
///取值
[obj valueForKey:@"name"];
- 首先按
getName
,name
,isName
的顺序方法查找getter方法
,找到的话会直接调用。 - 检查类方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默认行为),会按_name
,_isName
,name
,isName
的顺序搜索成员变量名 - 还没有找到的话,调用
valueForUndefinedKey:
KVC的使用
- 动态地取值和设值
- 用KVC来访问和修改私有变量
x.3 深拷贝 & 浅拷贝 & 完全深拷贝 & 不完全深拷贝
深拷贝
:深拷贝就是内容拷贝,会产生新的对象。
浅拷贝
:浅拷贝就是指针拷贝,与原指针指向的是同一块内存。
- 对于不可变对象copy就是浅拷贝,mutablecopy就是深拷贝。
- copy返回的是对象的不可变类型,mutablecopy返回的是对象的可变类型
但是: - 上述所说的深拷贝对于容器类对象来说叫做
不完全深拷贝
,即只拷贝了容器本身,容器内保存的对象都是浅拷贝。如果把容器内对象都做一次深拷贝,叫做完全深拷贝
。
x.4 block
blocks是C语言的扩充功能。可以用一句话来表示blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
block的作用类似于函数,可以像变量一样进行传递。
block有以下3种类型:
__NSGlobalBlock__
:全局block, 类似于我们创建的函数,会被存放在内存区域中的数据区。数据区用来存储全局的变量。block作用域内未使用任何局部变量,可使用全局变量和静态变量。
__NSStackBlock__
:栈block,存放在内存区域中的栈区,当一个作用域结束后,与之相关的栈中的数据都会以被清理,因此超出了其所在的作用域就会被回收。
__NSMallocBlock
: 堆block,堆中的block与OC对象一样,内存释放会受到引用计数的管理。MRC:当对一个栈block进行copy时,就会创建出堆block。ARC:block传入局部变量或属性后,系统会帮助我们自动copy,成为堆block。
x.4.1 block为什么使用copy?
block在没有使用外部变量时,内存存在全局区。
block在使用外部变量的时候,内存是存在于栈区。
当block copy之后,是存在堆区的。
存在于栈区的特点是对象随时有可能被销毁,一旦销毁在调用的时候,就会造成系统的崩溃。所以block要用copy关键字。
x.5 离屏渲染
GPU屏幕渲染有两种方式:
- On-Screen Rendering (当前屏幕渲染)
指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。 - Off-Screen Rendering (离屏渲染)
指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
- 创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。 - 上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
下面的情况或操作会引发离屏渲染:
- 为图层设置遮罩(
layer.mask
) - 将图层的
layer.masksToBounds
/view.clipsToBounds
属性设置为true
- 将图层
layer.allowsGroupOpacity
属性设置为YES
和layer.opacity小于1.0
- 为图层设置阴影(
layer.shadow *
)。 - 为图层设置
layer.shouldRasterize=true
///光栅化 - 具有
layer.cornerRadius
,layer.edgeAntialiasingMask
,layer.allowsEdgeAntialiasing
的图层 - 文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
- 使用CGContext在
drawRect :方法
中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
x.6 事件传递 & 响应链
一个触摸事件的响应过程如下:
- 当用户触摸屏幕时,UIKit会生成UIEvent对象来描述触摸事件。对象内部包含了触摸点坐标等信息。
- 通过
Hit Test
确定用户触摸的是哪一个UIView。这个步骤通过- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
方法来完成。
2.1 从视图层级最底层的 window 开始遍历它的子 View。
2.2 默认的遍历顺序是按照 UIView 中 Subviews 的逆顺序。
2.3 找到 hit-TestView 之后,寻找过程就结束了。 - 找到被触摸的UIView之后,如果它能够响应用户事件,相应的响应函数就会被调用。如果不能响应,就会沿着
响应链(Responder Chain)
寻找能够响应的UIResponder对象(UIView是UIResponder的子类)来响应触摸事件。也就是把事件传给nextResponder
- vc直接管理的view的nextResponder是VC。
vc.view.nextResponder = vc
- vc的nextResponder是他直接管理的view的superview。
vc.nextResponder = vc.view.superview
- 如果不是vc直接管理的view的nextResponder是他的superview。
view.nextResponder = view.superview
- uiwindow的nextResponder是UIApplication
- UIApplication的nextResponder是AppDelegate
x6.1 通过重写父view的hitTest:withEvent:
方法来解决超出父view的子view不能响应点击事件的问题?
@interface CustomParentView : UIView
@end
@implementation CustomParentView
///在上面的代码中,我们创建了一个名为`CustomParentView`的父视图子类,并重写了`hitTest:withEvent:`方法。
///在该方法中,我们遍历父视图的所有子视图,并判断点击的点是否在子视图的范围内。
///如果是,则返回子视图,否则将点击事件传递给默认的父视图的`hitTest:withEvent:`方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *subview in self.subviews) {
CGPoint subviewPoint = [subview convertPoint:point fromView:self];
if (CGRectContainsPoint(subview.bounds, subviewPoint)) {
return subview;
}
}
return [super hitTest:point withEvent:event];
}
@end
x.7 分类 & 扩展
分类的结构体如下:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
分类中可以添加实例方法/类方法/协议/属性 但不可以添加实例变量。
注意,在category中可以有属性(property),但是该属性只是生成了getter和setter方法的声明,并没有产生对应的实现,更不会添加对应的实例变量。如果想为实例对象添加实例变量,可以尝试使用关联引用技术。
-------------------------------------------- .h --------------------------------------------
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (LY)
@property (nonatomic, copy) NSString *tcl;
@end
NS_ASSUME_NONNULL_END
-------------------------------------------- .h --------------------------------------------
#import "UIViewController+LY.h"
#import <objc/runtime.h>
@implementation UIViewController (LY)
@dynamic tcl;
static char kTclKey;
- (void)setTcl:(NSString *)tcl
{
objc_setAssociatedObject(self, &kTclKey, tcl, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tcl
{
return objc_getAssociatedObject(self, &kTclKey);
}
@end
x.8 UIView & CALayer
- UIView可以响应事件,CALayer不可以。
- UIView继承UIResponder,属于UIKit框架。CALayer继承NSObject,属于QuartzCore框架。
- UIView是CALayer的代理。(UIView遵守了CALayerDelegate协议)
- UIView主要是对显示内容的管理,CALayer主要是对显示的绘制。
x.9 动态库 & 静态库
动态库形式:.dylib
和.framework
静态库形式:.a
和.framework
x.9.1动态库和静态库的区别
静态库:链接时,静态库会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝
系统动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存
x.10 imageNamed:
和 imageWithContentsOfFile:
imageNamed:
是带缓存的
x.11 UITableView滚动流畅性
- 提前计算好cell的高度,缓存在相应的数据源模型中
- 使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角
- cell.layer.shouldRasterize = YES;///光栅化
- (void)setupSubViews
{
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
button.backgroundColor = [UIColor redColor];
[button addTarget:self action:@selector(clicked:) forControlEvents:UIControlEventTouchUpInside];
// button.layer.masksToBounds = YES;
// button.layer.cornerRadius = 10;
///使用UIBezierPath画一个圆角
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:button.bounds cornerRadius:10];
CAShapeLayer *layer = [[CAShapeLayer alloc] init];
layer.frame = button.bounds;
layer.path = bezierPath.CGPath;
button.layer.mask = layer;
[self.view addSubview:button];
}
x.12 @synthesize
& @dynamic
当我们声明了一个属性name的时候,编译器会自动生成一个_name的私有成员变量
和set方法
和get方法
。当我们同时重写了set方法
和get方法
的时候,编译器就不会生成_name成员变量
了,需要通过@synthesize
定一个成员变量的名字。
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@end
@implementation ViewController
@synthesize name = _name;
///自己实现set方法
- (void)setName:(NSString *)name
{
_name = name;
}
///同时 自己实现了get方法 编译器不在自动生成_name这个成员变量 需要使用 @synthesize 自己定义一个成员变量名字
- (NSString *)name
{
return _name;
}
@end
@dynamic
就是告诉编译器 不要帮我生成set/get方法
当然也不会自动生成_name成员变量
了 由开发者自己实现。
@interface ViewController ()
@property (nonatomic, copy) NSString *name;
@end
@implementation ViewController
{
NSString *_name;
}
///就是告诉编译器 不要帮我生成set/get方法
@synthesize name;
///自己实现set方法
- (void)setName:(NSString *)name
{
_name = name;
}
///同时 自己实现了get方法 编译器不在自动生成_name这个成员变量 需要使用 @synthesize 自己定义一个成员变量名字
- (NSString *)name
{
return _name;
}
@end
x.13 UIViewController的生命周期函数
x.14 AFNetworking
x.15 为什么要在主线程刷新UI?
UIKit框架是线程不安全的框架。很多类的属性都被修饰成nonatomic,属性的读写是不安全的。异步操作存在读写问题。
x.15.1 那为什么苹果不把UIKit设计成线程安全的框架呢?
收益不高。如果把UIKit设计成线程安全的框架,并不会带来太多的便利,也不会提升太多的性能。甚至还会因为加解锁耗费大量的时间和性能,事实上,并发编程也没有因为UIKit是线程不安全的而变得困难。我们只需要保证UI操作是在主线程上完成的就可以了。
x.16 NSMutableDictionary
- key不能为nil
- object也不能为nil
- 下面3种方式不会崩溃,会移除掉对应的key
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
///语法糖 实际上是调用 [dict setObject:nil forKeyedSubscript:@"key0"];
dict[@"key0"] = nil;
/// dict[@"key0"] = nil; 相当于下面这行 obj为nil 不会崩溃。会自动调用removeObject:forKey方法。
[dict setObject:nil forKeyedSubscript:@"key0"];
///setValue:forKey:中的value能够为nil,但是当value为nil的时候,会自动调用removeObject:forKey方法。
[dict setValue:nil forKey:@"key2"];
- 但是以下情况就会崩溃。
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
///setObject:forkey:中的value是不能够为nil的,不然会报错。
[dict setObject:nil forKey:@"key1"];
///会奔溃 因为key为nil
[dict setValue:nil forKey:nil];
///会奔溃 因为key为nil
[dict setValue:@"12333" forKey:nil];
x.16.1 NSMutableDictionary
中的setObject:forKey:
与setValue:forKey:
方法有什么区别?
setValue:forKey:
:key只能是字符串。value能够为nil,但是当value为nil的时候,会自动调用removeObject:forKey
方法。
setObject:forKey:
:key可以是任意类型。value 不能是nil 否则程序崩溃。
x.17 TCP & UDP
- TCP(传输控制协议)和UDP(用户数据报协议)是
TCP/IP网络模型
中传输层的协议。 - TCP,是面向连接的,在服务端和客户端通信前要先建立连接。而UDP是无连接的,通信前不需要建立连接。
- TCP是点到点的通信,而UDP支持一对一、一对多、多对一。
- TCP拥有拥塞机制和流量控制,会根据网络情况调整传输速率。
- UDP相对于TCP不可靠。
- TCP是面向字节流的,UDP面向报文。
- TCP用于可靠传输的应用,文件传输。
- UDP用于实时应用,直播,会议。
*TCP/IP网络模型 链路层-网络层-传输层-应用层
x.18 HTTP & HTTPS
HTTP(超文本传输协议):是应用层协议。
图解HTTP书中提到,HTTPS是身披SSL外衣的HTTP 。
因为HTTP本身不具备加密功能,报文采用明文的方式进行传输,所以是不安全的。
HTTPS就是使用SSL/TLS进行加密传输,让客户端拿到服务端的公钥,然后客户端在本地生成一个随机的对称加密秘钥,使用服务端传输过来的公钥进行加密发给服务端,之后的数据传输都通过对称加密的秘钥来进行加解密,完成HTTPS流程。
*http端口号是80 https端口号是443
x.19 GET&POST
两种请求方式携带参数的方式不一样:GET请求的请求参数直接拼接到了请求的URL后面,POST请求的请求参数在request body里面,由于浏览器对url的长度限制所以get请求所携带的参数长度有限制而post请求没有,get请求多用于从服务器请求数据,post请求多用于在服务器增加或者修改数据。