2018-9-27lesson

Sep 27, 2018

最近是面试期么

内心一定要淡定

记录下最近面试问到的一些问题

之前从粉粉离开时,也没想到这是一个多事之秋。去了一家动漫IP 的公司,人没去之前老板已经有意决定停下互联网业务的,结果其中一个合伙人说再试一波,于是我以为其是想做事,就进去当小白鼠了。进去之后改版页面完,没目的没计划了。做了一个egret ”消消乐“的微信小游戏,学了该学的OpenGLES,然后中秋之前跟我说部门解散,,,

大哥,这才三个月啊。

所以说是一个多事之秋吧。

好吧,出来面试吧。

现在是9月份,面试第一个公司:”第一弹“,我个人觉得面试过程还好,然后说不合适。好吧,不合适就不合适吧,可惜了,距离我挺近的,比较看重距离。

面试题里面有个题目:[1, … 100] 内 不等随机大小无序数,求最大的数和第二大的数。

这个我做的时候直接是冒泡排序。但是有两层for 循环,效率不好,下面会给出最佳答案。

第二家说想做一个短视频、即时通讯、webRTC 这一类产品。我不看好,不能跟,而且离得有点远,开发员工才几个人,工资也很低。大哥,做这类的工资不可能才15k 啊,,,做成个半成品,全是SDK 的,我不愿意把青春花在简单组合各类SDK上面,做完到时发现不赚钱,老板说解散,到时我又屁颠屁颠要出来找工作,何必。后来,之前面试的小哥打电话过来问意愿,那边那个老板也打电话过来问意愿,只能拒绝。没有投靠的意义,看的出来老板只是想试试水。

第三家在之前我就有去面试的地方:徐汇慧谷创业中心。这家是做 AI提供用户数据给出用户穿衣服方案的公司。在找iOS 开发。先来个小胖哥,上来就说我看了你的博客了,还挺好,直接让技术经理来考你。这小胖哥有意思,人好。然后技术大哥来了就问技术的事,不与iOS 相关的,先问TCP协议(我没看面试题之类的(最近在搞网页)答得不好),然后出了两个题目:

  1. 数组array[1~n] 随机大小无序,求从大到小第k个数。解法:
1
2
3
4
5
6
7
8
9
10
let nums[n] = [0];
for (int i=0;i<array.count;i++) {
let element = array[i];
if (nums[element]) {//如果有值
nums[element] = nums[element] ++;
} else {
nums[element] = 1;
}
}
//这个时候从n 到 1 如果内部是>1 的都是有值的,>2那就是有两个值. 于是第k 个值也能得出。

这正是“第一弹”笔试的比较好的答案吧。

  1. 有一个圆盘时钟,从00:00:00 ~ 24:00:00 过程有多少次秒针、分针、时针是全部重叠的。
1
2
3
4
5
6
7
8
let s = 360°/60 * n; //秒针一圈第n秒时在什么角度
let m = 360°/(60*60) * n; //分针一圈第n秒时在什么角度
let h = 360°/(12*60*60) * n; //时针一圈第n秒时在什么角度
for (int n=0; n<24*60*60; n++) {
if (s / 360° == m / 360° == h / 360°) {//除以360° 是让其点化为单位角度
// 这就是相遇的点
}
}

这两个挺考智商的。但是我的脑子不好,我的学习都是不断的抄写他人知识,融合这些知识,所以直接回答纯粹的算法题我答的不好。比如第1题我还是使用冒泡排序,嘿嘿。当时甚至于冒泡排序还写错了。比较紧张吧:因为先回答了圆盘时钟的问题,却一直没解出来,导致思绪乱了,后面答第一个问题也更乱了。

下面是借口,借口:

一直学习,没顾上看面试题,没顾上看看算法题。面试完确实对我心里有点打击:我就是做个iOS,现在还需要算法了?iOS没有算法啊,苹果都给你优化过了,你写再好,比得过人家一个api?但是话说来,算法才是程序的灵魂,没有灵魂,写再多api 也不是程序。

