FrizzleFur / DailyLearning

My Daily Learning~
MIT License
61 stars 23 forks source link

面试总结 #1

Open FrizzleFur opened 6 years ago

FrizzleFur commented 6 years ago

一次好的面试是一次难得的交流机会。

FrizzleFur commented 6 years ago
时间 更新备注
2017-12-27 新建issue
2019-03-17 更新ref

====================================================

必问的点:

  1. 内存管理:ARC,循环引用,内存泄漏,野指针

ref:

  1. Block内存管理

ref:

进阶可能问的点

  1. App性能优化

  2. UITableView优化

ref:

项目

  1. 你做的比较值得满意的项目是什么?

  2. 你项目目前遇到的比较难的问题,你是如何解决的?

  3. 说下你最复杂项目的技术内容

  4. 你平时是怎么做技术积累的

A: 记录项目所用和RSS文章信息的重点,进行学习整理和实践,利用Github的issue做GTD规划和输出。

请你出一套iOS面试题 · Issue #30 · FrizzleFur/DailyLearning

====================================================

FrizzleFur commented 6 years ago

2017-12-23 初步整理

====================================================

面试题总结

阿里

内存管理

Block

Runloop & Runtime

优化和架构设计

其他

网易

内存管理

简单概括就是从 property的属性特性,引申出内存管理、对象模型、 runloop等知识,很杂很多,面试官从你对 property的属性特性的解释抓住重点,进而引申出对于内存管理的各种问题。3、 category和 extension的区别,比如 category.不能添加 property,怎么解决,用对象绑定等

Runtime & Runloop

设计模式优化

网络

其他

====================================================

重点问题

网络

内存管理

项目

设计模式

知名框架

混合开发

FrizzleFur commented 5 years ago

天猫超市

一面

  1. 图片100x100的jpg在磁盘中占用内存是多大?500kb吗?
  2. 图片从下载到显示有哪些优化的点?
    • 下载-内存缓存,缓存淘汰策略
    • 图片读取: 磁盘读取图片的解码的性能瓶颈
  3. 谈谈组件化在项目中的使用,中间件是如何解耦业务模块的?
  4. 介绍一下Cocoapod,有使用Cocoapod的插件化应用吗?
  5. 有研究过逆向吗?如何使用LLDB调试第三方App?
  6. 你是如何做集成化的?Jenkins等
  7. 你的知识学习渠道是哪里的?书本、博客、平台文档、教学视频、代码源码等。(偷师的好问题)
  8. 项目中那些问题让你觉得印象最深刻?你是如何解决的?
  9. 项目你推动了那些,并且落地实现的?

二面

  1. 用自己熟悉的语言,输入两个任意字符串,每一步删除一个其中一个字符串的一个字符,直到两个字符串最后相同。输出最小的步数。

例子: 输入:"ace" "cet" 输出:2 步骤:第一步ace->ce,然后cet->ce。

  1. 输入的每个字符串长度不超过300。
  2. 输入的字符串必须都是小写。 注意:
    • 1.1 输入的每个字符串长度不大于300;
    • 1.2 输入的字符串都是小写;

比如ctetec, cte ->tetec ->te

int minStep(char *s1, char *s2){

//  code
}

