iOS面试题知识点总结(下)

1,109 阅读11分钟

桥接模式和适配器模式的优缺点

  • 桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化实现化之间的桥接结构,来实现二者的解耦
  • 适配器模式(Adapter Pattern)是作为两个不兼容接口之间桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
  • 共同点:桥接适配器都是让两个东西配合工作
  • 不同点:
    • 适配器:改变已有的两个接口让他们相容
    • 桥接模式:分离抽象化实现,使两者的接口可以不同,目的是分离。

pod install的实现流程

  • 1.分析dependency
    • 对比本地Pod和podfile.lock文件中的版本,如果不一致会提示存在风险
  • 2.对比podfile是否发生变化add/remove pod依赖
    • 如果存在 ,会生成两个列表,一个是需要add的pods,一个是需要remove的pods。
  • 3.如果存在remove的,删除removepods(会删除podfile.lock里的版本依赖)
  • 4.添加需要的pods依赖。此时,如果是常规的CocoaPods库(基于git),会先去:
    • 1.Spec下查找对应的Pods文件夹
    • 2.找到对应的tag
    • 3.找到对应tag下面的podspec文件
    • 4.git clone下来代码并copy到Pod目录下
  • 5.运行pre-install hook
  • 6.生成Pod Project
  • 7.将该pod文件添加到工程中
  • 8.添加对应的framework、.a库、bundle等
  • 9.链接头文件,生成Target
  • 10.运行post-install hook
  • 11.生成podfile.lock ,之后生成文件副本mainfest.lock并将其放在Pod文件夹内。(如果出现 The sandbox is not sync with the podfile.lock这种错误,则表示manifest.lock和podfile.lock文件不一致),此时一般需要重新运行pod install命令
  • 12.配置原有的project文件(add build phase)
  • 13.添加了 Embed Pods Frameworks
  • 14.添加了 Copy Pod Resources
    • 其中,pre-install hookpost-install hook可以理解成回调函数,是在podfile里对于install之前或者之后(生成工程但是还没写入磁盘)可以执行的逻辑,逻辑为:
    pre_install do |installer| 
    # 做一些安装之前的hook
    end
    
    post_install do |installer| 
    # 做一些安装之后的hook 
    end
    

注意:pod install优先遵循 Podfile 里指定的版本信息,其次遵循 Podfile.lock 里指定的版本信息来安装对应的依赖库。

Pod install 和 Pod update的区别

  • pod install:它只是仅仅安装Podfile文件中的指定版本的库而已。并不会去检查和更新最新的版本
  • pod update:它尽可能的更新最新的三方库。前提是要符合Podfile对应库的版本限制。如果没有加pod ‘myPod’, ‘~>1.2’这种版本限制。则会更新最新的版本。

OC反射机制的实现

系统Foundation框架为我们提供了一些方法反射的API,我们可以通过这些API执行将字符串转为SEL等操作。由于OC语言的动态性,这些操作都是发生在运行时的,具体方法如下:

// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);

通过这些方法,我们可以在运行时选择创建那个实例并动态选择调用哪个方法。这些操作甚至可以由服务器传回来的参数来控制,我们可以将服务器传回来的类名和方法名实例为我们的对象

常用判断方法

在NSObject类中为我们提供了一些基础方法,用来做一些判断操作,这些方法都是发生在运行时动态判断的,具体方法如下:

// 当前对象是否这个类或其子类的实例
- (BOOL)isKindOfClass:(Class)aClass;
// 当前对象是否是这个类的实例
- (BOOL)isMemberOfClass:(Class)aClass;
// 当前对象是否遵守这个协议
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 当前对象是否实现这个方法
- (BOOL)respondsToSelector:(SEL)aSelector;

反射的优缺点

  • 优点:
    • 解耦合,消除类与类之间的依赖
  • 缺点:
    • 代码可读性降低,将原有逻辑复杂化了,不利于维护
    • 性能较差。(使用反射匹配字符串间接命中内存直接命中内存的方式要慢。当然,这个程度取决于使用场景,如果只是作为程序中很少涉及的部分,这个性能上的影响可以忽略不计。但是,如果在性能关键应用核心逻辑中使用反射,性能问题就尤其重要了)

tcp和udp的区别

  • TCP面向连接的,可靠的数据传输服务;UDP面向无连接的,尽最大努力的数据传输服务,不保证数据传输的可靠性
  • TCP面向字节流,UDP面向报文
  • TCP有拥塞控制,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有效,如直播,实时视频会议等)
  • TCP 只能是一对一的通信(TCP连接的端点是套接字socket),而 UDP 支持一对一、一对多、多对一和多对多的通信
  • TCP 的首部开销大有 20 个字节,比 UDP 的 8 个字节的首部要长

代码覆盖率检测工具(SanitizerCoverage)的实现原理

简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以__sanitizer_cov_trace_pc_ 为前缀函数调用插入到用户定义函数里借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持