我的观点是,面试的时候遇到了什么不清晰的地方,就好好记录下来。平常学必要的,不要全都学,现在那么多知识点,哪里学的完。这没什么好狡辩,尽力无悔即可。

唉,换工作比较麻烦,我早自知。但是我相信,现在困惑、困难,“树挪死,人挪活”,艰难刻苦总会过去的吧(目前还在艰难困苦中)。学到、实践到就是没白费过。

今天2018.9.26 两个面试。

上午的面试面着面着,快一小时了,才知道他们要招员工到张江去上班的,唉,我看你是10k~15k过来试着面试面试,没想到你比我还皮,这个比较浪费时间,,算了心平气和滚蛋了,嘎嘎。

下午离得稍微有点远,叫”Ratta” 雷塔的一个公司。我觉得功能方向稍微有点不太合适,也是试试去面试吧。

先笔试:考跟之前算法一样的东西(现在全部公司的笔试题都一样了?)。答题吧。

里面还有一个是跟我的APP“哈皮涂色”很类似的同步数据、展示数据的题目。
面试的是写c、c++ 的大哥。这人对基本数据比较了解。说一个数据{x:x,y:y,color:color} 这样一个int 数据元素要占用多少位?我肯定不知道啊,我就说bytes 是4字节,多少位我不知道。他说32位(int 是32位,NSInteger 是64,问这个干嘛哟,这个是读取速度,不是储存大小,我感觉这个大哥不是很靠谱,个人感觉)。然后他想问的如何压缩可以将数据不会存储那么大。我一时间哪能那么快想出来啊,能想出来答出来,那我肯定之前做过了,碰到过了,不然我答什么呀。

之后就是问随意的问题。

其中给他看了我的“哈皮涂色”,估计他有点懵逼了:这是他们公司的主营功能。他们有个硬件进行绘画,我直接就是手机上直接画。他们有点想知道核心功能,我才不告诉他们嘞,要知道?得花钱。

然后问了一些技术上的问题,他也做了些功课:

  1. 堆和栈的区别(还别说,没看面试题,我也都快忘光了,随便解答) 。

一个由c/C++编译的程序占用的内存分为以下几个部分:

  • 栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
  • 文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。
  • 程序代码区—存放函数体的二进制代码。
  1. 反射机制(从没用过~查询得知,其实就是使用字符动态查找创建类、方法,方便网络和客户端交互。反正我没这么用,多不安全啊。)
  2. https(我说上家公司没处理过,不过之前公司处理过,iOS 这里只需在网络那里配置下就好了,如果要双向验证,那就在iOS本地增加配置一个公钥,额,他还不信,害我跑回来看教程,没错啊,客户端要处理的真的很少。服务端也不多,nodejs 之前的我看过相关的教程,就增加配置个证书文件嘛)

参考网址

单向验证

1
2
3
4
5
6
7
8
9
10
11
//在你封装的网络工具类请求前初始化时增加以下代码
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
//设置证书模式,AFSSLPinningModeNone,代表前端包内不验证
//在单向认证时,前端不放证书,服务器去验证
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
// 如果是需要服务端验证证书,需要设置为YES
securityPolicy.allowInvalidCertificates = YES;
//validatesDomainName 是否需要验证域名,默认为YES;
securityPolicy.validatesDomainName = NO;
//设置验证模式
manager.securityPolicy = securityPolicy;

双向验证

1
2
3
4
5
6
7
8
9
10
11
12
13
//在你封装的网络工具类请求前初始化时增加以下代
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
//设置验证证书,该模式下许愿把证书打包进项目里,进行验证
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
//证书的路径
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"xxx" ofType:@".cer"];
NSData *dataSou = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [NSSet setWithObjects:dataSou, nil];
securityPolicy.allowInvalidCertificates = YES;
securityPolicy.validatesDomainName = YES;
[securityPolicy setPinnedCertificates:set];
//将验证方式赋值给管理者
manager.securityPolicy = securityPolicy;

HTTPS 处理过程是在“[应用层] - [SSL/TLS] - [TCP] - [IP]”之间。HTTP 的TCP 握手是三次,但是HTTPS 握手需要更多次,因为这里使用非对称加密、对称加密等加密手段对数据进行保护。

