RunLoop
? RunLoop
作用有哪些?RunLoop
可以称之为运行循环,在程序运行过程中循环做一些事情,如果没有 RunLoop
程序执行完毕就会立即退出,有 RunLoop
程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。RunLoop
,RunLoop
保证主线程不会被销毁,也就保证了程序的持续运行。App
中的各种事件(比如:触摸事件,定时器事件,Selector
事件等) 。RunLoop
就告诉 CPU
,现在没有事情做,我要去休息,这时 CPU
就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop
就会立马起来去做事情。// 获取响应 事件的视图,通过下面的方法判断触控点位置
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
// 判断触摸点是不是在这个view的坐标上。如果在坐标上,会分发事件给这个view的子view。后每个子view重复以上步骤,直至最底层的一个合适的view。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
那么事件响应链是如何响应的呢?可简称为 “由子及父” 的过程,即:
RunLoop
是开启的?app启动前会调用main函数,具体如下:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
mian函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行,所以在主线程中开启一个RunLoop,让主线程常驻 。
UIKit 并不是一个线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。
另一方面因为整个程序的起点 UIApplication 是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。 而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
PerformSelector
和RunLoop
的关系 ?当调用 NSObject 的 performSelecter:afterDelay:
后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread:
时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
在NSThread
执行的方法中添加while(true){},这样是模拟 RunLoop
的运行原理,结合GCD
的信号量,在 {}
中处理任务。
采用 RunLoop
的方式。参考这篇文章
让子线程永远活着,这时就要用到常驻线程:给子线程开启一个 RunLoop
注意:
子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。
子线程开启 RunLoop
的代码,先点击屏幕开启子线程并开启子线程 RunLoop
。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 创建子线程并开启
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show {
// 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
// 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
NSLog(@"%s",__func__);
// 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
// 添加Source [NSMachPort port] 添加一个端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一个Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 创建监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 给RunLoop添加监听者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子线程需要开启RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
// 用常驻线程处理事情
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
注意:
创建子线程相关的 RunLoop
,在子线程中创建即可,并且 RunLoop
中要至少有一个 Timer
或 一个 Source
保证 RunLoop
不会因为空转而退出,因此在创建的时候直接加入。如果没有加入 Timer
或者 Source
,或者只加入一个监听者,运行程序会崩溃。
RunLoop
吗? RunLoop
创建和销毁的时机又是什么时候呢?RunLoop
之间是一一对应的。但是在创建子线程时,子线程的 RunLoop
需要我们主动创建 。只需在子线程中获取当前线程的 RunLoop
对象即可 [NSRunLoop currentRunLoop]
;如果不获取,那子线程就不会创建与之相关联的 RunLoop
。RunLoop
在第一次获取时创建,在线程结束时销毁。RunLoop
有哪些 Mode
呢?滑动时发现定时器没有回调,是因为什么原因呢?5
个 Mode
1. kCFRunLoopDefaultMode :App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode :界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode : 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode : 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes : 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
App
为了响应 CFRunLoopSourceRef
事件源, RunLoop
会进行 Mode
切换以响应不同操作。RunLoop
的 Mode
为 kCFRunLoopDefaultMode
,即定时器属于 kCFRunLoopDefaultMode
。那么此时我们滑动 ScrollView
时, RunLoop
的 Mode
会切换到 UITrackingRunLoopMode
,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时, RunLoop
的 Mode
切换回 kCFRunLoopDefaultMode
,所以 NSTimer
就又管用了。RunLoop
中,并设置 RunLoop
的 Mode
为 NSRunLoopCommonModes
。NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才可以运行
// 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
KVO是关于RunTime
机制实现的
当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(NSKVONotifying_A),在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制
如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统就会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性复制是执行的是派生类的setter方法
键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:
和didChangeValueForKey:,
在一个被观察属性发生改变之前,willChangeValueForKey:
一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:
会被调用,继而observeValueForKey:ofObject:change:context:
也会被调用。且重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的。
重写被观察对象的automaticallyNotifiesObserversForKey
方法,返回NO
重写automaticallyNotifiesObserversOf
,返回NO。
注意:
关闭 kvo
后,需要手动在赋值前后添加willChangeValueForKey
和didChangeValueForKey
,才可以收到观察通知。
会触发。即使没有 setter 方法也会触发。
removeObserver
一个未注册的keyPath,导致错误:Cannot remove an observer A for the key path “str”,because it is not registered as an observer。解决办法:根据实际情况,增加一个添加keyPath的标记,在dealloc中根据这个标记,删除观察者。
添加的观察者已经销毁,但是并未移除这个观察者,当下次这个观察的keyPath发生变化时,kvo中的观察者的引用变成了野指针,导致crash。 解决办法:
在观察者即将销毁的时候,先移除这个观察者。
其实还可以将观察者observer委托给另一个类去完成,这个类弱引用被观察者,当这个类销毁的时候,移除观察者对象。参考KVOController。
优点:
能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步
能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现
能够提供观察的属性的最新值以及先前值
用key paths来观察属性,因此也可以观察嵌套对象
完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
我们观察的属性必须使用strings
来定义。因此在编译器不会出现警告以及检查
对属性重构将导致我们的观察代码不再可用
复杂的if
语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向
当释放观察者时不需要移除观察者
对象:OC中的对象指向的是一个objc_object指针类型,typedef struct objc_object *id;
从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
类:在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,typedef struct objc_class *Class;
对应的结构体如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
}
从结构体中定义的变量可知,OC的Class类型包括如下数据(即:元数据metadata
):super_class
(父类类对象);name
(类对象的名称);version
、info
(版本和相关信息);instance_size
(实例内存大小);ivars
(实例变量列表);methodLists
(方法列表);cache
(缓存);protocols
(实现的协议列表);
当然也包括一个isa指针
,这说明Class也是一个对象类型,所以我们称之为类对象
,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。
以下图中可以清楚的了解到OC对象、类、元类之间的关系
从图中可知:对象的isa指针
指向类
,类对象的isa指针
指向元类
,元类对象的isa指针
指向根元类
,根元类的isa指针
指向他本身
,从而形成一个闭环。
元类(Meta Class):是一个类对象的类,即:Class的类,这里保存了类方法等相关信息。
我们再看一下类对象中存储的方法、属性、成员变量等信息的结构体:
objc_ivar_list :
存储了类的成员变量,可以通过object_getIvar
或class_copyIvarList
获取;另外这两个方法是用来获取类的属性列表的class_getProperty
和class_copyPropertyList
,属性和成员变量是有区别的。
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
objc_method_list :
存储了类的方法列表,可以通过class_copyMethodList
获取。结构体如下:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list * _Nullable 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;
}
objc_protocol_list :
储存了类的协议列表,可以通过class_copyProtocolList
获取。结构体如下:
struct objc_protocol_list {
struct objc_protocol_list * _Nullable next;
long count;
__unsafe_unretained Protocol * _Nullable list[1];
};
metaclass
?metaclass
代表的是类对象的对象,存储了类的类方法,目的是将实例和类的相关方法列表
以及构建信息区
分开来,方便各司其职,符合单一职责设计原则。
class_copyIvarList
& class_copyPropertyList
区别?class_copyIvarList
:获取的是类的成员变量列表,即:@interface
{中声明的变量}
class_copyPropertyList
:获取的是类的属性列表,即:通过@property
声明的属性
class_rw_t
和 class_ro_t
的区别?class_rw_t
:代表的是可读写的内存区,这块区域中存储的数据是可以
更改的。
class_ro_t
:代表的是只读的内存区,这块区域中存储的数据是不可以
更改的。
OC对象中存储的属性
、方法
、遵循的协议数据
其实被存储在这两块儿内存区域
的,而我们通过RunTime
动态修改类的方法时,是修改在class_rw_t
区域中存储的方法列表。
category
如何被加载的?两个 category
的load
方法的加载顺序?两个 category
的同名方法的加载顺序?category
的加载是在运行时发生的,加载过程是:把category的实例方法
、属性
、协议
添加到类对象上,把category的类方法
、属性
、协议
添加到metaclass
上。
category
的load
方法执行顺序是根据类的编译顺序
决定的,即:xcode中的Build Phases
中的Compile Sources
中的文件从上到下
的顺序加载的。
category
并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category
添加的methodA
会排在原有类的methodA
的前面,因此如果存在category
的同名方法,那么在调用的时候,则会先找到最后一个编译
的 category
里的对应方法。
category :分类
注意:
为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。但是,我们可以使用 runtime
的 objc_setAssociatedObject
和 objc_getAssociatedObject
给该属性动态绑定。extension :扩展
@interface
和实现文件 @implement
一起形成了一个完整的类。extension
,所以对于系统一些类,如NSString
,就无法添加类扩展Extension
,因为在 extension
中添加的方法或属性必须在源类的文件的.m文件
中实现才可以,即:你必须有一个类的源码才能添加一个类的 extension
。消息转发机制:当接收者收到消息后,无法处理该消息时(即:找不到调用的方法SEL
),就会启动消息转发机制,流程如下:
第一阶段:动态解析
, 咨询接收者,询问它是否可以动态增加这个方法实现
第二阶段:在第一阶段中,接收者无法动态增加这个方法实现,那么将会进行快速转发
,系统将询问是否有其他对象可能执行该方法,如果可以,系统将转发给这个对象处理。
第三阶段:在第二阶段中,如果没有其他对象可以处理,那么进行慢速转发
,系统将该消息相关的细节封装成NSInvocation
对象,再给接收者最后一次机会,如果这里仍然无法处理,接收者将收到doesNotRecognizeSelector
方法调用,此时程序将 crash
。
// 第一阶段 咨询接收者是否可以动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
+ (BOOL)resolveClassMethod:(SEL)selector //处理的是类方法
// 第二阶段:询问是否有其他对象可以处理
- (id)forwardingTargetForSelector:(SEL)selector
// 第三阶段
// 慢速转发 1.签名 2.转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)invocation
// 无法识别该消息 crash
-(void)doesNotRecognizeSelector:(SEL)aSelector
OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)
方法进行调用,这个方法第一个参数是一个消息接收者对象。
RunTime通过这个对象的isa指针
找到这个对象的类对象
从类对象中的cache
中查找是否存在SEL对应的IMP
若不存在,则会在 method_list
中查找
如果还是没找到,则会到supper_class
中查找
仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)
进行消息转发
IMP:是方法的实现,即:一段c函数
SEL:是方法名
Method:是objc_method
类型指针,它是一个结构体,如下:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
使用场景:
实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)
来获取类的方法Method,其中用到了SEL
作为方法名
调用method_exchangeImplementations(Method1, Method2)
进行方法交换
我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types)
,该方法需要我们传递一个方法的实现函数IMP
,例如:
static void funcName(id receiver, SEL cmd, 方法参数...) {
// 方法具体的实现
}
函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。
load
:当类被装载的时候被调用,只调用一次
调用方式并不是采用RunTime
的objc_msgSend
方式调用的,而是直接采用函数的内存地址直接调用的。 多个类的load调用顺序,是依赖于compile sources
中的文件顺序决定的,根据文件从上到下的顺序调用 ;子类和父类同时实现load的方法时。父类的方法先被调用,本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)。 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照compile sources中的顺序调用的 load是被动调用的,在类装载时调用的,不需要手动触发调用 注意:当存在继承关系的两个文件时,不管父类文件是否排在子类或其他文件的前面,都是优先调用父类的,然后调用子类的。
例如:compile sources中的文件顺序如下:SubB、SubA、A、B,load的调用顺序是:B、SubB、A、SubA。
分析:SubB是排在compile sources中的第一个,所以应当第一个被调用,但是SubB继承自B,所以按照优先调用父类的原则,B先被调用,然后是SubB,A、SubA。
第二种情况:compile sources中的文件顺序如下:B、SubA、SubB、A,load调用顺序是:B、A、SubA、SubB,这里我给大家画个图梳理一下:
initialize:
当类或子类第一次
收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次
调用方式是通过RunTime
的objc_msgSend
的方式调用的,此时所有的类都已经装载完毕。
子类和父类同时实现initialize
,父类的先被调用,然后调用子类的。
本类与category
同时实现initialize
,category
会覆盖本类的方法,只调用category
的。
initialize
一次(这也说明initialize
的调用方式采用objc_msgSend
的方式调用的)。
initialize
是主动调用的,只有当类第一次被用到
的时候才会触发。
优点:
动态化更新方案
(例如: JSPatch):消息转发机制来进行JS和OC的交互,从而实现iOS的热更新
实现多重代理
利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。
间接实现多继承
OC本身不支持多继承,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
预防线上奔溃
利用消息转发机制对消息进行转发和替换,预防线上版本奔溃
缺点:
消耗性能(延长了消息发送的周期,提高了成本)
bug 的定位更加困难
RunTime
吗?举个例子。关联对象 Associated Objects 给category 添加属性
消息发送 Messaging
消息转发 Message Forwarding
方法调配 Method Swizzling 方法替换、动态添加方法
“类对象” NSProxy Foundation | Apple Developer Documentation
KVC、KVO About Key-Value Coding
动态获取 class 和 slector
RunTime
是如何把 weak
变量的自动置 nil
的?RunTime
对注册的类会进行布局,对于 weak
对象会放入一个 hash
表中。用 weak
对象指向的内存地址作为 key
,当此对象引用计数为 0 时会 dealloac
。假如 weak
对象的内存地址是 a,那么就会以 a 为键,在 hash
表中进行搜索,找出所有 a 对应的 weak
对象,从而置为 nil
。
weak
修饰的指针默认为 nil
。(在 OC 中对 nil
发送消息是安全的)
如果向一个nil对象
发送消息,首先在寻找对象的isa指针
时就是0地址
返回了,所以不会出现任何错误。也不会崩溃。
详解:
如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil);
如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0;
如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0;
如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
block
和函数类似, 只不过是直接定义在另一个函数里的, 和定义它的那个函数共享同一个范围内的东西。block
可以实现闭包, 有些人也称它作块
。
结构如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
由上图可知,block
实际上是由6部分组成的:
isa
指针
flags
,用于按bit
位表示的一些block
附加信息
reserved
,保留变量
invoke
,函数指针,指向具体的block
实现的函数调用地址
descriptor
,从它的结构体可以看出,主要表示该block
的附加描述信息,主要是size
大小,以及copy
和dispose
函数的指针
variables
,捕获的变量,block
能访问它的外部的局部变量,就是因为将这些变量(或变量地址)复制到了结构体中
block
是类吗?有哪些类型?block
是类。 它有三种类型:分别是ARC下:__NSGlobalBlock__
和__NSMallocBlock__
,切换到非ARC下的__NSStackBlock__
;
__NSGlobalBlock
__ :全局静态block,不访问任何外部变量,isa 指向_NSConcreteGlobalBlock
1.1. 这种块不会捕捉任何变量
,运行时也无须有状态来参与。
1.2. 全局块声明在全局内存里
, 在编译期已经完全确定了
。
__NSMallocBlock
__ :保存在堆上的block,引用计数为0时销毁,isa指向_NSConcreteMallocBlock
一个__NSStackBlock__
类型block
做调用copy
,那会将这个block从栈复制到堆上,堆上的这个block
类型就是__NSMallocBlock__
,所以__NSMallocBlock__
类型的block
是存储在堆区。如果对一个__NSMallocBlock__
类型block做copy
操作,那这个block的引用计数+1。
在ARC
环境下,编译器会根据情况,自动将栈上的block复制到堆上。
__NSStackBlock__
:保存在栈上的block,函数返回时销毁,isa指向_NSConcreteStackBlock
如果一个block
里面访问了普通的局部变量
,那它就是一个__NSStackBlock__
,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__
类型block
时要注意,一定要确保它还没被释放。如果对一个__NSStackBlock__
类型block做copy
操作,那会将这个block从栈复制到堆上。
int
变量被 __block
修饰与否的区别?block
的变量截获?没有被__block
修饰的int
,block
体中对这个变量的引用是值拷贝
,在block
中是不能被修改的。
通过__block
修饰的int
,block
体中对这个变量的引用是指针拷贝
,它会生成一个结构体
,复制这个变量的指针引用
,从而达到可以修改变量
的作用。
block
的变量截获:
__block
会将block
体内引用外部变量的变量进行拷贝,将其拷贝到block
的数据结构中,从而可以在block
体内访问或修改外部变量。
外部变量未被__block
修饰时,block
数据结构中捕获的是外部变量的值,通过__block
修饰时,则捕获的是对外部变量的指针引用。
注意:
block内部访问全局变量
时,全局变量不会被捕获到block
数据结构中。
block
在修改NSMutableArray
,需不需要添加__block
?如果修改的是NSMutableArray
的存储内容的话,是不需要添加__block
修饰的。
如果修改的是 NSMutableArray
对象的本身,那必须添加__block
修饰。 参考block
的变量捕获(第3点)。
block
怎么进行内存管理的?当block
内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。(全局静态block)
当block
内部引用了外部的非全局变量
的时候:
在MRC中,该block是在栈内存
中的
在ARC中,该block是在堆内存
中的。
也就是说,ARC
下只存在全局block
和堆block
。
通过__block
修饰的变量,在block
内部依然会对其引用计数+1,可能会造成循环引用。
通过__weak
修饰的变量,在block
内部不会对其引用计数+1,不会造成循环引用。
block
可以用strong
修饰吗?在MRC
环境中,是不可以的。strong
修饰符会对修饰的变量进行retain
操作,这样并不会将栈中的block
拷贝到堆内存中,而执行的block
是在堆内存中,所以用strong
修饰的block
会导致在执行的时候因为错误的内存地址,导致闪退。
在ARC
环境中,是可以的。因为在ARC
环境中的block
只能在堆内存
或全局内存
中,因此不涉及到从栈拷贝到堆中的操作。
__strong
、__weak
修饰?__weak
修饰的变量,不会出现引用计数+1,也就不会造成block
强持有外部变量,这样也就不会出现循环引用的问题了。
但是,我们的block
内部执行的代码中,有可能是一个异步操作,或者延迟操作。此时引用的外部变量可能会变成nil
,导致意想不到的问题,而我们在block
内部通过__strong
修饰这个变量时,block
会在执行过程中强持有这个变量,此时这个变量也就不会出现nil
的情况,当block
执行完成后,这个变量也就会随之释放了。
那么问题来了: Masonry
需要用 __weak
修饰吗?如果不用,那为什么呢?
Masonry
内部并没有使用 __weak
, 在 makeConstraints
或 updateConstraints
中 View 并没有持有 Block
,所以这个 block
只是一个 栈block
。当执行完 block(constraintMaker)
就出栈释放掉了,所以不会造成循环引用。
block
发生copy
的时机?一般情况在ARC
环境中,编译器将创建在栈中的block
会自动拷贝到堆内存中,而block
作为方法或函数的参数传递时,编译器不会做copy操作。
block
作为方法或函数的返回值时,编译器会自动完成copy操作。
当block
赋值给通过strong
或copy
修饰的id
或block
类型的成员变量时。
当 block
作为参数被传入方法名带有 usingBlock
的 Cocoa Framework
方法或 GCD
的 API
时。
block
访问对象类型的auto
变量时,在ARC
和MRC
下有什么区别?首先我们知道,在ARC
下,栈区创建的block会自动copy到堆区;而MRC
下,就不会自动拷贝了,需要我们手动调用copy函数。
我们再说说block
的copy
操作,当block
从栈区copy
到堆区的过程中,也会对block
内部访问的外部变量进行处理,它会调用Block_object_assign
函数对变量进行处理,根据外部变量是strong
还会weak
对block
内部捕获的变量进行引用计数+1或-1,从而达到强引用或弱引用的作用。
因此
在ARC
下,由于block
被自动copy
到了堆区,从而对外部的对象进行强引用,如果这个对象同样强引用这个block
,就会形成循环引用。
在MRC
下,由于访问的外部变量是auto
修饰的,所以这个block
属于栈区的,如果不对block
手动进行copy
操作,在运行完block
的定义代码段后,block
就会被释放,而由于没有进行copy
操作,所以这个变量也不会经过Block_object_assign
处理,也就不会对变量强引用。
简单说就是:
ARC
下会对这个对象强引用,MRC
下不会。
进程:
进程是一个具有独立功能的程序关于某次数据集合的一次运行活动,他是操作系统分配资源的基本单位。
进程是指系统正在运行中的一个应用程序,就是一段程序执行的过程。我们可以理解为手机上的一个app。
每个进程之间是独立的。每个进程均运行在起专用且受保护的内存空间内,拥有独立运行所需的全部资源。
进程是操作系统进行资源分配的单位。
线程:
程序执行流的最小单元
,线程是进程中的一个实体
。
一个进程想要执行任务,必须至少有一条线程。应用程序启动的时候,系统会默认开启一条线程,也就是主线程。
进程和线程的关系:
线程是进程的执行单元,进程的所有任务都在线程中执行。
线程是CPU
分配资源和调度的最小单位。
一个程序可对应多个进程(多进程);一个进程中可对应多个线程,但至少要有一条线程。
同个进程内的线程共享进程资源。
多进程:
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然程序是死的(静态的),进程是活动的(动态的)。
进程可以分为系统进程和用户进程。
系统进程:凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是出于运行状态下的操作系统本身
用户进程:运行用户程序时创建的运行在用户态下的进程。
进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。
在同一个时间里,同一个操作系统中如果允许两个或两个以上的进程
处于运行状态,这便是多进程。
多线程:
同一时间,CPU
只能处理1条线程,只有1条线程执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU
的调度线程的时间足够快,就造成了多线程并发执行的假象。
如果线程非常至多(N条),CPU
会在这些(N条)线程之间调度,消耗大量的CPU
资源,每条线程被调用执行的频率会降低(线程的执行效率降低)。
多线程的优点:
能适当提高程序的执行效率
能适当提高资源的利用率(CPU、内存利用率)
多线程的缺点:
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如若开启大量线程,会占用大量的内存空间,就会降低程序的性能
线程越多,CPU在调度线程的开销就越大
程序设计更加复杂:如线程之间的通信、多线程之间的数据共享等
NSThread
每个NSThread
对象对应一个线程,量级较轻(真正的多线程)。是对pthread
(其是POSIX线程的API,是C语言的技术,当然它可以直接操作线程)的抽象。NSOperation
/NSOperationQueue
面向对象的线程技术,是对GCD的抽象,容易理解和使用。GCD
—— Grand Central Dispatch
(派发) 是基于C语言
的框架,可以充分利用多核,是苹果推荐使用的多线程技术对比:
线程类型 | 优点 | 缺点 |
---|---|---|
NSThread | 1. 跨平台C语言标准库中的多线程框架 2. 使用简单 |
1. 过于底层使用很麻烦,需要封装使用。 2. 需要自己来管理线程的生命周期 、线程同步 、加锁 、睡眠 和唤醒 。过程不可避免的有一定的系统“开销” |
NSOperation / NSOperationQueue | 1. 更加面向对象,可以设置并发数量 ,可以设置优先级 可以设置依赖 ,可以任务执行状态控制 :isReady(是否准备好执行),isExecuting(是否正在执行),isFinished(是否执行完毕),isCancelled(是否被取消) 2. 不用关心线程的管理和数据的同步,把精力放在自己需要执行的任务或操作上就行了 3. GCD 的封装 |
用于相对复杂的场景,相对简单的官方推荐 GCD |
GCD(Grand Central Dispatch) | 1. iOS5后苹果推出的双核CPU优化的多线程框架,iOS 4.0 才能使用,是代替上面两个技术的高效而且强大的技术 2. 它基于block的特性导致它能极为简单的在不同代码作用域之间传递上下文,效率高 3. GCD自动根据系统负载来增减线程数量,这就减少了上下文的切换和提高了计算效率 4. 安全,无需加锁或其他同步机制 4. 它是基于C语言的 |
1. 不能设置并发数,需要写一些代码曲线方式实现并发 2. 不能设置优先级 |
3中队列:主线程队列、并发队列、串行队列
在GCD中有两种队列:串行队列
和并发队列
。两者都符合 FIFO
的原则,二者的主要区别是:执行的顺序不同和开启的线程数不同。
主线程队列: main queue
可以调用dispatch_get_main_queue()
来获得。因为main queue
是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
串行队列(Serial Dispatch Queue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为程序创建的。
并行队列(Concurrent Dispatch Queue):
同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效。
GCD
有哪些方法 api
?Dispatch Queue :
开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。
dispatch_async { queue, ^{
//想执行的任务
});
通过 dispatch_async 函数“追加”赋值在变量 queue 的“Dispatch Queue中”。
Dispatch Queue 的种类:
有两种Dispatch Queue,一种是等待现在执行中处理的 Serial Dispatch Queue
,另一种是不等待现在执行中处理的 Concurrent Dispatch Queue
。
dispatch_queue_create :
创建队列
Main Dispatch Queue 和 Global Dispatch Queue :
系统提供的两种队列
dispatch_set_target_queue :
变更队列执行的优先级
dispatch_after :
延时执行。
注意
的是dispatch_after
函数并不是在指定时间后执行处理,而只是在指定时间追加处理到 Dispatch Queue
。
dispatch_group :
调度任务组。
dispatch_group_notify
:最后任务执行完的通知,比如:
- (void)dispatch_group {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT , 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"thread1:%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"thread2:%@", [NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"thread3:%@", [NSThread currentThread]);
});
// 三个异步执行结束后,dispatch_group_notify 得到通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 4
NSLog(@"completed:%@", [NSThread currentThread]);
});
}
dispatch_group_wait
:
dispatch_group_wait
实际上会使当前的线程处于等待的状态,也就是说如果是在主线程执行dispatch_group_wait
,在上面的block
执行完之前,主线程会处于卡死的状态。可以注意到dispatch_group_wait
的第二个参数是指定超时的时间,如果指定为DISPATCH_TIME_FOREVER
(如上面这个例子)则表示会永久等待,直到上面的Block
全部执行完。除此之外,还可以指定为具体的等待时间,根据dispatch_group_wait
的返回值来判断是上面block
执行完了还是等待超时了。
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let group = dispatch_group_create()
dispatch_group_async(group, globalQueue) { () -> Void in
println("1")
}
dispatch_group_async(group, globalQueue) { () -> Void in
println("2")
}
dispatch_group_async(group, globalQueue) { () -> Void in
println("3")
}
//使用dispatch_group_wait函数
dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
println("completed")
}
dispatch_barrier_async
:
dispatch_barrier_async
就如同它的名字一样,在队列执行的任务中增加“栅栏”,在增加“栅栏”之前已经开始执行的block
将会继续执行,当dispatch_barrier_async
开始执行的时候其他的block
处于等待状态,dispatch_barrier_async
的任务执行完后,其后的block
才会执行。
dispatch_sync 和 dispatch_async
dispatch_sync : 把任务Block
同步追加到指定的Dispatch Queue
中
dispatch_async :把任务Block
异步追加到指定的Dispatch Queue
中
dispatch_apply
dispatch_apply
会将一个指定的block
执行指定的次数。如果要对某个数组中的所有元素执行同样的block
的时候,这个函数就显得很有用了,用法很简单,指定执行的次数以及Dispatch Queue
,在block
回调中会带一个索引,然后就可以根据这个索引来判断当前是对哪个元素进行操作:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_apply(10, globalQueue) { (index) -> Void in
print(index)
}
print("completed")
}
由于是Concurrent Dispatch Queue
,不能保证哪个索引的元素是先执行的,但是“completed
”一定是在最后打印,因为dispatch_apply
函数是同步的,执行过程中会使线程在此处等待,所以一般的,我们应该在一个异步线程
里使用dispatch_apply
函数:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(globalQueue, { () -> Void in
dispatch_apply(10, globalQueue) { (index) -> Void in
print(index)
}
print("completed")
})
print("在dispatch_apply之前")
}
dispatch_suspend / dispatch_resume
某些情况下,我们可能会想让Dispatch Queue
暂时停止一下,然后在某个时刻恢复处理,这时就可以使用dispatch_suspend
以及dispatch_resume
函数:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//暂停
dispatch_suspend(globalQueue)
//恢复
dispatch_resume(globalQueue)
}
注意:
暂停时,如果已经有block
正在执行,那么不会对该block
的执行产生影响。dispatch_suspend
只会对还未开始执行的block
产生影响。
Dispatch Semaphore
信号量在多线程开发中被广泛使用,当一个线程在进入一段关键代码之前,线程必须获取一个信号量,一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待前面的线程释放信号量。
信号量的具体做法是:当信号计数大于0时,每条进来的线程使计数减1,直到变为0,变为0后其他的线程将进不来,处于等待状态;执行完任务的线程释放信号,使计数加1,如此循环下去。
下面这个例子中使用了10条线程,但是同时只执行一条,其他的线程处于等待状态:
func testGroup3() -> void {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
let semaphore = dispatch_semaphore_create(1)
for i in 0 ... 9 {
dispatch_async(globalQueue, { () -> Void in
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
let time = dispatch_time(DISPATCH_TIME_NOW, (Int64)(2 * NSEC_PER_SEC))
dispatch_after(time, globalQueue) { () -> Void in
print("2秒后执行")
dispatch_semaphore_signal(semaphore)
}
})
}
}
取得信号量的线程在2秒后释放了信息量,相当于是每2秒执行一次。
通过上面的例子可以看到,在GCD
中,用dispatch_semaphore_create
函数能初始化一个信号量,同时需要指定信号量的初始值;使用dispatch_semaphore_wait
函数分配信号量并使计数减1,为0时处于等待状态;使用dispatch_semaphore_signal
函数释放信号量,并使计数加1。
另外dispatch_semaphore_wait
同样也支持超时,只需要给其第二个参数指定超时的时候即可,同Dispatch Group
的dispatch_group_wait
函数类似,可以通过返回值来判断。
注意:
如果是在OS X 10.8或iOS 6以及之后版本中使用,Dispatch Semaphore
将会由ARC
自动管理,如果是在此之前的版本,需要自己手动释放。
dispatch_once
函数通常用在单例模式上,它可以保证在程序运行期间某段代码只执行一次。如果我们要通过dispatch_once
创建一个单例类,在Swift可以这样:
class SingletonObject {
class var sharedInstance : SingletonObject {
struct Static {
static var onceToken : dispatch_once_t = 0
static var instance : SingletonObject? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = SingletonObject()
}
return Static.instance!
}
}
这样就能通过GCD的安全机制保证这段代码只执行一次。
GCD
主线程 & 主队列的关系?提交到主队列的任务在主线程执行。
主队列是主线中的一个串行队列。
所有的和UI相关的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI。
每一个应用程序只有唯一的一个主队列用来update UI
补充一点:如果在主线程中创建自定义队列(串行或者并行均可),在这个队列中执行同步任务,同样可以更新UI操作,主队列中可以更新UI,自定义队列也可以更新UI,但自定义队列的更新UI的前提是在主线程中执行同步任务。
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block)
在某队列开启同步线程
dispatch_barrier_sync()
障碍锁的方式同步
dispatch_group_create()
+ dispatch_group_wait()
dispatch_apply()
插队追加 操作同步
dispatch_semaphore_create()
+ dispatch_semaphore_wait()
信号量锁
串行NSOperationQueue队列并发数为1的时候 [NSOpertaion start] 启动任务即使同步操作 (NSOperationQueue.maxConcurrentOperationCount = 1)
pthread_mutex
底层锁函数
上层应用层封装的NSLock
NSRecursiveLock
递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中
NSConditionLock
& NSCondition
条件锁
@synchronized
同步操作 单位时间内只允许一个线程进入临界区
dispatch_once()
单位时间内只允许一个线程进入临界区
dispatch_once
实现原理 ?这个问题问的很傻吊也很高超.因为要解释清楚所有步骤需要记住里面所有代码
我认为这个问题应该从操作系统层面回答, 这个问题的核心是操作系统返回状态决定的,单位时间内操作系统只允许一个线程进入临界区,进入临界区的线程会被标记
回归到代码就是
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
|_____dispatch_once_f(val, block, _dispatch_Block_invoke(block))
|_______&l->dgo_once // &l->dgo_once 地址中存储的值。显然若该值为DLOCK_ONCE_DONE,即为once已经执行过
dgo_once
是dispatch_once_gate_s
的成员变量
typedef struct dispatch_once_gate_s {
union {
dispatch_gate_s dgo_gate;
uintptr_t dgo_once;
};
} dispatch_once_gate_s, *dispatch_once_gate_t;
有个内联函数static inline bool _dispatch_once_gate_tryenter(dispatch_once_gate_t l)
这个内联函数返回一个 原子性操作的结果
return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,(uintptr_t)_dispatch_lock_value_for_self(), relaxed)
比较+交换 的原子操作。比较 &l->dgo_once
的值是否等于 DLOCK_ONCE_UNLOCKED
这样就实现了我们的执行1次的GCD API.
互斥条件(Mutual exclusion) :
资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait):
进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
不可抢占条件(No pre-emption) :
有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
循环等待条件(Circular wait) :
若干个进程形成环形链,每个都占用对方申请的下一个资源。
死锁预防:
破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
死锁避免:
避免是指进程在每次申请资源时判断这些操作是否安全。例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
死锁检测:
死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
死锁解除:
这是与死锁检测结合使用的,它使用的方式就是剥夺。即:将某进程所拥有的资源强行收回,分配给其他的进程。
死锁的避免:
死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。
避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;
显然要避免死锁,必须事先知道系统拥有的资源数量及其属性。
锁类型 | 使用场景 | 备注 |
---|---|---|
pthread_mutex |
互斥锁 | PTHREAD_MUTEX_NORMAL ,#import |
OSSpinLock |
自旋锁 | 不安全,iOS 10 已启用 |
os_unfair_lock |
互斥锁 | 替代 OSSpinLock |
pthread_mutex (recursive) |
递归锁 | PTHREAD_MUTEX_RECURSIVE ,#import |
pthread_cond_t |
条件锁 | #import |
pthread_rwlock |
读写锁 | 读操作重入,写操作互斥 |
@synchronized | 互斥锁 | 性能差,且无法锁住内存地址更改的对象 |
NSLock | 互斥锁 | 封装 pthread_mutex |
NSRecursiveLock | 递归锁 | 封装pthread_mutex (recursive) |
NSCondition | 条件锁 | 封装 pthread_cond_t |
NSConditionLock | 条件锁 | 可以指定具体条件值 封装 pthread_cond_t |
琐是 ns
纳秒 us
微秒级别。
锁相关的概念定义:
临界区:
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
自旋锁:
是用于多线程同步的一种锁,线程反复检查锁变量是否可用。
a、
由于线程在这一过程中保持执行,因此是一种忙等待。
b、
一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
c、
自旋锁避免了进程上下文的调度开销
,因此对于线程只会阻塞很短时间的场合是有效的。
互斥锁(Mutex):
用于保护临界区,确保同一时间只有一个线程访问数据。 对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
a、
互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态;等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。
b、
性能开销成本,两次线程上下文切换的成本。
当线程加锁失败时,内核将线程的状态从【运行】切换到睡眠状态
,然后把CPU切换给其他线程运行;
当锁被释放时,之前睡眠状态的线程会变成就绪状态
,然后内核就会在合适的时间把CPU切换给该线程运行
读写锁:
是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
信号量(semaphore):
是一种更高级的同步机制,互斥锁可以说是 semaphore
在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
条件锁:
就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
递归锁(Recursive Lock):
也称为可重入互斥锁(reentrant mutex)
,是互斥锁的一种,同一线程对其多次加锁
不会产生死锁。 递归锁会使用引用计数机制,以便可以从同一线程多次加锁、解锁,当加锁、解锁次数相等时,锁才可以被其他线程获取。
NSOperationQueue
中的 maxConcurrentOperationCount
默认值默认值 -1。 这个值操作系统会根据资源使用的综合开销情况设置。
NSTimer
、CADisplayLink
、dispatch_source_t
的优劣?定时器类型 | 优势 | 劣势 |
---|---|---|
NSTimer | 使用简单 | 依赖 RunLoop ,具体表现在无 RunLoop 无法使用、NSRunLoopCommonModes 、不精确 |
CADisplayLink | 依赖屏幕刷新频率出发事件,最精.最合适做UI刷新 | 若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承 |
dispatch_source_t |
不依赖 RunLoop | 依赖线程队列,使用麻烦 使用不当容易Crash |
多线程可以访问同一个对象可分为3种
情况处理:
读且写
的话,需要加锁(读写锁满足)。pthread_rwlock
。获取一个读写锁用于读称为共享锁
,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁
。多进程访问同一个对象
一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。
所以
多个进程竞争,进程就会一直等待下去,形成死锁。
所以
我们就可以根据死锁的四个必要条件(互斥条件、请求与保持条件、不可抢占条件、不可剥夺条件
), 使用死锁的四个应对策略(死锁预防、死锁避免、死锁检测、死锁解除
)来解决死锁问题。
所以
我们也可以通过一些处理避免死锁:
死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件
是死锁产生的必要条件
,而不是存在这3个条件就一定产生死锁
,那么只要在逻辑上回避了第四个条件就可以避免死锁
。
避免死锁采用的是允许前三个条件存在
,但通过合理的资源分配算法来确保永远不会形成环形等待
的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
系统拒绝启动该进程
;系统就拒绝本次的分配
;总而言之:要避免死锁,必须事先知道系统拥有的资源数量及其属性。
Tableview
懒加载、Cell 复用高度缓存(因为 heightForRowAtIndexPath: 是调用最频繁的方法)
当 cell 的行高固定时,使用固定行高 self.tableView.rowHeight = xxx;
当 cell 的行高是不固定时,根据内容进行计算后缓存起来使用。第一次肯定会计算,后续使用缓存时就避免了多次计算;高度的计算方法通常写在自定义的cell中,调用时,既可以在设置 cell 高的代理方法中使用,也可以自定义的 model 中使用(且使用时,使用get方法处理)。
数据处理
使用正确的数据结构来存储数据;
数据尽量采用局部的 section,或 cellRow 的刷新,避免 reloadData;
大量数据操作时,使用异步子线程处理,避免主线程中直接操作;
缓存请求结果。
异步加载图片:SDWebImage 的使用
使用异步子线程处理,然后再返回主线程操作;
图片缓存处理,避免多次处理操作;
图片圆角处理时,设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给 CPU。
按需加载内容
滑动操作时,只显示目标范围内的 Cell 内容,显示过的超出目标范围内之后则进行清除;
滑动过程中,不加载显示图片,停止时才加载显示图片。
视图层面
(1)减少 subviews 的数量,自定义的子视图可以整合在形成一个整体的就整合成一个整体的子视图;
(2)使用 drawRect 进行绘制(即将 GPU 的部分渲染转接给 CPU ),或 CALayer 进行文本或图片的绘制。在实现 drawRect 方法的时候注意减少多余的绘制操作,它的参数 rect 就是我们需要绘制的区域,在 rect 范围之外的区域我们不需要进行绘制,否则会消耗相当大的资源;
(3)异步绘制,且设置属性 self.layer.drawsAsynchronously = YES;(遇到复杂界面,遇到性能瓶颈时,可能就是突破口);
(4)定义一种(尽量少)类型的 Cell 及善用 hidden 隐藏(显示) subviews;
(5)尽量使所有的 view 的 opaque 属性为 YES,包括 cell 自身,以提高视图渲染速度(避免无用的 alpha 通道合成,降低 GPU 负载);
(6)避免渐变,图片缩放的操作;
(7)使用 shadowPath 来画阴影;
(8)尽量不使用 cellForRowAtIndexPath: ,如果你需要用到它,只用一次然后缓存结果;
(9)cellForRowAtIndexPath 不要做耗时操作:如不读取文件 / 写入文件;尽量少用 addView 给 Cell 动态添加 View,可以初始化时就添加,然后通过 hide 来控制是否显示;
(10)我们在 Cell 上添加系统控件的时候,实际上系统都会调用底层的接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的 UITableViewCell 并且在它的 ContentView 上面添加控件时会相当消耗性能。所以目前最佳的方法还是继承 UITableViewCell,并重写 drawRect 方法;
(11)当我们需要圆角效果时,可以使用一张中间透明图片蒙上去使用 ShadowPath 指定 layer 阴影效果路径使用异步进行 layer 渲染(Facebook 开源的异步绘制框架 AsyncDisplayKit )设置 layer 的 opaque 值为 YES ,减少复杂图层合成尽量使用不包含透明(alpha)通道的图片资源尽量设置 layer 的大小值为整形值直接让美工把图片切成圆角进行显示,这是效率最高的一种方案很多情况下用户上传图片进行显示,可以让服务端处理圆角使用代码手动生成圆角 Image 设置到要显示的 View 上,利用 UIBezierPath ( CoreGraphics 框架)画出来圆角图片。
卡顿原因: 在一个VSync
内GPU
和CPU
的协作,未能将渲染任务完成放入到帧缓冲区,视频控制器去缓冲区拿数据的时候是空的,所以卡帧。
卡顿优化:
图片等大文件IO缓存
耗时操作放入子线程
提高代码执行效率(JSON to Model的方案,锁的使用等,减少循环,UI布局frame子线程预计算)
UI减少全局刷新,尽量使用局部刷新
监控卡帧:
CADisplayLink
监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync
的时长,上报调用栈。
在RunLoop
中添加监听,如果kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
中间的耗时超过VSync
的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看。那个部分耗时长即可。
离屏渲染(Off-Screen Rendering):分为CPU离屏渲染 和 GPU离屏渲染两种形式。GPU离屏渲染指的是在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作。
一般情况下,OpenGL
会将应用提交到 Reader Server 的动画直接渲染显示,但对于一些复杂的图像动画显示并不能直接渲染叠加显示,而是需要根据 Command Buffer 分通道进行渲染之后在组合,这一组合过程中,就有些渲染通道是不会直接显示的;Masking 渲染需要更多的渲染通道和合并的步骤;而这些没有直接显示在屏幕上的通道就是 Off-Screen Readering Pass。
true
YES
和layer.opacity小于1.0drawRect :
方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现UIBezierPath
和 Core Graphics
代替 layer
设置圆角。即:UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100,100,100,100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//开始对imageView进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size,NO,1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.boundscornerRadius:imageView.frame.size.width]addClip];
[imageView drawRect:imageView.bounds];
imageView.image=UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
1.2、使用 CAShapeLayer
和 UIBezierPath
代替 layer
设置圆角。即:UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//设置大小
maskLayer.frame = imageView.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.view addSubview:imageView];
mageView.layer.shadowColor = [UIColorgrayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
我们还可以通过设置shouldRasterize
属性值为YES来强制开启离屏渲染。其实就是光栅化(Rasterization)。既然离屏渲染这么不好,为什么我们还要强制开启呢?当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于UITableViewCell中,cell的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。ShadowPath
指定layer
阴影效果路径AsyncDisplayKit (Texttrue)
)UIBezierPath
(CoreGraphics框架)画出来圆角图片webp
。-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring -Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab -Wl,-rename_section,__TEXT,__const,__RODATA,__const -Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname -Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname -Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
xcasset
管理图片BitCode
Product
选择 Analyze
(快捷键: Command + Shift + B)Instruments
了。具体操作是通过 Xcode 打开项目,然后点击 Product
--> Profile
。Instruments
工具。选择 Leaks
选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作。Leaks
是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。 橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。Leaks Checks
,在 Details
所在栏中选择 CallTree
,并且在右下角勾选 Invert Call Tree
和 Hide System Libraries
,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。timer
的 invalidate
,并 timer 置为 nil;__weak
、__strong
__weak
、__strong
APP 启动分为热启动和冷启动。
pre-main
和 main()
。启动时间也是针对这两个阶段进行优化,下面我们也将从这两方面进行优化:
Total pre-main time: 866.86 milliseconds (100.0%)
dylib loading time: 328.28 milliseconds (37.8%)
rebase/binding time: 49.19 milliseconds (5.6%)
ObjC setup time: 62.85 milliseconds (7.2%)
initializer time: 426.38 milliseconds (49.1%)
slowest intializers :
libSystem.B.dylib : 7.52 milliseconds (0.8%)
libMainThreadChecker.dylib : 37.19 milliseconds (4.2%)
libglInterpose.dylib : 61.17 milliseconds (7.0%)
libMTLInterpose.dylib : 22.23 milliseconds (2.5%)
MyMoney : 392.50 milliseconds (45.2%)
pre-main 阶段主要由4部分组成:
rebase/binding
阶段优化很好,本阶段耗时也会很少+ load()
方法,调用 C/C++ 中的构造器函数。 initializer
阶段执行结束后, dylib 开始调用 main() 函数。在这一步,检查 + load()
方法,尽量把事情推迟到 + initialize()
方法里执行;并且控制 category 数量,去掉不必要的 category。didFinishLaunchingWithOptions
方法里执行了多项项业务,有一大部分业务并不是一定要在这里执行的,如支付配置、客服配置、分享配置等。整理该方法里的业务,能延迟加载的就往后推迟,防止其影响启动时间。didFinishLaunchingWithOptions
,将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 didFinishLaunchingWithOptions
里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。编写软件过程中,程序员面临着来自耦合性、内聚性以及可维护性、可扩展性、重用性、灵活性等多方面的挑战,设计模式是为了让程序具有更好的:
设计模式有 7 大原则:
单例模式
意图
:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决
:一个全局使用的类频繁地创建与销毁。
工厂模式
简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你的需求即可。
抽象工厂模式
抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定。
比如:以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。
代理模式
代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
优点:
缺点:
举一个生活中的例子:比如买飞机票,由于离飞机场太远,直接去飞机场买票不太现实,这个时候我们就可以上携程 App 上购买飞机票,这个时候携程 App 就相当于是飞机票的代理商。
观察者模式
观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
优点:
缺点:
策略模式
策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。
优点:遵循了开闭原则,扩展性良好。
缺点:随着策略的增加,对外暴露越来越多。
单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例。
优点:
1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
2. 避免对资源的多重占用
缺点:
1. 没有接口,不能继承,与单一职责原则冲突
2. 一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
MVC:
MVC即 Model-VIew-Controller。他是1970年代被引入到软件设计大众的。MVC模式致力于关注点的切分,这意味着 model 和 controller 的逻辑是不与用户界面(View)挂钩的。因此,维护和测试程序变得更加简单容易。
MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller
MVP
MVP 模式把应用分成了 3 个主要方面: Model 、View 、 Presenter。
MVP模式关键点:
MVVM
MVVM 即 Model-View-View Model。这个模式提供对 View 和 View Model 的双向数据绑定。这使得 View Model 的状态改变可以自动传递给View 。典型的情况是,View Model 通过使用 obsever 模式(观察者模式)来将 View Model 的变化通知给 Model。
Model :Model 层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。
View: View 代表了UI组件,像CSS,JQuery,html等。他只负责展示从 ViewModel 接收到的数据。也就是把模型转化成UI。
View Model :View Model 负责暴漏方法,命令,其他属性来操作 VIew 的状态,组装 model 作为 View 动作的结果,并且触发 view 自己的事件。
MVVM模式关键点:
业内常见的路由方案有3种:
Url-scheme注册(MGJRouter
)
iOS系统中默认是支持 Url Scheme方式的,例如可以在浏览器中输入: weixin:// 就可以打开微信应用。自然在APP内部也可以通过这种方法来实现组件之间的路由设计。
这种方式实现的原理是:在APP启动的时候,或者是向以下实例中的每个模块自己的 load
方法里面注册自己的断链(Url),以及对外提供服务(Block),通过url-scheme标记好,然后维护在url-router里面。 url-router中保存了各个组件对应的url-scheme,只要其它组件调用了 open url 的方法,url-router就会去根据url去查找对应的服务并执行。
URI
( web service
模式的资源通用表示方式)的格式。例如 appscheme://path
: ctd://home/scan
map
,key
是url
,value
是对应存放的block
数组,url
和block
都会常驻在内存中,当打开一个url
时,JLRoutes
就可以遍历这个全局的map
,通过url
来执行对应的block
。蘑菇街
的技术团队开源的一个router
,特点是使用简单方便。JLRoutes
的问题主要在于查找url的实现不够高效,通过遍历而不是匹配,还有就是功能偏多。HHRouter
的url
查找是基于匹配,所以会更高效,MGJRouter
也是采用的这种方法,但HHRouter
和 ViewController
绑定地过于紧密,一定程度上降低了灵活性。于是就有了 MGJRouter
, 从数据结构上看它和 HHRouter
是一样的。URL注册
对于实施组件化是完全没有必要的,拓展性和可维护性都降低;Open-url
的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度;但是可以通过传递parms
来解决,但是这个区分了远程调用和本地调用的接口;URL
去完成调度?是没有必要的,为啥要复杂化?URL
和服务的对应表,并且需要开发人员对这样一个表进行维护;URL
及服务,因此内存中需要保存这样一份表,当组件多起来以后就会出现一些内存的问题;本地调用
和远程调用
,它们的处理逻辑是不同的。正确的做法
应该是把远程调用通过一个中间层转化成本地调用,如果把两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个404页面
,但是本地调用可能需要做其它处理。如果不加以区分,那么就无法完成这种业务要求。 远程调用只能传递被序列化JSON
的数据,像UIImage
这样非常规的对象是不行的,所以如果组件接口要考虑远程调用,这里的参数与就不能是这类非常规对象。优缺点:
优点:
Url-Scheme
是借鉴前端Router
和 系统App 内跳转方法
得出来的解决方案。所以不管是H5、RN、Android、iOS 都通用。缺点:
URL
的map
规则是需要注册的,它们会在load
方法里面写。写在load
方法里面是会影响App启动速度的。URL
链接里面关于组件
和页面的名字
都是硬编码,参数
也都是硬编码。而且每个URL
参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担。而且URL短连接散落在整个App四处,维护起来实在有点麻烦。NSObject
的参数,URL
是不够友好的,它最多是传递一个字典。利用Runtime
实现的target-action
方式(CTMediator
)- 个人推荐
相较于 url-scheme
的方式进行组件间的路由, runtime
的方式利用了 OC运行时
的特征,实现了组件间服务的自动发现,无需注册即可实现组建间的调用。因此,不管从维护性
、可读性
、扩展性
来说,都是一个比较完美的方案。
target-action
的原理:
传统的中介者模式
。这个中间件 Mediator
会依赖其他组件,其他组件也会依赖 Mediator
。
但是能不能让 Mediator
不在依赖组件,各个组件之间不再依赖,组件间调用只依赖中间件 Mediator
呢 ?
官方 casa 大神
的优化建议是这样的:
利用 target-action
的方式,创建一个 target
的类,类中定义了一些 action
方法,这些方法的结果是返回一个 Controller
或其他 Object
。再给中间件 CTMediator
添加一个分类方法(category
),定义组件外部可调用的方法接口,内部实现 perform: target: action
的方法。该方法主要通过 runtime
中的 NSClassFromString
获取 target
类和 NSSelectorFromString
获取方法名,这样就可以执行先去创建的 target
类中的方法得到返回值,在通过分类中的方法传值。
优缺点:
优点:
Runtime
的特性,无需注册这一步。Target-Action
方案只有存在组件依赖Mediator
这一层依赖关系。在Mediator
中维护针对Mediator
的Category
,每个category
对应一个Target
,Category
中的方法对应Action
场景。Target-Action
方案也统一了所有组件间调用入口。url
中进行Native
前缀进行验证。缺点:
Target_Action
在Category
中将常规参数打包成字典,在Target
处再把字典拆包成常规参数,这就造成了一部分的硬编码。protcol-class
注册
通过协议
和类
绑定,核心思想和代理传值是一样的,遵循协议,实现协议中的方法。
主要思路:
CommonProtocol.h
,里面存放各个模块提供的协议。在各个模块依赖这个头文件,实现协议的方法。ProtocolMediator
, 提供模块的注册和获取模块的功能(其实就是将类和协议名进行绑定,放在一个字典里,key
是协议名字符串,value
是类)。协议
,核心代码如下:Class cls = [[ProtocolMediator sharedInstance] classForProtocol:@protocol(B_VC_Protocol)];
UIViewController *B_VC = [[cls alloc] init];
[B_VC action_B:@"param1" para2:222 para3:333 para4:444];
[self presentViewController:B_VC animated:YES completion:nil];
优缺点:
优点:
缺点:
Protocol
都要向ModuleManager
进行注册。保证项目的稳定性从4个方面来说:
CADisplayLink
Instruments
Instruments
来查看leaks
、代码方面:Delegate、Block、 Block、 NSNotification埋点:主要是为了收集数据和信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,包括访问数(Visits),访客数(Visitor),停留时长(Time On Site),页面浏览数(Page Views)和跳出率(Bounce Rate)等。
以大致分为两种:页面统计(track this virtual page view)、 统计操作行为(track this button by an event)。
手动埋点(代码埋点):
国内的主要第三方数据分析服务商,如百度统计、友盟、TalkingData、GrowingIO 等。
优点:
缺点:
自动化埋点(无埋点):
无埋点是指开发人员集成采集 SDK
后,SDK
便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码;或者是说用户展现界面元素时,通过控件绑定触发事件,事件被触发的时候系统会有相应的接口让开发者处理这些行为。现在市面上主流无埋点做法有两种
:一种是预先跟踪所有的渲染信息,一种是滞后跟踪的渲染信息。
数据分析师/数据产品通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。
优点:
可视化埋点:
SDK
外,不需要额外去写埋点代码,而是由业务人员通过访问分析平台的 圈选
功能来圈
出需要对用户行为进行捕捉的控件,并给出事件命名。圈选完毕后,这些配置会同步到各个用户的终端上,由采集 SDK
按照圈选的配置自动进行用户行为数据的采集和发送。git diff
?MVC
架构。
优点:
MVC
的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。MVC
模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。缺点:
依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。
MVC
是苹果官方推荐的项目架构,相对于 MVP
、 MVVM
架构来说入门相对的低一些;而且公司的项目不是很大,在综合人力成本等方面选择了 MVC
架构。
针对 Controller
臃肿问题作出优化,将数据相关进行抽离管理,向 MVVM
模式靠拢。
SDWebImage
SDWebImage
组织架构:
SDWebImageDownloader
:负责维持图片的下载队列;
SDWebImageDownloaderOperation
:负责真正的图片下载请求;
SDImageCache
:负责图片的缓存;
SDWebImageManager
:是总的管理类,维护了一个SDWebImageDownloader
实例和一个 SDImageCache
实例,是下载与缓存的桥梁;
SDWebImageDecoder
:负责图片的解压缩;
SDWebImagePrefetcher
:负责图片的预取;
UIImageView+WebCache
:和其他的扩展都是与用户直接打交道的。
AFNetWorking
AFNetWorking
组织架构:主要有5
个模块
AFHTTPSessionManager
:是对 NSURLSession
的封装,负责发送网络请求,是 AFNetWotking
中使用最多一个模块AFNetworkingReachabilityManager
:实时监测网络状态的工具类AFSecurityPolicy
:网络安全策略的工具类,主要是针对于 Https 服务Serializstion
:请求序列化工具类
AFURLRequestSerialization
:请求入参序列化工具基类AFURLResponseSerialization
:请求回参序列化工具基类
AFJSONResponseSerializer
: Json
解析器,AFNetWorking
的默认解析器AFXMLParserResponseSerializer
:XML
解析器AFHTTPResponseSerializer
: 万能解析器,直接返回二进制数据(NSData
),服务器不会对数据进行处理UIKit
: 对iOS UIKit
的扩展AFNetworking
的可能面试考点 :
AFNetworking
2.x怎么开启常驻子线程?为何需要常驻子线程?2.x
版本中 AFNetWorking
通过 RunLoop
开启了一个常驻子线程,具体代码是这样的:+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *RunLoop = [NSRunLoop currentRunLoop];
[RunLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[RunLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
为何要开启常驻子线程?NSURLConnection
的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到 NSURLConnection
的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个http
请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来。RunLoop
来开启常驻线程。AFURLSessionManager
与 NSURLSession
的关系,每次都需要新建 manager
吗?AFNetWorking
中 manager
与 session
是1对1的关系, AFNetWorking
会在 manager
初始化的时候创建对应的 NSURLSession
。同样, AFNetWorking
也在注释中写明了可以提供一个配置好的 manager
单例来全局复用。session
其实就是在利用 http2.0
的多路复用特点,减少访问同一个服务器时,重新建立 tcp
连接的耗时和资源。AFSecurityPolicy
如何避免中间人攻击?ATS的策略
,基本都切到 HTTPS
了,HTTPS
的基本原理还是需要了解一下的,这里不做介绍。Charles/Fiddler
等工具实际上就可以看成中间人攻击。SSL Pinning
。 AFSecurityPolicy
的 AFSSLPinningMode
就是相关设置项。SSL Pinning
的原理就是需要将服务器的公钥打包到客户端中, tls
验证时,会将服务器的证书和本地的证书做一个对比,一致的话才允许验证通过。typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,
AFSSLPinningModePublicKey, // 只验证证书中的公钥
AFSSLPinningModeCertificate, // 验证证书所有字段,包括有效期之内
};
由于数字证书存在有效期,内置到客户端后就存在失效后导致验证失败的问题,所以可以考虑设置为 AFSSLPinningModePublicKey
的模式,这样的话,只要保证证书续期后,证书中的公钥不变,就能够通过验证了。AFNetWorking 3.x
为什么不再需要常驻线程?AFNetWorking 2.x
使用 NSURLConnection
,痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。所以 AFNetWorking2.x
在权衡之后选择了常驻线程。AFNetWorking 3.x
之后使用了 NSURLSession
:self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
AFNetWorking 3.x
使用 NSURLSession
解决了 NSURLConnection
的痛点,从上面的代码可以看出, NSURLSession
发起的请求,不再需要在当前线程进行代理方法的回调。可以指定回调的 delegateQueue
,这样我们就不用为了等待代理回调方法而苦苦保活线程了。同时还要注意一下:
指定的用于接收回调的 Queue
的 maxConcurrentOperationCount
设为了 1
,这里目的是想要让并发的请求串行的进行回调。
为什么 3.0 中需要设置为 1 ?
self.operationQueue.maxConcurrentOperationCount = 1;
解答:功能不一样:3.0的operationQueue是用来接收NSURLSessionDelegate回调的,
鉴于一些多线程数据访问的安全性考虑,
设置了maxConcurrentOperationCount = 1 来达到串行回调的效果。
而2.0的operationQueue是用来添加operation并进行并发请求的,所以不要设置为1。
- (AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
为什么要串行回调?
- (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task {
NSParameterAssert(task);
AFURLSessionManagerTaskDelegate *delegate = nil;
[self.lock lock];
//给所要访问的资源加锁,防止造成数据混乱
delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)];
[self.lock unlock];
return delegate;
}
这边对 self.mutableTaskDelegatesKeyedByTaskIdentifier
的访问进行了加锁,目的
是保证多线程环境下的数据安全。既然加了锁,就算 maxConcurrentOperationCount
不设为 1
,当某个请求正在回调时,下一个请求还是得等待一直到上个请求获取完所要的资源后解锁,所以这边并发回调也是没有意义的。相反多 task
回调导致的多线程并发,还会导致性能的浪费。所以 maxConcurrentOperationCount = 1
。重构技巧:
适合节点:
Bug
的时候)重构是一个不断的过程。
MVC
或者 MVVM
模式进行总的架构设计。策略模式
:针对实现目标/功能的复杂度,判断情况选用 策略模式
。观察者模式
和 代理模式
针对实时情况而定。工厂模式
和 抽象工厂模式
:根据过程父子关系复杂程度和子类种类数量多少程度,判断是否使用 工厂模式
和 抽象工厂模式
。适配器模式
: 高度自定义问题,前端/移动端 根据数据格式做适配。(比如说 电商SKU
Cell
适配等)单例模式
:根据模块在项目的 唯一性
,重要性
等作出判断。(比如:应用的配置信息,用户的个人信息,本地数据库进行操作,数据上传云端,通信管理类等)展现层
、业务层
、数据层
性能统计
、Networking
、Patch
、网络诊断
、数据存储
模块。对于基础模块来说,其本身应该是自洽的,即可以单独编译或者几个模块合在一起可以单独编译。所有的依赖关系都应该是业务模块指向基础模块的。Runtime
实现的 target-action
方式(CTMediator
)顺序存储结构
:顺序存放
,每个结点只有一个元素。存储位置反映数据元素间的逻辑关系。
存储密度大
,但是插入、删除操作效率较差。(比如:数组
:1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如栈
和队列
等)。链式存储结构
:一组指针
,指针
反映数据元素间的逻辑关系。
哈希(散列)存储结构
:哈希函数
解决冲突的方法,将关键字散列
在连续的
有限的
地址空间内,并将哈希函数的值作为该数据元素的存储地址。
索引存储结构
:地址连续的内存空间
外,尚需建立一个索引表
。索引表中的索引指示结点的存储位置,兼有动态和静态的特性。链表:
是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针
链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
.
单向链表:
A->B->C->D->E->F->G->H。 这就是单向链表 ,H 是头 A 是尾,像一个只有一个头的火车一样。只能一个头拉着跑。
双向链表:
H<- A->B->C->D->E->F->G->H。 这就是双向链表。有头没尾,两边都可以跑 ,跟地铁一样 到头了,可以倒着开回来。
循环链表:
A->B->C->D->E->F->G->H->A,绕成一个圈。就像蛇吃自己的这就是循环。
数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。
优点:
缺点:
适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况。
链表:
是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针
链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
堆:
动态分配内存
,对其访问和对一般内存的访问没有区别。堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。最大堆
和最小堆
。优先队列
,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。栈:
先进后出
的数据结构,又称为先进后出的线性表
,简称 FILO
(—First-In/Last-Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。表尾进行插入和删除操作
的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。PUSH
和POP
。PUSH操作
在堆栈的顶部加入一个元素。POP操作
相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。自动分配内存空间
。队列:
public static int treeDepth(BinaryTreeNode root) {
if (root == null) {
return 0;
}
int left = treeDepth(root.left);
int right = treeDepth(root.right);
return left > right ? (left + 1) : (right + 1);
}
bool IsBalanced_1(TreeNode* pRoot,int& depth){
if(pRoot==NULL){
depth=0;
return true;
}
int left,right;
int diff;
if(IsBalanced_1(pRoot->left,left) && IsBalanced_1(pRoot->right,right)){
diff=left-right;
if(diff<=1 || diff>=-1){
depth=left>right?left+1:right+1;
return true;
}
}
return false;
}
bool IsBalancedTree(TreeNode* pRoot){
int depth=0;
return IsBalanced_1(pRoot,depth);
}
在计算机科学中,时间复杂性,又称时间复杂度。
算法的时间复杂度是一个函数
,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述
,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的
,亦即考察输入值大小趋近无穷时的情况。
空间复杂度(Space Complexity)是对一个算法在运行过程中
临时占用存储空间大小的量度
,记做S(n)=O(f(n))。比如:
直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量:时间复杂度
& 空间复杂度
。
冒泡排序:
稳定排序
算法。/**
* 【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
* 第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置
* 第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置
* …… ……
* 第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置
*/
void bublleSort(int *arr, int length) {
for(int i = 0; i < length - 1; i++) { //趟数
for(int j = 0; j < length - i - 1; j++) { //比较次数
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
选择排序
不稳定
的排序方法 。/**
* 【选择排序】:最值出现在起始端
*
* 第1趟:在n个数中找到最小(大)数与第一个数交换位置
* 第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
* 重复这样的操作...依次与第三个、第四个...数交换位置
* 第n-1趟,最终可实现数据的升序(降序)排列。
*
*/
void selectSort(int *arr, int length) {
for (int i = 0; i < length - 1; i++) { //趟数
for (int j = i + 1; j < length; j++) { //比较次数
if (arr[i] > arr[j]) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
直接插入排序
稳定的排序
算法。/**
*
* num[] 是已经排序好的,在插入一个数直接进行排序
*
*/
void insertSort2(int num[],int count) {
int i,j;
for (i = 1; i < count; i++) {
if (num[i] < num[i - 1]) { // 当前数比前一位的数小
int temp = num[i]; // 记住 当前数
for (j = i; j > 0; j--) { // 从当前数起 逆序
if (num[j - 1] > temp) num[j] = num[j - 1]; // 如果 当前数比前一位小,前一位后移
else break;
}
num[j] = temp;
}
}
}
二分插入排序
稳定
的排序算法。/**
* 折半查找:优化查找时间(不用遍历全部数据)
*
* 折半查找的原理:
* 1> 数组必须是有序的
* 2> 必须已知min和max(知道范围)
* 3> 动态计算mid的值,取出mid对应的值进行比较
* 4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
* 5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
*
*/
// 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置
int findKey(int *arr, int length, int key) {
int min = 0, max = length - 1, mid;
while (min <= max) {
mid = (min + max) / 2; //计算中间值
if (key > arr[mid]) {
min = mid + 1;
} else if (key < arr[mid]) {
max = mid - 1;
} else {
return mid;
}
}
return -1;
}
希尔排序
不稳定
排序算法。void shellSort(int num[],int count)
{
int shellNum = 2;
int gap = round(count/shellNum);
while (gap > 0) {
for (int i = gap; i < count; i++) {
int temp = num[i];
int j = i;
while (j >= gap && num[j - gap] > temp) {
num[j] = num[j - gap];
j = j - gap;
}
num[j] = temp;
}
gap = round(gap/shellNum);
}
}
快速排序
不稳定
的排序算法。void quickSort(int num[], int left, int right)
{
if (left >= right){ // 如果left >= right说明排序结束了
return ;
}
// 变量key为基准数,在此规定基准数为序列的第一个数,即左指针指向的数
int key = num[left];
int i = left; //左指针
int j = right; //右指针
int temp;
while (i != j) { // 该 while 循环结束一次表示比较了一轮
while(i < j && arr[j] >= key) { // 从右向左找第一个小于key的数
j--;
}
while(i < j && arr[i] < key) { // 从左向右找第一个大于等于key的数
i++;
}
if(i < j) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
arr[left] = arr[i];
arr[i] = key;
// 分治方法进行递归
quickSort(num, left, i - 1);
quickSort(num, i + 1, right);
}
堆排序
void maxHeapify(int num[], int start, int end) {
//建立父节点指标和子节点指标
int dad = start;
int son = dad * 2 + 1;
while (son <= end) { //若子节点指标在范围内才做比较
if (son + 1 <= end && num[son] < num[son + 1]) //先比较两个子节点大小,选择最大的
son++;
if (num[dad] > num[son]) //如果父节点大於子节点代表调整完毕,直接跳出函数
return;
else { //否则交换父子内容再继续子节点和孙节点比较
EXCHANGE(num[dad], num[son])
dad = son;
son = dad * 2 + 1;
}
}
}
void heapSort(int num[], int count) {
int i;
//初始化,i从最後一个父节点开始调整
for (i = count / 2 - 1; i >= 0; i--)
maxHeapify(num, i, count - 1);
//先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
for (i = count - 1; i > 0; i--) {
EXCHANGE(num[0], num[i])
maxHeapify(num, 0, i - 1);
}
}
- (NSString *)reversalString:(NSString *)originString{
NSString *resultStr = @"";
for (NSInteger i = originString.length -1; i >= 0; i--) {
NSString *indexStr = [originString substringWithRange:NSMakeRange(i, 1)];
resultStr = [resultStr stringByAppendingString:indexStr];
}
return resultStr;
}
头插法
:struct ListNode* reverseList(struct ListNode* head){
//新链表的头指针
struct ListNode* newhead = NULL;
//需要头插的结点
struct ListNode* cur = head;
while(cur)
{
//保存需要头插结点的下一个节点
struct ListNode* next = cur->next;
//将cur头插到新链表
cur->next = newhead;
newhead = cur;
cur = next;
}
return newhead;
}
迭代法
:struct ListNode* reverseList(struct ListNode* head){
struct ListNode* pre = NULL;
//需要反转指向的结点
struct ListNode* cur = head;
while(cur)
{
//保存需要头插结点的下一个节点
struct ListNode* next = cur->next;
//将cur头插到新链表
cur->next = pre;
pre = cur;
cur = next;
}
return pre;
}
- (void)merge {
/*
有序数组A:1、4、5、8、10...1000000,有序数组B:2、3、6、7、9...999998,A、B两个数组不相互重复,请合并成一个有序数组C,写出代码和时间复杂度。
*/
//(1).
NSMutableArray *A = [NSMutableArray arrayWithObjects:@4,@5,@8,@10,@15, nil];
NSMutableArray *B = [NSMutableArray arrayWithObjects:@2,@6,@7,@9,@11,@12,@13, nil];
NSMutableArray *C = [NSMutableArray array];
int count = (int)A.count+(int)B.count;
int index = 0;
for (int i = 0; i < count; i++) {
if (A[0]
两个思路:
# define SIZE 256
char GetChar(char str[])
{
if(!str)
return 0;
char* p = NULL;
unsigned count[SIZE] = {0};
char buffer[SIZE];
char* q = buffer;
for(p=str; *p!=0; p++)
{
if(++count[(unsigned char)*p] == 1)
*q++ = *p;
}
for (p=buffer; p
这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题。
假设两个视图为UIViewA、UIViewC,其中 UIViewA继承于UIViewB,UIViewB继承于UIViewD,UIViewC也继承于UIViewD;即 A->B->D,C->D
- (void)viewDidLoad {
[super viewDidLoad];
Class commonClass1 = [self commonClass1:[ViewA class] andClass:[ViewC class]];
NSLog(@"%@",commonClass1);
// 输出:2018-03-22 17:36:01.868966+0800 两个UIView的最近公共父类[84288:2458900] ViewD
}
// 获取所有父类
- (NSArray *)superClasses:(Class)class {
if (class == nil) {
return @[];
}
NSMutableArray *result = [NSMutableArray array];
while (class != nil) {
[result addObject:class];
class = [class superclass];
}
return [result copy];
}
- (Class)commonClass1:(Class)classA andClass:(Class)classB {
NSArray *arr1 = [self superClasses:classA];
NSArray *arr2 = [self superClasses:classB];
for (NSUInteger i = 0; i < arr1.count; ++i) {
Class targetClass = arr1[i];
for (NSUInteger j = 0; j < arr2.count; ++j) {
if (targetClass == arr2[j]) {
return targetClass;
}
}
}
return nil;
}
- (Class)commonClass2:(Class)classA andClass:(Class)classB{
NSArray *arr1 = [self superClasses:classA];
NSArray *arr2 = [self superClasses:classB];
NSSet *set = [NSSet setWithArray:arr2];
for (NSUInteger i =0; i
//求一个无序数组的中位数
int findMedian(int a[], int aLen)
{
int low = 0;
int high = aLen - 1;
int mid = (aLen - 1) / 2;
int div = PartSort(a, low, high);
while (div != mid) {
if (mid < div) {
//左半区间找
div = PartSort(a, low, div - 1);
}
else {
//右半区间找
div = PartSort(a, div + 1, high);
}
}
//找到了
return a[mid];
}
int PartSort(int a[], int start, int end)
{
int low = start;
int high = end;
//选取关键字
int key = a[end];
while (low < high) {
//左边找比key大的值
while (low < high && a[low] <= key) {
++low;
}
//右边找比key小的值
while (low < high && a[high] >= key) {
--high;
}
if (low < high) {
//找到之后交换左右的值
int temp = a[low];
a[low] = a[high];
a[high] = temp;
}
}
int temp = a[high];
a[high] = a[end];
a[end] = temp;
return low;
}
假设每个输入只对应一种答案,且同样的元素不能被重复利用。 示例:给定nums = [2, 7, 11, 15], target = 9 — 返回 [0, 1] 思路:
class Solution {
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
int[] result = new int[2];
for(int i = 0; i < len; i++){
for(int j = i+1; j < len; j++){
if(nums[i] + nums[j] == target){
result[0] = i;
result[1] = j;
return result;
}
}
}
return result;
}
}
HTTP协议
:超文本传输协议,他是基于TCP应用层协议
两
部分:请求报文和响应报文
客户端请求:
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
服务端响应:
HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
输出结果:
Hello World! My payload includes a trailing CRLF.
HTTPS 协议
:
HTTPS是一种通过计算机网络进行安全通信的传输协议(以安全为目标),经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的主要目的
是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性(传输加密
和身份认证
保证了传输过程的安全性)。
PS:
TLS
是传输层加密协议,前身是SSL协议
。HTTPS 的安全基础是 SSL
。
通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
HTTPS 的验证流程?
归纳为5
个步骤:
支持哪些hash算法
。密钥公钥
,网站地址
,证书颁发机构
,失效日期
等)。证书中有一个公钥来加密信息,私钥由服务器持有。合法性
(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。随机的对称密钥
(session key)并用公钥加密
,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。对称加密的算法和对应密钥
,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。数字证书都有哪些内容?
Issuer
– 证书的发布机构Valid from,Valid to
– 证书的有效期Public key
– 公钥字符串或数字
,进行加密/解密算法时使用。公钥和私钥都是密钥,只不过一般公钥是对外开放
的,加密时使用;私钥是不公开
的,解密时使用。Subject
– 主题Signature algorithm-
– 签名所使用的算法指纹解密
。指纹加密的结果就是数字签名。Thumbprint,Thumbprint algorithm
– 指纹以及指纹算法(一种HASH算法)使用证书机构的私钥加密后
和证书放在一起。主要
用来保证证书的完整性,确保证书没有修改过。指纹算法计算证书的hash值,和刚开始的值一样,则表示没有被修改过
。客户端如何检测数字证书是合法的
并是所要请求的公司的?
Issuer
(发布机构),然后会在操作系统或浏览器内置的
受信任的
发布机构中去找该机构的证书(为什么操作系统会有受信任机构的证书?先看完这个流程再来回答)。公钥
,解密本级证书,得到数字指纹。然后对本级证书的公钥进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。操作系统为什么会有证书发布机构的证书?
TCP:(Transmission Control Protocol )传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议
三次握手:
TCP 为什么要三次握手?而不是两次或者四次呢?
实现可靠数据传输
, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。双方序列号都已被对方确认
。四次挥手:
UDP:(User Datagram Protocol)用户数据报协议,是一种高速传输和实时性有较高的、无连接的、不可靠的 传输层协议。
TCP 和 UDP 的区别?
1、连接性:TCP 面向连接,UDP 无连接
2、可靠性:TCP 可靠的、保证消息顺序,UDP 不可靠(易丢包)、不能保证顺序
3、模式:TCP 流模式,UDP 数据报格式
4、资源损耗:TCP 更损耗数据
Socket:socket 是 “open—write/read—close” 模式的一种实现,那么socket 就提供了 这些操作对应的函数接口。使用socket 需要注意:
多态表现为了三个方面:动态类型、动态绑定、动态加载。之所以叫做多态,是因为必须到运行时(run time)才会做一些事情。
动态类型:
编译器编译的时候是不能被识别的(如 id 类型),要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。
动态绑定 :
动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词@selector
/SEL
即可。
而在OC中,其实是没有函数的概念的,我们叫消息机制
,所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。动态绑定的特定不仅方便,而且效率更高。
动态加载 :
让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。
OC 不支持多继承,但是可以用 代理(Delegate) 来实现多继承。runtime
消息转发等实现伪多继承
代理是一种设计模式,以 @protocol
形式体现,一般是一对一传递。
weak
修饰呢?block和代理的区别?
weak
修饰指明该对象并不负责保持delegate这个对象,delegate 这个对象的销毁由外部控制。用 strong
修饰该对象强引用 delegate,外界不能销毁 delegate对象,会导致循环引用。__weak
和 __strong
。 使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。
键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法,直接或通过实例变量访问的机制。非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。
KVC 底层实现原理:
当一个对象调用setValue:forKey:
方法时,方法内部会做以下操作:
key
的 set方法
,如果有set
方法,就会调用 set
方法,给该属性赋值set
方法,判断有没有跟 key
值相同且带有下划线的成员属性(_key)
如果有,直接给该成员属性进行赋值_key
,判断有没有跟key
相同名称的属性。如果有,直接给该属性进行赋值valueforUndefinedKey
和 setValue:forUndefinedKey:
方法KVC 使用场景:
OC 中,基本数据类型的默认关键字是atomic, readwrite, assign;普通属性的默认关键字是atomic, readwrite, strong。
读写权限:readonly,readwrite(默认)
原子性: atomic(默认),nonatomic。atomic读写线程安全,但效率低,而且不是绝对的安全,比如如果修饰的是数组,那么对数组的读写是安全的,但如果是操作数组进行添加移除其中对象的还,就不保证安全了。nonatomic禁止多线程,变量保护,提高性能。
引用计数:
retain/strong:表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。
assign:修饰基本数据类型,修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果使用assign指针继续访问原对象的话,就可能会导致内存泄漏或程序异常。这些数值主要存在于栈上。
weak:不改变被修饰对象的引用计数,所指对象在被释放后,weak指针会自动置为nil,不会造成野指针。比如自定义 IBOutlet
控件属性也是用 weak (因为父控件的 subViews 数组已经对它有了一次强引用)。
copy:分为深拷贝和浅拷贝
@property
的本质:@property
= ivar
+ setter
+ getter
@property
等于声明了ivar(数形变量),并实现了该属性的存取方法(setter + getter)。@property
作为 OC 的一项特性,主要就在于封装对象中的数据。@synthesize :
系统会自动生成该属性的 setter
和 getter
方法。@dynamic :
系统不会自动生成该属性的 setter
和 getter
方法,需要用户自己去实现CoreAnimation
来实现的;CALayer(CoreAnimation Layer)
类来管理的;CALayer
的管理器,访问它的根绘图和根坐标有关的属性(如:frame、bounds 等),实际上内部都是在访问他所包含的 CALayer
的相关属性;layer
,对应的是他的 CALayer
实例。CALayer
类似于 UIView 的子 View 树形结构,也可以向它的 layer
上添加子 layer
,来完成某些特殊的表示;subLayer
(非主 Layer)属性进行更改,系统将会自动进行动画生成。CALayer
的坐标系统比 UIView 多了一个 anchorPoint
属性,使用 CGPoint
结构标识,值域是 0 ~ 1
,是个比例值。setNeedsDisPlay
方法来重绘显示。3D
或仿射变换,可以分别设置层的 transform
或 affineTransform
属性。Quartz Core
的渲染能力,使二维图像可以被自由操纵,就好像是三维的。图像可以在一个三维坐标系中以任意角度被旋转、缩放和倾斜。CATransform3D
的一套方法提供了一些魔术般的变换效果。- (void)addScriptMessageHandler:(id)scriptMessageHandlername:(NSString*)name
第一个参数使用self,造成了强持有。解决办法。prepareLayout
方法,并在里面事先就计算好必要的布局信息并存储起来。prepareLayout
方法中的布局信息,重写 collectionViewContentSize
方法返回 UICollectionView的内容尺寸。layoutAttributesForElementsInRect:
方法返回指定区域 cell、Supplementary View 和 Decoration View 的布局属性。layoutAttributesForItemAtIndexPath:
;方法返回对应的 indexPath 的位置的 cell 的布局属性。layoutAttributesForSupplementaryViewOfKind: atIndexPath:
,方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载。layoutAttributesForDecorationViewOfKind: atIndexPath:
,方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载。shouldInvalidateLayoutForBoundsChange:
,当边界发生变化时,是否应该刷新。launchOptions
是启动参数,假如用户通过点击push通知启动的应用,这个参数里会存储一些push通知的信息。– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"程序载入后");
}
非活动状态
执行(一般在程序运行时,有来电,锁屏,按HOME键,下拉通知栏,双击HOME键等情况会调用此方法),在此期间,应用程序不接受消息或事件 。在此方法中可以暂停正在进行的任务,如禁用定时器,暂停游戏等。- (void)applicationWillResignActive:(UIApplication *)application {
NSLog(@"应用程序将要进入非活动状态(进入后台)");
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(@"应用程序已经进入后台运行");
}
活动状态
执行,若应用不在后台状态,而是直接启动,则不会回调此方法。- (void)applicationWillEnterForeground:(UIApplication *)application {
NSLog(@"应用程序将要进入前台运行");
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
NSLog(@"应用程序已进入前台,处于活动状态");
}
-(void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
NSLog(@"系统内存不足,需要进行清理工作");
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(@"应用程序将要退出");
}
-(void)applicationSignificantTimeChange:(UIApplication *)application {
NSLog(@"系统时间发生改变");
}
applicationWillEnterForeground:
。applicationDidBecomeActive
。applicationWillResignActive
。applicationDidEnterBackground
。application: didFinishLaunchingWithOptions:
方法applicationDidBecomeActive
,应用程序已经进入活动状态。NSCache
是一个非常奇怪的集合。默认为可变并且线程安全的
。这使它很适合缓存那些创建起来代价高昂的对象
。它自动对内存警告
做出反应并基于可设置的成本清理自己
。与NSDictionary相比,键是被retain而不是被拷贝的。
id
和 instanceType
有什么区别?instancetype
和 id
都是万能指针
,指向对象。id
在编译的时候不能判断对象的真实类型,instancetype
在编译的时候可以判断对象的真实类型。id
可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetyp
e 只能作为返回值类型。objc_msgSend
函数:id objc_msgSend(id theReceiver, SEL theSelector, ...)
第一个参数是消息接收者,第二个参数是调用的具体类方法的 selector,后面是 selector 方法的可变参数。以 [self setName:] 为例,编译器会替换成调用 objc_msgSend 的函数调用,其中 theReceiver 是 self,theSelector 是 @selector(setName:),这个 selector 是从当前 self 的 class 的方法列表开始找的 setName,当找到后把对应的 selector 传递过去。objc_msgSendSuper
函数:id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selectorstruct objc_super {
id receiver;
Class superClass;
};
setNeedsDisplay
和 layoutIfNeeded
两者是什么关系?setNeedsDisplay
和 setNeedsLayout
两个方法都是异步执行的。
setNeedsDisplay
会自动调用 drawRect
方法,这样可以拿到 UIGraphicsGetCurrentContext
进行绘制;setNeedsLayout
会默认调用 layoutSubViews
,给当前的视图做了标记;layoutIfNeeded
查找是否有标记,如果有标记及立刻刷新。setNeedsLayout
和 layoutIfNeeded
这二者合起来使用,才会起到立刻刷新的效果。swift
和 OC 的联系
swift
和 OC 共用一套运行时环境,swift
和 OC 可以互相桥接,互相引用混合编程;计数器
、ARC
、属性
、协议
、接口
、初始化
、扩展类
、命名参数
、匿名函数
等绝大多数概念,在 swift
中继续有效。swift
中有 OC 没有的一些概念。比如:元组
, 泛型
,函数式编程模式(如 map、filter、reduce 等)等。swift
相对于 OC 的优势
swift
容易阅读,语法和文件结构简洁化。swift
更容易维护,文件分离后结构更清晰。swift
更加安全,它是类型安全
的语言。swift
代码更少,语法更简洁,可以省去大量冗余的代码。swift
速度更快,运算性能更高。语言 |
Swift |
---|---|
优点 |
1. 语法更简洁 2. 报错精准(报错的时候直接显示报错行)3. 定义变量简单(定义变量不用区分整型,浮点型等等,变量使用var,常量使用let。)4. 可视化互动效果(开发工具带来了Xcode Playgrounds 功能,该功能提供强大的互动效果,能让Swift源代码在撰写过程中实时显示出其运行结果。) 5. 函数式编程的支持(Swift 语言本身提供了对函数式编程的支持;Objc 本身是不支持的,通过引入 ReactiveCocoa 这个库才可支持函数式编程。) |
缺点 |
1. Swift目前还没有得到全面性的推广 2. Swift 暂时还不稳定,在 Swift 5.0 之前 API 不稳定,之后变得稳定 3. 第三方库的支持不够多 4. App体积变大( App 体积大概增加 5-8 M 左右)5. 上线方式改变(在上线的时候,不能使用application Loader上传包文件,会提示你丢失了swift support files,应该使用xcode直接上传。) |
Optionals
)swift
引用了可选项类型,用于处理变量值不存在的情况。
Optionals
类似于 OC 中指向 nil 的指针
,但是适用于所有的数据类型,而非仅仅局限于类,Optionals
相比于 OC 中的 nil 指针
,更加安全和简明,并且也是 swift
诸多最强大功能的核心。
struct
和 class
struct
是值类型,而 class
是引用类型。defer
defer
关键字提供了一个安全和简便的方式来处理这件事,当离开当前的代码块时,会执行defer
对应的代码块。
func openFileAction(){
///打开文件
openFile()
defer{
closeFile()
}
///读文件
let isRead = readFile()
guard isRead else {
return
}
if emptyFile() {
return
}
print("读取成功")
}
guard
guard
当条件满足的时候,会顺序执行,如果 guard
条件不满足的时候,会进入 guard
内部,并执行 return 操作,终止代码的执行。
map
用于映射, 可以将一个列表转换为另一个列表。
数组元素类型转换
//swift为函数的参数自动提供简写形式,$0代表第一个参数,$1代表第二个参数
let array = ["1", "2", "3"]
let str1 = array.map({ "\($0)"}) //数组每个元素转成String类型
//字符串数组转NSInteger类型数组
let array1 = array.map { (obj) -> NSInteger in
return NSInteger(obj) ?? 0
}
//NSInteger类型数组转字符串数组
let array2 = array1.map { (obj) -> String in
return String(obj)
}
print("array1: \(array1)")
print("array2: \(array2)")
//str1 ["1", "2", "3"]
//array1: [1, 2, 3]
//array2: ["1", "2", "3"]
flatMap
功能跟map类似; 区别是flatMap会过滤nil元素, 并解包Optional
。
flatMap
还可以将多维数组转换为一维数组,对于N维数组, map函数仍然返回N维数组。
let array = [[1, 2, 3],[1, 2, 3],[1, 2, 3]]
let arrret = array.flatMap{$0}
let arrret1 = array.map{$0}
print(arrret)
print(arrret1)
//[1, 2, 3, 1, 2, 3, 1, 2, 3]
//[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
filter
用于过滤, 可以筛选出想要的元素
let array = [1, 2, 3]
let resultArray = array.filter { return $0 > 1 }
print(resultArray)
//[2, 3]
reduce
reduce
方法把数组元素组合计算为一个值。
//我们要求和
let numbers = [2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
//传统
var result = 0
for x in numbers {
result += x
}
//使用reduce
result = numbers.reduce(0,{$0+$1})
async
/await
? (swift 5.5 后,百度问)async
:
表示这个函数时可以异步执行
的,也就是说执行这段代码是可以不阻塞
当前线程。
await
:
在函数、属性和初始值设定项中,await
可用于表达式可以解除当前线程阻塞;除此之外,await
还可以用于异步序列。
Swift 中派发机制分为直接派发
、函数表派发
、消息派发
三种。
直接派发 (Direct Dispatch)
:
直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间。 例如函数内联等。 直接派发也有人称为静态调用。然而,对于编程来说直接调用也是最大的局限,而且因为缺乏动态性所以没办法支持继承。
函数表派发 (Table Dispatch)
:
函数表派发是编译型语言实现动态行为最常见的实现方式
。
函数表使用了一个 数组
来存储类声明的每一个函数的指针。大部分语言把这个称为 “virtual table
”(虚函数表),Swift 里称为 “witness table
”。每一个类都会维护一个函数表
,里面记录着类所有的函数,如果父类函数被 override
的话,表里面只会保存被 override
之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数。
消息派发 (Message Dispatch)
: Object-c的OO实现
消息机制是调用函数最动态的方式。也是 Cocoa
的基石,这样的机制催生了 KVO
,UIAppearence
和 CoreData
等功能。这种运作方式的关键在于开发者可以在运行时改变函数的行为。不止可以通过 swizzling
来改变,甚至可以用 isa-swizzling
修改对象的继承关系, 可以在面向对象的基础上实现自定义派发。
值类型
使用直接派发。class
和协议的extension
使用的是直接派发。class
和协议的初始化声明
使用的是函数表派发。class
的@obj extension
使用的是消息机制派发。final
:
final
允许类里面的函数使用直接派发。这个修饰符会让函数失去动态性。任何函数都可以使用这个修饰符,就算是 extension
里本来就是直接派发的函数。Objective-C
的运行时获取不到这个函数, 不会生成相应的 selector
。dynamic
:
dynamic
可以让类里面的函数使用消息机制派发。
dynamic
, 必须导入 Foundation
框架,里面包括了 NSObject 和 Objective-C 的运行时。dynamic
可以让声明在 extension
里面的函数能够被 override
。dynamic
可以用在所有 NSObject 的子类和 Swift 的原声类。这就是为什么KVO
的属性需要用dynamic
修饰。@objc
:
函数能被 Objective-C
的运行时捕获到。
使用 @objc
的典型例子就是给 selector
一个命名空间 @objc(abc_methodName)
,让这个函数可以被 Objective-C 的运行时调用。
@nonobjc
:
禁止消息机制派发这个函数,不让这个函数注册到 Objective-C 的运行时里。
@inline
:直接派发。
库的本质
是可执行的二进制文件
,是资源文件
和代码编译
的一个集合
。根据链接方式不同,可以分为动态库和静态库,其中系统提供的库都属于动态库
。
静态库:
静态库形式:.a
和 .framework
,作用
是在进行链接生成可执行文件时
,从静态库文件中拷贝需要的内容
到最终的可执行文件中。
被多次使用就有多份冗余拷贝。
//在使用gcc编译时采用 -static选项来进行静态文件的链接:
gcc -c main.c
gcc -static -o main main.o
动态库:
静态库形式: .dylib
和 .framework
,并不在链接时将需要的二进制代码都拷贝到可执行文件中
,而是拷贝一些重定位和符号表信息
,当程序运行时需要的时候再通过符号表从动态库中
获取(动态加载)。 系统只加载一次
,多个程序共用,节省内存。
动静态库区别:
库名称 | 优点 | 缺点 |
---|---|---|
静态库 | 1. 目标程序没有外部依赖,直接就可以运行。2. 效率教动态库高。 |
1. 会使用目标程序的体积增大。因为它将需要用到的代码从二进制文件中拷贝了一份 |
动态库 | 1. 不需要拷贝到目标程序中,不会影响目标程序的体积 。2. 同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库 )。3. 编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。实现动态更新 。 |
1. 动态载入会带来一部分性能损失(可以忽略不计)2. 动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux lib not found 错误)。 |
直播篇幅请点击跳转