iOS技术

iOS面试知识点总结(持续更新中)

2018-06-26  本文已影响32人  338487f86813

1.runtime 

交换方法、添加属性

要丢在onceToken中进行

给UIButton添加一个分类,在该类别中交换点击事件。

系统button触发事件是调用的 [sendAction:to:forEvent:] 方法

SEL sysSEL = @selector(sendAction:to:forEvent:);

SEL newSEL = @selector(newSendAction:to:forEvent:);

method sysMethod = class_getInstanceMethod([self class], sysSEL);

method newMethod = class_getInstanceMethod([self class], newSEL);

先进行添加,把新的 IMP 加给系统的 SEL

class_addMethod(self, sysSEL, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));

如果成功了,说明没有方法,同时已经把 sysSEL 对应的IMP映射改成了 newMethod,这时把 newSEL 映射改成 sysMethod就行了

如果失败了,说明有这个newMethod了,就 method_exchangeImplementations(sysMethod, newMethod); 把IMP映射交换就好了。

主要是怕找不到以前的 sysMethod 因为在后面 newMethod 中调用。

获取属性列表 

RootModel 中使用 class_copyIvarList([model class], &count) 来获取到model里的所有属性

a.初始化获取字节流 initWithCoder 遍历varList,使用 KVC: [setValue:DCoder forKey:varName] 赋值

b.编码存储字节流 enCoderWithCoder 遍历 varList, [enCodeObject:[self valueForKey:varName] forKey:varName] 进行编码

使用系统未开放的属性,例如UITextField的placeholder文字设置颜色,正常情况下我们无法获取到这个label,但是可以通过getIvarList,先打印拿到这个label名称,再通过key-path语法设置。

[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];

既可改变placeholder的颜色了。

序列化NSCoding协议

iOS系统中,所有的数据都是以字节流的形式进行存储的,所以要记录状态就要先将model中的属性编码成字节流,然后再keyarchive进行序列化操作

扩大UIButton响应范围,需要先增使用runtime添属性,然后设置重写hitTest方法

为button添加响应间隔,也需要先增加intervalTime属性

添加类: 

objc_allocateClassPair(父类, char类名, size大小一般为0)

//在这中间可以 class_addIvar()来为动态类创建新的成员变量

objc_registerClassPari(新建类指针)

但是使用 class_addProperty()就可以在 registerClassPari之后添加,具体原因不知道。在注册后这个类的成员变量size被固定了,但是property却可以继续添加。property、ivar有什么关系目前也没查明。

associate添加“属性”其实不是真正的添加了property这种意义上的属性,其实是在一个全局的叫做 associateManager 这个单例下新增了object引用,然后只是暂时“挂”在了这个类里面。不是真正意义上的与属性列表、类size等进行了修改。


2.KVO跟KVC

KVO

key-vlaueobserving 当对象的某个特定的property改动时,可以允许别的对象也接到这个通知。原理请见第40条。

前提条件就是这个类和它的 property 是 KVC compliant 的,即 valueForKey: 跟 setValue:forKey: 要实现。实现的条件至少有以下一条

1.这个类以key为名,声明了一个property

2.这个类实现了以key为名的accessor方法

3.这个类以key或者_key为名,声明了一个实例变量。

[addObserver:forKeyPath:options:context:] 方法增加观察者,其中option有一个很关键的选项:

NSKeyValueObservingOptionPrior,在观察的property值的改变前跟改变后,会各发一个change notification。

其中观察者必须实现 [observerValueForKeyPath:ofObject:change:context:] 方法,并要定义观察者如何响应 change notification

手动通知

重写 [NSObject 的 automaticallyNotifiesObserversForKey:] 方法并对要实现手动发送的 key 返回 NO, 其他地方用 super.

- (void)willChangeValueForKey:(NSString *)key;

do something;

- (void)didChangeValueForKey:(NSString *)key;

例如在改变一些 value 之前,进行一些其他操作,就可以在这两个方法中,包起来执行。

一般情况下kvo不跟多线程混用,但是kvo是同步运行的,就是在一个单一线程上面kvo,观察,那么可以保证在其他线程中的观察者在 setter 方法返回之前都被通知到。

KVC

允许我们使用属性的字符串名称来访问属性,字符串就是key

两个方法: [setValue:forKey:], [valueForKey:];

如果一个类不写 @property @synthesize 也可以使用 

- (NSString *)name;

- (void)setName:(NSString *)name;

其实就是getter、setter方法

这种KVC模式来实现属性,但是这样传入 nil 会报错。需要重写 NSObject 的 

- (void)setNilValueForKey:(NSString *)key  方法,在里面特别处理某一个 key 对应的name

还可以重写 

- (id)valueForUndefinedKey:(NSString*)key;  //动态访问一个没定义过的属性

- (void)setValue:(id)value forUndefinedKey:(NSString *)key; //设置一个没定义过的属性

这两个方法会让性能上拖后腿

有keyPath写法,可以在 NSSet 中方便地取出一些值 

NSArray *a = @[@4, @5, @9];

max = [a valueForKeyPath:@"@max.self"];

集合代理对象实现KVC

返回代理对象,使用者不知道这个返回值是一个真正的 NSArray 还是一个 代理对象

但是对外开放一些取值的方法,实现获取这个集合大小、取出集合中下标第几的值两个方法。

//contacts 是一个集合代理对象

- (NSUInteger)countOfContacts;

- (id)objectInContactsAtIndex:(NSUInteger)idx;

当调用 [object valueForKey:@"contacts"]; 时,会返回一个由这两个方法来代理所有调用方法的 NSArray 对象,而且这个 arr 还支持所有数组方法调用。但是使用者不知道这是一个真正的 arr 还是一个代理数组。

