iOS面试题知识点总结(上)

1,173 阅读14分钟

前言

最近忙着公司项目稳定性建设,所以没怎么写文章!但是马上就到了金九银十,今年由于教育行业大裁员,导致竞争更加激烈一些,下面是整理近1,2月来小伙伴面试遇到的一些知识点。由于收集的比较多,会分为上下两部分,后续还会推出算法特别专题。希望对大家的面试有所帮助,也希望每个面试的小伙伴都能拿到心仪的offer。答案不要死记硬背,最好理解,因为面试会发散,如果不理解,就无法应对发散问题

@property 的修饰词

  • 常见修饰词assign,weak,strong,retain,copy,nontomic,atomic,readonly,readwrite

atomic和nonatomic

  • 1.atomicnonatomic用来决定编译器生成gettersetter是否为原子操作
  • 2.atomic:系统生成的 getter/setter 保证 get、set 操作完整性不受其他线程影响getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象多线程的情况下是不能确定

assign

  • 1.这个修饰词是直接赋值的意思 , 整型/浮点型等数据类型都用这个词修饰
  • 2.如果没有使用 weak strong retain copy 修饰 , 那么默认就是使用 assign
  • 3.当然其实对象也可以用 assign 修饰 , 只是对象计数器不会+1 . ( 与 strong 的区别 )
  • 4.如果用来修饰对象属性 , 那么当对象被销毁指针是不会指向 nil 的 . 所以会出现野指针错误 . ( 与weak的区别 )

weak

  • 1.weak是弱引用,用weak描述修饰或者所引用对象的计数器不会加一
  • 2.引用的对象被释放的时候自动被设置为nil,大大避免野指针访问坏内存引起崩溃的情况
  • 3.另外weak还可以用于解决循环引用

weak原理概括

  • 1.weak表其实是一个hash(哈希)表Key是所指对象的地址Valueweak指针的地址数组
  • 2.Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针weak表其实是一个hash表Key是所指对象地址valueweak指针地址(这个地址的值是所指对象指针的地址)数组。 之所以value是数组,是因为一个对象可能被多个弱引用指针指向

weak原理实现

1.初始化

runtime会调用objc_initWeak函数objc_initWeak函数初始化一个新的weak指针指向对象的地址 示例:

NSObject *obj = [[NSObject alloc] init];
id __weak obj1 = obj;

当我们初始化一个weak变量时,runtime调用 NSObject.mm 中objc_initWeak函数,这个函数在Clang中声明如下:

id objc_initWeak(id *object, id value);

而对于 objc_initWeak() 方法的实现如下:

id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效,无效对象直接导致指针释放
 if (!newObj) {
     *location = nil;
     return nil;
 }
 // 这里传递了三个 bool 数值
 // 使用 template 进行常量参数传递是为了优化性能
 return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
 (location, (objc_object*)newObj);
}

这个函数仅仅是一个深层函数调用入口,而一般的入口函数中,都会做一些简单的判断(例如 objc_msgSend 中的缓存判断),这里判断了其指针指向的类对象是否有效无效直接释放,不再往深层调用函数。否则,object将被注册为一个指向value的__weak对象。而这事应该是objc_storeWeak函数干的。

注意:objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,或者指向一个有效的对象。

2.添加引用

objc_initWeak函数会调用 objc_storeWeak() 函数objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表objc_storeWeak的函数声明如下:

id objc_storeWeak(id *location, id value);
3.释放

调用clearDeallocating函数clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个对象地址weak表中删除,最后清理对象的记录。

当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

  • 1.调用objc_release
  • 2.因为对象的引用计数为0,所以执行dealloc
  • 3.在dealloc中,调用了 _ objc _ rootDealloc函数
  • 4.在 _ objc _ rootDealloc 中,调用了object _ dispose函数
  • 5.调用objc_destructInstance
  • 6.最后调用objc _ clear _ deallocating objc _ clear _ deallocating该函数的动作如下:
  • 1.从weak表中获取废弃对象的地址为键值的记录
  • 2.将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
  • 3.将weak表中该记录删除
  • 4.从引用计数表中删除废弃对象的地址为键值的记录

assign和weak的区别

  • 1.assign 可以用非 OC 对象,而 weak 必须用于 OC 对象
  • 2.weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空 nil

strong

  • 1.在ARC环境下,只要某一对象被一个strong指针指向,该对象不会被销毁
  • 2.如果对象没有被任何strong指针指向,那么就会被销毁
  • 3.在默认情况下,所有的实例变量局部变量都是strong类型的。可以说strong类型指针行为上非ARC下的retain是比较相似