这道题给了我们两个单词,问我们最少需要多少步可以让两个单词相等,每一步我们可以在任意一个单词中删掉一个字符。那么我们分析怎么能让步数最少呢,是不是知道两个单词最长的相同子序列的长度,并乘以2,被两个单词的长度之和减,就是最少步数了。其实这道题就转换成求Longest Common Subsequence最长相同子序列的问题,令博主意外的是,LeetCode中竟然没有这道题,这与包含万物的LeetCode的作风不符啊。不过没事,有这道题也行啊,对于这种玩字符串,并且是求极值的问题,十有八九都是用dp来解的,曾经有网友问博主,如何确定什么时候用greedy,什么时候用dp?其实博主也不不太清楚,感觉dp要更tricky一些,而且出现的概率大,所以博主一般会先考虑dp,如果实在想不出递推公式,那么就想想greedy能做不。如果有大神知道更好的区分方法,请一定留言告知博主啊,多谢!那么决定了用dp来做,就定义一个二维的dp数组,其中dp[i][j]表示word1的前i个字符和word2的前j个字符组成的两个单词的最长公共子序列的长度。下面来看递推式dp[i][j]怎么求,首先来考虑dp[i][j]和dp[i-1][j-1]之间的关系,我们可以发现,如果当前的两个字符相等,那么dp[i][j] = dp[i-1][j-1] + 1,这不难理解吧,因为最长相同子序列又多了一个相同的字符,所以长度加1。由于我们dp数组的大小定义的是(n1+1) x (n2+1),所以我们比较的是word1[i-1]和word2[j-1]。那么我们想如果这两个字符不相等呢,难道我们直接将dp[i-1][j-1]赋值给dp[i][j]吗,当然不是,我们还要错位相比嘛,比如就拿题目中的例子来说,"sea"和"eat",当我们比较第一个字符,发现's'和'e'不相等,下一步就要错位比较啊,比较sea中第一个's'和eat中的'a',sea中的'e'跟eat中的第一个'e'相比,这样我们的dp[i][j]就要取dp[i-1][j]跟dp[i][j-1]中的较大值了,最后我们求出了最大共同子序列的长度,就能直接算出最小步数了,参见代码如下:

class Solution {
public:
    int minDistance(string word1, string word2) {
        int n1 = word1.size(), n2 = word2.size();
        vector<vector<int>> dp(n1 + 1, vector<int>(n2 + 1, 0));
        for (int i = 1; i <= n1; ++i) {
            for (int j = 1; j <= n2; ++j) {
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return n1 + n2 - 2 * dp[n1][n2];
    }
};

[LeetCode] Delete Operation for Two Strings 两个字符串的删除操作 - Grandyang - 博客园

  1. 设计实现一个View类,保证一直在所有界面之上;

(继承UIWindow, 层级比UIWindowLevelAlert高) UIWindowLevelNormal < UIWindowLevelStatusBar < UIWindowLevelAlert

NSLog(@"%f  %f  %f",UIWindowLevelNormal,UIWindowLevelAlert,UIWindowLevelStatusBar);
// 0.000000 2000.000000 1000.000000
self.windowLevel = UIWindowLevelAlert + 1;  //如果想在 alert 之上,则改成 + 1
[self makeKeyAndVisible];
FrizzleFur commented 5 years ago

呆萝卜

一面

  1. 描述下类的结构?

  2. 谈谈循环引用场景,如果多个block嵌套的会引起循环引用吗

// 当前类self持有testBlk1、testBlk2,下面代码会引起循环引用吗?
__weak typeof(self) weakSelf = self;
self.testBlk1 = ^{
    self.testBlk2 = ^{
        [weakSelf doSomething];
    };
};
  1. 方法交换,多各类都实现的时候,执行结果是怎么样的?(依据编译顺序,依次执行)
  2. 事件响应和传递链-用户点击一个按钮,事件是如何响应和传递的?
  3. Objective-C中的消息转发机制
  4. 谈谈网络协议HTTP的三次握手🤝,为啥需要第三次握手?
  5. 分享你所做项目中最具挑战性的点。
  6. 说说RAC中冷热信号的概念,RACSubscriber订阅的是热信号吗?(我提到了项目中使用了RAC,所以问了下冷热信号)
  7. 分享你所做项目中最具挑战性的点。

数据结构

  1. 如何检测链表中的环?

一面解答

1. 描述下类的结构?

class对象

我们通过class方法或runtime方法得到一个class对象。class对象也就是类对象

Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];

// runtime
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);

每一个类在内存中有且只有一个class对象。可以通过打印内存地址证明

class对象在内存中存储的信息主要包括:

  1. isa指针
  2. superclass指针
  3. 类的属性信息(@property),类的成员变量信息(ivar)
  4. 类的对象方法信息(instance method),类的协议信息(protocol)

理解类的概念