应用环境:做一个很大的数据库中取值,例如通讯录,直接传入 lastObject 这种下标进去,就可以直接拿到值,而不用将整个数组读出来,然后取值。

KVO调试:  po [observedObject observationInfo]; 可以把 object 关联的 观察信息都打出来

KVV: 键值验证 

在KVC的时候,不会进行任何验证,也不会自动调用任何 KVV 的方法,控制器要做这些校验。一般来说 model 中来做这些事情

- (BOOL)validateName:(NSString *)nameP error:(NSError * __autoreleasing *)error;

例如去除name中的空格字符等操作。


3.存储方式

a.plist文件存储 只能存 dict array 等实现了 writeToFile: 方法的对象

b.NSUserDefaults  保存用户的数据

c.NSKeyedArchiver(NSCoding) 自定义model存储

d.本地文件夹写入文件存储  [NSData writeToFile:path atomically:YES];

e.SQLite3

f.coreData


4.runloop

本质就是一个do{}while(1)循环

CFRunLoop主要作用就是建立任务来监测 输入源 例如 用户输入设备,网络连接,周期性的或延迟事件,异步回调等等。 timer,observers 也是 sources

一个runloop有多种模式,而每次对应的线程在进入runloop时需要选择一种模式来执行。

CFRunLoopMode:

DefaultMode

UITrackingRunLoopMode,追踪scrollView滑动的状态,只要有滑动为了保证用户UI的流畅,主线程的runloop就会切换到这个模式下,滑动结束后再切换到其他模式。此模式优先级最高

UIInitializationRunLoopMode,APP启动的时候到显示第一个页面期间主线程会进入这个mode

NSRunLoopCommonModes,其实这是一个集合,不是一种模式,包括了 UITracking 跟 Default 两种模式

GSEventReceiveRunLoopMode 接受系统事件的内部 Mode,一般用不到这个。

timer创建 

a.schedualed方式会直接丢到当前线程里面,这种情况下,timer要加入UITracking或者Common模式下才可以无缝计时

b.timerWithTimeInterval 则要手动来写runloop,获取当前子线程的currentRunLoop,然后把timer添加到这个runloop中,指定模式,然后手动开启 [runloop run],然后在触发某个指定时间时获取主线程队列,在这个队列中执行刷新UI的方法 dispatch_get_main_queue。

如果是在子线程中开启子线程的runloop,就使用Defaultmode就好了,但是如果在子线程中创建timer,就只能在同一个线程中进行销毁timer,其他线程是无法销毁的。就把thread做成全局的属性,不管是gcd还是nsthread开启的线程,都是用 [NSThread currentThread]; 来获取这个线程,然后赋值,在不需要的时候 [performSelector:onThread:thread withObject:waitUntilDone:] SELcancel就好了

是一个懒加载 只有当 currentRunLoop方法调用时才会生成,当线程结束时,就会销毁掉

CFRunLoopSourceRef

source0: 非基于mach_port  平常使用的用户UI操作、timer等等

source1: 基于mach_port    CFMachPort  CFMessagePort 内核部分。主要是端口分发,事件分发等

CFRunLoopObserver

观察RunLoop的,一个 Observer 一次只能被一个 RunLoop 注册,当 RunLoop 切换状态时,就可以观测到。

kCFRunLoopEntry,即将进入RunLoop

kCFRunLoopBeforeTimers,即将处理Timer

kCFRunLoopBeforeSource,即将处理source

kCFRunLoopBeforeWaiting,即将进入休眠

kCFRunLoopAfterWaiting,即将从休眠中唤醒

kCFRunLoopExit,即将推出RunLoop

优化tableView:

监听主线程的RunLoop,如果当前的RunLoop处于kCFRunLoopBeforeWaiting状态,就回调,执行cell上的耗时任务。


5.响应链 hitTest

1.触碰屏幕 

2.屏幕将事件传给 IOKit -> 封装成 IOHIDEvent 对象

3.通过mach_port传给SpringBoard

4.SpringBoard收到事件后,触发了系统进程的主线程的runloop中的source回调

5.如果是在桌面上,则触发source0回调,系统进程来处理这次触摸事件

6.如果是在APP应用中,则触摸事件就会分发给APP进程,进入APP内部响应

7.APP进程的mach_port收到SpringBoard事件,主线程runloop被唤醒,触发source1回调

8.source1触发一个source0回调,将IOHIDEvent对象封装成UIEvent对象

9.source0将事件添加到UIApplication事件队列中,当触摸事件出队列后,寻找最佳响应者。

10.UIApplication传给 UIWindow,如果有多个UIWindow对象,则传给最后一个加上的window

11.如果UIWindow能响应这个触摸事件,则将事件传给子视图,向子视图传递时,也是先传给最后加上的子视图,而不是最先加上的父视图。

12.如果子视图无法响应该事件,返回父视图,再传给倒数第二个加上的子视图,一直循环,直到找到可以响应的子视图。如果找不到子视图了,那么这个父视图就是最终响应者。如果还能找到响应的子视图,就一直重复上面的过程。

注意:当透明度小于等于 0.01 时,无法响应事件。hidden=YES、userIntercationEnabled=NO、不在rect内。

hitTest方法:

a.当前视图无法响应事件,返回nil

b.当前视图能响应事件,但是无子视图响应事件,返回当前视图

c.当前视图能响应事件,子视图也响应事件,返回子视图中的响应者

地图气泡被截断,点击气泡每次响应的是地图,而不是气泡本身。这里就需要在annotationView中hitTest方法里面强行返回callOutView,截断地图的响应。

通过重写hitTest方法,CGRectContaintPoint 判断触摸事件是否符合rect,扩大UIButton的响应区域


6.消息发送 objc_msgSend

任何形式的方法调用都会变成 objc_msgSend(self, _cmd, 参数1, 参数2, ...)

