iOS DeveloperiOS面试iOS面试题

iOS 面试题三

2018-05-13  本文已影响183人  林大鹏

题目:

1. 讲一下你对iOS 内存管理的理解
2. KVO实现原理
3. 观察者模式
4. 如果让你实现 NSNotificationCenter,讲一下思路
5. 如果让你实现 GCD线程池,讲一下思路
6.Category 的实现原理,以及 Category 为什么只能加方法不能加实例变量。
7. swiftstructclass的区别
8. 在一个HTTPS连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么
9. 在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么
10. main()之前的过程有哪些?
11. 消息转发机制原理?
12. 说说你理解weak属性?
13. 遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?
14. UIViewCALayer区别联系
15. 什么是离屏渲染,为什么会触发离屏渲染,离屏渲染的危害

一. 讲一下你对 iOS 内存管理的理解

Objective-C的内存管理中,其实就是引用计数(reference count)的管理。内存管理就是在程序需要时程序员分配一段内存空间,而当使用完之后将它释放。如果程序员内存资源使用不当,有时不仅会造成内存资源浪费,甚至会导致程序crach

memory management from apple document.png

1. 引用计数(Reference Count)

为了解释引用计数,我们做一个类比:员工在办公室使用灯的情景。

引用Pro Multithreading and Memory Management for iOS and OS X的图.png

2. 内存管理规则
从上面员工在办公室使用灯的例子,我们对比一下灯的动作与Objective-C对象的动作有什么相似之处:

灯的动作Objective-C对象的动作.png

因为我们是通过引用计数来管理灯,那么我们也可以通过引用计数来管理使用Objective-C对象。

引用Pro Multithreading and Memory Management for iOS and OS X的图.png

Objective-C对象的动作对应有哪些方法以及这些方法对引用计数有什么影响?

image.png

当你alloc一个对象objc,此时RC=1;在某个地方你又retain这个对象objc,此时RC加1,也就是RC=2;由于调用alloc/retain一次,对应需要调用release一次来释放对象objc,所以你需要release对象objc两次,此时RC=0;而当RC=0时,系统会自动调用dealloc方法释放对象

3. Autorelease Pool

在开发中,我们常常都会使用到局部变量局部变量一个特点就是当它超过作用域时,就会自动释放。而autorelease pool局部变量类似,当执行代码超过autorelease pool块时,所有放在autorelease pool的对象都会自动调用release。它的工作原理如下:

引用Pro Multithreading and Memory Management for iOS and OS X的图.png

4. ARC管理方法
iOS/OS X内存管理方法有两种:手动引用计数(Manual Reference Counting)自动引用计数(Automatic Reference Counting)

自动引用计数(Automatic Reference Counting)简单来说,它让编译器来代替程序员来自动加入retainrelease方法来持有放弃对象所有权

ARC内存管理机制中,id其他对象类型变量必须是以下四个ownership qualifiers其中一个来修饰:
所以在管理Objective-C对象内存的时候,你必须选择其中一个,下面会用一些列子来逐个解释它们的含义以及如何选择它们。

__strong:被它修饰的变量持有对象的所有权(默认,如果不指定其他,编译器就默认加入)

__weak: 被它修饰的变量都不持有对象的所有权,而且当变量指向的对象的RC为0时,变量设置为nil。

__unsafe_unretained:被它修饰的变量都不持有对象的所有权,但当变量指向的对象的RC为0时,变量并不设置为nil,而是继续保存对象的地址;这样的话,对象有可能已经释放,但继续访问,就会造成非法访问(Invalid Access)。

__autoreleasing:相比之前的创建、使用和释放NSAutoreleasePool对象,现在你只需要将代码放在@autoreleasepool块即可。你也不需要调用autorelease方法了,只需要用__autoreleasing修饰变量即可。

5.Property(属性)

image.png

详见: iOS/OS X内存管理(一):基本概念与原理

二. KVO实现原理

KVO基本原理:

KVO原理图.png

三.观察者模式

观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新

iOS中典型的观察者模式是:NSNotificationCenterKVO

1. NSNotificationCenter

NSNotificationCenter.png

2. KVO

KVO的全称是Key-Value Observer,即键值观察。是一种没有中心枢纽观察者模式的实现方式。一个主题对象管理所有依赖于它的观察者对象,并且在自身状态发生改变的时候主动通知观察者对象

[object addObserver:self forKeyPath:property options:NSKeyValueObservingOptionNew context:]。

四. 如果让你实现 NSNotificationCenter,讲一下思路

五. 如果让你实现 GCD 的线程池,讲一下思路

线程池包含如下8个部分:

具体流程:

