iOS senior engineer interview

中级

一、Block

1.1 block的实质是什么?一共有几种block?都是什么情况下生成的?

block对象就是一个结构体,里面有isa指针指向自己的类(global malloc stack),有desc结构体描述block的信息,__forwarding指向自己或堆上自己的地址,如果block对象截获变量,这些变量也会出现在block结构体中。最重要的block结构体有一个函数指针,指向block代码块。block结构体的构造函数的参数,包括函数指针,描述block的结构体,自动截获的变量(全局变量不用截获),引用到的__block变量。(__block对象也会转变成结构体)


image.png
int main() {
    int (^blk)(int i) = ^(int i){
        int result =  i + 1;
        return result;
    };
    blk(3);
    return 0;
}
//==============上面是反编译以前的代码==================
struct __block_impl {
    void *isa;//isa表明结构体类型。
    int Flags;
    int Reserved;
    void *FuncPtr;//指向函数指针
};
//这个结构体及时Block反编译以后生成的主要结构。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    //初始化函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//表示这个Block是存储于栈上。
    impl.Flags = flags;
    impl.FuncPtr = fp;//函数指针赋值
    Desc = desc;
  }
};
//这个函数就是Block的具体实现,并且添加了一个默认实现。
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int i) {
    int result = i + 1;
    return result;
}
//Block的描述信息
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
};
//__main_block_desc_0的一个实例,其中Block_size初始化为__main_block_impl_0结构体的大小。
struct __main_block_desc_0 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main() {
    //int (*blk)(int i) = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    //上面一行转换为下面两行等价
    struct __main_block_impl_0 tmp = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    struct __main_block_impl_0 *blk = &tmp;
    
    //int blkRerurn = ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 3);
    //下面一行是上面一行的简化版
    (*blk->impl.FuncPtr)(blk,3);
    return 0;
}

1.2 为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?

__block变量i在转换为c语言后直接转换为一个__Block_byref_i_0类型的结构体
通过i->__forwarding->i获取外部变量并修改,然后将__main_block_impl_0copy下,再释放下,具体代码

1.2.2 为什么block里

1.先定义一个TestObj对象,他的属性有一个block对象
@interface TestObj : NSObject
@property (nonatomic, copy)void(^block)();
@end

@implementation TestObj

  • (void)dealloc {
    NSLog(@"%s",func);
    }
  • (instancetype)init {
    self = [super init];
    if (self) {
    __weak typeof(self) weakSelf = self;
    self.block = ^{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"%@",weakSelf);
    });
    };
    }
    return self;
    }

@end
2.再另一个类实例中定义一个testFunc方法

  • (void)testFunc{
    TestObj *obj = [TestObj new];
    obj.block();
    }
    执行testFunc方法,结果是打印的是(null),因为block里打印的方法是异步执行的,在 NSLog(@"%@",weakSelf);这句代码执行之前testFunc函数就结束,所以obj对象已经被release了。

1.3 模拟一下循环引用的一个情况?block实现界面反向传值如何实现?

二、Runtime

2.1 objc在向一个对象发送消息时,发生了什么?

runtime会根据对象的isa指针找到所对应的类,然后在类的方法列表、父类的方法列表里找对应的方法运行,但是在发送消息时,objc_msgSend不会返回值,只会在程序实际运行时决定

附注:c语言,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。而在 Objective-C 中,[object foo] 语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。

[array insertObject:foo atIndex:5];
//编译时转化成
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

2.2 objc中向一个nil对象发送消息将会发生什么?

向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了

2.3 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?

  • 对象没有这个方法,对象有这个方法但是调用时候已经释放
  • respondsToSelector这个方法可以来判断
    image.png

2.4 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?能否添加成员变量?能否添加属性变量?原理是什么?

  • 不能向编译后得到的类中增加实例变量,因为编译后的类已经注册在runtime中,objc_ivar_list和instance_size已经确定
  • 可以向运行时的类中增加实例变量,用class_addIvar方法,但是必须插在objc_allocateClassPairobjc_registerClassPair之间使用
  • 运行时不能添加成员变量,编译器会报错
  • 可以添加属性变量objc_setAssociatedObject,原理如下:
    image.png