首先先理解加解密数据性能,非对称加密RSA 性能需要CPU 计算,寻找素数,耗时相对大,优点就是公钥可以公布,但是如果仅做一次,互相之间传递secretKey 就是非常好的;对称加密AES 速度快,但是需要互相之间都保存有secretKey。

HTTPS 使用到的加密算法:

1
2
3
非对称加密算法:RSA,DSA/DSS 
对称加密算法:AES,RC4,3DES
HASH算法:MD5,SHA1,SHA256

最简单的理解或者说模拟出一个HTTPS 一次传输过程要处理的事(只讲算法之间要干的事,更多请阅读下面参考链接讲解的过程):

  • 客户端一开始和服务端使用公钥加密这次要使用的secretKey。因为大家都没有私钥,所以也不怕被截取。

  • 服务器保存此次对应的人和对应的secretKey,同时可以使用AES 对从客户端收到了secretKey 的回应,加密的密钥就是secretKey。

服务端使用RSA 使用secretKey 私钥加密数据给客户端。

客户端使用该secretKey 来给数据进行AES 加密。

这样就完成加解密的过程了。

上面的过程比较简单化,HTTPS 还做了很多,不过我只理解简单的,然后把他写下来。

可以参考HTTPS加密原理

继续看面试题

1:SEL Method

SEL 方法编号:
SEL methodId = @selector(func1);
执行SEL:
[self performSelector:methodId withObject:nil];
IMP 获得和使用:
IMP methodPoint = [self methodForSelctor:methodId];
调用:methodPoint();
IMP objectMethodPoint = [object methodFor:@selector(message)]
调用:theResult = objectMethodPoint(anObject, aSelector);

2:
反射机制
系统Foundation 下有这几个反射API

1
2
3
4
5
6
FOUNDATION_EXPORT NSString *NSSTringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NSAVAIABLE(10_5, 2_0);
1
2
3
4
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;

3:NSDictionary

iOS 的NSDictionary 是使用hash表来实现key value 的键值映射和存储的。于Typescript 一样,键都是转为string 类型查找。
根据这篇文章参考 能更深入了解NSDictionary 实现思路。

首先NSArray 是没有hash 表来映射快速查找方法。时间复杂度就是o(n),数据长度。NSDictionary 在setObject 时提前判别对象hash 值是否已经存在。判别分两步:Step1:== 运算符判别对象指针是否相等;Step2:判别是否是同一类型;Step3:判别内在属性是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKinkOfClass:[Person class]]) {
return NO;
}
return [self isEqualToPerson:(Person *)object];
}

- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}

BOOL haveEqualNames = (!self.name && !person.name) || (self.name isEqualToString:person.name);
BOOL haveEqualbirthday = (!self.birthday && !person.birthday) || (self.birthday isEqualToDate:person.birthday);

return haveEqualNames && haveEqualBirthdays;
}

以上部分可以采用RunTime 寻找属性判别。

Hash Table 查找成员过程:Step1:通过hash值直接找到目标位置;Step2:如果目标位置上有多个相同hash值成员,再按照数组方式查找。

Hash 调用时间:对象被添加至NSSet、设置NSDictionary 的key 时调用。

正确重置Person 的hash 方式实现:

大神Mattt Thompson在Equality中给出的结论就是

In reality, a simple XOR over the hash values of critical properties is sufficient 99% of the time(对关键属性的hash值进行位或运算作为hash值)

正确返回重置hash

1
2
3
- (NSUInter)hash {
return [self.name hash] ^ [self.birthday hash];
}

4:Block

Block本质就是对象,因为本身就有指针。

1
2
3
4
5
6
7
int main(int argc, const char * argv) {
void (^block)() = ^{
printf('hello world');
}
block();
return 0;
}

使用clang 指令
clang -rewrite-objc main.m 得到一个cpp 文件。
定义完block,创建了一个函数,在创建结构体时,同时把函数指针也传进去了。之后就可以拿出来使用了。

5:线程间通讯参考网址

1) NSThread