copy

预备知识点

  • 浅拷贝
    • 只是将对象内存地址多了一个引用,也就是说,拷贝结束之后,两个对象值不仅相同,而且对象所指的内存地址都是一样
  • 深拷贝
    • 拷贝一个对象的具体内容拷贝结束之后,两个对象的虽然是相同的,但是指向的内存地址是不同的两个对象之间互不影响互不干扰

copy的作用

  • 1.在非集合类对象中,对不可变对象进行copy操作,只仅仅是指针复制——浅复制,进行mutableCopy操作,是内容复制——深复制
  • 2.对于不可变集合类对象进行copy操作,只是改变了指针,其内存地址并没有发生变化进行mutableCopy操作内存地址发生了变化,但是其中的元素内存地址没有发生变化
  • 3.对于可变集合类对象,不管是进行copy操作还是mutableCopy操作,其内存地址都发生了变化,但是其中的元素内存地址都没有发生变化,属于单层深拷贝 注意:当将一个可变对象分别赋值给两个使用不同修饰词的属性后,改变可变对象的内容,使用strong修饰的会跟随着改变,但使用copy修饰的没有改变内容,这也是为什么NSString用copy修饰的原因

动态库静态库的区别

静态库编译期链接到目标代码中,动态库运行期载入到代码中。动态库只有一份多个程序共用静态库则是会在每个app中拷贝一份