附注:成员变量、实例变量、属性变量之间的区别?成员变量就是用{}括起来的对象,外界无法使用;属性变量就是@property修饰的,外界可以拿到;实例变量本质上就是成员变量,只是实例对象是针对类,定义了属性变量myButton会自动生成_myButton,并写好setter/getter方法


QQ图片20180310223645.jpg

2.5 runtime如何实现weak变量的自动置nil?

runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

2.6 给类添加一个属性后,在类结构体里哪些元素会发生变化?

objc_ivar_list(实例变量列表)和instance_size(实例内存大小)会变化

2.7 runtime的运用

2.7.1 kvo、kvc实现原理
  • kvc实现原理
    1)检查是否存在名为-set:的方法,并使用它做设置值
    2)如果上述方法不可用,则检查名为-_、-_is(只针对布尔值有效)、-_get和-_set:方法;
    3)如果没有找到访问器方法,可以尝试直接访问实例变量
    4)如果仍为找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法
    具体看这里

  • kvo实现原理
    自动创建一个原来类的子类,如NSKVONotifying_XX,将原来类的isa指针指向新建的类,重写setter方法,通过willChangeValueForKey,didChangeValueForKey来检测属性值的变化。

removeObserver就是将isa指针指向原来的类
具体demo看这里

2.7.2 category的实现

category内部是如何实现的?与该类原有方法的名称相同的时候,为什么原有方法会失效?

1. 将Category和它的元类注册到哈希表
2. 如果元类已经实现,则重建它的方法列表
2.7.3 添加属性
objc_setAssociatedObject
2.7.4 添加实例变量
2.7.5 获取私有方法
2.7.6 替换方法
2.8 + (void)load; 和 + (void)initialize;区别是什么
image.png

Runloop

  • runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
    主线程默认开启了runloop,其他线程默认没有开启
    具体参考
  • runloop的mode是用来做什么的?有几种mode?
    • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
    • UITrackingRunLoopMode:ScrollView滑动时
    • UIInitializationRunLoopMode:启动时
    • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
      公开的可以用的:
    • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
    • NSRunLoopCommonModes(kCFRunLoopCommonModes)
  • 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循* 环以后,滑动scrollview的时候NSTimer却不动了?

苹果是如何实现Autorelease Pool的?

其实就是指针组成的堆栈,当要释放时候,调用objc_autoreleasePoolPush将边界对象放进AutoreleasePoolPage栈顶,返回返回边界对象内存地址,接着就是pop操作,对晚于边界对象的对象发送release消息,并移动next指针到正确位置

类指针

  • isa指针?(对象的isa,类对象的isa,元类的isa都要说)

类方法和实例方法有什么区别?

  • 实例方法
    给对象发送消息,然后在对象的类的方法列表里查找对应的方法
  • 类方法
    给类发送消息,然后在类的meta-class的方法列表里查找对应的方法

高级

1.UITableview的优化方法(缓存高度,异步绘制,减少层级,hide,避免离屏渲染)

什么是离屏渲染, 就是GPU在当前屏幕缓冲区外开辟一块缓冲区进行操作

2.有没有用过runtime,用它都能做什么?(交换方法,创建类,给新创建的类增加方法,改变isa指针)

3.看过哪些第三方框架的源码?都是如何实现的?(如果没有,问一下多图下载的设计)

4.SDWebImage的缓存策略?

5.AFN为什么添加一条常驻线程?

  • 为什么要加常驻线程
    大量的创建网络请求线程并销毁线程会很耗资源
  • 为什么把线程加到runloop里
    AFN里的网络请求线程已经用单例创建了,会一直存在,为什么还要加到runloop里,是这样,runloop是需要时候才调用,不需要时候就会“闲置”,比如一个加载小时的页面,已经加载完了,后面基本上不需要网络请求,那为何一直开着网络线程呢

6. AsyncDisplayKit原理是什么

内部封装了UIView、CALayer,是得他们这些属性都可以在后台线程设置,从而完成了排版、绘制在后台线程的实现

造成UI卡顿主要包括:
- 排版:计算视图大小,计算文本高度,重新计算子视图的排版
- 绘制:文本绘制、图片绘制(预先解压)、元素绘制(Quartz)
- UI对象操作:UIView、CALayer等UI对象的创建、销毁、属性设置

8. 关于网络编程

1.iOS中socket使用
Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

