桥接模式和适配器模式的优缺点
- 桥接(Bridge)是用于
把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。 - 适配器模式(Adapter Pattern)是作为
两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。 - 共同点:
桥接和适配器都是让两个东西配合工作 - 不同点:
- 适配器:
改变已有的两个接口,让他们相容。 - 桥接模式:
分离抽象化和实现,使两者的接口可以不同,目的是分离。
- 适配器:
pod install的实现流程
- 1.
分析dependency对比本地Pod和podfile.lock文件中的版本,如果不一致会提示存在风险。
- 2.
对比podfile是否发生了变化,add/remove pod依赖- 如果存在 ,会生成两个列表,一个是需要add的pods,一个是需要remove的pods。
- 3.如果
存在remove的,删除remove的pods(会删除podfile.lock里的版本依赖) - 4.
添加需要的pods依赖。此时,如果是常规的CocoaPods库(基于git),会先去:- 1.
Spec下查找对应的Pods文件夹 - 2.
找到对应的tag - 3.
找到对应tag下面的podspec文件 - 4.
git clone下来代码并copy到Pod目录下
- 1.
- 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 hook和post-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中,模式是NSDefaultRunLoopMode,mainRunLoop负责所有主线程事件,例如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,假如野指针的崩溃不是随机的,那么问题简单的多
Xcode提供了Malloc Scribble对已释放内存进行数据填充,从而保证野指针访问是必然崩溃的。另外,Bugly借鉴这一原理,通过修改free函数,对已释放对象进行非法数据填充,也有效的提高了野指针的崩溃率
Zombie Objects
Zombie Objects是一种完全不同的野指针调试机制,将释放的对象标记为Zombie对象,再次给Zombie对象发送消息时,发生crash并且输出相关的调用信息。这套机制同时定位了发生crash的类对象以及有相对清晰的调用栈
解决方案
整理一下上述的内容,可以看到目前存在辅助信息+对象内存填充以及Zombie Objects这两种主要的应对方式。拿前者来说,填充已释放对象的内存风险高,经过尝试Xcode9的Malloc Scribble启动后已经不会填充对象的内存地址。其次,填充内存需要去hook更加底层的API,这意味着对代码能力要求更高。因此,借鉴Zombie Objects的实现思路去定位野指针异常是一个可行的方案
转发
转发是一项有趣的机制,它通过在通信双方中间,插入一个中间层。发送方不再耦合接收方,他只需要要将数据发送给中间层,由中间层来派发给具体的接收方。基于转发的思想,可以做许多有趣的东西:
消息转发
iOS的消息机制让我们可以给对象发送一个未注册的消息,通常这会引发unrecognized selector异常。但是在抛出异常之前,存在一个消息转发机制,允许我们重新指定消息的接收方来处理这个消息。正式这一机制实现了unrecognized selector crash的可行化
打破引用环
循环引用是ARC环境下最容易出现的内存问题,当多个对象之间的引用形成了引用环时,极有可能会导致环中的对象都无法被释放。借鉴Proxy的方式,可以实现破坏引用环的作用。插入WeakProxy层的方式实现了防crash
路由转发
组件化是项目体量达到一定程度时必须考虑的架构方案,将项目拆分基础组件和业务组件,加入中间层实现组件间解耦的效果。由于业务组件之间互不依赖,因此需要合适的方案实现组件通信,路由设计是一种常用的通信方式。各个模块实现canOpenURL:接口来判断是否处理对应的跳转逻辑,模块将参数信息拼接在url中传递:
通知的实现原理,是同步还是异步的
同步 原因:发生事件,通知中心广播,有可能有多个监听者,设计上使用同步的方式,能够保证所有的监听者都对通知作出响应!不会产生遗漏