比起类,可能对象的概念更熟悉一点,这是对象的定义: 对象的结构体

你会发现有一个定义成Class类型的isa,这是实例对象用以表明其所属类型的,指向Class对象的指针。通过Class搭建了类的继承体系(class hirerarchy)。

其实类也是对象,打开定义的头文件,发现是用一个结构体来存储类的信息。

typedef struct objc_class *Class;
struct objc_class {
    Class isa; // 指向metaclass 
    Class superclass;  // 指向父类Class
    const char *name;  // 类名
    uint32_t version;  // 类的版本信息
    uint32_t info;        // 一些标识信息,标明是普通的Class还是metaclass
    uint32_t instance_size;        // 该类的实例变量大小(包括从父类继承下来的实例变量);
    struct old_ivar_list *ivars;    //类中成员变量的信息
    struct old_method_list **methodLists;    类中对象方法列表
    Cache cache;    查找方法的缓存,用于提升效率
    struct old_protocol_list *protocols;  // 存储该类遵守的协议 
}

类的结构体存放着该类的信息:类的对象方法列表,实例变量,协议,父类等信息。 每个类的isa指针指向该类的所属类型元类(metaClass),用来表述类对象的数据。每个类仅有一个类对象,而每个类对象仅有一个与之相关的”元类”。 比如一个继承NSObjct名叫SomeClass的类,其继承体系如下: 类的继承体系

Objective-C中任何的类定义都是对象。即在程序启动的时候任何类定义都对应于一块内存。在编译的时候,编译器会给每一个类生成一个且只生成一个”描述其定义的对象”,也就是水果公司说的类对象(class object),它是一个单例(singleton). 因此,程序里的所有实例对象(instance object)都是在运行时由Objective-C的运行时库生成的,而这个类对象(class object)就是运行时库用来创建实例对象(instance object)的依据。

Programming with Objective-C的说法就是:Classes Are Blueprints for Objects, 类是对象的抽象设计图。

查询类型信息

有时候会需要查询一个"objct"对象的所属的类,有人会这样写:

id objct = /* ... */
if ([objct class] == [SomeClass class]) {
    //objct is an instance of SomeClass.
}

其实Objective-C中提供了专门用于查询类型信息的方法,由于runtime在运行时的动态性,对于对象所属类的查询,建议使用isKindOfClassisMemberOfClass,因为某些对象可能实现了消息转发功能,从而判断可能不准确.