开启方法

开启 SanitizerCoverage 的方法是:在 build settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard。如果含有 Swift 代码的话,还需要在 “Other Swift Flags” 中加入 -sanitize-coverage=func 和 -sanitize=undefined。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用

NSTimer 为啥不准

  • 1.NSTimer被添加在mainRunLoop中,模式是NSDefaultRunLoopModemainRunLoop负责所有主线程事件,例如UI界面的操作,复杂的运算,这样就会造成timer的阻塞
  • 2.模式的切换,当创建的timer被加入到NSDefaultRunLoopMode时,此时如果有滑动UIScrollView的操作runLoop 的mode会切换为TrackingRunLoopMode,这是timer会停止回调

解决办法

  • 1.在子线程中创建timer,在主线程进行定时任务的操作
  • 2.在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作
  • 3.gcd实现定时器

关联对象实现weak属性

weak:修饰OBJC对象不会持有指向修饰对象,同样指向的对象引用计数就不会增加,当指向的对象被释放释放的时候,weak修饰的对象会被置为nil。 因为堆内存是动态的,所以当某个地址对象被释放时候所有指向他的指针都应该被置为空weak就是为了满足避免循环引用,同时在对象被释放的时候可以被置为空的情况而存在的。
assign是为了修饰栈内存中数值对象,当使用assign修饰了一个OBJC对象的时候,可能造成野指针。原因在上面刚刚提到。

Category 属性需要使用runtime中的关联来实现set 和 get 方法。但runtime只提供如下几种修饰实现,并没有weak

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

思路是这样的,虽然runtime没有开放weak解决方案,但OBJC对象可以实现weak的,所以让需要被修饰对象去持有一个strong的对象,然后当被修饰对象被释放的时候,持有对象也会被释放,那么我们就可以捕捉到释放事件,进而使用OBJC_ASSOCIATION_ASSIGN 实现弱引用,在释放事件里面再将其释放掉,进而实现weak功能

// 定义一个对象,使用block来回调析构函数。
typedef void (^DeallocBlock)();
@interface OriginalObject : NSObject
@property (nonatomic, copy) DeallocBlock block;
- (instancetype)initWithBlock:(DeallocBlock)block;
@end

@implementation OriginalObject

- (instancetype)initWithBlock:(DeallocBlock)block
{
    self = [super init];
    if (self) {
        self.block = block;
    }
    return self;
}
- (void)dealloc {
    self.block ? self.block() : nil;
}
@end

Category添加属性

// Category 
// NSObject+property.h
@interface NSObject (property)
@property (nonatomic, weak) id objc_weak_id;
@end