六.Category实现原理,以及Category 为什么只能加方法不能加实例变量

详见: iOS 面试题一

category是可以添加属性,不能添加实例变量对吧!之所以不能添加实例变量,是因为一个实例变量编译阶段,就会在objc_classclass_ro_t这里进行存储布局,而category是在运行时才进行加载的,

然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:

// 从 `class_data_bits_t `调用 `data` 方法,将结果从 `class_rw_t `强制转换为 `class_ro_t `指针
const class_ro_t *ro = (const class_ro_t *)cls->data();
// 初始化一个 `class_rw_t` 结构体
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
// 设置`结构体 ro` 的值以及 `flag`
rw->ro = ro;
// 最后设置正确的` data`。
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);

运行时加载的时候class_ro_t里面的方法、协议、属性等内容赋值给class_rw_t,而class_rw_t里面没有用来存储相关变量数组,这样的结构也就注定实例变量是无法在运行期进行填充.

七. swift 中 struct和class的区别

swift中,class引用类型struct值类型值类型传递赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型区别

class有这几个功能struct没有的:

struct也有这样几个优势:

详见: 答卓同学的iOS面试题

八.在一个HTTPS连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么

HTTPS加密流程.png
  1. 客户端打包请求。包括url,端口,你的账号密码等等。账号密码登陆应该用的是Post方式,所以相关的用户信息会被加载到body里面。这个请求应该包含三个方面网络地址,协议,资源路径。注意,这里是HTTPS,就是HTTP + SSL / TLS,在HTTP上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙

  2. 服务器接受请求。一般客户端的请求会先发送到DNS服务器DNS服务器负责将你的网络地址解析成IP地址,这个IP地址对应网上一台机器。这其中可能发生Hosts HijackISP failure的问题。过了DNS这一关,信息就到了服务器端,此时客户端会和服务器的端口之间建立一个socket连接socket一般都是以file descriptor的方式解析请求。这个过程相当于是服务器端分析是否要向客户端发送钥匙模板

  3. 服务器端返回数字证书服务器端会有一套数字证书(相当于是个钥匙模板),这个证书会先发送给客户端。这个过程相当于是服务器端客户端发送钥匙模板

  4. 客户端生成加密信息。根据收到的数字证书(钥匙模板)客户端会生成钥匙,并把内容锁上,此时信息已经加密。这个过程相当于客户端生成钥匙并锁上请求。

  5. 客户端发送加密信息。服务器端会收到由自己发送出去的数字证书加锁的信息。 这个时候生成的钥匙也一并被发送到服务器端。这个过程是相当于客户端发送请求。

  6. 服务器端解锁加密信息。服务器端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端`解锁请求、生成、加锁回应信息。

  7. 服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于服务器端客户端发送回应。

  8. 客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。

HTTPS加密过程详解请去https原理:证书传递、验证和数据加密、解密过程解析

详见: 答卓同学的iOS面试题

九. 在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么

响应链大概有以下几个步骤:

(备注:UIResponderUIView的父类,UIViewUIControl的父类。)
RunLoop这边我大概讲一下

详见: 答卓同学的iOS面试题

十. main()之前的过程有哪些?

1)dyld 开始将程序二进制文件初始化

2)交由ImageLoader 读取image,其中包含了我们的类,方法等各种符号(Class、Protocol 、Selector、 IMP

3)由于runtimedyld 绑定了回调,当image加载到内存后,dyld会通知runtime进行处理

4)runtime 接手后调用map_images解析和处理

5)接下来load_images 中调用call_load_methods方法,遍历所有加载进来的Class,按继承层次依次调用Class+load和其他Category+load方法

6)至此 所有的信息都被加载内存

7)最后dyld调用真正的main函数

注意:dyld缓存上一次把信息加载内存的缓存,所以第二次第一次启动快一点

十一. 消息转发机制原理?

消息转发.png

举个 :

新建一个HelloClass的类,定义两个方法:

@interfaceHelloClass:NSObject

- (void)hello;

+ (HelloClass *)hi;
@end

1. 动态方法解析

对象在接收到未知的消息时,首先会调用所属类的类方法-resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法”。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。

void functionForMethod(id self, SEL _cmd)

{

    NSLog(@"Hello!");

}

Class functionForClassMethod(id self, SEL _cmd)

{

    NSLog(@"Hi!");

    return [HelloClass class];

}

#pragma mark - 1、动态方法解析

+ (BOOL)resolveClassMethod:(SEL)sel

{

    NSLog(@"resolveClassMethod");

    NSString *selString = NSStringFromSelector(sel);

    if ([selString isEqualToString:@"hi"])

    {

        Class metaClass = objc_getMetaClass("HelloClass");

        class_addMethod(metaClass, @selector(hi), (IMP)functionForClassMethod, "v@:");

        return YES;

    }

    return [super resolveClassMethod:sel];

}

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

    NSLog(@"resolveInstanceMethod");

    NSString *selString = NSStringFromSelector(sel);

    if ([selString isEqualToString:@"hello"])

    {

        class_addMethod(self, @selector(hello), (IMP)functionForMethod, "v@:");

        return YES;

    }

    return [super resolveInstanceMethod:sel];

}

2. 备用接受者

动态方法解析无法处理消息,则会走备用接受者。这个备用接受者只能是一个新的对象,不能是self本身,否则就会出现无限循环。如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。

#pragma mark - 2、备用接收者

- (id)forwardingTargetForSelector:(SEL)aSelector

{

    NSLog(@"forwardingTargetForSelector");

    NSString *selectorString = NSStringFromSelector(aSelector);

    // 将消息交给_helper来处理    if ([selectorString isEqualToString:@"hello"]) {

        return _helper;

    }

    return [super forwardingTargetForSelector:aSelector];

}

在本类中需要实现这个新的接受对象

@interfaceHelloClass()
{
    RuntimeMethodHelper *_helper;
}

@end

@implementation HelloClass
- (instancetype)init{

    self = [super init];

    if (self){

        _helper = [RuntimeMethodHelper new];

    }
    return self;
}

RuntimeMethodHelper 类需要实现这个需要转发的方法:

#import"RuntimeMethodHelper.h"

@implementationRuntimeMethodHelper
- (void)hello
{
    NSLog(@"%@, %p", self, _cmd);

}
@end

3. 完整消息转发

如果动态方法解析和备用接受者都没有处理这个消息,那么就会走完整消息转发:

#pragma mark - 3、完整消息转发

- (void)forwardInvocation:(NSInvocation *)anInvocation

{

    NSLog(@"forwardInvocation");

    if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {

        [anInvocation invokeWithTarget:_helper];

    }

}

/*必须重新这个方法,消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象*/

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

    if (!signature)

    {

        if ([RuntimeMethodHelper instancesRespondToSelector:aSelector])

        {

            signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];

        }

    }

    return signature;

}

详见: 2018-iOS面试题

十二. 说说你理解weak属性?

weak实现原理:

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针weak表其实是一个hash(哈希)表Key所指对象的地址Valueweak指针的地址(这个地址的值是所指对象的地址)数组

追问的问题一:

1.实现weak后,为什么对象释放后会自动为nil?

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

追问的问题二:

2.当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?

1、调用objc_release

2、因为对象的引用计数为0,所以执行dealloc

3、在dealloc中,调用了_objc_rootDealloc函数

4、在_objc_rootDealloc中,调用了object_dispose函数

5、调用objc_destructInstance

6、最后调用objc_clear_deallocating,详细过程如下:

a. 从weak表中获取废弃对象的地址键值的记录

b. 将包含在记录中的所有附有 weak修饰符变量的地址,赋值为 nil

c. 将weak表中该记录删除

d. 从引用计数表中删除废弃对象的地址为键值的记录

十三. 遇到tableView卡顿嘛?会造成卡顿的原因大致有哪些?

可能造成tableView卡顿的原因有:

十四. UIViewCALayer的区别和联系

联系:

区别:

详见:详解 CALayer 和 UIView 的区别和联系

详见: 详解 CALayer 和 UIView 的区别和联系

十五. 什么是离屏渲染,为什么会触发离屏渲染, 离屏渲染的危害

1. 什么是离屏渲染:

GPU渲染机制:

CPU 计算好显示内容提交到GPUGPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

GPU屏幕渲染有以下两种方式:

如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPUApp内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。

2. 为什么会触发离屏渲染

设置了以下属性时,都会触发离屏绘制:

- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- cornerRadius, 如果能够只用 cornerRadius 解决问题,不设置masksToBounds,则不会引起离屏渲染,如果既设置了cornerRadius,又设置了masksToBounds,就会触发离屏渲染

因为当设置锯齿,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。

这句话的意思是当你给一个控件设置锯齿,阴影,遮罩的时候,这时候控件需要去混合各个图层的像素来找出各个图层的正确显示效果,所以触发离屏渲染

3. 离屏渲染的危害

相比于当前屏幕渲染离屏渲染的代价是很高的,主要体现在两个方面:

要想进行离屏渲染,首先要创建一个新的缓冲区

离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境切换是要付出很大代价的。

详见:离屏渲染学习笔记

上一篇 下一篇

猜你喜欢

热点阅读