1
2
3
4
5
6
7
8
- (void)thread {
NSString *imageUrlString;
[self performSelectorInBackground:@selector(loadImage:) withObject:imageUrlString];
}
- (void)loadImage:(NSString *)imageUrlString {
NSData *imageData = [NSData dataWithContentURL:[NSUrl urlWithString:imageUrlString]];
[self performSelectorOnMainThread:@selector(updateUI:) withObject:imageData];
}

2) GCD

1
2
3
4
5
6
7
8
9
10
- (void)gcd {
NSString *imageUrlString;
__Weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSData *imageData = [NSData dataWithContentURL:[NSUrl urlWithString:imageUrlString]];
dispatch_async(dispatch_get_main_queue, ^{
weakSelf.imageView.image = [UIImage imageWithData:imageData];
});
});
}

3) NSOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)operation {
__weak typeof(self) weakSelf = self;
DownLoadOperation *downLoadOp = [DownLoadOperation new];
downLoadOp.imageUrlString = xxx;
downLoadOp.finishedBlock = ^(UIImage *image) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
weakSelf.imageView.image = image;
}];
};
NSOperationQueue *concurrentQueue = [NSOperationQueue new];
[concurrentQueue addOperation:downLoadOp];
}

@interface DownloadOperation : NSOperation
@property (nonatomic, copy) NSString *imageUrlString;
@property (nonatomic, copy) FinishedBlock finishedBlock;
@end
@implementation DownloadOperation
- (void)main {
@autoreleasepool {
NSData *imageData = [NSData dataWithContentURL:[NSUrl urlWithString:self.imageUrlString]];
if (self.finishedBlock)
self.finishedBlock([UIImage imageWithData:imageData]);
}
}
@end

4) 加密算法

廖雪峰-node-crypto

MD5和SHA1

1
2
3
4
5
const crypto = require('crypto');
const hash = crypto.createHash('md5');//crypto.createHash('sha1');/sha256/sha512
hash.update('Hello, world');
hash.update('hello, nodejs');
console.log(hash.digest('hex'));

Hmac 可以把Hmac理解为用随机数“增强”的哈希算法

1
2
3
4
5
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'your secret-key');
hmac.update('Hello, world');
hmac.update('Hello, nodejs');
hmac.digest('hex');

AES 对称加密算法,加解密都用同一个密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const crypto = require('crypto');
function aesEncrypt(data, key) {
const cipher = crypto.createCipher('aes192', key);
let crypted = cipher.update(data, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}
function aesDecrypt(encrypted, key) {
const decipher = crypto.createDecipher('aes192', key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}

let data = 'Hello, this is a secret message!';
let key = 'Password';
let encrypted = aesEncrypted(data, key);
let decrypted = aesDecrypted(encrypted, key);

RSA 加解密,非对称加密

首先得到对应的公钥和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs'),
crypto = require('crypto');
function loadKey(file) {
return fs.readFileSync(file, 'utf8');
}
let prvKey = loadKey('./rsa-prv.pem'),
pubKey = loadKey('./rsa-pub.pem'),
message = 'Hello, world';

let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
//enc_by_prv.toString('hex');

let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
//dec_by_pub.toString('utf8');

//然后还可以反过来对称加密

5) category 里添加属性

category 它是在运行期编译的时候加上的,编译完成后对象内存已经确定,不能添加“_”的成员变量。

runtime 能添加对象的原因是因为:关联对象由AssociationsManager 管理,AssociationManager 里面是一个静态AssociationsHashMap 来存储所有的关联对象的。所以直接保存新的关联对象kv键值对即能添加成功。

6) autoreleasepool

autoreleasepool本质上就是一个指针堆栈

@autoreleasepool{} 其实是实现了

1
2
void *poolToken = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(poolToken);

7) KVC KVO深入研究

参考博客

KVC 运用isa-swizzling 类型混合指针机制。

[site setValue:@”sitename” forKey:@”name”]; ->

1
2
3
SEL sel = sel_get_uid("setValue:forKey:");
IMP method = objc_msg_lookup(site->isa, sel);
method(site, sel, @"sitename", @"name");

