#资源: runloop: hit-alibaba.github.io/interview/i…
GCD:joeleee.github.io/2017/02/21/…
字节跳动的博客:mp.weixin.qq.com/s/Drmmx5Jtj… 基础准备
项目准备
计算机基础
1.什么是宏
宏是一种语义替换,根据预定义的规则将特定的输入转化成特定的输出
2.宏怎么工作
宏在预编译阶段,编译器将宏展开,进行代码替换。
3.定义了重复的宏会怎么样
4.宏具体怎么定义
- 定义对象
#define Height_Tabbar (Height_StatusBar >20? 83:49)
- 定义函数
#define sendMessage(msg) \
({\
dispatch_async(dispatch_get_main_queue(), ^{\
NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];\
[notificationCenter postNotificationName:SendNotification object:nil userInfo:@{@"msg":msg}];\
});\
})
//使用
sendMessage(@"发个消息试试");
//有返回的宏函数定义
#define getSum(a,b) \
({\
(a+b);\
})
总结:定义函数,在除最后一行的后面加,在整个方法外面加().
-
运算符# 把出现在#后面的转化成字符串。 例如 :#define demo1(n) "123"#n NSLog(@"%s",demo1(4,5,6)); 打印结果为123456
-
运算符##
拼接##两侧的为一个符号。
例如:#define weakify(o) __weak typeof(o) weak##o = o;定义弱引用
项目中的用法自定义一个类
#define WBChatCellBasicViewDefineEvent(eventName, viewClass) \
FOUNDATION_EXTERN NSString * const WBChatCellViewEvent##eventName; \
@class viewClass; \
@interface WBChatTableViewModel () \
- (BOOL)chatCellView:(viewClass *)view did##eventName:(WBChatDataRecord *)record; \
@end
#define WBChatCellBasicViewDefineImplementation(eventName) \
NSString * const WBChatCellViewEvent##eventName = @#eventName;
5.宏使用注意事项
- 减少使用,因为增加编译时间。
- 重复定义会使第一个无效。
- 可以使用静态变量的不适用宏。
6.iOS系统常用的宏
- UNAVAILABLE_ATTRIBUTE 告诉编译器该方法不可用,如果强行调用编译器会提示错误。比如某个类在构造的时候不想直接通过init来初始化,只能通过特定的初始化方法()比如单例,就可以将init方法标记为unavailable;
- NSAssert(condition,desc);断言,如果条件不成立,则打印描述信息,并终止程序。
oc基础
1.深拷贝和浅拷贝?
深拷贝如果是拷贝一个数组,里面有对象,能修改里面的对象吗? 里面对象的地址是浅拷贝,如果需要深拷贝,需要归解档生成NSData。
2.atomic怎么线程安全,是绝对安全的吗
不是绝对安全的。atomic在setter和getter方法中都加了锁。对于atomic修饰的读写都是安全的。但是一个对象的线程安全并不只是读写安全。还可能,一个线程将修饰对象release,一个线程去写或读。 对于self.a = self.a + 1;也是不安全的。在x线程执行加号的时候,另一个Y线程读取了a的值。之后a的值更新。Y线程取到的就不是最新的值。
3.响应链?
- www.cnblogs.com/guoshaobin/…
- 响应链是最佳响应者的nextresponse属性构成的一条链。事件通过响应链寻找处理 事件的响应者。如果知道applecation都没有处理,则事件丢弃。
- 获取最佳响应者,事件通过UIWindow向其子视图进行寻找。调用hittest和pointInside两个方法,递归查找最合适的view.
- 事件查找步骤:
- 判断当前视图是否 userInteactionEnable = yes,alpha > 0, hidden =yes。如果是返回nil.
- 判断当前视图是否包含点。如果不包含,返回nil.
- 如果包含点,则倒序遍历subviews.如果没有subviews,返回本身。
- 每个subview,转换点坐标在当前subview.
- subview递归调用hittest.
- 如果找到hitview,返回hitview。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"-----%@",self.nextResponder.class);
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
//判断点在不在这个视图里
if ([self pointInside:point withEvent:event]) {
//在这个视图 遍历该视图的子视图
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
//转换坐标到子视图,self上的point点坐标转换到以subview为基准的坐标。
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
//递归调用hitTest:withEvent继续判断
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//在这里打印self.class可以看到递归返回的顺序。
return hitTestView;
}
}
//这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。
NSLog(@"命中的view:%@",self.class);
return self;
}
//不在这个视图直接返回nil
return nil;
}
点击一次,让hitview的父视图也相应???
3.1如何让一个特定view响应?
重写特定view的hitTest:inView的方法。返回特定的View. 也可以在特定View中重写hitTest:inView。但是可能会被先遍历到的符合条件的view截胡。
4.bounds和frame的区别?
- frame是视图相对于父视图坐标系统的位置信息;bounds是视图以自己为坐标的位置信息。
- frame改变bounds,影响子视图的frame,不影响自己。改变自己的bounds,相当于改变子视图的坐标系。
- 旋转view,frame改变,bounds不变。如果子视图没有一起旋转,那么frame改变,如果一起旋转,则frame不变。
- 缩放view,frame的origin和size都改变,bounds的origin不变,size改变。/r
- scrollview怎么实现的:不断改变外层视图的bounds,内部视图就会自动滚动。
5.super调用的底层实现?
- [super selector]执行,首先生成一个结构体 struct objc_super2{ id receiver;//实际消息接受者 Class current_class;//接受者的类对象 }
- 系统调用objc_messagesendsuper(objc_super2,sel)方法。第一个参数为生成的结构体,第二个参数为调用的方法。
- 通过superClass指向父类的方法列表,查找方法名。找到,用receiver调用。
6.selector和SEL,IMP分别是什么?
IMP是函数的指针 @selctor () 生成的是SEL类型的消息名称。 SEL是消息名称的类型。
7.协议声明属性?
协议中声明属性只有对应的setter和getter方法,没有实例变量。而setter,getter方法就是操作的成员变量,所以调用会导致崩溃。想要调用的话,需要自己声明成员变量, @synthesize cellType = _cellType;
8.子类能不能继承父类的实例变量?
如果实例变量没有公开,没有在.h中声明,则不能使用。 如果只公开了属性,也不能直接使用实例变量。
9.类的扩展是否可以写多个?
可以,可以在多个扩展中实现不同的方法。
KVO
- KVO原理
- 当类的某个属性添加addobserve的时候,系统利用runtime创建该类的子类,NSKeyNotifyning_类名.
/**
* 创建一个新类和元类.
*
* @param superclass 这个类是新创建的类的父类,可以传入Nil去创建一个新根类.
* @param name 这个字符串是类的名字(例:"NSObject")
* @param extraBytes 一般传入0
* @return 新的类,如果返回的是Nil,那么就是这个类创建失败了(例:创建的是"NSObject"类,然而这个类已经存在了)
*/
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
size_t extraBytes)
/**
* 注册使用`objc_allocateClassPair`方法创建的类
*
* @param cls 需要注册的类(不能为Nil)
*/
objc_registerClassPair(Class _Nonnull cls)
- 将本类对象的isa指针指向生成的子类.
/*
指定isa指针
*/
object_setClass(self, subclass);
- 在子类重写监听属性的setter方法,setter方法中,赋值前加willChange,赋值后添加didChange方法.
- 执行两个方法后,内部会触发observeKeypathdidChange方法.
-
自己写一个kvo,怎么写? 自己可以利用runtime实现,类似于Aspects原理.
-
kvo监听实例变量或不用点方法赋值都检测不到变化
-
kvo不romove为啥会崩溃?
-
kvo监听kvc能否成功? 能,因为kvo监听该属性后,会生成setter方法。而kvc的原理是如果没有找到该属性,就会去查找属性的setter方法,会找到kvo生成的方法。完成调用。
kvc原理
block的问题:
1.block作为参数的时候是怎么引起循环引用的?用__block避免循环引用靠谱吗?
- 因情况不同而定,主要看有没有互相持有.像一般的request的callback持有self.但是self不持有callback.所以,不加weakself的时候,callback回来还会执行里面self的东西.如果加上weakself,则不会执行callback中self的操作.
- __block 修饰变量,在block实现中需要将变量置为nil,并且执行block,才可以避免循环引用。
2.为什么执行block之前需要先判断这个block存不存在?
- 因为不判断,如果block不存在会崩溃,抛出读取某块地址出错.
- 因为block的结构体中有指向具体实现的函数指针.没有这个实现,指针就会指向一块什么都没有的地址.就会引起崩溃.
3.为什么要加__strong?
为了保证self在block中生命周期是完整的.block不持有self,self有可能被释放掉.所以通过__strong对self的引用计数+1.
4.为什么系统的usingBlock和GCD的block不需要使用weakself?
因为不会造成循环引用,使用self,block会持有self.但是self不会持有block.
5.block的格式
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
6.block的分类?
| NSGlobalBlock | NSStackBlock | NSMallocBlock |
|---|---|---|
| 不捕获auto变量 | 捕获auto变量 | [NSSStackBlock copy] |
4种情况是自动拷贝到堆的block,属于NSMallocBlock
- block作为函数的返回值
- block使用__strong修饰
- cocoa API使用usingBlock做为函数参数
- GCD使用的block作为参数.
7.为什么__block修饰对象之后可以修改内部值?
- 因为__block修饰的对象会生成一个回溯指针,指向其原来的地址. 可以通过这个指针去修改.
- 用__block修饰的变量,会生成一个对象,对象结构体中包含__isa,__forwarding,__size,__var(使用值)。forwarding对指向捕获对象的地址。
- __block修饰全局变量或static变量会报错。
- __block变量和auto变量在栈上不会被强引用,在堆上会被强引用。
8.没有__block修饰的对象可以修改其属性吗?
可以.不能修改对象指针的指向.没有__block修饰的NSMutableArray也可以修改内部值.
9.block是什么?
- block本质是oc对象,内部有isa指针。
- block是包含了函数调用和函数调用环境的oc对象
10.block捕获变量
- 局部变量,block捕获的是值。
- static变量捕获的是指针。用__block修饰会报错。
- 全局变量不被捕获。用__block修饰会报错。
runtime
1.oc的消息机制
oc接受到消息,执行底层objc_msgSend方法.通过三层
消息发送 :查找方法,并执行.如果没有找到.则进入到动态解析(可以说的详细点.)
动态解析 :以前解析过的,直接使用.
消息转发:进行消息处理.如果没有处理,直接crash.
2.消息转发机制流程是什么样的?
第一步动态解析,调用resolveInstanceSelector方法,判断有没有动态实现.如果没有进入第二步.调用forwardingTargetSelector返回转发对象.如果没有返回转发对象,进入第三步,完整消息转发,生成方法的签名,然后转发消息.
3.什么是runtime,平时用在哪里?
runtime是运行时调用的底层api,实现很多动态函数. 用途: 消息转发, 分类添加实例变量, hook方法, 获取私有的成员变量.
4.计算结果 ???????
NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]);
NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]);
NSLog(@"%d", [[MJPerson class] isKindOfClass:[MJPerson class]]);
NSLog(@"%d", [[MJPerson class] isMemberOfClass:[MJPerson class]]);
分析:
- 第一个: 前:NSObject类对象 比较的是NSObject的元类和NSobjectr的类对象.基类的元类是基类的子类.所以正确.
- 第二个: memberClass必须前后类完全一致.
- 第三个: 前面 : MJPerson的元类 后面:MJPerson的类对象. 但是他俩之间没有子类和父类的关系.
- 第四个: memberClass必须前后类完全一致. isKindOfClass 内部实现会对前面的调用对象进行一次【 class】操作。 结果: 1 0 0 0
class的实例方法和类方法不同
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
isMemberOfClass的类方法和实例方法的不同:类方法需要对调动者进行isa取值,实例方法需要对调用者取类对象.isKindOfClass方法一样。
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
isMemberOfClass和isKindOfClass区别在于:isMemberOfClass 调用者进行isa或类对象取值之后和传入cls参数进行等号判断。isKindOfClass进行的是循环和父类比较
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
5.为什么分类不能添加成员变量,可以添加属性?
因为类对象的结构体ivars是const修饰的一维只读数组. 属性是可读可写的二维数组.
6.class结构体包含哪些信息?
- class结构体包含isa指针,记录了类的基本信息。
- 父类superClass
- 方法缓存cache,内部用哈希表(bucket)缓存使用过的方法。
- class_rw_o结构体。class_rw_t结构体中包含方法列表,属性列表,协议列表,都是二维数组,是可读可写的,class_ro_t结构体。
- class_ro_t结构体中包含baseMethodList、baseProtocols、ivars、baseProperties,是一维数组,只读的。记录类的初始内容。
category
1.关联对象存储在哪里
-
关联对象存储在全局的AssociationManager管理的hashmap中,hasmapkey为disguised_object(value地址值转为unsigned long 类型,再逐位取反),value为该对象的关联对象map.关联对象map对应的为设置的key,和association结构体.association结构体存储policy和value.
-
关联对象在对象执行dealloc的时候,会被清除。
2.分类的加载步骤
runloop
###1.runloop的理解 runloop是一个对象.以NSRunloop和CFRunloop的形式存在.他可以保持一个app持续处于运行状态,处理app中的各个事件,节约cpu的耗能(一会休息,一会运行).
2.runloop和线程的关系
- runloop和线程是一一对应的关系,以key,value的格式存放在全局字典中.
- 当获取runloop的时候,才会创建该线程的runloop.有一个监听回调,线程结束,runloop销毁.
从代码中可以看出,如果是第一次获取,则会先获取主线程的runloop
3.runloop和autoreleasepool的关系
autoreleasepool基于runloop创建.observer在loop entry状态的时候,创建autoreleasepool.在休眠之前销毁autoreleasepool,然后创建新的autoreleasepool.在exit的状态下,清除autoreleasepool.
6.source0 和source1有什么不一样?
- source1包含一个mach_port 和一个回调,会主动唤醒线程.处理系统事件.
- source0只包含一个回调,不能主动触发事件,先调用
7.runloop的几种状态?
entry -- beforeTimes -- beforeSources -- beforeWaiting -- afterWaiting -- exit 进入loop - >即将处理Timer ->即将处理source ->即将进入休眠 - >即将唤醒 ->退出loop
8.runloop的各种作用?
- 保持程序的持续运行
- 处理系统的各种事件 :手势事件,定时器事件.
- 节约cpu的耗能,有事情的时候处理事情,没事情的时候休眠.
9.mode在runloop中的作用?
- CFRunloopModeReff是runloop的运行模式.
- 一个runloop有多中mode,一个mode汇总有多个source0,source1,timer,observer.
- runloop一次只能运行一种mode.
10.mode常见mode?
UITrackingRunloopMode, kCFRunloopDefaultMode是常见的两种mode
11.为什么设置为common之后就可以执行两种mode.
被标记为common的mode,系统会将commonItems中的事件添加到common标记的mode中,从而可以同时执行两个mode中的事件.
12.runloop在app中的应用?
- 线程保活,场景:反复在子线程重复做事情.传统每次做事情都要创建一个线程.现在可以在一个保活的子线程中做,前提是任务是非并发的情况下执行.AFN中使用了这种方式.
- 解决NStimer在页面滚动的时候继续计时
- 使用CADisplayLink监控卡顿
13.线程保活的步骤
- 首先创建一个子线程
- 添加nsport在子线程中
- 使用runmode让线程run起来.
- 添加操作在子线程中.
- 在合适的时机停止runloop.
14.runloop的工作机制?
runloop就是进入某一种mode,拿出这种mode下的各种事件处理 /// 用DefaultMode启动 void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); }
/// 用指定的Mode启动,允许设置RunLoop超时时间 int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }
/// RunLoop的实现 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
15.怎么找到runloop在底层的入口?
touchesbegan中打印东西,打断点在touchesbegan,lldb中bt查看调用栈。 可以找到入口为CFRunloopSpecific.
16.用performSelector去调用多个参数方法怎么做?
将方法包装成NSInvocation,然后invocation去invoke启动调用。
-(id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects{
NSMethodSignature *sig = [PerformSelectorWithArguments instanceMethodSignatureForSelector: aSelector];
if (sig == nil) {
NSLog(@"没有此方法");
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.selector = aSelector;
invocation.target = self;
//从第二位是因为去除self,sel.
for (int i = 0; i < sig.numberOfArguments - 2; i ++) {
NSString *str = objects[i];
[invocation setArgument:&str atIndex:i+2 ];
}
[invocation invoke];
//添加返回值
id returnValue = nil;
if (sig.methodReturnType) {
[invocation getReturnValue:&returnValue];
}
return returnValue;
}
-(NSString *)testWithParam:(NSString *)p0 param1:(NSString *)p1 param2:(NSString *)p2{
NSLog(@"p0 : %@,p1 : %@, p2:%@",p0,p1,p2);
return @"返回值";
}
多线程
1.队列组的作用?
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
// 添加异步任务
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1-%@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4-%@", [NSThread currentThread]);
}
});
任务2和任务1并发执行,最后执行任务3.任务4.任务3,任务4.并发交替执行.
2.多读单写的线程安全法
实现可同时读,不可以同时读写,不可以同时写.
- 使用pthread_rwlock读写锁可以实现
- (void)read {
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)write
{
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- 使用dispatch_barrier_async
for (int i = 0; i < 10; i++) {
[self read];
[self read];
[self read];
[self write];
}
}
- (void)read {
dispatch_async(self.queue, ^{
sleep(1);
NSLog(@"read");
});
}
- (void)write
{
dispatch_barrier_async(self.queue, ^{
sleep(1);
NSLog(@"write");
});
}
注意: -使用berrir表示此时有且只有这一条线程执行任务. -读和写在同一个队列. -队列必须是自己创建的并发队列,berrir才能发挥这个作用.如果传进去的是串行队列,则相当于没有使用berrir.
3. 全局队列和自定义并发队列的区别
- 全局队列是唯一的,即使生成多个,也只有一个地址.
- 并发队列即使名字不一样,地址也不同.
4.锁
自旋锁和互斥锁.
5.死锁的情况
定义: 两个任务互相等待.
- 主线程中同步执行一个任务.
- NSLock,多次上锁
- 在串行队列中sync一个任务.
6.线程安全的手段
- 加锁
- 信号量
- 栅栏
7.信号量
作用: - 加锁
- 控制最大并发数 具体操作:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//临界区,即待加锁的代码区域
dispatch_semaphore_signal(semaphore);
});
}
8.GCD控制网络请求顺序
9.dispatch_once—的实现原理?不使用gcd实现单例?
oncetoken为0时,执行block. oncetoken为-1时,跳过block. oncetoken为其他值时,阻塞线程,等待oncetoken的值变化。
oncetoken第一次执行代码时,值为0,执行block.执行的过程中,值变为140734537148864。处于等待,阻塞线程。
具体源码分析:www.jianshu.com/p/b25535a74… 有时间再研究。
#import "Person.h"
static Person *person = nil;
@implementation Person
+(instancetype)sharedSingleton{
if (!person) {
@synchronized (self) {
if (!person) {
person = [[super allocWithZone:NULL]init];
}
}
}
return person;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
return [Person sharedSingleton];
}
-(id)copy
{
return self;
}
-(id)mutableCopy
{
return self;
}
@end
内存管理
1.CADisplayLink和NSTimer的区别
-执行频率不同 : CADisplayLink屏幕刷新一次,就执行一次selector.NSTimer可以直接在初始化方法设置.
- 精确度不同: CSDisplayerLink在正常情况下都会在屏幕刷新完之后执行.NSTimer在runloop阻塞情况下,会在下一个周期执行.
2.内存的几大区域
- 栈区 (高地址) : 局部变量
- 堆区 : 通过alloc,malloc创建的对象
- 数据区 :(初始化,未初始化)全局变量,(初始化,未初始化)静态变量,字符串.
- 代码区(低地址) :编译之后的代码
3.对iOS内存管理的理解
iOS内存是通过引用计数器管理内存的.遵循谁持有,谁释放的原则.
4.ARC帮我们做了什么?
- retain操作,引用计数 +1
- release操作,引用计数-1
5.weak指针实现的原理
weak修饰的对象,将该对象和该指针存在一个哈希表里.key为改对象,value为改指针.当该对象的引用为0的时候,清除key和value
6.autorelease对象在什么实际会调用release?
在runloop休眠之前,会对多有对象进行pop操作 loop退出之前,会对对象进行pop操作.
7.方法中的局部变量,出了方法后会立即释放吗?
不是,如果当前loop没有退出,还会持有.
8.tagedPointer的原理和好处
- tagedPointer用来存储NSNumber,NSString,NSDate小对象.
- 传统NSNumber需要动态分配内存在堆中,指针中存放的是堆内存的地址.tagedPointer的指针存放的是tag+ data.将值直接存在指针中,不需要分配堆内存.指针中存不下数据时,才会动态分配堆内存.
- 好处:当取数据的值时,直接从指针中取,节约调用开销.
- 判断一个指针是不是tagedPointer,iOS中,最高有效位开头为1.
9.执行结果是什么?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
会崩溃 因为底层是调用set方法,set方法转化为mrc下,会先release老的_name.再将新的name copy给_name.
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
之前的name在多线程下会被释放两次.所以会崩溃.
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
会正常执行 因为短的字符串会使用tagedPoint存储.存储在指针中.所以不会调用set方法.故不会有release和retain操作.
9.引用计数的存储方式
64位架构中,直接存储在isa的指针的. 如果不够存储,则存储在sideTable散列表中refcountMap中.
objc_object::rootRetainCount()
{
assert(!UseGC);
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
if (bits.indexed) { //判断是否是优化过的isa指针.
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) { //引用计数不是存储在isa中,而是存储在sidetable中.
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
10.autorelease在什么时候释放?
背景知识: @autoreleasepool{
}在本质上是在开头调用autoreleasepoolobj = objc_autoreleasePoolPush(); 在结尾调用 objc_autoreleasePoolPop(autoreleasepoolobj);
push方法和pop方法做了什么? autorelease对象通过autoreleasepoolpage管理,每个autoreleasepoolpage有4096byte.存放内部成员变量,剩下的空间存放autorelease对象的地址。 page和page之间通过双向链表的形式连接在一起。通过parent和child指向来连接上一个和下一个。next指针永远指向能存放下一个autorelease对象的地址。 begin()函数是开始存储autorelease对象的开始地址。 end()函数是结束地址。 push()方法,将一个POOL_BOUNDARY放进链表中,并将该地址返回。 pop()方法,将该autoreleasepool中的所有autorelease对象进行release操作。直到POOL_BOUNDARY地址。嵌套的autoreleasepool一样的。 runloop注册两个observer,一个监听kCFRunloopEntry ,进入runloop调用push函数。 一个监听kCFRunloopBeforeWaiting | kCFRunloopExit,进入休眠之前,先进性pop,再进行push.推出runloop之前,进行pop操作。
所以,是在autoreleasepool对象调用相对应的pop函数的时候释放。在遇到自己所处的autoreleasepool的下括号的时候调用pop函数。或者由所属runloop本次休眠之前调用release.
11。计时器会引起内存泄漏,怎样避免?
CADisplayLink,NSTimer产生强引用。 计时器和self之间会产生强引用。 即使使用weakself,NSTimer内部也会强引用self. 解决办法:使用中间变量。 NSTimer - 强-> 第三方 -弱-》vc -强-》NSTimer
12 。静态分析和动态分析的区别?
静态分析是在不运行程序的情况下,通过上下文语法分析内存泄漏。比如,未使用初始化的变量。 动态分析,需要在运行程序,分析用户每一步操作的内存情况。
13,检查内存泄漏的方式?
第一种,静态分析。 第二种,采用xcode的leaks工具,进行动态分析。 第三种,采用例如MLeakFinder的第三方工具分析内存泄漏。 以MLeakFinders为例,以vc为单位,在vc销毁时,hook vc的退出方法,在hook方法执行完成之后,dispatch_after 2秒,用weakself持有self,并执行self的一个方法。如果能执行成功,则表示有内存泄漏,如果不能执行成功,说明self已经释放。
14.下面的变量存在哪????
- (void)viewdidload{ int a = 1; int b = a; NSString *str = @"123"; }
a,b存在栈区,
str :如果是taged point存在栈区tag+data,@"123"也在栈区。如果不是taged point,str存在栈区,@“123”存在常量区。
15.内存碎片什么情况下会发生?
16. MRR 和ARC的区别?
不同点: MRC:需要自己管理创建的对象内存。 ARC:系统在编译期将内存管理函数自动插入到代码中,无需自己管理.
共同点:多使用引用计数器管理内存。
17.autoreleasepool在系统中的应用场景?
可以用在想要释放对象所有权,但又不希望立马释放对象所有权时使用。例如:方法的返回值。
18.怎样去调试分析内存问题?
编译阶段,使用 Clang Static Analyzer 如果内存问题还是增长,可以使用NSZombie,
19.dealloc中引用self会有什么后果,分贝用weak和strong修饰呢?
weakself 在dealloc中修饰self,会直接导致崩溃,因为weak的赋值机制会判断,如果对象正在释放中,则直接崩溃。具体源码解释 strongself修饰self,可能会导致崩溃,野指针。因为对象可能已经释放掉,引用计数不会加1,strongself指向一个未知内存,并调用方法,导致崩溃。
架构
1.讲一下mvc和mvvm,mvp?
mvc:m是model数据模型,v是view视图层,vc是视图控制器。 vc持有view,持有model.在vc中建立model和view的连接。view中持有model,建立 view和model的映射。 优点:对apple版的mvc,对vc进行了瘦身,将vc的一部分工作放在了view中做。 缺点:view对model依赖,增加了耦合性 mvp:m是数据模型,view是视图层,p是present中介。 p持有vc,生成view和model.将view和vc进行关联,view和modle进行映射。 优点:view可重复利用。vc不重。 mvvm:m是数据模型,view是视图层,vm是持有vc,生成view和model,映射view和 model.view持有viewmodel,并监听viewmodel的model属性。 优点:耦合性低,view可以复用。
- (instancetype)initWithController:(UIViewController *)controller
{
if (self = [super init]) {
self.controller = controller;
// 创建View
MJAppView *appView = [[MJAppView alloc] init];
appView.frame = CGRectMake(100, 100, 100, 150);
appView.delegate = self;
appView.viewModel = self;
[controller.view addSubview:appView];
// 加载模型数据
MJApp *app = [[MJApp alloc] init];
app.name = @"QQ";
app.image = @"QQ";
// 设置数据
self.name = app.name;
self.image = app.image;
}
return self;
}
2.mvc和mvvm的不同?
- mvc代码少,简单,但是vc繁重。
- mvvm将vc中的部分代码拿到vm中实现。vm实现view和model的映射和,负责view和vc建立关系。解耦model和view.
3.什么是架构?
是一种设计方案,可以是类与类,模块与模块的关系
4.mvp和mvvm的区别和相似点?
mvp和mvvm都是将vc中的工作放在一个中间类中。 mvvm会增加在view中监听vm的数据变化。
数据结构
1.数组和链表的区别?
| 数组 | 链表 | | ------ | ------ | ------ | | 静态分配内存 | 动态分配内存 | | 内存连续 | 内存不连续 | |插入删除比较低效| 插入删除比较高效| |查询时间复杂度比较低为o(1) |查询时间复杂度比较高,为o(n)| |插入,删除时间复杂度为o(n) |插入,删除时间复杂度为o(1)|
2.可变数组怎么分配内存,怎么实现扩容呢?
1.在堆内存中分配内存存储数组的容量,长度,指向数组的首地址指针。 2.在堆中开辟一块存储数据(线性表存储)。 3.如果容量不够,则开辟另一个空间,赋值之前的数据,销毁之前的空间。
3.hashmap在oc中怎么实现?
字典的实现就是用hashmap实现的。
项目
1.你们项目有多少用户?日活多少?
2.你在项目中负责什么?
1.负责我的tab。 2.负责性能优化(启动时间,包瘦身) 3.负责部分h5和需求开发工作 4.负责建立混合私有库 5.负责作业模块
3.你做的最好的是哪块?
1.是最近做的一个联动表单。实现了从两周的工作两天完成。
2.混合框架私有库 主要作用:赋予h5一些原生功能;传递给h5原生的数据;h5和原生的交互。 app://xindongfang/?apppush&
4.你觉得你那块比较擅长?
准备h5的性能优化和缓存的知识。h5像原生一样流畅,加载时间快。
第三方库
1.讲一个你熟悉的第三方库?
软性问题
1.有什么优点和缺点?
优点:自主学习能力比较强。 逻辑比较缜密,思考问题比较全面。
缺点:知识广度有待加强。爱生气(但不是跟同事)。
2. 职业规划?
1.学习一个脚本语言,写脚本。 2.在项目中,找出一个自己可以深入挖掘的点。 3.加强对编译器和链接库的了解。
3. 为什么离职?
因为公司架构调整,不适合我了。---被合并到别的组。
4. 工作流程是什么样的?
需求评审 -- 需求分配 -- 技术方案制定,评审 -- 技术方案不过关再单独审 -- 开发需求 -- 代码评审 -- 自测 --提测。
5.和别人比你的优势是什么?
1..我的bug率低。因为我比较严谨且代码强壮。
2.我会写h5,会写iOS,可以交叉补位。还对h5和原生的交互比较熟悉。
网络
1.怎样防止抓包工具抓包?AFN怎么实现的?
2.NSURLSession实现断点续传?
3. 网络七层都有啥?每层的作用是什么
物理层 - 数据链路层 - 网络层 - 传输层 - 会话层 - 表达层 - 应用层
物理层:网络传输的物理设备,将0,1的信息编码转化为电流强弱进行传输。
数据链路层:负责跨网络链接的
网络层:负责查找ip地址
传输层: 负责数据的可靠传输
会话层:
表达层:
应用层:负责端到端的链接。
什么是http?
http是超文本传输协议,是计算机世界里字体两点之间传输包含普通文本,图片,视频等超文本数据的协议,规范。服务端和服务端之间也可以传输超文本,不只在服务端和浏览器。
http的状态码有什么?
2** :请求成功,正在处理报文
3** :重定位,需要客户端重新请求
4** :客户端报错,服务端无法处理
5** :服务端报错,服务端处理请求时发生了内部错误。
http的相关字段有什么,表示什么意思?
host :指定服务器的域名
content-Type:返回的数据格式
connection:是否持久连接,一般设置为keep-alive,兼容HTTP 1.0
content-encoding:返回数据解压缩方式
content-length:返回的数据长度
get和post的区别?
http的优缺点?
缺点:明文传输,不验身份,无法保证报文完整性
优点:简单
4. Http和https的区别
https 是http之前进行SSL/TLS握手之后,再进行http通信。
提高数据传输安全性和数据完整性。
安全性通过非对称加密认证双方身份,对称加密传输数据.
数据完整性通过摘要算法对数据明文加密,服务端解密,并使用摘要算法加密,查看是否加密结果一致。
6. 抓包的原理是什么?
客户端安装charles证书中心,charles截获服务器的证书,发送自己的证书给客户端。这样,客户端将charles作为服务端进行通信。服务端将charles作为客户端进行通信。 详细介绍
6. 怎么避免抓包
使用https进行通信,同时不随意安装根证书(证书中心).
7. socket,长连接,心跳逻辑。
8. 断点续传是怎么做的
9. ssl是否了解,建联过程
等同于下方https的建立过程
10. 发了一个HTTP:post请求,主要经历几个步骤。请求包含哪些内容。
需要经历三次握手,4次挥手。 请求包含host,accept-encoding,accepcppt-type,connection
11. http的基本概念:重传机制,滑动窗口什么的
重传机制:
- 超时重传
方式:当报文syn超出时间没有接收到回执报文,则认定报文数据丢失,重新传送数据。
解决的问题:报文丢失可找回。
问题:如果rto(超时时间)过长,则重发太慢,性能差。 - 快速重传
方式:当连续收到3次相同的ack回执,则认为ack回执之前的报文丢失,触发重传。
解决的问题:报文丢失可在3次回执之后重发,效率较高。
问题:有可能因为网络延迟,收到重复的数据包 - sack
方式:回执报文会将收到的地图发送给发送方,发送方根据地图确认哪块数据丢失。
解决的问题:可确定具体报文丢失的段。
问题:不能确认是 - D-sack
方式:同sack
解决的问题:解决通知发送方哪些报文重复发送了。
滑动窗口
定义:无需确认回答,可以继续发送数据的最大值。
确认字段:tcp头中的window。
由谁决定窗口的大小:由接收方决定窗口的大小。
发送方的滑动窗口组成和滑动:
由4部分组成L:发送出去并且收到应答的数据,发送出去没有收到应答的数据,未发送可以发送到数据,暂时不能发送的数据
接收方的滑动窗口组成和滑动:
由3部分组成:接受到并回应的数据,未接收到但可以接收的数据,不可以接收数据的部分。
接收端和发送端的窗口大小是相等的吗?
差不多,如果应用读取数据速度变快,接收端窗口变大,会和发送端同步新的窗口大小,但这会有时间差。
流量控制
-
目的:让发送方根据接收方的实际接受能力发送数据量。
-
流量控制的方式和场景: 方式:通过控制窗口的大小实现控制流量。 场景:
-
窗口关闭的概念和隐患及解决办法: 概念:当接收方窗口大小设置为0时,发送方会关闭窗口。 隐患:当接收方再次发送非0的窗口设置时,报文丢失,发送方未接收到重启窗口通知。接收方和发送方处于互相等待状态,发生死锁。 解决办法:当发送方接受到窗口为0的设置时,开始定时器。超时向接收方发送窗口探测报文,对方在收到探测报文时,给出窗口大小。
-
糊涂窗口综合征: 现象:当接收方来不及处理数据时,会导致发送方发送的窗口越来越小。导致发送的数据量越来越小。 解决办法: 接收方:避免发送小窗口,当窗口大小小于(mss,缓存空间/2)时,会向发送方通告窗口为0. 发送方:避免发送小数据,等到窗口大小大于一定值,以及受到之前发送数据的ack回包之后,发送数据,之前一直囤积数据。
拥塞控制
- 目的:避免发送方的数据填满整个网络
- 方式:发送方维护一个状态变量叫拥塞窗口。发送窗口的值是拥塞窗口和接受窗口的较小值。
12.tcp和udp的区别?
tcp是可靠传输,拥有流量控制,超时重传,拥塞控制。 udp只管传输数据,是否接受到,是否完整接受,不在考虑范围。适用于广播式的传输。
13.https的建联过程?
自己总结的流程:
-
服务端将自己的公钥注册在ca机构进行签名,生成证书。证书中包含发给客户端的公钥。
-
客户端发起client hello,发送c随机数,加密方式,TLS版本号值服务端。
-
服务端发送数字证书和s随机数,以及加密方式至客户端。
-
客户端收到数字证书,使用ca机构的公钥解密证书,得到服务端的公钥。客户端用服务端的公钥加密pre_master报文发送至服务端。
-
客户端将s随机数,c随机数,premaster三个随机数组成秘钥发送给服务端,告知通信加密方式。且finished
-
服务端使用私钥解密报文,将s随机数,c随机数,premaster三个随机数组成秘钥,改用秘钥进行通信。并发送finished
-
客户端发送 ack.
-
进入http通信阶段,双端使用会话秘钥加密报文。
公众号总结的流程
1. client hello,向服务端发送
a. c随机数
b. 支持的TLS版本
c. 支持的密码套件列表。
2. server hello,向客户端发送
a. 确认的TLS版本
b. s随机数
<
c. 确认的密码套件列表
d. 数字证书
3. 客户端回应
a. 用CA私钥验证数字证书的真实性,取出服务器的公钥
b. 一个pre_master(用于合成会话秘钥),用服务器的公钥加密 pre_master.
c. 加密通信算法改变,表示以后会使用会话秘钥进行通信。
d. 将以上的数据做过额摘要发送给服务端,并表示会话结束
4. 服务器响应
a. 通过三个随机数,生成会话秘钥。加密通信算法改变通知,表示随后的信息使用会话秘钥加密通信。
b. 发送之前所有的内容作为摘要,发送客户端以供校验。同时表示握手结束。
14.http的演进过程
算法
1.二叉树的后序遍历(非递归)?
private static ArrayList<Integer> postOrder(TreeNode root) {
if(root==null) return new ArrayList<Integer>();
// 存储:"根右左"的遍历顺序
Stack<Integer> reverseRes = new Stack<Integer>();
Stack<TreeNode> s = new Stack<TreeNode>();//辅助栈,保存待遍历的节点
s.push(root);
while (!s.isEmpty()) {
TreeNode tem = s.pop();
reverseRes.push(tem.val);//存储:"根右左"的遍历顺序,先入"根"节点
// “右左”的遍历顺序,所以在栈(LIFO)中对应的就是:先进"左",再进"右"
if (tem.left != null)
s.push(tem.left);
if (tem.right != null)
s.push(tem.right);
}
ArrayList<Integer> res = new ArrayList<Integer>();//获得“根左右”的遍历序列
while (!reverseRes.isEmpty()) {
res.add(reverseRes.pop());
}
return res;
}
2.二叉树前序遍历?
public static void preOrderIteration(TreeNode head) {
if (head == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(head);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.print(node.value + " ");
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}
3.二叉树中序遍历?
public static void inOrderIteration(TreeNode head) {
if (head == null) {
return;
}
TreeNode cur = head;
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || cur != null) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode node = stack.pop();
System.out.print(node.value + " ");
if (node.right != null) {
cur = node.right;
}
}
}
//cur :储存入栈的变量
//node:存储出栈的变量
//1.保留树根节点 2.遍历节点的左子树,直至没有左节点
//3.pop出栈 4.将pop节点的有子树入栈。
4.两个链表相交,求交点?
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
5.链表是否有环?环的第一个节点是哪个?
- 使用快慢指针,快指针在走两步,慢指针走1步。如果有环,总会相遇。如果没环,则会指向null.
- 如果有环,两个指针第一次相遇.f:快指针步数,s:慢指针步数。a:环前面的节点数 b:环后面的节点数。f = 2s ; f = s + nb. ====》s = nb ; f = 2nb.
- 则fast指针指向头,向前走a步,则慢指针走a+nb步,两者相遇刚好是环节点。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (true) {
if (fast == null || fast.next == null) return null;
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
6.链表的反转?
public:
ListNode* reverseList(ListNode* head) {
ListNode* cur = NULL, *pre = head;
while (pre != NULL) {
ListNode* t = pre->next;
pre->next = cur;
cur = pre;
pre = t;
}
return cur;
}
};
7.二分法
- 只要面试题里给出的是有序数组,想想是否可以使用二分法。