先从isa指针找到 self 的类, 去类的 cacheMethodList 中查询,再到 methodList 中找,如果都找不到,就找 superClass 指针,循环这个步骤。一直到元类都没有,就转向拦截调用,进行消息动态解析。

直接在类中添加 runtime 动态解析转发函数: 加入#import

+ (BOOL)resolveClassMethod:(SEL)sel;

+ (BOOL)resloveInstanceMethod:(SEL)sel;

- (id)forwardingTargetForSelector:(SEL)aSelector;

- (NSMethodSignature *)methodSignatureForSelector(SEL)aSelector;

- (id)forwardInvocation:(NSInvocation *)anInvocation;

先 reslove 确定方法是否动态添加,如果是 则在里面解决了,不是,则执行 forwardingTarget 方法,把消息转给指定的target来处理,这里做处理效率很高。如果这里也没处理,则返回nil。转到 methodSignature,这个方法用来给方法做签名,将调用的方法参数类型和返回值封装并返回,如果nil,那么消息无法处理 unrecognized selector sent to instance 报错。如果可以处理,则进入 forwardInvocation 方法,这里有方法调用的全部信息,修改方法实现,修改响应对象,然后invoke执行。失败同样报错  unrecoginzed selector sent to instance。

object_getClass()和 [objc class]区别

object_getClass() 找isa指针,实例、类对象、元类、根元类

[objc class] 分为类方法、实例方法。实例方法获取isa指针,类方法return self。

类的层级关系:

        isa          isa                    isa                  isa                  ...

objc --> class --> metaclass --> rootclass --> rootclass ..

        class            class          class          ... 

objc ----> class ----> class ----> class ...

isa跟superclass区别

isa是指向谁创建了这个对象,superclass是继承关系父类。

消息传递先找isa,获取对象所对应的类对象方法列表,如果找不到就找superclass,找到superclass后还是找isa,一直循环下去直到NSObject。


7.VC周期


8.view周期


9.AFN封装


10.onceToken内部原理

截断CUP的流水线特性,MacOS跟iOS通用gcd,但具体CUP处理应该有所不同。


11.block内部原理  copy辅助函数,copy属性,堆、栈  strongself、weakself

(返回值) (^block名称) (参数, 参数) = ^(参数, 参数) {}


12.https流程


13.NSOperation、GCD、NSThread

a.NSOperationBlock,NSInvocationOperation 指定 NSOperationQueue 用起来跟gcd有点类似

b.NSThread currentThread, performSelector onThread ... 指定某个线程,然后释放定时器等操作

c.dispatch_apply, after; dispatch_queue_t = dispatch_queue_creat("myQueue", DISPATHC_QUEUE_CONCURRENT)获取异步队列

group    semaphore   

barrier用在同一个并发队列中执行,async123 插入barrier_async0 async456

就是在并发队列中,123异步执行完了以后要等待 0 执行,然后456.


14.锁  semaphore group 自璇锁、NSCondition

a.dispatch_semaphore_t当value为1时,可以当做锁来使用  wait减一  signal加一

如果作为锁来使用,把总信号量设为1,在没有等待的时候,性能比 pthread_mutex还要高,但是有等待的时候,性能就会下降。对比 OSSpinLock来说,优势在于等待的时候不会消耗CPU资源。

b.@synchronized一般没怎么用,就是括起来在括号里面写处理,系统加了很多隐藏处理。

c.NSLock

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[lock lock];

需要被锁住的操作

[lock unlock];

})

除了基本的 lock 跟 unlock,还有 [lock trylock] 和 [lock lockBeforeDate:] 两个方式,trylock就是尝试枷锁,如果所不可用(已经被锁住了),返回NO,不会阻塞线程。lockBeforeDate 会在指定 data之前尝试加锁,如果指定时间之前不能枷锁,就返回NO。代表锁住了。

d.NSRecursiveLock递归锁,如果在线程中有递归套用,则不可以使用 NSLock,需要使用递归锁来解决。

e.NSConditionLock条件锁,[lock lockWhenCondition:] 和 [lock unlockWhenCondition:] 组合起来,加入条件来锁跟开锁。

f.NSCondition最基础的条件竞态。手动控制线程的 wati跟 signal。

线程1 {

[condition lock]; 加锁

处理

[condition wait]; 把当前这个线程等待,下面的代码都不跑了

...

[condition unlock]; 解锁

}

线程2 {

[condition lock];

处理

[condition signal]; 告诉其他地方,如果是同样的这个 condition,使用了wait的可以动了,这个线程已经处理好了,你那边能继续跑了。

[condition unlock];

}

其实使用起来很像 semaphore(1),生产者、消费者关系。

g.pthread_mutex,c语言的锁。跟NSLock很像

f.pthread_mutex(recursive),c语言递归锁

h.OSSpinLock自旋锁,性能最高的锁。

在等待的时候,会一直do while忙等,缺点是在等待的时候会消耗大量的CPU资源,不适用于较长时间的任务,可以使用 semaphore(1) 来玩。

空间操作耗时:

semaphore 跟 OSSpinLock 效率远远超过其他

由于 OSSpinLock 不安全,如果是考虑性能的话,就用semaphore(1) 来玩吧。


15.MVC、MVVM、MVP

Model中处理json、引用helper来做一些业务数据上的转化。

View中纯布局,以及与数据无关的视图状态变化

Controller中进行网络请求、数据变更,然后传入model给view,刷新view状态


16.类的生成

SEL在编译时,根据方法名,生成一个唯一的 int 标识,转成 char* 来使用,即方法的 ID

IMP就是方法的地址,函数指针。


17.通知中心 主线程与子线程


18.深拷贝浅拷贝,如何copy一个model


19.autorelease 自动释放池

autorelease pool:当对象被加入自动释放池后,会被添加在释放池的列表中,当池子被释放时,就会对这个列表中的所有对象发起一次 count-1 的消息。