KVC实现分三步.Step1:根据方法名找到运行方法所需要的环境参数;Step2:从自己isa指针结合参数找到具体方法实现接口;Step3:直接查找得来具体方法实现。

KVO在这篇文章能很好的解释博客

KVO后续。。。

系统默认注册5个Mode:

  1. kCFRunLoopDefaultMode:App 默认Mode,主线程。
  2. UITrackingRunLoopMode:界面跟踪Mode,ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响。
  3. UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode, 启动完成后就不在使用。
  4. GSEventReceiveRunLoopMode:接受系统时间内部Mode,通常用不到。
  5. kCFRunLoopCommonModes

NSTimer 对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode) 添加到主运行循环后,滑动scrollView NSTimer 却不动了?

NSTimer 是在defaultRunLoop 调用消息的,当我们滑动scrollview 的时候, NSDefaultRunLoopMode 模式自动切换成UITrackingRunLoopMode 下。所以NSTimer 需要在NSRunLoopCommonMode

RunTime 添加类、方法、实例变量

1
2
3
4
5
6
7
8
9
Class myClass = objc_allocateClassPair([NSObject class], "Person", 0);

class_addMethod 其中types参数为"i@:@“,按顺序分别表示:具体类型可参照[官方文档](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html)i
返回值类型int,若是v则表示void@ 参数id(self): SEL(_cmd)@ id(str)
V@:表示返回值是void 带有SEL参数 (An object (whether statically typed or typed id)

class_addMethod(myClass, @selector(addMethodForMyClass:), (IMP)addMethodForMyClass, "V@:");

BOOL isSuccess = class_addIvar(myClass, "name", sizeof(NSString *), 0, "@");

探讨atomic 一定是线程安全么

atomic 是在getter 和setter 存取方法的线程保证原子性,并不能保证整个对象的存取动作是线程安全的。

在不同线程中写、读同一个对象,虽然写、读过程是原子性,但是由于等待之后,得到被修改的对象,还是:最终结果属于不可预测。

要保证存、读可预测,可使用下面几个api锁住存取动作,保证线程安全、可预测:

NSLock/NSLockLock/@synchronized(obj){}/pthread_mutex_t/OSSPinLock

其中性能比较:

OSSPinLock > pthread_mutex_t > NSLockLock > NSLock > @synchronized

读YY作者提的几点优化TableView 效率

对于GPU 等待显示器在一次VSync 时间内,CPU 或者GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次VSync 再显示,而这时显示器就保留了之前的内容不变,这就是界面卡顿的原因。

CPU 应该优化处理的几个点:

  1. 对象创建
  2. 对象调整:frame、bounds 会对CALayer 绘制、创建动画等有比较大开销
  3. 对象销毁,可以用一个小Tip:把对象捕获到block 中,然后扔到后台队列去销毁:
1
2
3
4
5
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
})
  1. 布局计算
  2. Autolayout(复杂试图尽量使用其他如ComponentKit、AsyncDisplayKit 代替)
  3. 文本计算(尽量缓存)
  4. 文本渲染
  5. 图片解码
  6. 图像绘制(CoreGraphic 是线程安全的,可以放到后台进行绘制)

最近在搞React + Mobx 这一套

我已经确定使用这套组合了。因为直接使用vue 感觉没有成就感。React 函数式 + 初学Mobx 的便利(redux 实在是太繁琐了,很多例子很被他绕晕了,真的真的放弃,虽然已经有点入门redux 了;)

html首先碰到

第一个问题:Warning: A component is changing an uncontrolled input of type checkbox

原因是我的(input:checkbox) 在checkd时出现传递了undefined 进去,需要判别一下:(this.props.todo.finished === null ||this.props.todo.finished === undefined) ? false : this.props.todo.finished

第二个问题:Warning: Each child in an array or iterator should have a unique "key"

原因是在组件内部如:

1
<div>{for(...){}}</div>

有for循环,那么需要给里面的子控件加上key={aString} 的参数:

1
2
3
4
5
<ul>
{this.props.todolist.todos.map(todo => (
<TodoView todo={todo} key={todo.id.toString()}></TodoView>
))}
</ul>