面试总结
FrizzleFur opened this issue · 6 comments
一次好的面试是一次难得的交流机会。
时间 | 更新备注 |
---|---|
2017-12-27 | 新建issue |
2019-03-17 | 更新ref |
====================================================
必问的点:
- 内存管理:ARC,循环引用,内存泄漏,野指针
ref:
- DailyLearning/解析-内存管理机制.md at master · FrizzleFur/DailyLearning
- DailyLearning/OC的类和元类.md at master · FrizzleFur/DailyLearning
- DailyLearning/解析-AutoRelease原理.md at master · FrizzleFur/DailyLearning
- Block内存管理
ref:
- DailyLearning/解析-Block的使用.md at master · FrizzleFur/DailyLearning
- DailyLearning/解析-Block的内存管理.md at master · FrizzleFur/DailyLearning
- Block · Issue #7 · FrizzleFur/DailyLearning
进阶可能问的点
-
App性能优化
-
UITableView
优化
ref:
项目
-
你做的比较值得满意的项目是什么?
-
你项目目前遇到的比较难的问题,你是如何解决的?
-
说下你最复杂项目的技术内容
-
你平时是怎么做技术积累的
A: 记录项目所用和RSS文章信息的重点,进行学习整理和实践,利用Github的issue做GTD规划和输出。
请你出一套iOS面试题 · Issue #30 · FrizzleFur/DailyLearning
====================================================
2017-12-23 初步整理
====================================================
面试题总结
阿里
内存管理
- 阐述
@property
的属性特性有哪些,默认情况下属性的特性是哪些,以及这些特性的作用: - 阐述
readwrite
、readonly
的作用 - 阐述
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图层) - 介绍下
MVVM
,ViewModel
的作用? APP
内组件间通信方式有哪些?- 有用过组件化吗?如何实现?
其他
- 熟悉
Web
和原生的JS交互吗?详细介绍下 - 混合开发
Hybird
的使用,是否会使用Weex
或者ReactNative
?
网易
内存管理
- 内存管理,
MRC
和ARC
,@property
的本质 AutoReleasePool
的实现机制Copy
的用法,深拷贝和浅拷贝Block
有几种?如何管理内存的?- 内存泄漏有几种方式?1.循环引用,2.定时器
NSTimer
的管理 3. 野指针?
简单概括就是从 property
的属性特性,引申出内存管理、对象模型、 runloop
等知识,很杂很多,面试官从你对 property的属性特性的解释抓住重点,进而引申出对于内存管理的各种问题。3、 category和 extension的区别,比如 category
.不能添加 property,怎么解决,用对象绑定等
Runtime & Runloop
Runloop
, 详细介绍下Runloop
,Runloop
有什么作用?Runtime
:Runtime
的对象管理的底层原理Runtime
, 介绍Runtime
,Runtime
下的ARC是怎么管理的?- 在支付流程中,如果用户切换到微信,复制口令,回到原来App继续支付,如何不被弹出的口令打断?(我这边是说使用通知去做,显然不好)
设计模式优化
- App优化策略有哪些?你是如何实现的?
- 简述
Delegate
、NSNotification
、Block
、KVO
之间的模式和优缺点,如何选择 - 什么是
KVO
?它的实现原理? - 多线程有哪些?你是怎么用的?
网络
Http
协议是怎么样的?- 熟悉网络
Socket
吗? - 请论述一个数据包从客户端到服务端的整个传输过程
Http
和Https
的区别
其他
SDWebImage
的原理,图片内存过大时候,如何管理内存的图片?- 说说
Copy
的用法,深拷贝和浅拷贝 - 类
Category
和Extension
的区别 NSArray
和NSSet
的使用和区别,性能效率?- 你用过的文件存储方式有哪些?
- Feed流,
UITableView
显示图片如何优化显示和存储? MVVM
和MVC
的优缺点- 多线程并发,有多个任务,如何让任务2在任务1完成后开始执行?
====================================================
重点问题
网络
Http
协议是怎么样的?- 熟悉网络
Socket
吗? - 请论述一个数据包从客户端到服务端的整个传输过程
内存管理
- 内存管理,
MRC
和ARC
,@property
的本质 Copy
的用法,深拷贝和浅拷贝Block
内存管理- 内存泄露的场景
- 文件存储方式
项目
- 详细谈谈你对做项目过程中你完成的最值得骄傲的地方(或者你克服的难题)
设计模式
MVVM
和MVC
的优缺点- 简述
Delegate
、NSNotification
、Block
、KVO
之间的模式和优缺点,如何选择
知名框架
SDWebImage
的原理,图片内存过大时候,如何管理内存的图片?
混合开发
- 混合开发
Hybird
的应用,是否会使用Weex
或者ReactNative
?
天猫超市
一面
- 图片100x100的jpg在磁盘中占用内存是多大?500kb吗?
- 图片从下载到显示有哪些优化的点?
- 下载-内存缓存,缓存淘汰策略
- 图片读取: 磁盘读取图片的解码的性能瓶颈
- 谈谈组件化在项目中的使用,中间件是如何解耦业务模块的?
- 介绍一下Cocoapod,有使用Cocoapod的插件化应用吗?
- 有研究过逆向吗?如何使用LLDB调试第三方App?
- 你是如何做集成化的?Jenkins等
- 你的知识学习渠道是哪里的?书本、博客、平台文档、教学视频、代码源码等。(偷师的好问题)
- 项目中那些问题让你觉得印象最深刻?你是如何解决的?
- 项目你推动了那些,并且落地实现的?
二面
- 用自己熟悉的语言,输入两个任意字符串,每一步删除一个其中一个字符串的一个字符,直到两个字符串最后相同。输出最小的步数。
例子:
输入:"ace" "cet"
输出:2
步骤:第一步ace->ce,然后cet->ce。
- 输入的每个字符串长度不超过300。
- 输入的字符串必须都是小写。
注意:- 1.1 输入的每个字符串长度不大于300;
- 1.2 输入的字符串都是小写;
比如cte
和tec
, cte
->te
, tec
->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 - 博客园
- 设计实现一个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];
呆萝卜
一面
-
描述下类的结构?
-
谈谈循环引用场景,如果多个block嵌套的会引起循环引用吗
// 当前类self持有testBlk1、testBlk2,下面代码会引起循环引用吗?
__weak typeof(self) weakSelf = self;
self.testBlk1 = ^{
self.testBlk2 = ^{
[weakSelf doSomething];
};
};
- 方法交换,多各类都实现的时候,执行结果是怎么样的?(依据编译顺序,依次执行)
- 事件响应和传递链-用户点击一个按钮,事件是如何响应和传递的?
- Objective-C中的消息转发机制
- 谈谈网络协议HTTP的三次握手🤝,为啥需要第三次握手?
- 分享你所做项目中最具挑战性的点。
- 说说RAC中冷热信号的概念,
RACSubscriber
订阅的是热信号吗?(我提到了项目中使用了RAC,所以问了下冷热信号) - 分享你所做项目中最具挑战性的点。
数据结构
- 如何检测链表中的环?
一面解答
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对象在内存中存储的信息主要包括:
- isa指针
- superclass指针
- 类的属性信息(@Property),类的成员变量信息(ivar)
- 类的对象方法信息(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
在运行时的动态性,对于对象所属类的查询,建议使用isKindOfClass
和isMemberOfClass
,因为某些对象可能实现了消息转发功能,从而判断可能不准确.
理解元类(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)的。
类的继承
类用super_class
指针指向了超类,同样的,元类用super_class
指向类的super_class
的元类。
说的更拗口一点就是,根元类把它自己的基类设置成了super_class
。
在这样的继承体系下,所有实例、类以及元类(meta class
)都继承自一个基类。
这意味着对于继承于NSObject
的所有实例、类和元类,他们可以使用NSObject
的所有实例方法,类和元类可以使用NSObject
的所有类方法
这些文字看起来莫名其妙难以理解,可以用一份图谱来展示这些关系:
如上图,对象是由按照类所定义的各个属性和方法“制造”的,类作为对象的模板,也可看成是对象。正如工厂里面的模子也是要专门制作模子的机器生产,元类
(meta class
)就是设计、管理 类
(class)的角色。所以图上直观的表现出类和元类平行的父类链,表明实例方法和类方法都是并行继承的,每个对象都响应了根类的方法。
需要弄清的有两点:
- 所谓的元类就是根类的元类的一个实例,而根元类的实例就是它自己。
- 根元类的父类是根类。
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接收到手指的事件之后,就会去调用UIWindow
的hitTest:withEvent:
,看看当前点击的点是不是在window内,如果是则继续依次调用其 subView的hitTest:withEvent:方法,直到找到最后需要的view。调用结束并且hit-test view确定之后,便可以确定最合适的 View。
- 事件响应
响应者链的事件传递过程
- 如果view的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图
- 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
- 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
- 如果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的存在就是解决这个问题的。
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)。
铭师堂 (部分一面)
- 组件化拆分
- 私有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.多继承
-
NSTimer
如何安全使用?
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
有赞
一面
- 程序从启动到展示的过程
- main(),开启主线程,开启runloop,创建自动释放池,创建UIMainApplication对象,加载StoryBoard? ,加载VC...
- 分类有属性吗?可以分类添加属性吗?
- 消息的调用流程:
- 消息发送:消息函数,Objc中发送消息是用中括号把接收者和消息括起来,只到运行时才会把消息和方法实现绑定。
- 消息的转发流程
- 如果使用[object message]调用方法,object无法响应message时就会报错。用performSelector…调用就要等到运行时才确定是否能接受,不能才崩溃。
- Method转发机制分为三步:
- 动态方法解析resolveInstanceMethod
- 重定向接收者forwardingTargetForSelector
- 最后进行转发forwardInvocation
- 动画如何暂停?
- 设计一个NSNotificationCenter:
- RAC: 讲一下RACSubject 和 RACSignal的区别
- RACSubject 信号提供者,自己可以充当信号,又能发送信号。订阅后发送
参考
- 动画如何暂停?
//暂停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;
}