http协议 对应于应用层
tcp协议 对应于传输层
ip协议 对应于网络层
三者本质上没有可比性。 何况HTTP协议是基于TCP连接的。

TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

我 们在传输数据时,可以只使用传输层(TCP/IP),但是那样的话,由于没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用应用层 协议,应用层协议很多,有HTTP、FTP、TELNET等等,也可以自己定义应用层协议。WEB使用HTTP作传输层协议,以封装HTTP文本信息,然 后使用TCP/IP做传输层协议将它发送到网络上。

SOCKET原理

套接字(socket)概念套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
应 用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应 用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
建立socket连接建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连 接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户 端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
SOCKET连接与TCP连接创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。
Socket连接与HTTP连接由 于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用 中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。
而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。
很 多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给 客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以 保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

TCP与UDP
TCP和UDP都是传输层的协议:

TCP是传输控制层协议,是面向连接、可靠的,点对点的;

UDP是用户数据报协议,是不需要连接、不可靠的,点对多点的;

TCP侧重于安全传输,UDP侧重于快速传输。

TCP的三次握手:a、用户端向服务器发送syn包,用户端进入send状态,服务器等待接收;b、服务器接收到syn包并确认后,发送一个syn+ack包给用户端,服务器进入recv状态;c、用户端收到服务器的返回的syn+ack包后,再发送一个确认包ack给服务器,此时用户端和服务器都进入established状态,连接成功。

HTTP与HTTPS
http是超文本传输协议,是短连接,用户端向服务器请求数据后,服务器响应,连接断开。http是应用层面向对象的协议,它由请求报文和响应报文组成:

请求报文:请求行、请求头、空行和请求体;

响应报文:响应行、响应头、响应体。

http有GET和POST两种请求方式:

GET:请求内容拼接在url的后面,可以通过截取url‘?’后面的内容得到,多个参数由'&'分隔,账号和密码会被明文显示在url的后面,是不安全的,而且传输数据较少,不超过1024字节;

POST:请求参数写在请求数据里面,相对GET是比较安全的,提交数据放在http包的包体中,传输数据较多,理论上没有上限。

http协议是基于socket的,http的底层就是socket通信。

https是安全超文本传输协议,是基于http协议开发的,s指的是secure安全,它通过是安全套接字层来完成用户端和服务器端的通信,可以说是http的安全版协议。

Socket与其他
socket是通信的基石,是支持TCP/IP协议的基本通信单元。socket是基于TCP/IP的协议的封装,它本身并不是一个协议,而是一个接口API,它包含了传输协议,主机IP、主机进程端口号及服务器IP、服务器进程端口号五大基础部分组成。

在实际应用中,应用层通过传输层通信时,TCP常遇到为多个程序提供并发服务的问题,这时就会有多个TCP或者多个程序需要用同一个TCP协议端口传输数据,但是服务器无法区分是具体是哪个程序,这时就有了socket,应用层和传输层可以通过socket抽象层来判断具体为哪个程序进行通信,提供并发服务。

socket一般成对出现,一个是客户端ClientScoket,一个是服务端ServiceSocket。socket连接分三步:

服务器监听:ServiceSocket不会规定客户端的IP与端口号,服务器是处于等待连接的状态,监听网络访问,等待客户端连接;

客户端请求:ClientScoket不仅包含自己的主机IP和进程端口号,还包含服务端的IP和进程端口号,通过服务端的IP和端口号找寻对应的ServiceSocket发送连接请求;

连接确认:当服务器监听或者说接收到ClientScoket的连接请求时,就响应ClientScoket的请求,并新建一个线程,发送给客户端完整的ServiceSocket,一旦客户端确认此ServiceSocket后,连接完成,ServiceSocket继续处于等待状态,等待其他客户端进程的连接。

ios中创建socket连接五步走:创建socket、连接服务器、用户发送数据到服务器、服务器响应数据返回用户、关闭socket。

socket与TCP:

socket是可以支持TCP和UDP协议的,如果是使用TCP协议进行连接,那么就是一个TCP连接。

socket与http:

socket一般都是基于TCP的,所以是一个长连接,可以进行通信。但实际应用中应用层向传输层通信还需要穿越多个中间节点,例如路由器、网关、防火墙等,而防火墙一般是默认关闭处于不活跃的连接的,所以需要轮询服务器,保证连接不被关闭。