20.其他

UIImage创建对象时,使用 imageNamed 跟 init 区别: imageNamed创建的对象把图片加载到内存后就不再释放了。


21.tableViewCell复用原理

1.在runloop每一次循环(1/60s符合CADisplayLink最小单位)调用一次layoutSubview方法,方法中包含了 [self _reloadDataIfNeed],[self _layoutTableView]和父类[super _layoutSubViews]。

2.其中 reloadDataIfNeed 方法检查了数据源,如果数据源被更新,那么tableview将会移除所有的显示的和未显示可复用的cell,重新计算并且缓存高度等数据。

3.因为tableview是继承scrollview的,所以通过contentoffset来拿到当前显示的bounds。每次显示处理的都是这个bounds范围内的cell三个数组来控制:cashedCells记录当前正在显示的cell的引用,availableCells记录当前正在显示但是滚动后不再显示的cell,reuseableCells记录当availableCells中的cell完全隐藏后的可复用的cell,同时从availableCells中移除这个cell。

4.availableCells创建时是reuseableCells的拷贝,但是创建之后根据indexPath取到cell后,就把这些cell都从数组中移除,只保留刚刚隐藏的cell。然后再传给reuseableCells,并且从availableCells中移除。说白了就是一个cashedCells与reuseableCells的中间传递数组,用来协调当前显示的cell与复用cell之间关系用的。


22.tableVIew的优化

1.使用runloop来优化,首先获取主线程runloop,创建cfrunloopobserver,关联主线程runloop,监听kCFRunLoopBeforeWaiting状态,每次主线程进入等待之前,就会加载回调。回调中每次取出一个taskArr中的任务,执行任务,taskArr中直接存block对象。限制tastArr的数量,添加新的时候删掉第一个,保证不超过maxCount。在cellforrow中添加task进tastArr,这样就完成了一次利用主线程的runloop休眠之前加载复杂操作。然后保证runloop加载完cell后不进入休眠,就要开启一个CADisplayLink来保持runloop一直处于监听状态,使用NSTimer也是可以的。

2.避免离屏渲染,例如layer.cornerRadius圆角设置、阴影等操作,都改用 UIBezierPath 跟 CAShaperLayer 组合画出来,(CoreGraphicAPI来绘制)

3.cell上采集到数据后,使用缩略图,将图片压缩,待需要展示完整图片的时候才使用原图。

4.肥皂滑效果。


23.离屏渲染

概念:

on-screen:当前屏幕渲染

off-screen:离屏渲染

当我们需要预渲染的效果时,就需要另外开辟一块缓冲区,根据传入的上下文来做渲染工作。这个缓冲区不是当前屏幕的,所以就叫做off-screen render即离屏渲染。

离屏渲染的开销是比较大的,需要将上下文先从当前屏切换到离屏进行渲染,完了之后再将结果跟上下文传回当前屏,开销比较大。


24.SDWebImage缓存原理


25.NSOperation跟gcd的区别

1.operation是gcd封装的,operation是操作一个对象,gcd执行的任务是由block构成的。在operation中可以随时取消已经设定要执行的任务,gcd要取消加入queue的block很麻烦。

2.operation可以设置依赖关系,即使两个operation处在同一个并行队列,也可以让a任务完成后才执行b任务,gcd就需要加入barrier等操作。

3.operation可以加入kvo,监听一个operation是否完成或取消,很有效的掌控状态。gcd如果想要做监控,

4.operation可以设置任务优先级,就算在并发队列也可以随意指定优先级。gcd只能指定队列优先级,想要区分block任务优先级要加一些代码。一个套一个,第一个做完在做第二个,或者barrier


26.runloop是什么

1.runloop就是运行循环,如果没有runloop,程序运行完就会退出,runloop相当于在程序内部开了一个死循环。

2.Mach是XNU内核,进程、线程和虚拟内存通过port发送消息进行通信,runloop通过mach_msg()函数发送消息。如果没有port消息,内核会将线程处置为等待状态mach_msg_trap()。如果有消息,先判断消息类型处理事件,并通过modeItem的callback进行回调。

3.作用: 保证程序持续进行,处理APP中的各种事件,节省CUP资源提高性能。

4.主线程runloop是在main函数时启动的,即程序运行的时候就创建了。也就是iOS系统在开启APP线程的时候(对于APP而言就是主线程)。子线程中的runloop需要手动创建,currentRunLoop方法获取的时候进行懒加载创建,然后开启runloop。线程结束时同时销毁掉。

5.runloop对象是用字典进行存储的,key对应线程对象,value对应runloop。

6.每次启动runloop时,只能启动一个mode,一共5个mode,

UITrackingRunLoop,界面跟踪mode

defaultRunLoop,默认级别低

UIInitializationRunLoop程序启动时初始化进入的第一个mode,启动完成后就不再使用

GSEventReceiveRunLoopMode接受系统事件的内部mode,通常用不到

然后还有一个commonMode占位

7.CFRunLoopSourceRef: source0用户操作,source1系统事件。 

8.CFRunLoopObserverRef: 

即将进入、即将处理NSTimer、即将处理Sources、即将进入休眠、刚从休眠中唤醒、即将推出runloop、所有状态改变。

9.runloop跟thread没有必然关系,如果想要保证thread跑完后不死,就需要创建一个runloop来维持这个线程不被回收掉。应用场景就是需要专门维持一个线程,一直跑一些运算处理,比如cell上面的运算,但是这个线程又不能跑完一次就死掉,这时候就要把这个线程的runloop开启来维持线程不死。

10.维持线程不死常用两个手法:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];

或者