pod源码方式引入私有库,打包成动态库/静态库使用pod集成步骤

  • 1.执行命令pod lib create 项目名
  • 2.打开.podspec文件,修改类库配置信息(s.public_header_files = 'Pod/Classes/.h'//需要暴露出来的文件 当然也可以指定多个文件。如:'Pod/Classes/YPAPI/**/.h','Pod/Classes/YPBase/YPModel /.h','Pod/Classes/YPBase/YPUtils/YPUtils.h','Pod/Classes/YPBind/YPViewController/YPCardListController.h','Pod/Classes/YPBase/YPBaseController/YPBaseViewController.h','Pod/Classes/**/')
  • 3.进入Example文件夹,执行pod install,让demo项目安装依赖项并更新配置
  • 4.使用一个cocoapods的插件cocoapods-packager来完成类库的打包
    • 安装打包插件,终端执行以下命令:sudo gem install cocoapods-packager
    • 打包,执行pod package 项目名.podspec --library --force 其中--library指定打包成.a文件,如果不带上将会打包成.framework文件。--force是指强制覆盖

    问题1: xcode12及以上版本打包出现静态库arm64冲突问题

    • 原因: xcode12苹果新增了模拟器的arm64架构和真机的arm64冲突了 修复方式: 如果是手动合并出错在Xcode里进行架构忽略即可,忽略方式网上一大推,就是忽略模拟器架构即可; 如果你是用pod packets打包出错,在pod spec文件里忽略arm64 即可

自动释放池原理

  • 1.自动释放池是由 AutoreleasePoolPage双向链表的方式实现的
  • 2.当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage的栈中
  • 3.调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
  • 4.自动释放池和线程是紧密相关的,每一个自动释放池只对应一个线程

http 与 https 区别

  • 1.使用 HTTPS 协议需要到 CA(Certificate Authority,数字证书认证机构) 申请证书,一般免费证书较少,因而需要一定费用
  • 2.协议头不一样。http的URL以"http://"开头,而https的URL以"https://"开头
  • 3.安全性不一样。http没有用于数据加密和数据完整性校验的安全机制,而https通过数字证书来保障双方的通信
  • 4.监听端口不一样。http监听80端口,而https监听443端口
  • 5.建立连接时候:https比http多了TLS的握手过程

OC为什么能支持运行时的这些动态特性?

Objective-C具有相当多的动态特性,表现为三方面:动态类型(Dynamic typing)动态绑定(Dynamic binding)动态加载(Dynamic loading)。动态——必须到运行时(run time)才会做的一些事情

卡顿的原理,项目中的界面优化是怎么做的,异步渲染会存在什么问题?

异步渲染会存在什么问题:答案就是无法查看渲染出来的视图图层

碰撞检测的逻辑

判断一个点是否在某个view的区域内:- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

//将相对于aView的点point转化为相对于view坐标系的点,并返回
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
//将点从view中转换至 aview中并返回一个相对于aView的坐标点
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
//将相对于view的rect转化为相对于view坐标系的rect,并返回一个rect值
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
//将点从aView转换至view中并返回一个相对于aView的尺寸
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

判断代码

//这个是判断的origin,更精确的可以使用image的中心判断
//    [imageView2 pointInside:[imageView3 convertPoint:CGPointMake(imageView3.bounds.origin.x+imageView3.bounds.size.width/2, imageView3.bounds.origin.y) toView:imageView2] withEvent:nil]
    if ([imageView2 pointInside:[imageView3 convertPoint:imageView3.bounds.origin toView:imageView2] withEvent:nil]) {
        [tiplabel setText:@"碰撞了"];
    }
    else{
        [tiplabel setText:@"没有碰撞"];
    }

里氏代换是什么

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方子类一定可以出现。LSP是继承复用基石,只有当衍生类可以替换掉基类软件单位功能不受到影响时,基类才能真正被复用,而衍生类能够基类基础增加新的行为。里氏代换原则是对"开-闭"原则的补充。实现"开-闭"原则的关键步骤就是抽象化。而基类子类继承关系就是抽象化具体实现,所以里氏代换原则对实现抽象化的具体步骤的规范。

开闭原则

开闭原则规定“软件中对象(类,模块,函数等等)应该对于扩展开放的,但是对于修改封闭的”,这意味着一个实体允许不改变它源代码的前提下变更它行为。该特性在产品化环境中是特别有价值的,在这种环境中,改变源代码需要代码审查单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。

SD中大图加载方案

  • 1.设置图片缓存方式 防止图片过大崩溃的情况
  • 2.在- (void)didReceiveMemoryWarning方法中清空磁盘内存缓存
  • 3.在- (void)dealloc方法中再次清理缓存

自动释放池-AutoReleasePool

临时变量什么时候释放?

  • 1.如果在正常情况下,一般是超出作用域会立即释放
  • 2.如果将临时变量加入了自动释放池,会延迟释放,即在runloop休眠或者autoreleasepool作用域之后释放

AutoreleasePool原理

  • 1.自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表形式连接
  • 2.自动释放池的压栈出栈主要是通过结构体构造函数析构函数调底层objc_autoreleasePoolPushobjc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法
  • 3.每次调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个POOL_BOUNDARY,并返回插入POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况
    • 当page存在,且不满时,调用add方法将对象添加至page的next指针处,并next递增
    • 当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中
    • 当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中
  • 4.当执行pop操作时,会传入一个值,这个就是push操作返回值即POOL_BOUNDARY的内存地址token。所以pop内部实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置

AutoreleasePool能否嵌套使用?

  • 1.可以嵌套使用,其目的是可以控制应用程序内存峰值,使其不要太高
  • 2.可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的
  • 3.自动释放池的多层嵌套其实就是不停的pushs哨兵对象,在pop时,会先释放里面的,再释放外面

哪些对象可以加入AutoreleasePool?alloc创建可以吗?

  • 1.使用new、alloc、copy关键字生成的对象retain了的对象需要手动释放不会被添加到自动释放池中
  • 2.设置为autorelease对象不需要手动释放,会直接进入自动释放池
  • 3.所有 autorelease对象,在出了作用域之后,会被自动添加最近创建自动释放池

AutoreleasePool的释放时机是什么时候?

  • 1.App 启动后,苹果在主线程 RunLoop注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
  • 2.第一个 Observer 监视的事件Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647优先级最高,保证创建释放池发生在其他所有回调之前
  • 3.第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠)调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush()`` 释放旧的池创建新池;Exit(即 将退出 Loop) 调用 _objc_autoreleasePoolPop() 释放自动释放池。这个Observerorder 2147483647优先级最低,保证其释放池子发生在其他所有回调之后。

thread 和 AutoreleasePool的关系

  • 1.每个线程,包括主线程内都维护了自己的自动释放池堆栈结构
  • 2.新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除
  • 3.对于当前线程来说,会将自动释放对象放入自动释放池栈顶;在线程停止时,会自动释放掉该线程关联所有自动释放池

总结

每个线程都有与之关联自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶pool销毁时,会被出栈,对于当前线程来说,释放对象被压栈栈顶线程停止时,会自动释放与之关联自动释放池

RunLoop 和 AutoreleasePool的关系

  • 4.主程序RunLoop每次事件循环之前,会自动创建一个 autoreleasePool
  • 5.并且会在事件循环结束时,执行drain操作释放其中的对象