RunLoop
1、什么是 RunLoop? RunLoop 作用有哪些?
RunLoop 可以称之为运行循环,在程序运行过程中循环做一些事情,如果没有 RunLoop 程序执行完毕就会立即退出,有 RunLoop 程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。
基本作用:
保持程序持续运行。程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的 RunLoop ,RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行。
处理App中的各种事件(比如:触摸事件,定时器事件,Selector 事件等) 。
节省CPU资源,提高程序性能。程序运行起来时,当什么操作都没有做的时候,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop 就会立马起来去做事情。
2、app 如何接收到触摸事件的 ?
APP进程的mach port接收来自SpringBoard的触摸事件,主线程的RunLoop被唤醒,触发source1回调。
source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
source0回调将触摸事件添加到UIApplication的事件队列,当触摸事件出队后UIApplication为触摸事件寻找最佳响应者。
寻找到最佳响应者之后,接下来的事情便是事件在响应链中传递和响应。
那么事件响应链是如何传递的呢 ? 可简称为 “由父及子” 的过程,即:
触摸事件的传递是从父控件传递到子控件
也就是从UIApplicaiton->window->寻找处理事件的最合适的view
两个重要的方法:
// 获取响应 事件的视图,通过下面的方法判断触控点位置
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
// 判断触摸点是不是在这个view的坐标上。如果在坐标上,会分发事件给这个view的子view。后每个子view重复以上步骤,直至最底层的一个合适的view。
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
那么事件响应链是如何响应的呢?可简称为 “由子及父” 的过程,即:
事件响应会先从底层最合适的view开始,然后随着上一步找到的链一层一层响应touch事件。默认touch事件会传递给上一层。
如果到了viewController的view,就会传递给viewController。
如果viewController不能处理,就会传递给UIWindow。
如果UIWindow无法处理,就会传递给UIApplication。
如果UIApplication无法处理,就会传递给UIApplicationDelegate。
如果UIApplicationDelegate不能处理,则会丢弃该事件。
在这里插入图片描述
3、为什么只有主线程的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,让主线程常驻 。 4、为什么只在主线程刷新 UI ?
UIKit 并不是一个线程安全的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。
另一方面因为整个程序的起点 UIApplication 是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。 而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上同时更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
5、PerformSelector和RunLoop的关系 ?
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
6、如何使线程保活?
在NSThread执行的方法中添加while(true){},这样是模拟 RunLoop 的运行原理,结合GCD 的信号量,在 {} 中处理任务。
采用 RunLoop 的方式。参考这篇文章
让子线程永远活着,这时就要用到常驻线程:给子线程开启一个 RunLoop
注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。
子线程开启 RunLoop 的代码,先点击屏幕开启子线程并开启子线程 RunLoop 。
-(void)touchesBegan:(NSSet<UITouch *> *)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 ,或者只加入一个监听者,运行程序会崩溃。
7、子线程默认有RunLoop吗? RunLoop 创建和销毁的时机又是什么时候呢?
线程和 RunLoop 之间是一一对应的。但是在创建子线程时,子线程的 RunLoop 需要我们主动创建 。只需在子线程中获取当前线程的 RunLoop 对象即可 [NSRunLoop currentRunLoop] ;如果不获取,那子线程就不会创建与之相关联的 RunLoop。
RunLoop 在第一次获取时创建,在线程结束时销毁。
8、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
1、KVO 实现原理
KVO是关于RunTime机制实现的
当某个类的对象属性第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(NSKVONotifying_A),在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制
如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统就会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性复制是执行的是派生类的setter方法
键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForKey:一定会被调用,这就会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的。
2、如何手动关闭 KVO ?
重写被观察对象的automaticallyNotifiesObserversForKey方法,返回NO
重写automaticallyNotifiesObserversOf ,返回NO。
注意:关闭 kvo 后,需要手动在赋值前后添加willChangeValueForKey和didChangeValueForKey,才可以收到观察通知。
3、通过 KVC 修改属性会触发 KVO 吗?
会触发。即使没有 setter 方法也会触发。 4、哪些情况下使用 kvo 会崩溃,怎么防护崩溃?
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。 5、KVO 的优缺点?
优点:
能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步
能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现
能够提供观察的属性的最新值以及先前值
用key paths来观察属性,因此也可以观察嵌套对象
完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查
对属性重构将导致我们的观察代码不再可用
复杂的if语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向
当释放观察者时不需要移除观察者
RunTime
1、介绍下 RunTime 的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
对象: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对象、类、元类之间的关系 aHR0cHM6Ly9pbWcyMDIwLmNuYmxvZ3MuY29tL2Jsb2cvOTA3MjU5LzIwMjAwMy85MDcyNTktMjAyMDAzMDUxMTEwMjM1MDYtOTkxOTU1MTQzLnBuZw.png
从图中可知:对象的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]; };
2、为什么要设计 metaclass ?
metaclass 代表的是类对象的对象,存储了类的类方法,目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。 3、class_copyIvarList & class_copyPropertyList区别?
class_copyIvarList:获取的是类的成员变量列表,即:@interface{中声明的变量}
class_copyPropertyList:获取的是类的属性列表,即:通过@property声明的属性 4、class_rw_t 和 class_ro_t 的区别?
class_rw_t:代表的是可读写的内存区,这块区域中存储的数据是可以更改的。
class_ro_t:代表的是只读的内存区,这块区域中存储的数据是不可以更改的。
OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过RunTime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表。 5、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 里的对应方法。
6、category & extension区别?能给 NSObject 添加 Extension 吗?结果如何?
category :分类
给类添加新的方法
不能给类添加成员变量
通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性
是运行期决定的。
注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。但是,我们可以使用 runtime 的 objc_setAssociatedObject 和 objc_getAssociatedObject 给该属性动态绑定。
extension :扩展 可以给类添加成员变量,但是是私有的 可以給类添加方法,但是是私有的 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件 @implement一起形成了一个完整的类。 伴随着类的产生而产生,也随着类的消失而消失 必须有类的源码才可以给类添加extension,所以对于系统一些类,如NSString,就无法添加类扩展 不能给 NSObject添加 Extension,因为在 extension 中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的 extension 。
7、消息转发机制,消息转发机制和其他语言的消息机制优劣对比?
消息转发机制:当接收者收到消息后,无法处理该消息时(即:找不到调用的方法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
8、在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么 ?
OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象。
RunTime通过这个对象的isa指针找到这个对象的类对象
从类对象中的cache中查找是否存在SEL对应的IMP
若不存在,则会在 method_list中查找
如果还是没找到,则会到supper_class中查找
仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发
9、IMP、SEL、Method的区别和使用场景
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,方法对应的参数,这个顺序是固定的。 10、load、initialize方法的区别什么?在继承关系中他们有什么区别?
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,这里我给大家画个图梳理一下:
image.png
initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次
调用方式是通过RunTime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕。
子类和父类同时实现initialize,父类的先被调用,然后调用子类的。
本类与category同时实现initialize,category会覆盖本类的方法,只调用category的。
initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)。
initialize是主动调用的,只有当类第一次被用到的时候才会触发。
11、说说消息转发机制的优劣?
优点:
动态化更新方案
(例如: JSPatch):消息转发机制来进行JS和OC的交互,从而实现iOS的热更新
实现多重代理
利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。
间接实现多继承
OC本身不支持多继承,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。
预防线上奔溃
利用消息转发机制对消息进行转发和替换,预防线上版本奔溃
缺点:
消耗性能(延长了消息发送的周期,提高了成本)
bug 的定位更加困难
12、iOS你在项目中用过 RunTime 吗?举个例子。
关联对象 Associated Objects 给category 添加属性
消息发送 Messaging
消息转发 Message Forwarding
方法调配 Method Swizzling 方法替换、动态添加方法
“类对象” NSProxy Foundation | Apple Developer Documentation
KVC、KVO About Key-Value Coding
动态获取 class 和 slector
13、RunTime 是如何把 weak变量的自动置 nil 的?
RunTime对注册的类会进行布局,对于 weak 对象会放入一个 hash 表中。用 weak 对象指向的内存地址作为 key,当此对象引用计数为 0 时会 dealloac。假如 weak 对象的内存地址是 a,那么就会以 a 为键,在 hash 表中进行搜索,找出所有 a 对应的 weak 对象,从而置为 nil。
weak 修饰的指针默认为 nil。(在 OC 中对 nil 发送消息是安全的) 14、objc中向一个 nil 对象发送消息将会发生什么?
如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。也不会崩溃。 详解:
如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil); 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*) ,float,double,long double 或者long long的整型标量,发送给nil的消息将返回0; 如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0; 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的。
Block
1、block的内部实现,结构体是什么样的?
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能访问它的外部的局部变量,就是因为将这些变量(或变量地址)复制到了结构体中
2、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从栈复制到堆上。
3、一个int变量被 __block 修饰与否的区别?block 的变量截获?
没有被__block修饰的int,block体中对这个变量的引用是值拷贝,在block中是不能被修改的。
通过__block修饰的int,block体中对这个变量的引用是指针拷贝,它会生成一个结构体,复制这个变量的指针引用,从而达到可以修改变量的作用。
block的变量截获:
__block会将block体内引用外部变量的变量进行拷贝,将其拷贝到block的数据结构中,从而可以在block体内访问或修改外部变量。
外部变量未被__block修饰时,block数据结构中捕获的是外部变量的值,通过__block修饰时,则捕获的是对外部变量的指针引用。
注意:block内部访问全局变量时,全局变量不会被捕获到block数据结构中。
4、block在修改NSMutableArray,需不需要添加__block ?
如果修改的是NSMutableArray的存储内容的话,是不需要添加__block修饰的。
如果修改的是 NSMutableArray对象的本身,那必须添加__block修饰。 参考block的变量捕获(第3点)。
5、block怎么进行内存管理的?
当block内部引用全局变量或者不引用任何外部变量时,该block是在全局内存中的。(全局静态block)
当block内部引用了外部的非全局变量的时候:
在MRC中,该block是在栈内存中的
在ARC中,该block是在堆内存中的。
也就是说,ARC下只存在全局block和堆block。
通过__block修饰的变量,在block内部依然会对其引用计数+1,可能会造成循环引用。
通过__weak修饰的变量,在block内部不会对其引用计数+1,不会造成循环引用。
6、block可以用strong修饰吗?
在MRC环境中,是不可以的。strong修饰符会对修饰的变量进行retain操作,这样并不会将栈中的block拷贝到堆内存中,而执行的block是在堆内存中,所以用strong修饰的block会导致在执行的时候因为错误的内存地址,导致闪退。
在ARC环境中,是可以的。因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作。
7、解决循环引用时为什么要用__strong、__weak修饰?
__weak修饰的变量,不会出现引用计数+1,也就不会造成block强持有外部变量,这样也就不会出现循环引用的问题了。
但是,我们的block内部执行的代码中,有可能是一个异步操作,或者延迟操作。此时引用的外部变量可能会变成nil,导致意想不到的问题,而我们在block内部通过__strong修饰这个变量时,block会在执行过程中强持有这个变量,此时这个变量也就不会出现nil的情况,当block执行完成后,这个变量也就会随之释放了。
那么问题来了: Masonry 需要用 __weak 修饰吗?如果不用,那为什么呢?
Masonry 内部并没有使用 __weak , 在 makeConstraints 或 updateConstraints 中 View 并没有持有 Block ,所以这个 block 只是一个 栈block 。当执行完 block(constraintMaker) 就出栈释放掉了,所以不会造成循环引用。
8、block 发生copy 的时机?
一般情况在ARC环境中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作。
block作为方法或函数的返回值时,编译器会自动完成copy操作。
当block赋值给通过strong或copy修饰的id或block类型的成员变量时。
当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。
9、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下不会。
多线程
1、什么是进程?什么是线程?进程和线程的关系?什么是多进程?什么是多线程?
进程:
进程是一个具有独立功能的程序关于某次数据集合的一次运行活动,他是操作系统分配资源的基本单位。
进程是指系统正在运行中的一个应用程序,就是一段程序执行的过程。我们可以理解为手机上的一个app。
每个进程之间是独立的。每个进程均运行在起专用且受保护的内存空间内,拥有独立运行所需的全部资源。
进程是操作系统进行资源分配的单位。
线程:
程序执行流的最小单元,线程是进程中的一个实体。
一个进程想要执行任务,必须至少有一条线程。应用程序启动的时候,系统会默认开启一条线程,也就是主线程。
进程和线程的关系:
线程是进程的执行单元,进程的所有任务都在线程中执行。
线程是CPU分配资源和调度的最小单位。
一个程序可对应多个进程(多进程);一个进程中可对应多个线程,但至少要有一条线程。
同个进程内的线程共享进程资源。
多进程:
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然程序是死的(静态的),进程是活动的(动态的)。
进程可以分为系统进程和用户进程。
系统进程:凡是用于完成操作系统的各种功能的进程就是系统进程,他们就是出于运行状态下的操作系统本身
用户进程:运行用户程序时创建的运行在用户态下的进程。
进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。
在同一个时间里,同一个操作系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。
多线程:
同一时间,CPU 只能处理1条线程,只有1条线程执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU的调度线程的时间足够快,就造成了多线程并发执行的假象。
如果线程非常至多(N条),CPU会在这些(N条)线程之间调度,消耗大量的CPU资源,每条线程被调用执行的频率会降低(线程的执行效率降低)。
多线程的优点:
能适当提高程序的执行效率
能适当提高资源的利用率(CPU、内存利用率)
多线程的缺点:
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512kb),如若开启大量线程,会占用大量的内存空间,就会降低程序的性能
线程越多,CPU在调度线程的开销就越大
程序设计更加复杂:如线程之间的通信、多线程之间的数据共享等
2、iOS开发中有多少类型的线程?分别对比?
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有哪些队列,默认提供哪些队列?
3中队列:主线程队列、并发队列、串行队列
在GCD中有两种队列:串行队列和并发队列。两者都符合 FIFO 的原则,二者的主要区别是:执行的顺序不同和开启的线程数不同。
主线程队列: main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
串行队列(Serial Dispatch Queue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只能开启一个线程,一个线程执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为程序创建的。
并行队列(Concurrent Dispatch Queue):
同时允许多个任务同时执行。(可以开启多个线程,并且同时执行)。并发队列的并发功能只有在异步(dispatch_async) 函数下才有效。
4、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函数通常用在单例模式上,它可以保证在程序运行期间某段代码只执行一次。如果我们要通过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的安全机制保证这段代码只执行一次。 5、GCD主线程 & 主队列的关系?
提交到主队列的任务在主线程执行。
主队列是主线中的一个串行队列。
所有的和UI相关的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI。
每一个应用程序只有唯一的一个主队列用来update UI
补充一点:如果在主线程中创建自定义队列(串行或者并行均可),在这个队列中执行同步任务,同样可以更新UI操作,主队列中可以更新UI,自定义队列也可以更新UI,但自定义队列的更新UI的前提是在主线程中执行同步任务。
6、如何实现同步?有多少方式就说多少
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() 单位时间内只允许一个线程进入临界区
7、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)
1
比较+交换 的原子操作。比较 &l->dgo_once 的值是否等于 DLOCK_ONCE_UNLOCKED
这样就实现了我们的执行1次的GCD API. 8、什么情况下会死锁?死锁的应对策略有哪些?怎么避免死锁?
死锁发生的四个必要条件是:
互斥条件(Mutual exclusion) :
资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait):
进程已获得了一些资源,但因请求其它资源被阻塞时,对已获得的资源保持不放。
不可抢占条件(No pre-emption) :
有些系统资源是不可抢占的,当某个进程已获得这种资源后,系统不能强行收回,只能由进程使用完时自己释放。
循环等待条件(Circular wait) :
若干个进程形成环形链,每个都占用对方申请的下一个资源。
一般死锁的应对策略有:
死锁预防:
破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率。
死锁避免:
避免是指进程在每次申请资源时判断这些操作是否安全。例如,使用银行家算法。死锁避免算法的执行会增加系统的开销。
死锁检测:
死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略。
死锁解除:
这是与死锁检测结合使用的,它使用的方式就是剥夺。即:将某进程所拥有的资源强行收回,分配给其他的进程。
死锁的避免:
死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件是死锁产生的必要条件,也就是说要产生死锁必须具备的条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。
避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;
显然要避免死锁,必须事先知道系统拥有的资源数量及其属性。
9、有哪些类型的线程锁?分别介绍下作用和使用场景? 锁类型 使用场景 备注 pthread_mutex 互斥锁 PTHREAD_MUTEX_NORMAL,#import <pthread.h> OSSpinLock 自旋锁 不安全,iOS 10 已启用 os_unfair_lock 互斥锁 替代 OSSpinLock pthread_mutex(recursive) 递归锁 PTHREAD_MUTEX_RECURSIVE,#import <pthread.h> pthread_cond_t 条件锁 #import <pthread.h> pthread_rwlock 读写锁 读操作重入,写操作互斥 @synchronized 互斥锁 性能差,且无法锁住内存地址更改的对象 NSLock 互斥锁 封装 pthread_mutex NSRecursiveLock 递归锁 封装pthread_mutex(recursive) NSCondition 条件锁 封装 pthread_cond_t NSConditionLock 条件锁 可以指定具体条件值 封装 pthread_cond_t 13、iOS各种锁的性能,琐是毫秒级别还是微妙级别?
琐是 ns 纳秒 us微秒级别。
参考自YY大神的不再安全的 OSSpinLock。单位是 ns 纳秒。
锁耗时
锁相关的概念定义:
临界区:
指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
每个进程中访问临界资源的那段程序称为临界区,每次只允许一个进程进入临界区,进入后不允许其他进程进入。
自旋锁:
是用于多线程同步的一种锁,线程反复检查锁变量是否可用。
a、 由于线程在这一过程中保持执行,因此是一种忙等待。
b、 一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
c、 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
互斥锁(Mutex):
用于保护临界区,确保同一时间只有一个线程访问数据。 对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
a、 互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态;等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。
b、 性能开销成本,两次线程上下文切换的成本。
当线程加锁失败时,内核将线程的状态从【运行】切换到睡眠状态,然后把CPU切换给其他线程运行;
当锁被释放时,之前睡眠状态的线程会变成就绪状态,然后内核就会在合适的时间把CPU切换给该线程运行
读写锁:
是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁。用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
信号量(semaphore):
是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
条件锁:
就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
递归锁(Recursive Lock):
也称为可重入互斥锁(reentrant mutex),是互斥锁的一种,同一线程对其多次加锁不会产生死锁。 递归锁会使用引用计数机制,以便可以从同一线程多次加锁、解锁,当加锁、解锁次数相等时,锁才可以被其他线程获取。
11、NSOperationQueue 中的 maxConcurrentOperationCount 默认值
默认值 -1。 这个值操作系统会根据资源使用的综合开销情况设置。 12、NSTimer、CADisplayLink、dispatch_source_t 的优劣? 定时器类型 优势 劣势 NSTimer 使用简单 依赖 RunLoop,具体表现在无 RunLoop 无法使用、NSRunLoopCommonModes、不精确 CADisplayLink 依赖屏幕刷新频率出发事件,最精.最合适做UI刷新 若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承 dispatch_source_t 不依赖 RunLoop 依赖线程队列,使用麻烦 使用不当容易Crash 13、多线程可以访问同一个对象吗 ?多进程呢?
多线程可以访问同一个对象可分为3种情况处理:
如果只是只读,不用加锁。
如果只写的话,需要加锁。
如果需要读且写的话,需要加锁(读写锁满足)。
使用读写锁 pthread_rwlock。获取一个读写锁用于读称为共享锁,获取一个读写锁用于写称为独占锁,因此这种对于某个给定资源的共享访问也称为共享-独占上锁。
多进程访问同一个对象
一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。
一个程序里有很多个进程
一个程序几个进程在于这个程序的开发者的设置,可以是1个,也可以是多个的。一个应用程序,启动多个处理进程。换言之,所有进程隶属于当前应用程序;这是所谓的多进程服务。
一个程序只有一个进程但被开启很多个
启动多个同一应用程序,每个应用程序都是单进程。这个场景有些应用程序会禁用掉,有些是可以的,看应用程序的定位。如果允许,那么需要解决数据共享的问题(主要是数据写入);如果不允许,那么只能启动一个此类应用程序。
所以 多个进程竞争,进程就会一直等待下去,形成死锁。
所以 我们就可以根据死锁的四个必要条件(互斥条件、请求与保持条件、不可抢占条件、不可剥夺条件), 使用死锁的四个应对策略(死锁预防、死锁避免、死锁检测、死锁解除)来解决死锁问题。
所以 我们也可以通过一些处理避免死锁:
死锁的预防是通过破坏产生条件来阻止死锁的产生,但这种方法破坏了系统的并行性和并发性。
死锁产生的前三个条件是死锁产生的必要条件,而不是存在这3个条件就一定产生死锁,那么只要在逻辑上回避了第四个条件就可以避免死锁。
避免死锁采用的是允许前三个条件存在,但通过合理的资源分配算法来确保永远不会形成环形等待的封闭进程链,从而避免死锁。该方法支持多个进程的并行执行,为了避免死锁,系统动态的确定是否分配一个资源给请求的进程。方法如下:
如果一个进程的当前请求的资源会导致死锁,系统拒绝启动该进程;
如果一个资源的分配会导致下一步的死锁,系统就拒绝本次的分配;
总而言之:要避免死锁,必须事先知道系统拥有的资源数量及其属性。
优化
1、TableView 有什么好的性能优化方案?
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 框架)画出来圆角图片。
2、界面卡顿和检测你都是怎么处理?
卡顿原因: 在一个VSync内GPU和CPU的协作,未能将渲染任务完成放入到帧缓冲区,视频控制器去缓冲区拿数据的时候是空的,所以卡帧。
卡顿优化:
图片等大文件IO缓存
耗时操作放入子线程
提高代码执行效率(JSON to Model的方案,锁的使用等,减少循环,UI布局frame子线程预计算)
UI减少全局刷新,尽量使用局部刷新
监控卡帧:
CADisplayLink 监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync的时长,上报调用栈。
在RunLoop中添加监听,如果kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting中间的耗时超过VSync的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看。那个部分耗时长即可。
3、谈谈你对离屏渲染的理解?
离屏渲染(Off-Screen Rendering):分为CPU离屏渲染 和 GPU离屏渲染两种形式。GPU离屏渲染指的是在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作。
一般情况下,OpenGL会将应用提交到 Reader Server 的动画直接渲染显示,但对于一些复杂的图像动画显示并不能直接渲染叠加显示,而是需要根据 Command Buffer 分通道进行渲染之后在组合,这一组合过程中,就有些渲染通道是不会直接显示的;Masking 渲染需要更多的渲染通道和合并的步骤;而这些没有直接显示在屏幕上的通道就是 Off-Screen Readering Pass。
如何检查离屏渲染?
通过勾选Xcode的Debug->View Debugging–>Rendering->Run->Color Offscreen-Rendered Yellow项。
离屏渲染(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 的图层
使用CGContext在drawRect : 方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现
离屏渲染的优化方案 ?
圆角优化 :
1.1、 使用 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];
Shadow 优化
对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。示例如下:
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之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。
其他的一些优化建议
3.1、当我们需要圆角效果时,可以使用一张中间透明图片蒙上去
3.2、使用ShadowPath指定layer阴影效果路径
3.3、使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit (Texttrue))
3.4、设置layer的opaque值为YES,减少复杂图层合成
3.5、尽量使用不包含透明(alpha)通道的图片资源
3.6、尽量设置layer的大小值为整形值
3.7、直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
3.8、很多情况下用户上传图片进行显示,可以让服务端处理圆角、
3.9、使用代码手动生成圆角Image设置到要显示的View上,利用UIBezierPath(CoreGraphics框架)画出来圆角图片
4、如何降低APP包的大小?
资源优化:
删除无用图片
使用 LSUnusedResources 查找无用图片。注意 [UIImage imageNamed:[NSString stringWithFormat:“icon_%d.png”,index]]; 这种使用图片的方式,可能会被误删。
删除重复资源:Json、Plist、Extension 等
压缩图片资源
使用 ImageOptim 无损压缩图片。
使用 TinyPNG 有损压缩图片。使用的时候直接执行 tinypng *.png -k token 脚本即可。
其他技巧:
用 LaunchScreen.storyboard 替换启动图片。
本地大图片都使用 webp。
资源按需加载,非必要资源都等到使用时再从服务端拉取。
编译选项优化:
Optimization Level 在 release 状态设置为 Fastest/Smallest。
Strip Debug Symbols During Copy 在 release 状态设置为 YES。
Strip Linked Product 在 release 状态设为 YES。
Make String Read-Only 在 release 状态设为 YES。
Dead Code Stripping 在 release 状态设为 YES。
Deployment PostProcessing 在 release 状态设为 YES。
Symbols hidden by default 在 release 状态设为 YES。
可执行文件优化:
使用 LinkMap 分析库的使用情况
三方库优化
删除不使用的三方库。
功能用的少但是体积大的三方库可以考虑自己重写。
合并功能重复的三方库。
代码分析
用 AppCode 进行代码扫描
去掉无用的类及文件
清理 import
去掉空方法
去掉无用的 log
去掉无用的变量
其他技巧(选用):
将业务打包成动态库。如果动态库的加载时机不控制好,会影响 App 的启动速度,权衡使用。
动态化。将一部分 Native 界面用 RN/Weex 重写。
去除 Swift 代码,Swift 的标准库是打包在安装包里的,一般都有 10M+。然后苹果官方说等到 Swift Runtime 稳定之后会合并到 iOS 系统里,那时候使用 Swift 就不会显著增加包大小了。
在 target -> Build Settings -> Other Link Flags 里添加如下指令,会把 TEXT 字段的部分内容转移到 RODATA 字段,避免苹果对 TEXT 字段的审核限制。当然其实跟安装包瘦身好像没有什么关系,所以除非快不行了否则不建议操作。
-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
5、日常如何检查内存泄露?
静态分析:
在 Xcode 菜单点击 Product 选择 Analyze (快捷键: Command + Shift + B)
Xcode 会分析出可能 造成内存泄露的语句,
动态内存分析:
2.1、分析内存泄露不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。那就需要用到Instruments了。具体操作是通过 Xcode 打开项目,然后点击 Product --> Profile。
2.2、按上面操作,build 成功后跳出 Instruments 工具。选择 Leaks 选项,点击右下角的【choose】按钮,这时候项目程序也在模拟器或手机上运行起来了,在手机或模拟器上对程序进行操作。
2.3、点击左上角的红色圆点,这时项目开始启动了,由于 Leaks 是动态监测,所以手动进行一系列操作,可检查项目中是否存在内存泄漏问题。 橙色矩形框中所示绿色为正常,如果出现如右侧红色矩形框中显示红色,则表示出现内存泄漏。
2.4、选中Leaks Checks,在 Details 所在栏中选择 CallTree,并且在右下角勾选 Invert Call Tree 和 Hide System Libraries,会发现显示若干行代码,双击即可跳转到出现内存泄漏的地方,修改即可。
分析内存泄露原因:
3.1、检查 NSTimer 的使用:
在需要释放的位置 释放 Timer, 即调用 timer 的 invalidate,并 timer 置为 nil;
注意 NSTimer 的初始化方法(一些方法是 iOS 10 才适配),适配系统版本
注意 循环引用问题, 合理使用 __weak、__strong
3.2、检查代理(Delegate)的使用:
delegate 的强引用问题:使用 assign、weak 修改 delegate 属性
3.3、检查 Block 使用:
Block 最容易犯的就是循环引用问题。合理使用 __weak、__strong
6、APP启动时间应从哪些方面优化?
APP 启动分为热启动和冷启动。
热启动是由于某种原因,APP的状态由running切换为suspend,但是此时APP并没有被系统kill掉,当我们再次把APP切换到前台的时候,APP会恢复之前的状态继续运行,这种就是热启动。我们平时所说的APP在后台的存活时间,其实就是APP能执行热启动的最大时间间隔。
冷启动则是APP从被加载到内存到运行的状态。我们所说的启动优化一般是针对冷启动来说的。
就苹果而言,它将启动分为两个阶段: pre-main 和 main()。启动时间也是针对这两个阶段进行优化,下面我们也将从这两方面进行优化:
pre-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部分组成:
dylib loading(动态库的加载):
这个阶段 dylib 会分析应用依赖的 dylib。由此可知: 应用依赖的 dylib 越少越好。在这一步优化的宗旨是减少 dylib 数量:
1.1、移除不必要的 dylib ;
1.2、合并多个 dylib 成一个 dylib 。
rebase/binding :
这个阶段主要是注册 Objc 类。所以指针数量越少越好。可做的优化有:
2.1、清理项目中无用的类
2.2、删减没有被调用到或者已经废弃的方法
2.3、删减一些无用的静态变量
可以通过 AppCode 等工具实现项目中未使用的代码扫描
ObjeC setup :
这个阶段基本不用优化。若 rebase/binding 阶段优化很好,本阶段耗时也会很少
initializer :
在这个阶段,dylib 开始运行程序的初始化函数,调用每个类和分类的 + load() 方法,调用 C/C++ 中的构造器函数。 initializer 阶段执行结束后, dylib 开始调用 main() 函数。在这一步,检查 + load() 方法,尽量把事情推迟到 + initialize() 方法里执行;并且控制 category 数量,去掉不必要的 category。
在这里我们修改了部分原本代码中直接在 +load 函数初始化逻辑改为在 +initialize 中加载,也就是到使用时才加载。
main() 函数之后的优化:
didFinishLaunchingWithOptions 优化
目前 App 的 didFinishLaunchingWithOptions 方法里执行了多项项业务,有一大部分业务并不是一定要在这里执行的,如支付配置、客服配置、分享配置等。整理该方法里的业务,能延迟加载的就往后推迟,防止其影响启动时间。
整理 didFinishLaunchingWithOptions ,将业务分级,对于非必须的业务移到首页显示后加载。同时,为了防止以后新加的业务继续往 didFinishLaunchingWithOptions 里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。
首页渲染优化 :
减少启动期间创建的 UIViewController 数量
通过打符号断点-[UIViewController viewDidLoad] 发现,如果App 启动过程中创建了 12 个 UIViewController(包括闪屏),即在启动过程中创建了 12 个视图控制器,导致首页渲染时间较长
延迟首页耗时操作
如果 App 首页有个侧滑页面及侧滑手势,并且该页面是用 xib 构建的,将该 ViewController 改为代码构建,同时延迟该页面的创建时机,等首页显示后再创建该页面及侧滑手势,这个改动节省了 300-400ms。
去除启动时没必要及不合理的操作
项目中使用了自定义的侧滑返回,在每次 push 的时候都会截图,启动的时候自定义导航栏会截取两张多余首页的图片,并且截图用的 API (renderInContext) 性能较差,耗时 800ms 左右,去掉启动截图的操作。
闪屏请求回调里写plist文件的操作放在主线程,导致启动时占用主线程,将文件读写移到子线程操作。
架构设计
1、设计模式是为了解决什么问题的?
编写软件过程中,程序员面临着来自耦合性、内聚性以及可维护性、可扩展性、重用性、灵活性等多方面的挑战,设计模式是为了让程序具有更好的:
代码重用性(相同功能代码,不用多次编写)
可读性(编程规范性)
可扩展性(增加新功能时十分方便)
可靠性(增加新功能后,对原来的功能没有影响)
实现高内聚,低耦合的特性
设计模式有 7 大原则:
单一职责原则
一个类只负责一个职责,一个函数只解决一个问题
接口隔离原则
大接口改多个小接口,原因外部不需要大接口这么多方法,更易控制
依赖反转原则
即面向接口编程,尽量不要声明具体类,而是使用接口,实现解耦
里氏替换原则
能出现父类的地方就一定可以用子类代替,即不要重写父类种的已实现的方法
开闭原则
面向扩展开放,面向修改封闭。即不要修改一个已实现的类,更不要修改类中的方法,应该选择创建新类或者创建新方法的方式解决
迪米特法则
又叫最少知道原则,即对外暴露的public方法尽量少,实现高内聚;且只和直接朋友通信
合成复用原则
即不要重复自己,不要在项目内copy代码,应该选择将要copy的代码抽离出来,实现多个类复用
2、常见的设计模式有哪些?
单例模式
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
工厂模式
简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你的需求即可。
抽象工厂模式
抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定。
比如:以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。
代理模式
代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
优点:
代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;
可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务。
缺点:
由于使用了代理模式,因此程序的性能没有直接调用性能高;
使用代理模式提高了代码的复杂度。
举一个生活中的例子:比如买飞机票,由于离飞机场太远,直接去飞机场买票不太现实,这个时候我们就可以上携程 App 上购买飞机票,这个时候携程 App 就相当于是飞机票的代理商。
观察者模式
观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
优点:
观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
观察者模式在观察目标和观察者之间建立一个抽象的耦合;
观察者模式支持广播通信;
观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。
缺点:
如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
策略模式
策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。
优点:遵循了开闭原则,扩展性良好。
缺点:随着策略的增加,对外暴露越来越多。
3、谈谈单例的优缺点?
单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例。 优点:
-
在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
-
避免对资源的多重占用 缺点:
-
没有接口,不能继承,与单一职责原则冲突
-
一个类应该只关心内部逻辑,而不关心外面怎么样来实例化 4、聊聊 MVC、MVP、MVVM设计模式?
MVC: MVC即 Model-VIew-Controller。他是1970年代被引入到软件设计大众的。MVC模式致力于关注点的切分,这意味着 model 和 controller 的逻辑是不与用户界面(View)挂钩的。因此,维护和测试程序变得更加简单容易。 MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller mvc模式 Model:Model代表了描述业务路逻辑,业务模型、数据操作、数据模型的一系列类的集合。这层也定义了数据修改和操作的业务规则。 View: View代表了UI组件,像CSS,JQuery,html等。他只负责展示从 controller 接收到的数据。也就是把model转化成UI。 Controller:Controller 负责处理流入的请求。它通过View来接受用户的输入,之后利用Model来处理用户的数据,最后把结果返回给View。Controll就是View和Model之间的一个协调者。
MVP MVP 模式把应用分成了 3 个主要方面: Model 、View 、 Presenter。 MVP模式图解 Model:Model层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。 View:View代表了UI组件,像CSS,JQuery,html等。他只负责展示从 Presenter 接收到的数据。也就是把模型(译者注:非 Model 层模型)转化成UI。 Presenter:Presenter 负责处理 View 背后所有的UI事件。它通过 View 接收用户输入,之后利用 Model 来处理用户的数据,最后把结果返回给 View 。与 View 和 Controller 不同, View 和 Presenter 之间是完全解耦的,他们通过接口来交互。另外 Presenter 不像 Controller 处理进入的请求。
MVP模式关键点: 用户和 View 交互。 View 和 Presenter 是一对一关系。意味着一个 Presenter 只映射一个 View 。 View 持有 Presenter 的引用(译者注:应该是通过接口交互,并不直接引用Presenter),但是 View 不持有 Model 的引用(译者注:即使接口,也不会)。 在 View 和 Presenter 之间可以双向交互。
MVVM MVVM 即 Model-View-View Model。这个模式提供对 View 和 View Model 的双向数据绑定。这使得 View Model 的状态改变可以自动传递给View 。典型的情况是,View Model 通过使用 obsever 模式(观察者模式)来将 View Model 的变化通知给 Model。 MVVM 模式图解
Model :Model 层代表了描述业务逻辑和数据的一系列类的集合。它也定义了数据修改和操作的业务规则。
View: View 代表了UI组件,像CSS,JQuery,html等。他只负责展示从 ViewModel 接收到的数据。也就是把模型转化成UI。
View Model :View Model 负责暴漏方法,命令,其他属性来操作 VIew 的状态,组装 model 作为 View 动作的结果,并且触发 view 自己的事件。
MVVM模式关键点:
用户和View交互。
View 和 ViewModel 是多对一关系。意味着一个 ViewModel 可以映射多个 View。
View 持有 ViewModel 的引用,但是 ViewModel 没有任何 View 的信息。
View 和 ViewModel 之间有双向数据绑定关系。
5、常见的路由方案,以及优缺点对比
业内常见的路由方案有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去查找对应的服务并执行。
URL 的命名规范
遵循网上的 URI ( web service 模式的资源通用表示方式)的格式。例如 appscheme://path : ctd://home/scan
常见的案例
JLRouters
本质可以理解为保存一个全局的map,key是url,value是对应存放的block数组,url和block都会常驻在内存中,当打开一个url时,JLRoutes就可以遍历这个全局的map,通过url来执行对应的block。
MGJRouter
蘑菇街的技术团队开源的一个router,特点是使用简单方便。JLRoutes的问题主要在于查找url的实现不够高效,通过遍历而不是匹配,还有就是功能偏多。HHRouter的url查找是基于匹配,所以会更高效,MGJRouter也是采用的这种方法,但HHRouter和 ViewController 绑定地过于紧密,一定程度上降低了灵活性。于是就有了 MGJRouter, 从数据结构上看它和 HHRouter 是一样的。
蘑菇街方案不好的地方:
URL注册对于实施组件化是完全没有必要的,拓展性和可维护性都降低;
基于 Open-url 的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度;但是可以通过传递parms来解决,但是这个区分了远程调用和本地调用的接口;
模块内部是否仍然需要使用URL去完成调度?是没有必要的,为啥要复杂化?
当组件多起来的时候,需要提供一个关乎URL和服务的对应表,并且需要开发人员对这样一个表进行维护;
这种方式需要在APP启动时,每个组件需要到路由管理中心注册自己的URL及服务,因此内存中需要保存这样一份表,当组件多起来以后就会出现一些内存的问题;
混淆了本地调用和远程调用,它们的处理逻辑是不同的。正确的做法应该是把远程调用通过一个中间层转化成本地调用,如果把两者混为一谈,后期可能会出现无法区分业务的情况。比如对于组件无法响应的问题,远程调用可能直接显示一个404页面,但是本地调用可能需要做其它处理。如果不加以区分,那么就无法完成这种业务要求。 远程调用只能传递被序列化JSON的数据,像UIImage这样非常规的对象是不行的,所以如果组件接口要考虑远程调用,这里的参数与就不能是这类非常规对象。
routable-ios
HHRouter
优缺点:
优点:
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_Protocol> *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进行注册。
组件方法的调用是分散在各处的,没有统一的入口,也就没法做组件不存在时或者出现错误时的统一处理。
6、如果保证项目的稳定性?
保证项目的稳定性从4个方面来说:
开发过程:
开发规范
代码规范
自测习惯
XMind、PDMan、PostMan、Jenkins、Sonar 等工具使用
Git 、Svn 、禅道、TAPD等使用规范
FPS 监控 : CADisplayLink
CPU 使用率 : Instruments
内存 : Instruments来查看leaks 、代码方面:Delegate、Block、 Block、 NSNotification
启动时间: 优化
耗电要求
代码检查:
CodeReview 习惯
代码检查: OCLint、SwiftLint 、Sonar 等
测试:
单元测试
UI 测试
功能测试
异常测试
线上:
监控(日志系统):Crash监控、网络监控、性能监控、行为监控
修复:JSPatch、RN
7、手动埋点、自动化埋点(无埋点)、可视化埋点
埋点:主要是为了收集数据和信息,用来跟踪应用使用的状况,后续用来进一步优化产品或是提供运营的数据支撑,包括访问数(Visits),访客数(Visitor),停留时长(Time On Site),页面浏览数(Page Views)和跳出率(Bounce Rate)等。 以大致分为两种:页面统计(track this virtual page view)、 统计操作行为(track this button by an event)。
手动埋点(代码埋点):
国内的主要第三方数据分析服务商,如百度统计、友盟、TalkingData、GrowingIO 等。
优点:
使用者控制精准,可以非常精确地选择什么时候发送数据
使用者可以比较方便地设置自定义属性、自定义事件,传递比较丰富的数据到服务端
缺点:
埋点代价比较大,每一个控件的埋点都需要添加相应的代码,不仅工作量大,而且限定了必须是技术人员才能完成
更新的代价比较大,每一次更新埋点方案,都必须改代码,然后通过各个应用市场进行分发,并且总会有相当多数量的用户不喜欢更新APP,这样埋点代码也就得不到更新了
所有前端埋点方案都会面临的数据传输时效性和可靠性的问题了,这个问题就只能通过在后端收集数据来解决了
自动化埋点(无埋点):
无埋点是指开发人员集成采集 SDK 后,SDK 便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码;或者是说用户展现界面元素时,通过控件绑定触发事件,事件被触发的时候系统会有相应的接口让开发者处理这些行为。现在市面上主流无埋点做法有两种:一种是预先跟踪所有的渲染信息,一种是滞后跟踪的渲染信息。
数据分析师/数据产品通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。
优点:
由于采集的是全量数据,所以产品迭代过程中是不需要关注埋点逻辑的,也不会出现漏埋、误埋等现象
无埋点方式因为收集的是全量数据,可以大大减少运营和产品的试错成本,试错的可能性高了,可以带来更多启发性的信息
无需埋点,方便快捷
减少了因为人员流动带来的沟通成本
无需开发,业务人员埋点即可
支持先上报数据,后进行埋点
缺点:
缺点与可视化埋点相同,未解决个性化自定义获取数据的问题,缺乏数据获取的灵活性
企业针对SDK开发难度较大,一般由数据分析企业研发提供,使用第三方提供的埋点方案,有如下缺陷:
a、数据源丢失,应用上报的数据上传至第三方服务端,可能造成
企业泄密或用户的关键数据丢失;
b、供应商数据丢包问题,无法根据应用特性进行改善
无埋点采集全量数据,给数据传输和服务器增加压力
仅仅支持客户端
可视化埋点:
可视化埋点是指开发人员除集成采集 SDK 外,不需要额外去写埋点代码,而是由业务人员通过访问分析平台的 圈选 功能来圈出需要对用户行为进行捕捉的控件,并给出事件命名。圈选完毕后,这些配置会同步到各个用户的终端上,由采集 SDK 按照圈选的配置自动进行用户行为数据的采集和发送。
优点:
可视化埋点很好地解决了代码埋点的埋点代价大和更新代价大两个问题。但是,可视化埋点能够覆盖的功能有限,目前并不是所有的控件操作都可以通过这种方案进行定制
埋点只需业务同学接入,无需开发支持
缺点:
无法做到自定义获取数据,可视化埋点覆盖的功能有限
企业针对SDK开发难度相比代码埋点大
仅支持客户端行为
8、设计一个图片缓存框架(LRU) 9、如何设计一个 git diff ? 10、设计一个线程池?画出你的架构图 11、你的app架构是什么?有什么优缺点?为什么这么做?怎么改进?
MVC 架构。
优点:
耦合性低
视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动MVC的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。
重用性高
MVC模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。
部署快,生命周期成本低
MVC使开发和维护用户接口的技术含量降低。使用MVC模式使开发时间得到相当大的缩减,它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上。
可维护性高
分离视图层和业务逻辑层也使得WEB应用更易于维护和修改。
缺点:
完全理解MVC比较复杂
由于MVC模式提出的时间不长,加上同学们的实践经验不足,所以完全理解并掌握MVC不是一个很容易的过程。
调试困难
因为模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难,每个构件在使用之前都需要经过彻底的测试。
不适合小型,中等规模的应用程序
在一个中小型的应用程序中,强制性的使用MVC进行开发,往往会花费大量时间,并且不能体现MVC的优势,同时会使开发变得繁琐。
增加系统结构和实现的复杂性
对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。
视图与控制器间的过于紧密的连接并且降低了视图对模型数据的访问
视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。
依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。
MVC 是苹果官方推荐的项目架构,相对于 MVP 、 MVVM 架构来说入门相对的低一些;而且公司的项目不是很大,在综合人力成本等方面选择了 MVC 架构。
针对 Controller 臃肿问题作出优化,将数据相关进行抽离管理,向 MVVM 模式靠拢。
12、看过哪些第三方框架的源码,它们是怎么设计的?
SDWebImage
SDWebImage 组织架构:
SDWebImage 组织架构
SDWebImageDownloader :负责维持图片的下载队列;
SDWebImageDownloaderOperation:负责真正的图片下载请求;
SDImageCache:负责图片的缓存;
SDWebImageManager:是总的管理类,维护了一个SDWebImageDownloader 实例和一个 SDImageCache 实例,是下载与缓存的桥梁;
SDWebImageDecoder:负责图片的解压缩;
SDWebImagePrefetcher:负责图片的预取;
UIImageView+WebCache:和其他的扩展都是与用户直接打交道的。
SDWebImage 图片加载流程:
SDWebImage 原理图
判断图片URL 是否为 nil,是则做出错处理并返回;
URL MD5加密生成 key;
根据 key 读取内存(memory)缓存, 有则拿到图片返回,否则往下;
根据 key 读取磁盘(disk)缓存,有则拿到图片返回,否则往下;
根据URL 下载图片,下载成功则将图片保存到 内存和磁盘中返回图片
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 。
13、可以说几个重构的技巧么?你觉得重构适合什么时候来做?
重构技巧:
重复代码的抽象提炼
冗长方法的分隔
嵌套条件分支的优化
去掉一次性的零时变量
消除过长参数列表
提取类或继承体系中的常量
让类提供应该提供的方法
拆分冗长的类
提取继承体系中重复的属性与方法到父类
适合节点:
【增】在增加新功能的时候(增加新功能的时候,发现需要重构来便于新功能的添加)
【删】在扩展不再简单的时候(消除重复)
【改】修复缺陷(修复 Bug 的时候)
【查】代码审查(通过交流提出了很多修改的主意)
重构是一个不断的过程。
14、开发中常用架构设计模式你怎么选型?
首先我们从App 架构来说:
针对项目的大小程度,功能复杂程度,模块的多少,项目成本和时间等来选用 MVC 或者 MVVM 模式进行总的架构设计。
其次项目中:
策略模式 :针对实现目标/功能的复杂度,判断情况选用 策略模式 。
观察者模式 和 代理模式 针对实时情况而定。
工厂模式 和 抽象工厂模式 :根据过程父子关系复杂程度和子类种类数量多少程度,判断是否使用 工厂模式 和 抽象工厂模式 。
适配器模式 : 高度自定义问题,前端/移动端 根据数据格式做适配。(比如说 电商SKU
模式,列表 Cell 适配等)
单例模式 :根据模块在项目的 唯一性 ,重要性 等作出判断。(比如:应用的配置信息,用户的个人信息,本地数据库进行操作,数据上传云端,通信管理类等)
15、你是如何组件化解耦的?
首先得分层:
常见的结构有3层,4层的。我一般用3层:展现层、业务层、数据层
根据功能:
基础功能组件:
基础模块是任何一个App都需要用到的。如:性能统计、Networking、Patch、网络诊断、数据存储模块。对于基础模块来说,其本身应该是自洽的,即可以单独编译或者几个模块合在一起可以单独编译。所有的依赖关系都应该是业务模块指向基础模块的。
基础模块之间尽量避免产生横向依赖。
业务组件:
根据不同的业务拆分。如:支付业务组件、播放组件、商城组件、消息组件 等。
组件方案采用 Runtime 实现的 target-action 方式(CTMediator)
————————————————