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对象、类、元类之间的关系
从图中可知:对象的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,这里我给大家画个图梳理一下:
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)
比较+交换 的原子操作。比较 &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
设置圆角。即:
1.2、使用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];
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 阶段优化:
如图所示:
pre-main 阶段主要由4部分组成: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%)
- 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
里扔,可以新建一个类负责启动事件,新加的业务可以往这边添加。
- 目前 App 的
- 首页渲染优化 :
- 减少启动期间创建的 UIViewController 数量
通过打符号断点-[UIViewController viewDidLoad] 发现,如果App 启动过程中创建了 12 个 UIViewController(包括闪屏),即在启动过程中创建了 12 个视图控制器,导致首页渲染时间较长 - 延迟首页耗时操作
如果 App 首页有个侧滑页面及侧滑手势,并且该页面是用 xib 构建的,将该 ViewController 改为代码构建,同时延迟该页面的创建时机,等首页显示后再创建该页面及侧滑手势,这个改动节省了 300-400ms。 - 去除启动时没必要及不合理的操作
项目中使用了自定义的侧滑返回,在每次 push 的时候都会截图,启动的时候自定义导航栏会截取两张多余首页的图片,并且截图用的 API (renderInContext) 性能较差,耗时 800ms 左右,去掉启动截图的操作。
闪屏请求回调里写plist文件的操作放在主线程,导致启动时占用主线程,将文件读写移到子线程操作。
- 减少启动期间创建的 UIViewController 数量
- didFinishLaunchingWithOptions 优化
- pre-main 阶段优化:
架构设计
1、设计模式是为了解决什么问题的?
编写软件过程中,程序员面临着来自耦合性、内聚性以及可维护性、可扩展性、重用性、灵活性等多方面的挑战,设计模式是为了让程序具有更好的:
- 代码重用性(相同功能代码,不用多次编写)
- 可读性(编程规范性)
- 可扩展性(增加新功能时十分方便)
- 可靠性(增加新功能后,对原来的功能没有影响)
- 实现高内聚,低耦合的特性
设计模式有 7 大原则:
- 单一职责原则
一个类只负责一个职责,一个函数只解决一个问题 - 接口隔离原则
大接口改多个小接口,原因外部不需要大接口这么多方法,更易控制 - 依赖反转原则
即面向接口编程,尽量不要声明具体类,而是使用接口,实现解耦 - 里氏替换原则
能出现父类的地方就一定可以用子类代替,即不要重写父类种的已实现的方法 - 开闭原则
面向扩展开放,面向修改封闭。即不要修改一个已实现的类,更不要修改类中的方法,应该选择创建新类或者创建新方法的方式解决 - 迪米特法则
又叫最少知道原则,即对外暴露的public方法尽量少,实现高内聚;且只和直接朋友通信 - 合成复用原则
即不要重复自己,不要在项目内copy代码,应该选择将要copy的代码抽离出来,实现多个类复用
2、常见的设计模式有哪些?
-
单例模式
意图
:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决
:一个全局使用的类频繁地创建与销毁。 -
工厂模式
简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你的需求即可。 -
抽象工厂模式
抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让子类去做决定。
比如:以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。 -
代理模式
代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
优点:- 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;
- 可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务。
缺点:
- 由于使用了代理模式,因此程序的性能没有直接调用性能高;
- 使用代理模式提高了代码的复杂度。
举一个生活中的例子:比如买飞机票,由于离飞机场太远,直接去飞机场买票不太现实,这个时候我们就可以上携程 App 上购买飞机票,这个时候携程 App 就相当于是飞机票的代理商。
-
观察者模式
观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
优点:- 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;
- 观察者模式在观察目标和观察者之间建立一个抽象的耦合;
- 观察者模式支持广播通信;
- 观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。
缺点:
- 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;
- 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
-
策略模式
策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。
优点:遵循了开闭原则,扩展性良好。
缺点:随着策略的增加,对外暴露越来越多。
3、谈谈单例的优缺点?
单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实例存在,整个系统只能使用一个对象实例。
优点:
1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
2. 避免对资源的多重占用
缺点:
1. 没有接口,不能继承,与单一职责原则冲突
2. 一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
4、聊聊 MVC、MVP、MVVM设计模式?
-
MVC:
MVC即 Model-VIew-Controller。他是1970年代被引入到软件设计大众的。MVC模式致力于关注点的切分,这意味着 model 和 controller 的逻辑是不与用户界面(View)挂钩的。因此,维护和测试程序变得更加简单容易。
MVC设计模式将应用程序分离为3个主要的方面:Model,View和Controller
- 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。
- 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。
-
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
- JLRouters
-
优缺点:
优点:Url-Scheme
是借鉴前端Router
和系统App 内跳转方法
得出来的解决方案。所以不管是H5、RN、Android、iOS 都通用。- 服务器可以动态的控制页面的跳转,可以统一页面出问题后错误处理,三端统一。
缺点:
URL
的map
规则是需要注册的,它们会在load
方法里面写。写在load
方法里面是会影响App启动速度的。- 大量的硬编码。
URL
链接里面关于组件
和页面的名字
都是硬编码,参数
也都是硬编码。而且每个URL
参数字段都必须要一个文档进行维护,这个对于业务开发人员也是一个负担。而且URL短连接散落在整个App四处,维护起来实在有点麻烦。 - 对于传递
NSObject
的参数,URL
是不够友好的,它最多是传递一个字典。
- 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比较复杂
-
MVC
是苹果官方推荐的项目架构,相对于MVP
、MVVM
架构来说入门相对的低一些;而且公司的项目不是很大,在综合人力成本等方面选择了MVC
架构。 -
针对
Controller
臃肿问题作出优化,将数据相关进行抽离管理,向MVVM
模式靠拢。
12、看过哪些第三方框架的源码,它们是怎么设计的?
-
SDWebImage
SDWebImage
组织架构:
SDWebImageDownloader
:负责维持图片的下载队列;
SDWebImageDownloaderOperation
:负责真正的图片下载请求;
SDImageCache
:负责图片的缓存;
SDWebImageManager
:是总的管理类,维护了一个SDWebImageDownloader
实例和一个SDImageCache
实例,是下载与缓存的桥梁;
SDWebImageDecoder
:负责图片的解压缩;
SDWebImagePrefetcher
:负责图片的预取;
UIImageView+WebCache
:和其他的扩展都是与用户直接打交道的。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
: 对iOSUIKit
的扩展
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
)
数据结构
1、数据结构的存储一般常用的有几种?各有什么特点?
顺序存储结构
:
数据元素顺序存放
,每个结点只有一个元素。存储位置反映数据元素间的逻辑关系。存储密度大
,但是插入、删除操作效率较差。(比如:数组
:1-2-3-4-5-6-7-8-9-10,存储是按顺序的。再比如栈
和队列
等)。
链式存储结构
:
每个结点除了包含数据元素信息外还包含一组指针
,指针
反映数据元素间的逻辑关系。- 这种存储方式不要求存储空间连续,便于进行插入和删除操作,但是存储空间利用率较低。
- 另外,由于逻辑上相邻的数据元素在存储空间上不一定相邻,所以不能对其进行随机存取。
哈希(散列)存储结构
:
通过哈希函数
解决冲突的方法,将关键字散列
在连续的
有限的
地址空间内,并将哈希函数的值作为该数据元素的存储地址。- 其特点是存取速度快,只能按关键字随机存取,不能顺序窜出,也不能折半存取。
索引存储结构
:
索引存储除了数据元素存储在一地址连续的内存空间
外,尚需建立一个索引表
。索引表中的索引指示结点的存储位置,兼有动态和静态的特性。
2、集合结构 线性结构 树形结构 图形结构
- 集合结构:就是一个集合,就是一个圆圈中有很多个元素,元素与元素之间没有任何关系 。
- 线性结构 :就是一个条线上站着很多个人。 这条线不一定是直的。也可以是弯的。也可以是直的,相当于一条线被分成了好几段的样子。 线性结构是一对一的关系。
- 树形结构 :做开发的肯定或多或少的知道xml 解析 。树形结构跟他非常类似。也可以想象成一个金字塔。树形结构是一对多的关系。
- 图形结构:这个就比较复杂了。 无穷、无边、 无向(没有方向)图形机构。你可以理解为多对多,类似于我们人的交集关系。
3、链表、单向链表、双向链表、循环链表
-
链表:
是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针
链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:- 一个是存储数据元素的数据域。
- 另一个是存储下一个结点地址的指针域。
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到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,绕成一个圈。就像蛇吃自己的这就是循环。
4、数组和链表区别
-
数组是可以在内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。
优点:- 按照索引查询元素速度快
- 按照索引遍历数组方便
缺点:
- 数组的大小固定后就无法扩容了
- 数组只能存储一种类型的数据
- 添加,删除的操作慢,因为要移动其他的元素。
适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况。 -
链表:
是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针
链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:- 一个是存储数据元素的数据域。
- 另一个是存储下一个结点地址的指针域。
相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
5、堆、栈和队列
-
堆:
- 堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即
动态分配内存
,对其访问和对一般内存的访问没有区别。堆是指程序运行时申请的动态内存,而栈只是指一种使用堆的方法(即先进后出)。 - 是一种比较特殊的数据结构,可以被看做一棵树的数组对象,具有以下的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
- 堆分为两种情况,有
最大堆
和最小堆
。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。 - 堆常用来实现
优先队列
,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
- 堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即
-
栈:
- 栈是一种具有
先进后出
的数据结构,又称为先进后出的线性表
,简称FILO
(—First-In/Last-Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。 - 栈是限定仅在
表尾进行插入和删除操作
的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。 - 堆栈中定义了一些操作。两个最重要的是
PUSH
和POP
。PUSH操作
在堆栈的顶部加入一个元素。POP操作
相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。 - 系统会给栈
自动分配内存空间
。 - 常用:
- 递归(如:逆序输出)
- 语法检查,符号成对出现
- 数制转换
- 二叉树的一些操作
- 栈是一种具有
-
队列:
- 队列是一种
先进先出
(FIFO
—first in first out)的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。 - 队列是
只允许在一端进行插入操作、而在另一端进行删除操作
的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,特殊之处
在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作(队头做删除,队尾做插入)。和栈一样,队列是一种操作受限制的线性表。
- 队列是一种
6、二叉树相关操作
7、输入一棵二叉树的根结点,求该树的深度?
- 如果一棵树只有一个结点,它的深度为1。
- 如果根结点只有左子树而没有右子树, 那么树的深度应该是其左子树的深度加1。
- 同样如果根结点只有右子树而没有左子树,那么树的深度应该是其右子树的深度加1。
- 如果既有右子树又有左子树, 那该树的深度就是其左、右子树深度的较大值再加1。
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); }
8、输入一课二叉树的根结点,判断该树是不是平衡二叉树?
- 重复遍历结点
- 先求出根结点的左右子树的深度;
- 然后判断它们的深度相差不超过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);
}
算法
1、时间复杂度
在计算机科学中,时间复杂性,又称时间复杂度。
算法的时间复杂度是一个函数
,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述
,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的
,亦即考察输入值大小趋近无穷时的情况。
2、空间复杂度
空间复杂度(Space Complexity)是对一个算法在运行过程中
临时占用存储空间大小的量度
,记做S(n)=O(f(n))。比如:
直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量:时间复杂度
& 空间复杂度
。
3、常用的排序算法
-
冒泡排序:
- 原理:就是重复地走访过要排序的元素列,依次比较两个相邻的元素,顺序不对就交换,直至没有相邻元素需要交换,也就是排序完成。
- 这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
- 冒泡排序是一种
稳定排序
算法。 - 时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是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; } } } }
-
选择排序
- 选择排序(Selection sort)是一种简单直观的排序算法。原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
- 选择排序是
不稳定
的排序方法 。 - 时间复杂度:最好和平均情况下都是O(n²)
/** * 【选择排序】:最值出现在起始端 * * 第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; } } } }
-
直接插入排序
- 插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,
- 插入排序的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止
- 直接插入排序是
稳定的排序
算法。 - 时间复杂度:最好情况(初始情况就是正序)下是o(n),平均情况是o(n²)
/** * * 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)计算 0 ~ i-1 的中间点,用 i 索引处的元素与中间值进行比较,如果 i 索引处的元素大,说明要插入的这个元素应该在中间值和刚加入i索引之间,反之,就是在刚开始的位置到中间值的位置,这样很简单的完成了折半**;
(2)在相应的半个范围里面找插入的位置时,不断的用(1)步骤缩小范围,不停的折半,范围依次缩小为 1/2 1/4 1/8 …快速的确定出第 i 个元素要插在什么地方;
(3)确定位置之后,将整个序列后移,并将元素插入到相应位置。 - 二分插入排序是
稳定
的排序算法。 - 时间复杂度:最好情况(刚好插入位置为二分位置)下是O(log₂n),平均情况和最坏情况是o(n²)
/** * 折半查找:优化查找时间(不用遍历全部数据) * * 折半查找的原理: * 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; }
-
希尔排序
- 希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,排序完成。
- 希尔排序是
不稳定
排序算法。 - 时间复杂度:O(n^(1.3—2))
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); } }
-
快速排序
- 快速排序(Quicksort)是对冒泡排序的一种改进。
- 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 快速排序是
不稳定
的排序算法。 - 时间复杂度:最差为O(n^2),平均为O(nlogn),最好为O(nlogn)。
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); }
-
堆排序
- 是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点
- 在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
- 最大堆调整(Max Heapify): 将堆的末端子节点作调整,使得子节点永远小于父节点
- 创建最大堆(Build Max Heap): 将堆中的所有数据重新排序
- 堆排序(HeapSort): 移除位在第一个数据的根节点,并做最大堆调整的递归运算
- 堆排序是一个非稳定的排序算法。
- 时间复杂度:O(nlogn)
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); } }
4、字符串反转
- (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;
}
5、链表反转(头差法)
头插法
:
将链表每个节点依次取下来头插到新链表,即为原链表的反转;因为改变了当前节点的 next 指向,必须先保存 next 地址。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; }
迭代法
:
在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点。不要忘记在最后返回新的头引用。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; }
6、有序数组合并
- (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]<B[0]) {
[C addObject:A[0]];
[A removeObject:A[0]];
}
else if (B[0]<A[0]) {
[C addObject:B[0]];
[B removeObject:B[0]];
}
if (A.count==0) {
[C addObjectsFromArray:B];
NSLog(@"C = %@",C);
index = i+1;
NSLog(@"index = %d",index);
return;
}
else if (B.count==0) {
[C addObjectsFromArray:A];
NSLog(@"C = %@",C);
index = i+1;
NSLog(@"index = %d",index);
return;
}
}
//(2).
//时间复杂度
//T(n) = O(f(n)):用"T(n)"表示,"O"为数学符号,f(n)为同数量级,一般是算法中频度最大的语句频度。
//时间复杂度:T(n) = O(index);
}
7、查找第一个只出现一次的字符(Hash查找)
两个思路:
- hash 不同编译器对字符数据的处理不一样,所以hash之前先把字符类型转成无符号类型;
- 空间换时间,用buffer数组记录当前只找到一次的字符,避免二次遍历。
# 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<q; p++)
{
if(count[(unsigned char)*p] == 1)
return *p;
}
return 0;
}
8、查找两个子视图的共同父视图
这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题。
假设两个视图为UIViewA、UIViewC,其中 UIViewA继承于UIViewB,UIViewB继承于UIViewD,UIViewC也继承于UIViewD;即 A->B->D,C->D
- 方法1:
- (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; }
- 方法2:
方法一明显的是两层for循环,时间复杂度为 O(N^2) 一个改进的办法:我们将一个路径中的所有点先放进NSSet中.因为NSSet的内部实现是一个hash表,所以查询元素的时间的复杂度变成 O(1),我们一共有N个节点,所以总时间复杂度优化到了O(N)- (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<arr1.count; ++i) { Class targetClass = arr1[i]; if ([set containsObject:targetClass]) { return targetClass; } } return nil; }
9、无序数组中的中位数(快排思想)
//求一个无序数组的中位数
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;
}
10、给定一个整数数组和一个目标值,找出数组中和为目标值的两个数
假设每个输入只对应一种答案,且同样的元素不能被重复利用。 示例:给定nums = [2, 7, 11, 15], target = 9 — 返回 [0, 1] 思路:
- 第一层for循环从索引0到倒数第二个索引拿到每个数组元素,
- 第二个for循环遍历上一层for循环拿到的元素的后面的所有元素。
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;
}
}
网络
1、谈谈对 HTTP、HTTPS 的理解
-
HTTP协议
:超文本传输协议,他是基于TCP应用层协议- 是无连接 无状态 的,需要通过cookies 或者 session 来保持会话
- HTTP 分为
两
部分:请求报文和响应报文- 请求报文
四
个部分组成:请求行
、请求头
、空行
、请求体
- 请求报文
四
个部分组成:状态行
、响应头
、空行
、响应体
- 请求报文
客户端请求: 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.
-
URL 构成:
-
协议构成:
请求行、请求头、请求体
-
常用的请求方式?
答: GET、POST、PUT、DELETE、HEAD、OPTIONS?
-
GET 和 POST 的区别?
- GET 把参数通过 ? 和 & 拼接在URL 后面,POST 放在 body 里面
- GET 有长度限制(一般2048字符),POST没有限制
- 由于参数的存放,POST 相对于 GET 安全,相对安全是因为 POST 仍可以被抓包
- GET 是可以被缓存的,POST 不可被缓存(后台使用Redis、Memcached 登记室除外)
-
HTTPS 协议
:
HTTPS是一种通过计算机网络进行安全通信的传输协议(以安全为目标),经由HTTP进行通信,利用SSL/TLS建立全信道,加密数据包。HTTPS使用的主要目的
是提供对网站服务器的身份认证,同时保护交换数据的隐私与完整性(传输加密
和身份认证
保证了传输过程的安全性)。
PS:
TLS
是传输层加密协议,前身是SSL协议
。HTTPS 的安全基础是SSL
。
通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:- HTTPS特点:
基于HTTP协议,通过SSL或TLS提供加密处理数据、验证对方身份以及数据完整性保护
通过抓包可以看到数据不是明文传输,而且HTTPS有如下特点:
- 内容加密:采用混合加密技术,中间者无法直接查看明文内容
- 验证身份:通过证书认证客户端访问的是自己的服务器
- 保护数据完整性:防止传输的内容被中间人冒充或者篡改
- 混合加密:结合非对称加密和对称加密技术。客户端使用对称加密生成密钥对传输数据进行加密,然后使用非对称加密的公钥再对秘钥进行加密,所以网络上传输的数据是被秘钥加密的密文和用公钥加密后的秘密秘钥,因此即使被黑客截取,由于没有私钥,无法获取到加密明文的秘钥,便无法获取到明文数据。
- 数字摘要:通过单向hash函数对原文进行哈希,将需加密的明文“摘要”成一串固定长度(如128bit)的密文,不同的明文摘要成的密文其结果总是不相同,同样的明文其摘要必定一致,并且即使知道了摘要也不能反推出明文。
- 数字签名技术:数字签名建立在公钥加密体制基础上,是公钥加密技术的另一类应用。它把公钥加密技术和数字摘要结合起来,形成了实用的数字签名技术。
- 收方能够证实发送方的真实身份;
- 发送方事后不能否认所发送过的报文;
- 收方或非法者不能伪造、篡改报文。
非对称加密过程需要用到公钥进行加密,那么公钥从何而来?其实公钥就被包含在数字证书中,数字证书通常来说是由受信任的数字证书颁发机构CA,在验证服务器身份后颁发,证书中包含了一个密钥对(公钥和私钥)和所有者识别信息。数字证书被放到服务端,具有服务器身份验证和数据传输加密功能。
-
HTTPS 的验证流程?
归纳为5
个步骤:- 客户端发起一个http请求,告诉服务器自己
支持哪些hash算法
。 - 服务端把自己的信息以数字证书的形式返回给客户端(证书内容有
密钥公钥
,网站地址
,证书颁发机构
,失效日期
等)。证书中有一个公钥来加密信息,私钥由服务器持有。 - 验证证书的合法性:
客户端收到服务器的响应后会先验证证书的合法性
(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。 - 生成随机密码(RSA签名):
如果验证通过,或用户接受了不受信任的证书,客户端就会生成一个随机的对称密钥
(session key)并用公钥加密
,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。 - 生成对称加密算法:
验证完服务端身份后,客户端生成一个对称加密的算法和对应密钥
,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。
- 客户端发起一个http请求,告诉服务器自己
-
数字证书都有哪些内容?
Issuer
– 证书的发布机构
发布证书的机构,指明证书是哪个公司创建的(并不是指使用证书的公司)。出了问题具体的颁发机构是要负责的。Valid from,Valid to
– 证书的有效期
证书的使用期限。过了这个期限证书就会作废,不能使用。Public key
– 公钥
通常是一个字符串或数字
,进行加密/解密算法时使用。公钥和私钥都是密钥,只不过一般公钥是对外开放
的,加密时使用;私钥是不公开
的,解密时使用。Subject
– 主题
证书是颁发给谁了,一般是个人或公司名称或机构名称或公司网站的网址。Signature algorithm-
– 签名所使用的算法
数字证书的数字签名所使用的加密算法,根据这个算法可以对指纹解密
。指纹加密的结果就是数字签名。Thumbprint,Thumbprint algorithm
– 指纹以及指纹算法(一种HASH算法)
指纹和指纹算法会使用证书机构的私钥加密后
和证书放在一起。
主要
用来保证证书的完整性,确保证书没有修改过。
使用者在打开证书时根据指纹算法计算证书的hash值,和刚开始的值一样,则表示没有被修改过
。
-
客户端如何检测数字证书是
合法的
并是所要请求的公司的?- 首先应用程序读取证书中的
Issuer
(发布机构),然后会在操作系统或浏览器内置的
受信任的
发布机构中去找该机构的证书(为什么操作系统会有受信任机构的证书?先看完这个流程再来回答)。 - 如果找不到就说明证书是水货,证书有问题,程序给错误信息。
- 如果找到了,或用户确认使用该证书。就会拿上级证书的
公钥
,解密本级证书,得到数字指纹。然后对本级证书的公钥进行数字摘要算法(证书中提供的指纹加密算法)计算结果,与解密得到的指纹对比。如果一样,说明证书没有被修改过。公钥可以放心使用,可以开始握手通信了。
- 首先应用程序读取证书中的
-
操作系统为什么会有证书发布机构的证书?
- 证书发布机构除了给别人发布证书外,自己也有自己的证书。
- 在操作系统安装好时,受信任的证书发布机构的数字证书就已经被安装在操作系统中了,根据一些权威安全机构的评估,选取一些信誉很好并且通过一定安全认证的证书发布机构,把这些证书默认安装在操作系统中并设为信任的数字证书。
- 发布机构持有与自己数字证书对应的私钥,会用这个私钥加密所有他发布的证书及指纹整体作为数字签名。
- HTTPS特点:
2、TCP、UDP 和 Socket
-
TCP:(Transmission Control Protocol )传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议
-
三次握手:
- 客户端发送 SYN(SEQ=x)报文给服务器端,进入 SYN_SEND 状态。
- 服务器端收到 SYN 报文,回应一个 SYN (SEQ=y)ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
- 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1) 报文,进入 Established 状态。
-
TCP 为什么要三次握手?而不是两次或者四次呢?
- 为了
实现可靠数据传输
, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。 - 如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。
- 如果是四次或者其他,多于三次则是累赘,因为三次已经可以确保
双方序列号都已被对方确认
。
- 为了
-
四次挥手:
- 某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
- 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认。
- 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
- 接收这个最终FIN的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。
既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要4个分节。
-
-
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 需要注意:
- 心跳的保持
- ping 和 pong 的呼应
- 离开页面要断开,进入页面再重新连接
Object-C(简称 OC) 语言特性
1、多态
多态表现为了三个方面:动态类型、动态绑定、动态加载。之所以叫做多态,是因为必须到运行时(run time)才会做一些事情。
-
动态类型:
编译器编译的时候是不能被识别的(如 id 类型),要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。
-
动态绑定 :
动态绑定(dynamic binding)貌似比较难记忆,但事实上很简单,只需记住关键词
@selector
/SEL
即可。而在OC中,其实是没有函数的概念的,我们叫
消息机制
,所谓的函数调用就是给对象发送一条消息。这时,动态绑定的特性就来了。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法,最终形成的这个SEL变量就代表一个方法的引用。动态绑定的特定不仅方便,而且效率更高。 -
动态加载 :
让程序在运行时添加代码模块以及其他资源。用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有和程序运行时整合的新类。
2、继承
OC 不支持多继承,但是可以用 代理(Delegate) 来实现多继承。runtime
消息转发等实现伪多继承
4、代理(Delegate)
代理是一种设计模式,以 @protocol
形式体现,一般是一对一传递。
- 代理为什么用
weak
修饰呢?block和代理的区别?- 一般以weak关键词以规避循环引用。
用weak
修饰指明该对象并不负责保持delegate这个对象,delegate 这个对象的销毁由外部控制。用strong
修饰该对象强引用 delegate,外界不能销毁 delegate对象,会导致循环引用。 - block 和代理的区别:
- 运行成本:代理运行成本低,block 运行成本高。
因为block出栈需要将使用的数据从栈内存拷贝到堆内存,如果本身就在堆内存的话计数器会+1,使用完或block置为nil后才消除;
delegate 只保留了一个指针对象,直接回调,没有额外的消耗。 - 写法更简练,更紧凑。
- block 注重结果的传输。
- block 要防止循环引用,善用
__weak
和__strong
。 - 公共接口,当方法较多后者调用太频繁建议永 delegate。
- 运行成本:代理运行成本低,block 运行成本高。
- 一般以weak关键词以规避循环引用。
5、通知(NSNotificationCenter)
使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。
-
如果实现通知机制?
6、KVC (Key-value Coding)
键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法,直接或通过实例变量访问的机制。非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。
-
KVC 底层实现原理:
当一个对象调用setValue:forKey:
方法时,方法内部会做以下操作:- 判断有没有指定
key
的set方法
,如果有set
方法,就会调用set
方法,给该属性赋值 - 如果没有
set
方法,判断有没有跟key
值相同且带有下划线的成员属性(_key)
如果有,直接给该成员属性进行赋值 - 如果没有成员属性
_key
,判断有没有跟key
相同名称的属性。如果有,直接给该属性进行赋值 - 如果都没有,就会调用
valueforUndefinedKey
和setValue:forUndefinedKey:
方法
- 判断有没有指定
-
KVC 使用场景:
- KVC 属性赋值
- 添加私有成员变量
- 字典和模型之间的互转
7、属性
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:分为深拷贝和浅拷贝
- 浅拷贝:对内存地址的复制,让目标对象指针和原对象指向
同一片内存空间
会增加引用计数。 - 深拷贝:对对象内容的复制,开辟新的内存空间。
- 可变对象的copy和mutableCopy都是深拷贝
不可变对象的copy是浅拷贝,mutableCopy是深拷贝
copy方法返回的都是不可变对象
- 浅拷贝:对内存地址的复制,让目标对象指针和原对象指向
-
8、@property 的本质是什么?ivar 、 setter 、getter 是如何生成并添加到这个类中的?
@property
的本质:
@property
=ivar
+setter
+getter
即:@property
等于声明了ivar(数形变量),并实现了该属性的存取方法(setter + getter)。@property
作为 OC 的一项特性,主要就在于封装对象中的数据。
OC 通常把其所需要的各种数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。
9、@synthesize 、@dynamic 的区别
@synthesize :
系统会自动生成该属性的setter
和getter
方法。@dynamic :
系统不会自动生成该属性的setter
和getter
方法,需要用户自己去实现
10、UIView 和 CALayer 的关系?
- UIView :
- UIView 是 iOS 系统中的界面元素的基础,所有的界面元素都继承自它;
- 它本身完全是由
CoreAnimation
来实现的; - 它真正绘图部分,是由
CALayer(CoreAnimation Layer)
类来管理的; - UIView 本身更像一个
CALayer
的管理器,访问它的根绘图和根坐标有关的属性(如:frame、bounds 等),实际上内部都是在访问他所包含的CALayer
的相关属性; - UIView 的属性
layer
,对应的是他的CALayer
实例。 - UIView 可以响应时间,Layer 不可以,因为 UIView 继承自 UIResponder。
- CALayer :
CALayer
类似于 UIView 的子 View 树形结构,也可以向它的layer
上添加子layer
,来完成某些特殊的表示;- UIView 的 layer 树形在系统内部分别是:
- 逻辑树,这里的代码是可操控的;
- 动画树,是一个中间层,系统就在这一层上更改属性,进行各种渲染操作;
- 显示树,其内容就是当前正在被显示在屏幕上的内容。
- 动画的运作:对 UIView 的
subLayer
(非主 Layer)属性进行更改,系统将会自动进行动画生成。 - 坐标系统:
CALayer
的坐标系统比 UIView 多了一个anchorPoint
属性,使用CGPoint
结构标识,值域是0 ~ 1
,是个比例值。 - 渲染:当更新层,改变不能立即显示在屏幕上。当所有的层都准备好时,可以调用
setNeedsDisPlay
方法来重绘显示。 - 变换:要在一个层中添加一个
3D
或仿射变换,可以分别设置层的transform
或affineTransform
属性。 - 变形:
Quartz Core
的渲染能力,使二维图像可以被自由操纵,就好像是三维的。图像可以在一个三维坐标系中以任意角度被旋转、缩放和倾斜。CATransform3D
的一套方法提供了一些魔术般的变换效果。
11、ViewController 不走 dealloc 的情况
- controller 使用了 NSTimer,并未对它进行销毁。
- block 块内使用了 self,造成了循环引用。
- 使用了 delegate,用了 strong 修饰,造成了强持有。记得用 weak/assign 修饰代理。
- controller 中使用了 WKWebView,
- (void)addScriptMessageHandler:(id)scriptMessageHandlername:(NSString*)name
第一个参数使用self,造成了强持有。解决办法。
12、UICollectionView 自定义 layout 如何实现?
- 重写
prepareLayout
方法,并在里面事先就计算好必要的布局信息并存储起来。 - 基于
prepareLayout
方法中的布局信息,重写collectionViewContentSize
方法返回 UICollectionView的内容尺寸。 - 重写
layoutAttributesForElementsInRect:
方法返回指定区域 cell、Supplementary View 和 Decoration View 的布局属性。 - 重写
layoutAttributesForItemAtIndexPath:
;方法返回对应的 indexPath 的位置的 cell 的布局属性。 - 重写
layoutAttributesForSupplementaryViewOfKind: atIndexPath:
,方法返回对应indexPath的位置的追加视图的布局属性,如果没有就不用重载。 - 重写
layoutAttributesForDecorationViewOfKind: atIndexPath:
,方法返回对应indexPath的位置的装饰视图的布局属性,如果没有也不需要重载。 - 重写
shouldInvalidateLayoutForBoundsChange:
,当边界发生变化时,是否应该刷新。
13、AppDelegate 的生命周期?从后台到前台调用了哪些方法?从前台到后台调用了哪些方法?第一次启动调用了哪些方法
- 生命周期:
- 当应用程序启动时(不包括已在后台的情况下转到前台),调用此回调。
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
,应用程序已经进入活动状态。
- 先调用
14、NSCache 优于 NSDictionary 的几点?
NSCache
是一个非常奇怪的集合。默认为可变并且线程安全的
。这使它很适合缓存那些创建起来代价高昂的对象
。它自动对内存警告
做出反应并基于可设置的成本清理自己
。与NSDictionary相比,键是被retain而不是被拷贝的。
- 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减时间最久为被使用的对象。
- NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便。
- NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了。
15、id
和 instanceType
有什么区别?
- 相同点:
instancetype
和id
都是万能指针
,指向对象。 - 不同点:
id
在编译的时候不能判断对象的真实类型,instancetype
在编译的时候可以判断对象的真实类型。id
可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetyp
e 只能作为返回值类型。
16、self 和 super 的区别 ?
- self 调用自己方法,super 调用父类方法
- self 是类,super 是预编译指令
- [self class] 和 [super class] 输出是一样的
- self和super底层实现原理
- 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
- 而当使用 super 时,则从父类的方法列表中开始找,然后调用父类的这个方法;
- 当使用 self 调用时,会使用
objc_msgSend
函数:
第一个参数是消息接收者,第二个参数是调用的具体类方法的 selector,后面是 selector 方法的可变参数。以 [self setName:] 为例,编译器会替换成调用 objc_msgSend 的函数调用,其中 theReceiver 是 self,theSelector 是 @selector(setName:),这个 selector 是从当前 self 的 class 的方法列表开始找的 setName,当找到后把对应的 selector 传递过去。id objc_msgSend(id theReceiver, SEL theSelector, ...)
- 当使用 super 调用时,会使用
objc_msgSendSuper
函数:
第一个参数是个objc_super的结构体,第二个参数还是类似上面的类方法的selectorid objc_msgSendSuper(struct objc_super *super, SEL op, ...)
struct objc_super { id receiver; Class superClass; };
17、setNeedsDisplay
和 layoutIfNeeded
两者是什么关系?
- UIView 的
setNeedsDisplay
和setNeedsLayout
两个方法都是异步执行的。setNeedsDisplay
会自动调用drawRect
方法,这样可以拿到UIGraphicsGetCurrentContext
进行绘制;setNeedsLayout
会默认调用layoutSubViews
,给当前的视图做了标记;layoutIfNeeded
查找是否有标记,如果有标记及立刻刷新。
只有setNeedsLayout
和layoutIfNeeded
这二者合起来使用,才会起到立刻刷新的效果。
Swift
1、Swift 和 OC
swift
和 OC 的联系swift
和 OC 共用一套运行时环境,swift
和 OC 可以互相桥接,互相引用混合编程;- 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直接上传。) |
2、Swift 的可选项类型(Optionals
)
swift
引用了可选项类型,用于处理变量值不存在的情况。
Optionals
类似于 OC 中指向 nil 的指针
,但是适用于所有的数据类型,而非仅仅局限于类,Optionals
相比于 OC 中的 nil 指针
,更加安全和简明,并且也是 swift
诸多最强大功能的核心。
3、Swift 中的 struct
和 class
- 相比于 OC 中的结构体,Swift 对结构体的使用比重大了很多,结构体成了实现面向对象的重要工具。
- 相比于 C++ 和 OC 中的结构体只能定义一组相关的成员变量,在 Swift 不仅可以定义成员变量(属性),还可以定义成员方法。 因此在 Swift 中,我们可以把结构体看做是一种轻量级的类。
- Swift 中结构体不具有继承性,也不具备运行时类型强制转换、使用析构器和使用引用计等能力。
- Swift 中
struct
是值类型,而class
是引用类型。
值类型的变量直接包含他们得数据,而引用类型的变量存储对他们的数据引用。
因此引用类型的变量被称为对象,因此对一个变量操作可能影响另一个变量所引用的对象。
而对于值类型都有他们自己的数据副本,因此对一个值类型的变量操作不可能影响到另一个值类型的变量。
4、swift 中 defer、guard?
-
defer
defer
关键字提供了一个安全和简便的方式来处理这件事,当离开当前的代码块时,会执行defer
对应的代码块。func openFileAction(){ ///打开文件 openFile() defer{ closeFile() } ///读文件 let isRead = readFile() guard isRead else { return } if emptyFile() { return } print("读取成功") }
-
guard
guard
当条件满足的时候,会顺序执行,如果guard
条件不满足的时候,会进入guard
内部,并执行 return 操作,终止代码的执行。
5、Swift 中高阶函数有哪些?
-
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})
6、Swift 为什么将String,Array,Dictionary设计成值类型?
- 值类型相比引用类型,最大的优势在于内存使用的高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说Swift这样设计,大幅减少了堆上的内存分配和回收的次数。同时copy-on-write又将值传递和复制的开销降到了最低。
- String,Array,Dictionary设计成值类型,也是为了线程安全考虑。通过Swift的let设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程内存访问和操作的问题。
- 设计成值类型还可以提升API的灵活度。例如通过实现Collection这样的协议,我们可以遍历String,使得整个开发更加灵活高效。
7、Swift 中的 async
/await
? (swift 5.5 后,百度问)
-
async
:
表示这个函数时可以异步执行
的,也就是说执行这段代码是可以不阻塞
当前线程。- 函数/方法可以是异步的,属性也可以是异步的。
- 当您将函数标记为异步时,您就允许它挂起。当一个函数挂起自己时,它也会挂起它的调用者。所以它的调用者也必须是异步的。
- 为了指出异步函数中它可能挂起一次或多次的位置,使用了 await 关键字。
- 当异步函数被挂起时,线程不会被阻塞。
- 当异步函数恢复时,从它调用的异步函数返回的结果流回原始函数,并从上次停止的地方继续执行。
-
await
:
在函数、属性和初始值设定项中,await
可用于表达式可以解除当前线程阻塞;除此之外,await
还可以用于异步序列。
8、Swift 消息派发机制有几种?详细说说。
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
:直接派发。
-
其他
1、什么是静态库?什么是动态库?有什么区别?
库的本质
是可执行的二进制文件
,是资源文件
和代码编译
的一个集合
。根据链接方式不同,可以分为动态库和静态库,其中系统提供的库都属于动态库
。
-
静态库:
静态库形式:.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 错误)。