// NSObject+property.m
@implementation NSObject (property)
- (id)objc_weak_id {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setObjc_weak_id:(id)objc_weak_id {
    OriginalObject *ob = [[OriginalObject alloc] initWithBlock:^{
        objc_setAssociatedObject(self, @selector(objc_weak_id), nil, OBJC_ASSOCIATION_ASSIGN);
    }];
    // 这里关联的key必须唯一,如果使用_cmd,对一个对象多次关联的时候,前面的对象关联会失效。
    objc_setAssociatedObject(objc_weak_id, (__bridge const void *)(ob.block), ob, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(self, @selector(objc_weak_id), objc_weak_id, OBJC_ASSOCIATION_ASSIGN);
}

具体体验weak 和 assgin的区别:

@property (nonatomic,weak) id      weakPoint;
@property (nonatomic,assign) id    assignPoint;
@property (nonatomic,strong) id    strongPoint;

self.strongPoint = [NSDate date];
self.weakPoint = self.strongPoint;
self.assignPoint = self.strongPoint;
self.strongPoint = nil;
NSLog(@"%p", self.weakPoint); // print 0x0 指针置为空。
NSLog(@"%p", self.assignPoint); // crash 因为self.assignPoint指针指向的对象已经被释放。

测试weak是否正确:

self.strongPoint = [NSDate date];
self.objc_weak_id = self.strongPoint;
self.weakPoint = self.strongPoint;
NSLog(@"%p", self.weakPoint); // print 指针。
NSLog(@"%p", self.objc_weak_id); // print 相同的指针。
self.strongPoint = nil;

NSLog(@"%p", self.weakPoint); // print 0x0 指针置为空。
NSLog(@"%p", self.objc_weak_id); // print 0x0 指针置为空。

Category中添加 property 相对添加方法少一些,而添加weak property少之又少,但实现通过这次实践,你可以明白什么是weak,什么是assign,而不是仅仅知道delegate中用weak,NSInteger用assign。

runtime 如何实现 weak 属性

  • 1.初始化时:runtime 调用 objc_initWeak 函数初始化一个新的 weak 指针指向对象的地址
  • 2.添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数,objc_storeWeak() 的作用是更新指针指向(指针可能原来指向着其他对象,这时候需要将该 weak 指针与旧对象解除绑定,会调用到 weak_unregister_no_lock),如果指针指向的新对象非空,则创建对应弱引用表,将 weak 指针新对象进行绑定,会调用到 weak_register_no_lock。在这个过程中,为了防止多线程竞争冲突,会有一些锁的操作
  • 3.释放时:调用 clearDeallocating 函数clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录

iOS 野指针

什么是野指针?

当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称迷途指针(即通常说的野指针)。

普通指针变成野指针需要满足两个条件:
1、其所指向的对象被释放或者收回,2、其本身没有做任何的修改

需要指出一点:访问野指针本身是没有问题的,不会引起异常;只有使用野指针时才会异常(比如OC里给对象发消息),表现就是闪退。

野指针异常

野指针异常堪称crash界的半壁江山,相比起NSException而言,野指针有这么两个特点:

1.随机性强

造成野指针是多样化的:首先内存被释放后不代表内存会立刻被覆写或者数据受到破坏,这时候访问这块内存也不一定会出错。其次,多线程技术带来了复杂的应用运行环境,在这个环境下,未加保护的数据可能是致命的。此外,设计不够严谨的代码同样也是造成野指针异常的重要原因之一

2.难以定位

NSException是高抽象层级上的封装,这意味着它可以提供更多的错误信息给我们参考。而野指针几乎出自于C语言层面,往往我们能获得的只有系统栈信息,单单是定位错误代码位置已经很难了,更不要说去重现修复

定位

解决野指针最大的难点在于定位。通常线上出现了crash需要修复时,开发者最重要的一个步骤是重现crash。而上文提到了野指针的两个特性会阻碍我们定位问题,对于这两个特性,确实也能做一些对应的处理来降低它们的干扰性:

采取辅助信息

辅助信息包括设备信息,用户信息等信息,往往可以用来重现问题。比如用户行为可以形成用户使用路径,从而重现用户使用场景。而在发生crash时,采集当前页面信息,配合用户使用路径可以快速的定位到问题发生的大概位置。经过验证,辅助信息确实有效的减少了系统栈对于问题重现的干扰

提高野指针崩溃率

由于野指针不一定会发生崩溃这一特性,即便我们通过堆栈信息辅助信息确定了大致范围,不代表我们能顺利的重现crash。一个优秀的野指针崩溃可以造成一天开发,三天debug,假如野指针的崩溃不是随机的,那么问题简单的多 image.png Xcode提供了Malloc Scribble已释放内存进行数据填充,从而保证野指针访问必然崩溃的。另外,Bugly借鉴这一原理,通过修改free函数,对已释放对象进行非法数据填充,也有效的提高了野指针崩溃率

Zombie Objects

Zombie Objects是一种完全不同的野指针调试机制,将释放的对象标记Zombie对象,再次给Zombie对象发送消息时,发生crash并且输出相关的调用信息。这套机制同时定位了发生crash的类对象以及有相对清晰的调用栈

解决方案

整理一下上述的内容,可以看到目前存在辅助信息+对象内存填充以及Zombie Objects这两种主要的应对方式。拿前者来说,填充已释放对象内存风险高,经过尝试Xcode9Malloc Scribble启动后已经不会填充对象的内存地址。其次,填充内存需要去hook更加底层的API,这意味着对代码能力要求更高。因此,借鉴Zombie Objects的实现思路去定位野指针异常是一个可行的方案

转发

转发是一项有趣的机制,它通过在通信双方中间插入一个中间层发送方不再耦合接收方,他只需要要将数据发送给中间层,由中间层来派发具体的接收方。基于转发的思想,可以做许多有趣的东西:

消息转发

iOS的消息机制让我们可以给对象发送一个未注册的消息,通常这会引发unrecognized selector异常。但是在抛出异常之前,存在一个消息转发机制,允许我们重新指定消息的接收方来处理这个消息。正式这一机制实现了unrecognized selector crash的可行化 image.png

打破引用环

循环引用是ARC环境下最容易出现的内存问题,当多个对象之间的引用形成了引用环时,极有可能会导致环中的对象都无法被释放。借鉴Proxy的方式,可以实现破坏引用环的作用。插入WeakProxy层的方式实现了防crash image.png

路由转发

组件化是项目体量达到一定程度时必须考虑的架构方案,将项目拆分基础组件业务组件,加入中间层实现组件间解耦的效果。由于业务组件之间互不依赖,因此需要合适的方案实现组件通信路由设计是一种常用的通信方式。各个模块实现canOpenURL:接口来判断是否处理对应的跳转逻辑,模块将参数信息拼接在url中传递: image.png

通知的实现原理,是同步还是异步的

同步 原因:发生事件通知中心广播,有可能有多个监听者设计上使用同步的方式,能够保证所有的监听者都对通知作出响应!不会产生遗漏