前言
最近忙着公司项目稳定性建设,所以没怎么写文章!但是马上就到了金九银十,今年由于教育行业大裁员,导致竞争更加激烈一些,下面是整理近1,2月来小伙伴面试遇到的一些知识点。由于收集的比较多,会分为上下两部分,后续还会推出算法特别专题。希望对大家的面试有所帮助,也希望每个面试的小伙伴都能拿到心仪的offer。答案不要死记硬背,最好理解,因为面试会发散,如果不理解,就无法应对发散问题
@property 的修饰词
- 常见修饰词
assign,weak,strong,retain,copy,nontomic,atomic,readonly,readwrite
atomic和nonatomic
- 1.
atomic
和nonatomic
用来决定编译器生成
的getter
和setter是否为原子操作
。 - 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
是所指对象的地址
,Value
是weak指针的地址数组
- 2.Runtime维护了一个
weak表
,用于存储指向某个对象的所有weak指针
。weak表
其实是一个hash表
,Key
是所指对象
的地址
,value
是weak指针
的地址
(这个地址的值是所指对象指针的地址)数组。之所以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_autoreleasePoolPush
和objc_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()
来释放自动释放池
。这个Observer
的order
是2147483647
,优先级最低
,保证其释放池子
发生在其他所有回调之后。
thread 和 AutoreleasePool的关系
- 1.
每个线程
,包括主线程
在内都维护
了自己的自动释放池堆栈结构
- 2.新的
自动释放池
在被创建
时,会被添加到栈顶
;当自动释放池销毁
时,会从栈中移除
- 3.对于
当前线程
来说,会将自动释放
的对象放入自动释放池
的栈顶
;在线程停止
时,会自动释放掉
与该线程关联
的所有自动释放池
总结
每个线程
都有与之关联
的自动释放池堆栈结构
,新的pool在创建时
会被压栈到栈顶
,pool销毁
时,会被出栈
,对于当前线程
来说,释放对象
会被压栈
到栈顶
,线程停止
时,会自动释放
与之关联
的自动释放池
RunLoop 和 AutoreleasePool的关系
- 4.
主程序
的RunLoop
在每次事件循环之前
,会自动创建一个 autoreleasePool
- 5.并且会在
事件循环结束
时,执行drain操作
,释放其中的对象