[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

[[NSRunLoop currentRunLoop] run];


27.设计模式

装饰者模式: 是在运行时增加行为,继承是在编译时增加行为。少用继承多用组合。是一种动态地往一个类中添加新的行为的设计模式,比生成子类更灵活,可以给某个对象添加一些功能,而不是整个类添加一些功能。

使用场景: 给旧类添加新的代码,但是这些新代码逻辑、属性、字段会增加旧类的责任度,但是这些新加入的东西仅仅是为了满足一些特定情况才会执行的行为需求。使用装饰者模式把要装饰的功能放在单独的类中,让这个类包含他所需要装饰的对象,当执行特殊行为时,来包装对象就行了。

举个例子,卖咖啡的系统:每一种咖啡其实都是水+咖啡+牛奶+其他配料组成,如果每出一款新的咖啡就要重新维护代码就太烦了。所以把咖啡的组成都抽象出来一个类,包含价格属性,基础类水每毫升:xx元,装饰类咖啡豆每颗:xx元,牛奶每毫升:xx元,配料A:xx元,...这样就可以只需要创建水这个类的实例,利用装饰类来不停地装饰这杯咖啡,比如

利用多态,基础类跟装饰类都继承CaffeClass

CaffeClass *caffe = [[CaffeClass alloc] init];

caffe = [[WaterClass alloc] initWith:caffe];

caffe = [[Milk alloc] initWith:caffe];

...不停装饰,添加不同的配料。这样子这个caffe对象就拥有了好多好多独特定制的属性,例如价格、名称等属性都是在被装饰的时候,在装饰类中进行处理的。总价格price+=装饰者price,name组合变化。

caffe = [[包装 alloc] initWith:caffe];

递交客户...

观察者模式: KVO/NSNotification

单例模式: dispatch_once_token

代理模式: protocol与delegate

工厂方法模式: +方法(可以粗略理解为静态方法,静态类)

原型模式: NSCopying协议,需要实现 -copyWithZone 方法。自定义person类,遵循协议,实现 -(id)copyWithZone:(NSZone *)zone{} 就可以在外界使用 [person copy]方法了。

使用场景: 当需要复制创建很多数据一样的实例时,使用原型模式。

模板方法模式: 继承,把公共的放到父类中去。

单一职责原则: 类似MVC的设计,将各个模块能抽离出的都抽离出来,各司其职。

策略模式: 学习中...

买了一本《iOS 设计模式》学习中...


28.ARC原理

UIKit通过 RunLoopObserver 在当前线程的RunLoop两次sleep间对 autorelease pool 进行 pop 和 push 将这次 loop 中产生的 autorelease 对象释放掉

在程序启动的时候runloop就创建了2个observer:

runloopObserver1观察entry,order是-2147483647优先级最高,在所有回调之前创建自动释放池。

runloopObserver2观察 beforewaiting和exit,order是2147483647优先级最低,保证所有操作都完成后才释放这释放池里的对象。beforewaiting准备进入睡眠时调用 objc_autoreleasePoolPush() 和 __objc_autoreleasePoolPop() 释放旧的池,并创建新的池。

观察到exit时调用_objc_autoreleasePoolPop()来释放自动释放池,这个observer的


29.AFN中kvo是如何监听网络状态的


30.代理跟协议有什么区别

delegate: 本身是一种设计模式,是把一个类需要做的一部分事情让另一个类来完成,这个实际做事的类叫做delegate。

protocol: 就是定义了一组函数,却没有做实现。类似java的interface。

一般来说,AVC拥有BVC指针,但是BVC里面有一定的操作后,需要回传给AVC数据,这时候就可以在BVC里面创建一个protocol,设置delegate。当BVC中的操作变化时,将值通过protocol中定义的方法回传给delegate,而这个delegate就是AVC。

这种情况下protocol中定义的方法,是为delegate来服务使用的,组合使用。


31.代理跟block区别

1.代理消耗比block小,因为代理只是定义了一个方法列表,在协议对象中objc_protocol_list添加了一个节点,在运行时向遵守协议的的对象发送消息就行了。block涉及到栈到堆拷贝操作和block对象引用计数+1,delegate是一个弱引用只多了一个查表动作,时间和空间上的消耗都大于代理。

2.代理更加面向过程,block更加面向结果。如果要拿到维持多种时间之间的状态就用delegate,如果是仅仅拿到结果,就用block。一般来说使用单个block来表达多个状态,block表达多个状态会显得很混乱,需要很明确的文档注释才方便阅读。

3.在一个回调对象需要处理多个回调消息时,一般使用代理,可以把多个状态放在一个protocol中处理,而不是每个回调都注册一个block来处理,这样会很麻烦。如果一个block接收多个回调信息,就会显得很乱可读性太差。一般来说一个对象有两个或以上不同的事件,用delegate。

4.如果一个对象是单例,在回调需要在不同类中进行处理时,delegate需要不停地变更,这种情况是不适合用delegate,而是用block。(当然这种情况更适合用通知)

5.block比较线性,例如做一个状态的判断,block一般来说要么返回对象(直接拿对象指针就好了),要么返回对象确定的结果(直接读属性就行了),所以如果对象在回调时需要额外的信息,大多数情况下使用delegate。


32.构建一个APP,需要准备的点

1.完成日志系统,使用高效的fprintf来作为打印,替换NSLog做成自己的打印宏。description方法有很多操作。

2.定制提交commitMessage填写规范

3.代码规范,例如空格、{}、换行、注释

4.累积一个踩坑手册,收集各种坑

5.页面布局统一,例如使用masonry

6.统计埋点

7.App架构,选择MVVM、MVP等模式

8.植入crash统计平台

9.bug管理系统

10.项目管理工具

11.发布前checklist,一直累积下去,会很有效。


33.压缩 .ipa 体积

1.配置编译选项 (Levels选项内)Genetate Debug Symbols 设置为NO,这个配置选项应该会让你减去小半的体积。

2.舍弃架构armv7,因为armv7用于支持4s和3gs,4s是2011年11月正式上线,虽然还有小部分人在使用,但是追求包体大小的完全可以舍弃了。

3.编译的版本必须是发布版本,

4.查找内部使用到的第三方库,一方面可以进行删减代码,用不到的类,直接删除,还有第三方库中的图片资源统统删除掉,如果能够自己手写实现的,那费功夫自己写吧

5.如果不需要使用透明,就把png换成jpeg。

6.一般情况用矢量图,对一些特定的需要高质量图片的用位图。


34.代理如何实现一对多(多播代理)

1.实现一个中间层,实际上实现代理方法的是这个中间层。将事件传递给中间层,中间层开放一个数组,存放block通知的任务。当中间层得到代理方法回调时,遍历这个数组,将这个数组里面注册过的block的实例对象执行通知任务。例如: A、B、C三个类需要同时响应E这个类里面的protocol,创建一个中间层D,遵循E的protocol,并且E.delegate = D。在E触发事件需要回调的时候,D直接实现protocol获取回调信息,然后开放一个block任务数组接收A、B、C三个类的任务,在protocol中遍历数组,执行block。这样就可以实现所谓的代理一对多了。

2.利用runTime的消息转发机制,在代理类中实现methodSignatureForSelector:与forwardInvocation:两个方法。并且重写responseSelector:默认YES。在forwardInvocation:中遍历代理数组,挨个发送消息。说实话这种方式没有第一种科学,虽然用了runtime但是从设计角度上来说不可取。


35.并发、串行、主队列的理解

1.无论是使用async还是sync来操作 dispatch_get_main_queue(),都是一个效果,就是同步串行的,因为主队列是串行队列,而刷新UI等操作只能在主队列上进行,所以始终都要等待主队列做完。但是不管怎样,最好用async来操作主队列。所以要区分主线程中sync(main_queue)是死锁,但是在主线程里开启一个thread,在这个thread中sync(main_queue)执行却不会死锁(可以理解为子线程刷新UI)跟async(main_queue)一个意思。

2.在一个线程thread中,只要不直接对当前这个thread采用同步执行串行队列,一般情况不会死锁。解释了第一条的问题。

3.基本理解点: 

串行队列: 必须严格按照顺序取出任务,一个做完才做下一个。

并发队列: 线程队列中的任务随机取出,是交替来做整个队列中的任务,不一定一个做完才做下一个。

sync: 是指这个线程中,必须等待队列任务处理完才能继续跑线程中其他的代码。

async: 是指当前这个线程不需要等待队列任务处理完,就可以处理线程中其他的代码。

并行队列: 是指多核的并行,同一时间可以处理不同的进程。不是交替进行的。


36.static与const、宏、extern

const修饰的变量是只读的,即使 &var = xxx 去修改,也无法更改这个const var的值。

static修饰的变量只能在当前文件访问,不能使用extern来引用。默认情况下的全局变量,例如定义一个 int a = 33;那么可以通过extern跨文件来访问,但是static修饰之后,只能在这个文件中使用。

使用extern+const组合,来定义全文件全局常量,替代常量宏。因为宏在编译时会进行替换,无形中增加了很多的代码消耗,使用const来读取固定常量字符串会比较好一点。

创建constClass,.h申明:

UIKIT_EXTERN NSString *const kNotification_logout;

.m实现:

NSString *const kNotification_logout = @"notification_logout";

注意const后面接的是什么就表明那一个变量不可变,如果是 const *kNotification_logout,那么修饰的是 &p 这个指针的地址不可变,而不是指针指向的内容 *p 不可变。如果面试的话估计会有坑

宏是预编译,const是编译阶段处理。

宏不会做编译检查,简单替换。const会进行编译检查,会报错。

宏可以定义一些方法,const不行。

大量使用宏会让编译时间增加,每次都要进行替换操作。


37.静态类与单例如何取舍

这俩都用到工厂模式设计模式。

静态类,粗暴理解为不需要实例化的工具类。.h充满了+方法,.m做实现。好处是使用起来方便。但是无法继承成员变量,在编译期间就加载了。方法中创建的对象随着作用域结束就释放掉了,如果是对数据库链接操作,利用这个特性反而能保证一定的安全性。

单例,使用的时候要实例化,分配内存。好处是可以继承、实现接口、可以从外部额外引用单例达到多单例配置状态。可以延时加载,在需要产生的时候才加载。创建后一直保留在内存中,如果是做数据库这样的操作,尽量不要放在单例中,因为可能会有多个地方都需要链接数据库,单例维护释放比较麻烦。


38.图片压缩


39.socket

1.socket本质是TCP/IP的一套API

2.socket提供的实际上是进程通信端点,进程通信之前必须各自创建一个端点,否则是无法建立通信的。

3.客户端 -> socket -> HTTP协议(传输层TCP/IP协议)-> socket -> 服务器

服务器会开放一个端口一直监听,直到有客户端访问。

socket流程:

1.创建客户端socket

2.连接到服务器socket

3.客户端socket发送数据到服务器socket

4.客户端socket接收从服务器返回的数据

5.关闭客户端socket

伪代码使用流程:

1.创建socket,协议一般用AF_INET即IPV4,socket类型用SOCK_STREAM即TCP,protocol用0会自动根据socket类型找对应的协议。

int clientSocket = socket(AF_INET, SOCK_STREAM, 0);

2.创建用的配置结构体 sockadd_in,指定 sin_family 协议AF_INET,sin_port 端口号 htons(80),sin_add.s_addr 地址 inet_addr("111.222.25.36")。发起连接 connect(客户端socket, &连接配置结构体, 配置结构体的大小)。 connect 函数返回值为 int,如果是0 则成功,其他都为错误代码。

struct hostent *remoteHostEnt = gethostbyname([host UTF8String]);//获取IP地址

//查看是否可以解析出来 remoteHostEnt != NULL

struct in_addr *remoteInAddr = (struct in_addr *)remoteHostEnt->h_addr;

struct sockaddr_in socketParam;//创建连接配置结构体

socketParam.sin_family = AF_INET;//与clientSocket保持一致

socketParam.sin_addr = *remoteInAddr;//拿到地址或者直接传入inet_addr("地址")

socketParam.sin_port = htons(port);

3.send(客户端socket, 发送字符串.utf8string, 字符串长度, 发送方式标志一般0),返回值是一个 ssize_t 即字节数,如果成功就返回发送的字节数,失败返回 SOCKET_ERROR

ssize_t sendRet = send(clientSocket, sendString.UTF8String, stringlen(sendString), 0);//向服务器发送数据

4.创建接收数据的容器即缓冲区,并指定大小 uint8_t buffer[1024] 接收数据 recv(客户端socket, buffer, sizeof(buffer), 0表示阻塞必须等待服务器返回数据) 返回值是接收的内容字节长度。创建一个可变数据对象 NSMutableData *dataM,每次把接收的内容添加到 dataM 中,[dataM addpendBytes:buffer length:接收内容字节长度],上面的接收过程放在一个while循环里面做,判断条件是接收字节长度不为0。循环完成之后将 NSData 转化成 NSString,读出来。

unit8_t buffer[1024];

NSMutableData *dataM = [[NSMutableData alloc] init];

ssize_t recvCount = -1;//初始化接受字节长度

while (recvCount != 0) {

recvCount = recv(clientSocket, buffer, sizeof(buffer), 0);

[dataM appendBytes:buffer length:recvCount];

}//直到接收完服务器发送的数据

NSString *htmlString = [[NSString alloc] initWithData:dataM encoding:NSUTF8StringEncoding];

5.close(客户端socket)

close(clientSocket);

socket跟tcp、http的关系

1.socket是支持tcp/ip协议的网络通信基本操作单元,是通信过程中端点的抽象表示

2.为了区别不同的应用程序和连接,每个应用程序与TCP/IP协议交互都提供了socket接口,应用层和传输层通过socket接口,区分来自不同的应用程序进程或者网络连接的通信。

3.socket创建连接时,如果指定的是AF_INET协议,这个socket连接就是一个TCP连接。

4.一般来说socket连接就是TCP连接,会一直保持连接。但是实际开发中会有各种因素例如防火墙、网关等导致连接中断,因此需要采用轮询方式告诉网络这个连接处于活跃状态。

5.http连接是"请求-响应"模式,需要客户端请求后,服务器才能返回数据。如果服务器要主动向客户端推送数据来保持实时同步的话,如果是socket连接就可以保持实时,而http连接需要客户端定时向服务器发送请求,保持在线状态的同时查询服务器是否有最新的数据来保证及时刷新。

TCP和UDP区别

TCP是一种流模式的协议,UDP是一种数据报模式的协议


40.kvo原理

文档原话:

Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

大概意思:使用了 isa-swizzling技术。当一个对象被注册了观察者属性后,这个对象的isa指针就会被修改指向一个中间类,而不是当前这个类。

创建一个对象person,如果 [person addObserver...] 注册一个观察者来观察person对象后,这个person对象的isa指针就被替换了,系统利用runtime修改isa指针指向一个runtime动态生成的类NSKVONotifying_Person,这个类继承自原来的isa类Person。并且为这个runtimeClass重写了setter方法,在setter方法中修改前与修改后分别做出通知调用。当修改观察的key的value时,isa指针找到的是派生类的setter方法,这时候就会触发willChangeValueForKey: 并且记录旧值,在发生改变后 didChangevlueForKey: 会被触发。之后 observeValueForKey:ofObject:change:context: 也会被调用。


41.进程跟线程

进程: 程序执行的的一个实例。资源分配的最小单位,一个进程最少拥有一个线程,可以拥有多个线程。多进程之间是相互独立的,有独立的堆栈空间和数据段,每当开启一个进程时,就会有独立的数据表来维护这个进程的代码段、堆栈段、数据段,开销很大。多进程程序,如果死掉一个进程不会对另一个进程造成太大影响。

线程: 进程的一个实体。程序执行的最小单位,线程如果死掉,那么这个进程就完蛋了。多线程有用独立的堆栈段,但是共享数据段,数据彼此之间使用相同的地址空间,切换速度很快,开销小,效率高。但是多线程是进程中的不同执行路径,一个线程死掉,就代表整个多线程都死掉了,无法并发切换了,也就是这个进程死掉了。

通信机制: 进程相对复杂很多,管道、信号、消息队列、共享内存、套接字等通信机制。线程因为数据段共享,所以很容易通过port等方式来进行通信。

取舍: 需要频繁创建销毁的用线程,进程代价太大了。大量计算并且切换频繁用线程。需要稳定安全,用进程。


42.weak原理

当初始化对象时,会调用objc_initWeak()函数创建一个新的weak指针指向该对象地址。

NSObject *obj = [[NSObject alloc] init];(系统创建 id __weak obj1 = obj;)

系统创建了一个weak列表来管理所有weak引用的对象,把这个指针 obj1 加入这个列表中 objList,同时这个对象也创建了一个弱引用自己的指针列表 pointerList,存放的是所有其他弱引用自身的指针。

当对象 obj1 的引用计数为0时,系统会从 objList 来查询这个 obj1的key。查到以后,找到这个对象,遍历 pointerList,把所有弱引用该对象的指针都置为nil。

第一个全局的weak列表可能是一个NSDictionary,hash表查找,key是obj1地址,value是 pointerList数组。在dealloc中处理所有弱引用自身的地址列表,都指向nil。


43.main()函数运行之前系统都干了什么

1.dyld开始将程序二进制文件初始化

2.ImageLoader读取image,包含了类、方法、符号等等(Class、Protocol、Selector、IMP)

3.runtime与dyld绑定了回调,当ImageLoader完成操作后,dyld通知runtime进行map_images做解析处理

4.然后 load_images 调用 call_load_methods 方法,把所有的类加载进来按照继承关系依次调用+load跟category里面的+load方法

5.所有信息都被加载到内存中后,dyld调用真正的main函数。dyld会缓存上一次的加载信息,所以第二次启动会比第一次快。


44.TCP跟UDP

TCP:

三次握手:

1.客户端发送SYN(syn=j)包给服务端,进入 SYN_SEND 阶段,等待服务端确认。

2.服务端收到syn包,确认客户端的SYN(ack=j+1),并且发一个SYN(syn=k)包给客户端,就是 SYN+ACK包,这时候服务端进入 SYN_RECV 状态。

3.客户端收到 SYN+ACK 包,向服务端再发送确认包 ACK(ack=k+1)。发送完后客户端跟服务端进入 ESTABLISHED 状态,完成三次握手确认。

其中 SYN 是用来同步信息的,ACK 是用来应答的。

三次握手的目的是确定服务端的指定端口,同步客户端与服务端的如窗口大小、序列号等TCP连接信息。客户端在调用 connect() 函数时触发三次握手过程,握手过程中不传递数据。

四次挥手:four-way handshake

1.客户端终止连接,先向服务端发送 FIN 包,告诉服务端现在要终止连接了。

2.服务端收到 FIN 包后,发送一个应答 ACK 包给客户端,告知客户端已经收到中断请求,但是现在不能中断,要等待先前的报文发送完毕后才能终止。

3.服务端发送完先前请求的报文后,紧接着发出一个 FIN 包给客户端。

4.客户端收到 FIN 包,反馈一个 ACK 包告诉服务端关闭连接了。


45.HTTP

http协议是建立在TCP协议之上的一种应用,客户端每次发送请求都需要等待服务端回送响应,并且在请求结束后释放连接。这种连接称作一次性连接,是一种短连接,所以如果要保持客户端在线状态,就需要不断地向服务端发送请求。一次连接现在可以处理多个请求。端口:80。

https是以http为通道,在下层加入了SSL层即安全套接字层(secure socket layer)。在http于tcp之间有一个加密/身份验证层。有一个CA证书验证过程。端口:443。

1.服务端存放公开的公钥证书,客户端获取服务端的公钥证书。

2.本地产生随机密钥做对称加密后,再使用服务端的公钥证书对这个客户端对称密钥做加密。

3.客户端把加密过的密钥发送给服务端,服务端用自己的私钥进行解密获取客户端的对称密钥。

4.之后客户端跟服务端就都有了对称密钥,进行密文通信。


46.如何混淆protocol,例如tableview处于不同的vc中,统计didselect点击事件


47.链表反转

123456 -> 213456 -> 321456 ...

每次记录表头head,将新摸到的插在表头前面,更新表头。

第一次 表头 1 head,当前位置now 1,摸到下一个 next 2,临时 tmp = next->next记录2 原来的next也就是3。把next插入到head之前 next->next = head, head下一个节点指向第三个节点 head->next = tmp。完成后结果就是 213456,然后刷新head位置,head = next。下次循环还是插入head之前,now是跟着一直往下沉的不需要动。循环条件是now->next != NULL。


48.堆排序、归并排序、快速排序

iOS系统默认的排序是堆排序

Android系统下,数值使用快速排序,对象使用归并排序

堆排序:[2,5,10,1,3,9]

1.首先构建完全二叉树,升序做成小堆,降序做成大堆。小堆是指二叉树中所有父节点都小于子节点,大堆反之。每次把子节点跟父节点比较,从最后的 2n+1跟 2n+2 分别与n进行比较,如果不符合条件就交换。

2.构建好二叉树之后,每次取出最后一位与第一位交换,并且输出最后一位。如果是做降序构建大堆,这样就把当前二叉树中最大的数输出来了,从数组中删除删除最后一位,添加到新的接收数组中去。

3.然后再次构建大堆二叉树,每次将数组最后一位与第一位进行交换,每次都把当前树中最大的节点输出,删除、再添加到接收数组中。一直循环下去,直到最后剩下1个节点,直接输出。

4.堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。辅助空间为1。


49.队列的理解

1.下面会输出什么

dispatch_queue_t queue = dispatch_queue_create("11", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{

        YLYLog(@"aa");

        dispatch_sync(queue, ^{

            YLYLog(@"bb");

        });

        YLYLog(@"cc");

    });

输出结果是 aa

因为sync包起来的是一个整体的任务称作task1,丢带串行队列中运行,在执行过程中又遇到了一个新的任务,这个任务是也在这个串行队列中同步执行task2,就相当于把这个任务放在了task1之后。但是因为这是一个串行队列,task1没执行完,就不会继续执行下一个任务,task1执行中遇到了sync的task2,所以这个线程会等待task2执行完才能继续执行task1,很明显形成了一个死锁问题。task1等待task2执行完才能继续打印cc,可是task2在串行队列中必须等待task1执行完才能执行。

2.如果把上面的taks2改成async

 dispatch_async(queue, ^{

            YLYLog(@"bb");

        });

输出结果:aa,cc,bb。cc跟bb的顺序固定,一定是先cc后bb。

原因:在这个串行队列中,task1执行时,添加了一个task2加入到队列末尾,但是在task2是使用async包起来的,异步就代表这个线程不需要等待任务做完才能继续跑下面的代码,所以task2不会阻碍task1下面的打印cc。但是因为队列的原因,task2一定排在task2之后执行。

上一篇下一篇

猜你喜欢

热点阅读