http是短连接,服务器响应数据后就断开。而实际应用中,经常需要服务端与客户端保持数据实时与同步,这就需要服务器发送数据给客户端,但http只能让客户端先建立连接并请求数据,服务器才能响应数据。而采用socket长连接就不需如此,服务器可以直接将数据发送给客户端。

9. ReactNative原理

面向对象

三大特征
  • 封装
    让有些人知道,让有些人不知道

  • 继承
    继承父辈的所有武功,并对这些武功进行发扬光大

  • 多态
    a) 老子生的儿子也是千差万别的,这个差别就是多态
    b) 常用手段:
    覆盖(重写):函数名和参数都一样,只是函数的实现不一样
    重载:函数名一样,参数不一样

Obj-C部分

内存管理
  • assign与weak区别
    weak比assign多一个功能,就是当属性指向的对象消失时候,weak会让属性置为nil,这样向weak修饰的属性发送消息就不会崩溃,而assign则不会,所以不能用assign修饰对象;
    为什么要用assign修饰基本数据类型?因为基础数据类型一般分配在栈上,栈的内存会由系统自己自动处理,不会造成野指针

  • strong(retain)跟copy区别
    strong指针复制,指向同一块内存区域;而copy只是内容复制,新开辟一片内存

image.png
自动释放池

当一个自动释放池子被销毁,会对池子里对象发送一条release消息

  1. 与线程关系
    每个线程对应一个NSAutoreleasePool,当新池子被创建,push进栈,当池子被释放内存,pop出栈
类与对象
  • 静态方法(类方法)pk实例方法
    a) 静态方法
    静态方法用static修饰,属于类,不属于实例,效率高但常驻内存,存在堆上,类方法中不可直接使用实例变量

b) 实例方法
实例方法属于实例,不常驻内存,存在栈上

Runtime

Obj-C不同于c是一个动态语言,当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。

UIApplication
image.png
atomic与nonatomic
  • atomic
    a) 定义:
    原子操作(原子性是指事务的一个完整操作,操作成功就提交,反之就回滚. 原子操作就是指具有原子性的操作)
    b) 属性中的使用:
    属性设置成atomic ,意思就是 setter /getter函数是一个原子操作,如果多线程同时调用setter时,不会出现某一个线程执行完setter所有语句之前,另一个线程就开始执行setter,相当于函数头尾加了锁,保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的,而且关键一点——并发访问性能会比较低
    c) 如何使用
    需要与@synthesize/@dynamic配和使用

  • nonatomic
    a) 定义:
    非原子操作,一般不需要多线程支持的时候就用它,这样在 并发访问的时候效率会比较高
    b) 使用:
    在objective-c里面通常对象类型都应该声明为非原子性的. iOS中程序启动的时候系统只会自动生成一个单一的主线程.程序在执行的时候一般情况下是在同一个线程里面对一个属性进行操作.

  • 两者在多线程的使用
    如果在程序中,我们确定某一个属性会在多线程中被使用,并且需要做数据同步,就必须设置成原子性的;但也可以设置成非原子性的,然后自己在程序中用加锁之类的来做数据同步,据说,atomic要比nonatomic慢大约20倍。一般如果条件允许,我们可以自己加锁操作。

  • 举个栗子

//设置属性name为nonatomic
@property (nonatomic, copy) NSString *name;

//创建两个线程同时操作属性name
- (IBAction)onclickAtomic:(id)sender {
    WS(weakSelf);
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        weakSelf.name = [weakSelf.name stringByAppendingString:@" will "];
        NSLog(@"name:%@", weakSelf.name);
    }];
    
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        weakSelf.name = [weakSelf.name stringByAppendingString:@" javion "];
        NSLog(@"name:%@", weakSelf.name);
    }];
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setName:@"MyQueue"];
    [queue addOperations:@[op1, op2] waitUntilFinished:NO];
}

结果果然出现了奇葩情况

image.png

于是我把属性改成了atomic,然后就正常了

image.png
通俗的例子:

A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:

  1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
  2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
    如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。

我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

附其他网站的面试题

王隆帅-iOS常见面试题汇总

你可能感兴趣的:(iOS senior engineer interview)