FrizzleFur/DailyLearning

面试总结

FrizzleFur opened this issue · 6 comments

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

时间 更新备注
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

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

2017-12-23 初步整理

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

面试题总结

阿里

内存管理

  • 阐述@property的属性特性有哪些,默认情况下属性的特性是哪些,以及这些特性的作用:
  • 阐述readwritereadonly的作用
  • 阐述assign的作用,当使用assign修饰Objective-C对象时,会怎么样?为什么assIgn用于基础类型时,不会发生基础类型丢失?
  • 阐述引用循环发生的原因?由weak引起的引用循环,发生循环时一定会泄露吗?
  • 阐述copy mutableCopy的作用,以及深拷贝和浅拷贝的机制?

Block

  • Block内存管理(白板上写出代码,画出堆栈内存示意图)
  • 如何避免Block的循环引用?说说除了使用__weak还有什么方法吗?__strong什么时候使用?

Runloop & Runtime

  • 什么是Runloop?, Runloop和线程的关系
  • Runtime有哪些应用?比如 method swizzling?介绍一下消息传递机制?

优化和架构设计

  • 如何优化一个UITableView? (* 高度缓存;* 布局渲染优化, *图片缓存 *cell的缓存,滚动停止时取出Cell, *减少Cell上的UI图层)
  • 介绍下MVVMViewModel的作用?
  • APP内组件间通信方式有哪些?
  • 有用过组件化吗?如何实现?

其他

  • 熟悉Web和原生的JS交互吗?详细介绍下
  • 混合开发Hybird的使用,是否会使用Weex或者ReactNative

网易

内存管理

  • 内存管理, MRCARC@property的本质
  • AutoReleasePool的实现机制
  • Copy的用法,深拷贝和浅拷贝
  • Block有几种?如何管理内存的?
  • 内存泄漏有几种方式?1.循环引用,2.定时器NSTimer的管理 3. 野指针?

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

Runtime & Runloop

  • Runloop, 详细介绍下RunloopRunloop有什么作用?
  • RuntimeRuntime的对象管理的底层原理
  • Runtime, 介绍RuntimeRuntime下的ARC是怎么管理的?
  • 在支付流程中,如果用户切换到微信,复制口令,回到原来App继续支付,如何不被弹出的口令打断?(我这边是说使用通知去做,显然不好)

设计模式优化

  • App优化策略有哪些?你是如何实现的?
  • 简述DelegateNSNotificationBlockKVO之间的模式和优缺点,如何选择
  • 什么是KVO?它的实现原理?
  • 多线程有哪些?你是怎么用的?

网络

  • Http协议是怎么样的?
  • 熟悉网络Socket吗?
  • 请论述一个数据包从客户端到服务端的整个传输过程
  • HttpHttps的区别

其他

  • SDWebImage的原理,图片内存过大时候,如何管理内存的图片?
  • 说说Copy的用法,深拷贝和浅拷贝
  • CategoryExtension的区别
  • NSArrayNSSet的使用和区别,性能效率?
  • 你用过的文件存储方式有哪些?
  • Feed流,UITableView显示图片如何优化显示和存储?
  • MVVMMVC的优缺点
  • 多线程并发,有多个任务,如何让任务2在任务1完成后开始执行?

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

重点问题

网络

  • Http协议是怎么样的?
  • 熟悉网络Socket吗?
  • 请论述一个数据包从客户端到服务端的整个传输过程

内存管理

  • 内存管理, MRCARC@property的本质
  • Copy的用法,深拷贝和浅拷贝
  • Block内存管理
  • 内存泄露的场景
  • 文件存储方式

项目

  • 详细谈谈你对做项目过程中你完成的最值得骄傲的地方(或者你克服的难题)

设计模式

  • MVVMMVC的优缺点
  • 简述DelegateNSNotificationBlockKVO之间的模式和优缺点,如何选择

知名框架

  • SDWebImage的原理,图片内存过大时候,如何管理内存的图片?

混合开发

  • 混合开发Hybird的应用,是否会使用Weex或者ReactNative

天猫超市

一面

  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];

呆萝卜

一面

  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指针必须指向包含这些类方法的类结构体。
这就引出了元类的定义:元类是类对象的类
简单说就是:

  • 当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
  • 当你给类发消息时,消息是在寻找这个类的元类的方法列表。
  • 元类是必不可少的,因为它存储了类的类方法。每个类都必须有独一无二的元类,因为每个类都有独一无二的类方法。

"元类的类”

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

  • 所有的元类都使用根元类(继承体系中处于顶端的类的元类)作为他们的类。这就意味着所有NSObject的子类(大多数类)的元类都会以NSObject的元类作为他们的类
  • 所有的元类使用根元类作为他们的类,根元类的元类则就是它自己。也就是说基类的元类的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的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

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

  • 引申: TCP四次挥手

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

  • 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

  • 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

  • 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

  • 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

  • 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

  • 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

  • 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

这是因为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

铭师堂 (部分一面)

  • 组件化拆分
    • 私有pod制作
    • 业务层剥离
  • 多线程
    • NSOperation优点?
    • NSOperation最大并发数?maxConcurrentOperationCount
  • NSProxy-谈谈NSProxy?
    • NSProxy是一个实现了NSObject协议的根类。
  • WKWebview的性能相较于UIWebView,原因?
  • WKWebview的Cookie如何存储?
  • 性能优化
    • data预加载
    • 图片下载和缓存
    • 布局计算
    • 离屏渲染
  • 内存暴增的情况
    • 循环引用
    • 野指针什么时候出现?
  • NSTimer如何安全使用?
    • 使用代理target, or NSProxy
    • 如何检测
  • 设计模式
    • 分类是一种什么设计模式?
      • 装饰模式
    • 子类是一种什么设计模式?
      • 策略模式

解答

  • NSProxy-谈谈NSProxy?

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

  • (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

方式二: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的各种坑 - 简书

有赞

一面

  • 程序从启动到展示的过程
    • main(),开启主线程,开启runloop,创建自动释放池,创建UIMainApplication对象,加载StoryBoard? ,加载VC...
  • 分类有属性吗?可以分类添加属性吗?
  • 消息的调用流程:
    • 消息发送:消息函数,Objc中发送消息是用中括号把接收者和消息括起来,只到运行时才会把消息和方法实现绑定。
    • 消息的转发流程
      • 如果使用[object message]调用方法,object无法响应message时就会报错。用performSelector…调用就要等到运行时才确定是否能接受,不能才崩溃。
      • Method转发机制分为三步:
        • 动态方法解析resolveInstanceMethod
        • 重定向接收者forwardingTargetForSelector
        • 最后进行转发forwardInvocation
  • 动画如何暂停?
  • 设计一个NSNotificationCenter:
  • RAC: 讲一下RACSubject 和 RACSignal的区别
    • RACSubject 信号提供者,自己可以充当信号,又能发送信号。订阅后发送

参考

  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;
}