理解元类(meta class

为了调用类里的类方法,类的isa指针必须指向包含这些类方法的类结构体。 这就引出了元类的定义:元类是类对象的类。 简单说就是:

"元类的类”

元类,就像之前的类一样,它也是一个对象。你也可以调用它的方法。自然的,这就意味着他必须也有一个类。

关于这两点,原文是这样描述的:

A metaclass is an instance of the root class's metaclass; the root metaclass is itself an instance of the root metaclass.

所谓的元类就是根类的元类的一个实例。

第二点: And the root metaclass's superclass is the root class,就说名 根元类 (Root Class meta)的父类是 根类 (Root Class class).可以看到图中的 根元类 (Root Class meta)的superclass是指向 根类 (Root Class class)的。

类的图解.png

类的继承

类用super_class指针指向了超类,同样的,元类用super_class指向类的super_class的元类。 说的更拗口一点就是,根元类把它自己的基类设置成了super_class。 在这样的继承体系下,所有实例、类以及元类(meta class)都继承自一个基类。 这意味着对于继承于NSObject的所有实例、类和元类,他们可以使用NSObject的所有实例方法,类和元类可以使用NSObject的所有类方法 这些文字看起来莫名其妙难以理解,可以用一份图谱来展示这些关系:

类和元类

如上图,对象是由按照类所定义的各个属性和方法“制造”的,类作为对象的模板,也可看成是对象。正如工厂里面的模子也是要专门制作模子的机器生产,元类 (meta class)就是设计、管理 (class)的角色。所以图上直观的表现出类和元类平行的父类链,表明实例方法和类方法都是并行继承的,每个对象都响应了根类的方法。

需要弄清的有两点:

  1. 所谓的元类就是根类的元类的一个实例,而根元类的实例就是它自己。
  2. 根元类的父类是根类。

4. 事件响应和传递链-用户点击一个按钮,事件是如何响应和传递的?

参考解答:

事件的传递和响应分两个链:

传递链:由系统向离用户最近的view传递。UIKit –. active app’s event queue –. window –. root view –>……–>lowest view 响应链:由离用户最近的view向系统传递。initial view –. super view –. …..–. view controller –. window –. Application

触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中 UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口,这时候执行事件传递流程 找到一个最合适的视图来处理触摸事件。(这时候如果某一个view上添加了手势,且该手势能响应对应事件,则走手势的响应,根据手势的设置来决定是否阻断下面的步骤,但是事件传递过程依旧。如没有或者不能响应则继续走下面步骤) 在 UIApplication接收到手指的事件之后,就会去调用UIWindowhitTest:withEvent:,看看当前点击的点是不是在window内,如果是则继续依次调用其 subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,便可以确定最合适的 View。

响应者链的事件传递过程

  1. 如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图
  2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  3. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  4. 如果UIApplication也不能处理该事件或消息,则将其丢弃

从上面可以看出,事件的传递方向是(hittest就是事件的传递):

UIApplication -> UIWindow ->ViewController-> UIView -> initial view

而Responder传递方向是(还记得nextResponder吗):

Initial View -> Parent View -> ViewController -> Window -> Application

6. 谈谈网络协议HTTP的三次握手🤝,为啥需要第三次握手?

参考解答:

为什么TCP客户端最后还要发送一次确认呢? 主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。 如果只有两次通信的话,这时候Sever不确定Client是否收到了确认消息,有可能这个确认消息由于某些原因丢了。

如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

这是因为Server端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。

而关闭连接时,当收到Client的FIN报文时,仅仅表示Client不再发送数据了但是还能接收数据,Server方也未必全部数据都发送给Client方了,所以Server方可以立即close,也可以发送一些数据给Client对方后,再发送FIN报文给Client方来表示同意现在关闭连接,因此,Server方ACK和FIN一般都会分开发送。

8. 说说RAC中冷热信号的概念,RACSubject订阅的是热信号吗?

在 RAC 的世界中,所有的热信号都属于一个类 —— RACSubject

RACSubscriber

对冷信号进行了订阅会使得变为热信号,如果调用不当,导致源信号被订阅了多次,而实际上只要发送一次信号请求就可以了,即对源信号只有订阅一次就够了,就造成多次调用的问题。 那我们如何做到对源信号只订阅一次呢,RACSubject的存在就是解决这个问题的。

RAC的冷信号和热信号-RACSubject - 简书

10. 如何检测链表中的环?

参考解答:

方式 1: 快慢指针 假设有两个学生A和B在跑道上跑步,两人从相同起点出发,假设A的速度为2m/s,B的速度为1m/s,结果会发生什么? 答案很简单,A绕了跑道一圈之后会追上B! 将这个问题延伸到链表中,跑道就是链表,我们可以设置两个指针,a跑的快,b跑的慢,如果链表有环,那么当程序执行到某一状态时,a==b。如果链表没有环,程序会执行到a==NULL,结束。

listnode_ptr fast=head->next; 
listnode_ptr slow=head;
while(fast)
{
    if(fast==slow)
    {
        printf("环!\n");
        return 0;
    }
    else
    {
        fast=fast->next;
        if(!fast)
        {
            printf("无环!\n");
            return 0;
        }
        else
        {
            fast=fast->next;
            slow=slow->next;
        }
    }
}
printf("无环!\n"); 
return 0;

方式 2: 反转指针

每过一个节点就把该节点的指针反向。

当有环的时候,最后指针会定位到链表的头部,如果到最后,都没有再到头部,那说明链表不存在循环。

这个方法会破坏掉链表,所以如果要求是不能破坏链表的话,我们最后就还需要反转一下,再将链表恢复。

这个方法使用的空间复杂度为O(1),其实是使用了3个指针,用于进行反转。同时,时间复杂度为O(n)。

【算法】如何判断链表有环 | iTimeTraveler

FrizzleFur commented 5 years ago

铭师堂 (部分一面)

解答

NSProxy是一个虚类。它有什么用处呢? OC中类是不支持多继承的,要想实现多继承一般是有protocol的方式,还有一种就是利用NSProxy。有同学可能会问为什么不用NSObject来做?同样都是基类,都支持NSObject协议,NSProxy 有的NSObject 都有。但是点进NSProxy .h可以看见NSProxy没有init方法,而且NSProxy自身的方法很少,是一个很干净的类。这点很重要,因为NSObject自身的分类特别多,而消息转发的机制是当接收者无法处理时才会通过forwardInvocation:来寻求能够处理的对象.在日常使用时,我们很难避免不使用NSObject的分类方法比如valueForKey这个方法NSObject就不会转发。

1.多继承

block类型的,新api。iOS 10之后才支持,因此对于还要支持老版本的app来说,这个API暂时无法使用。当然,block内部的循环引用也要避免。 /// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references

方式二:NSProxy的方式 建立一个proxy类,让timer强引用这个实例,这个类中对timer的使用者target采用弱引用的方式,再把需要执行的方法都转发给timer的使用者。

@interface ProxyObject : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation ProxyObject

+ (instancetype)proxyWithTarget:(id)target {
    ProxyObject* proxy = [[self class] alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

@end

@implementation ProxyTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:ti target:[ProxyObject proxyWithTarget: aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo];
    return timer;
}
@end

方式三:封装timer,弱引用target 类似NSProxy的方式,建立一个桥接timer的实例,弱引用target,让timer强引用这个实例。

@interface NormalTimer : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;
@end

@implementation NormalTimer
- (void)dealloc{
    NSLog(@"timer dealloc");
}

- (void)timered:(NSTimer*)timer{
    [self.target performSelector:self.selector withObject:timer];
}
@end

@interface NSTimer(NormalTimer)
+ (NSTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
@end

@implementation NSTimer(NormalTimer)
+ (NSTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    NormalTimer* normalTimer = [[NormalTimer alloc] init];
    normalTimer.target = aTarget;
    normalTimer.selector = aSelector;
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:ti target:normalTimer selector:@selector(timered:) userInfo:userInfo repeats:yesOrNo];
    return timer;
}
@end

参考 IOS定时器操作和NSTimer的各种坑 - 简书

FrizzleFur commented 5 years ago

有赞

一面

参考

  1. Objc Runtime 总结 | 星光社 - 戴铭的博客
//暂停layer上面的动画
- (void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

//继续layer上面的动画
- (void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}

- (void)animationPause {
    // 当前时间(暂停时的时间)
    // CACurrentMediaTime() 是基于内建时钟的,能够更精确更原子化地测量,并且不会因为外部时间变化而变化(例如时区变化、夏时制、秒突变等),但它和系统的uptime有关,系统重启后CACurrentMediaTime()会被重置
    CFTimeInterval pauseTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
    // 停止动画
    self.layer.speed = 0;
    // 动画的位置(动画进行到当前时间所在的位置,如timeOffset=1表示动画进行1秒时的位置)
    self.layer.timeOffset = pauseTime;
}
- (void)animationContinue {
    // 动画的暂停时间
    CFTimeInterval pausedTime = self.layer.timeOffset;
    // 动画初始化
    self.layer.speed = 1;
    self.layer.timeOffset = 0;
    self.layer.beginTime = 0;
    // 程序到这里,动画就能继续进行了,但不是连贯的,而是动画在背后默默“偷跑”的位置,如果超过一个动画周期,则是初始位置
    // 当前时间(恢复时的时间)
    CFTimeInterval continueTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
    // 暂停到恢复之间的空档
    CFTimeInterval timePause = continueTime - pausedTime;
    // 动画从timePause的位置从动画头开始
    self.layer.beginTime = timePause;
}