近期整理的iOS面试题。不定期更新中。如有问题,欢迎斧正。
-
派发
Swift 有三种派发方式 1静态派发 2消息派发(动态派发) 3函数表派发
OC 只有消息派发(动态派发)
如果不需要多态,静态派发。(多态就是不同的对象响应同一个方法时做出不同的反应,父类sing() 子类实现的时候可以是hipop,可以是流行等) 父类指针指向子类指针,父类指针调用子类对象方法时,首先在子类里查找,没找到就到父类里找。
如果需要覆写,函数表派发。
如果需要对Objective-C可见和动态性,消息派发。
静态派发: 是指在编译时就能确定要调用哪个方法或属性的实现
调用 final、static 修饰的方法或属性。
调用 struct 或 enum 中的方法或属性。
在这些情况下,编译器能够确定要调用的实现,因为它们不能被子类或扩展改变。
函数表派发: 引用类型(Class) 默认使用这种派发方式。 是指通过函数表来调用方法或函数的派发机制。函数表是一个包含指向方法或函数实现的指针的表格,可以在运行时动态更新。当你调用一个方法或函数时,编译器会使用函数表来查找要调用的实现,然后执行它。比静态派发多了两次读和一次跳转。更大的灵活性和可扩展性。每一个类都会创建维护一个函数表,用来记录函数指针,创建子类的时候也会创建一个函数表,当函数是override的时候,存新的指针以区分父类函数。
消息派发: 是指在运行时根据实际类型决定要调用哪个方法或属性的实现。
OC主要用的就是消息派发比如:
- 使用isMemberOfClass检查实例对象是否属于特定类型的类
检查类是否可以调用某个函数 respondsToSelector
在运行时通过swizzling修改函数的实现,也可以通过isa-swizzling修改对象。
使用在运行时添加方法实现class_addMethod
什么时候用消息派发
调用一个非 final 的类的实例方法或属性。
调用一个使用 override 关键字重写的方法或属性。
@objc dynamic 修饰的方法或属性(结构体或枚举它们的方法和属性在编译时就已经确定了要调用的实现,故还是会静态派发)。
在这些情况下,编译器不能确定要调用的实现,因为它们可能被子类或扩展重写。因此,编译器会生成额外的代码来支持动态派发,这些代码会在运行时根据实际类型选择要调用的方法或属性。
-
什么情况下用static
static
关键字可以用来修饰方法、属性、下标脚本、以及嵌套类型。使用 static
关键字可以将这些成员声明为类型级别的,而不是实例级别的。这意味着,无论创建多少个该类型的实例,这些成员的值都是相同的,并且可以通过类型本身来访问,而不需要通过实例。
Static 修饰的全局变量 存储在 静态存储区。 修饰的局部变量由栈区变为静态存储区。
静态属性或方法:当你希望一个属性或方法属于类而不是实例时,可以使用
static
关键字来声明。这些属性或方法可以通过类名访问,而不需要创建实例。类型属性或方法:使用
static
关键字还可以将属性或方法声明为类型属性或类型方法。类型属性和类型方法与类本身相关,而不是与实例相关。嵌套类型: 使用
static
关键字还可以声明嵌套类型为静态类型。这些嵌套类型不需要从外部类的实例中访问,因此可以通过类名直接访问。static与const作用: 声明一个只读的静态变量
头文件不能用static:static
关键字可以用来修饰全局变量和函数,但不可以用于头文件中声明的类和方法。因为 Objective-C 中的类和方法都是动态派发的,即使使用static
修饰,也无法将其转变为静态派发。(~~~**@interface内不能用,但是如果在声明之外用是可以的,.h文件也可以直接使用static修饰方法**~~~)-
Swift 语言类型
Swift 是一种静态类型语言,而 Objective-C 是一种动态类型语言。
在静态类型语言中,变量在编译时被分配类型,并且类型检查发生在编译时。这意味着编译器可以在编译时检测到一些类型相关的错误,从而提高代码的安全性和可靠性。在 Swift 中,变量、常量和函数的类型都必须在声明时指定,并且不能在运行时更改。
相比之下,在动态类型语言中,变量的类型通常在运行时确定,并且类型检查也发生在运行时。这意味着开发者可以更加灵活地编写代码,但也可能会导致更多的运行时错误和不可预测的行为。在 Objective-C 中,变量、常量和方法的类型通常是在运行时解析的。
需要注意的是,Swift 在某些情况下也支持动态类型,例如使用 Any
或 AnyObject
类型,或使用动态派发机制来调用 Objective-C 中的代码。但总体来说,Swift 是一种静态类型语言。
-
Swift比OC更安全吗
相对于 Objective-C,Swift 在一些方面提供了更多的安全性和可靠性。
类型安全:Swift 是一种静态类型语言,这意味着编译器可以在编译时检测到类型错误,从而避免在运行时出现类型不匹配的问题。
可选项:Swift 中引入了可选项(Optional)的概念,这可以帮助开发者处理变量值为空的情况,从而避免一些崩溃或错误。非可选都要初始化,保证使用的时候有值。
内存管理:Swift 中使用自动引用计数(ARC)来管理内存,从而减少了手动管理内存的工作量,同时也避免了一些内存泄漏和野指针等问题。更多的使用值类型,没有引用计数的问题,减少了内存泄漏的风险。
字符串处理:Swift 中的字符串处理更加安全和可靠,避免了在 Objective-C 中常见的字符串越界和缓冲区溢出等问题。
访问控制:Swift 中提供了访问控制机制,可以控制模块、类、属性、方法等的访问权限,从而增强了代码的安全性和可靠性。枚举类型:Swift 中的枚举类型更加强大和灵活,可以附加关联值,从而避免了 Objective-C 中需要使用指针等手段来实现的问题。
总的来说,Swift 在安全性和可靠性方面比 Objective-C 有所提高,但在某些方面可能也会带来一些不便之处,比如在处理字符串时需要进行一些额外的转换操作。
OC中的常量(const)是编译期决定的,Swift中的常量(let)是运行时确定的
-
OC中 static 与 const
static:
Static 修饰的变量只能初始化一份 系统中只有一份内存
Static修饰的关键字不会改变局部变量作用域 但是可以改变生命周期 直到项目结束才会销毁 (比如单例的创建)
Static修饰的全局变量 作用域是当前文件 外部类访问不到 好处是 不会被其他文件访问修改, 其他文件也可以有相同名字变量
Swift 的static
可以修饰属性和方法 修饰了不能重写 相当于 class final 修饰 。 static 可以修饰计算属性 存储属性 类型的方法 而class 是不能修饰存储属性的
const:
被const关键字修饰的实例变量,在初始化之后,其值就不能改变了,
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或者二者同时指定为const;
对于类的成员函数,若指定其为const类型, 则表明他是一个函数,不能修改类的成员变量
-
NSInteger
NSInteger是基本数据类型,是Int或者Long的别名,会根据系统是32位还是64位来决定是本身是int还是long。
int在32位系统4字节 在64位系统4字节。 而long在32位系统是4字节,在64位系统8字节。
-
struct 和 class
Struct 是值类型 不可以继承 小数据模型的拷贝 传值更安全 swift struct还可以实现协议 初始化方法基于属性 无法修改自身属性值 函数要添加 mutating(为了修改变量值) 值类型 不用deinit
Class 可以继承 是引用类型 class可以多次引用
struct内存是分配在栈上,class内存分配在堆上。
-
swift propertyWrapper 属性包装
我们可以给一个类或者结构体 比如Email加@propertyWrapper属性(包装器对象必须包含一个称为被包装的值的非静态属性
。),然后重写get set方法。当我们使用的时候 使用@Email 来修饰属性,那么这个属性就默认使用这个包装的get set方法。
-
actor
是一个引用类型 类似于class 有自己的identity 解决的是 同时只有一个task访问state 这时候就可以用到 await
-
swift async await
Swift 协程。
当函数异步时候就允许挂起,当挂起自己时候,他的调用者也会挂起 所以也必须是异步的
Await 指出异步函数中一次或者多次挂起的位置
挂起时 不会造成线程阻塞
函数恢复的时候 从返回结果流回原函数 并从上次停止的地方继续执行
解决异步带来的嵌套和代码混乱问题 还解决帮助并发编程。
-
协程
协程类似于子程序 在一个线程中 可以执行一个子程序 子程序可以中断 然后执行其他的子程序 然后适当的时候回来继续执行
协程优点是
无需上下文切换开销 避免了无意义调度 由此可以提高性能
无需原子操作锁以及同步的开销
方便切换控制流 简化编程模式
高并发+高扩展性+低成本
协程缺点是
进行阻塞操作的时候会阻塞整个程序
-
线程和进程的区别
进程是操作系统分配资源的最小单位,线程是程序执行的最小单元
一个进程由一个或者多个线程组成,线程是一个进程中代码的不同执行路线
同一进程的线程共享本进程的地址空间和内存等资源,而进程之间则是独立的地址空间。
线程切换比进程快,消耗资源大
进程将CPU分给线程,即真正在CPU上运行的是线程。
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
两者均可并发执行。
-
多进程和多线程
多进程优点
编程相对容易:不需要考虑锁和同步资源的问题
内存隔离,一个进程崩溃不会影响其他进程,方便调试
可以同时运行多个任务
程序因IO阻塞时,可以释放CPU,让CPU为其他程序服务
当系统有多个CPU时,可以为多个程序同时服务
多进程缺点
进程的创建和销毁过程需要消耗较多的计算机资源;
逻辑控制复杂,需要和主程序交互;
多进程调度开销比较大
需要跨进程边界
多线程优点就是
线程共享数据
线程通讯方便快捷
线程更加轻量级,更容易切换
多个线程更容易管理
使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视频的下载
发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好
多线程缺点
大量的线程降低代码的可读性;
更多的线程需要更多的内存空间;
当多个线程对同一个资源出现争夺时候要注意线程安全的问题。
没有内存隔离,单个线程崩溃会导致整个应用的退出。
-
多个异步都结束后再执行后边的
GCD 栅栏函数 挡住前边的 前边的都执行完了 再执行后边的 dispatch_barrier_async
group dispatch_group_enter 和 dispatch_group_leave 最后dispatch_group_notify
-
进程间通讯方式
url scheme
Keychain
Pasteboard
DocumentInteractionController
Local socket
Air drop
UIActivityViewController
APP Groups
-
Swift 线程
NSThread: 封装程度最小最轻量级的,使用更灵活,但是需要手动管理线程的生命周期、线程同步和线程加锁等。(start cancel exit)
NSOperation: 基于GCD封装的,比GCD可控性更强;可以加入操作依赖(addDependency
)、设置操作队列最大可并发执行的操作个数(setMaxConcurrentOperationCount
)、取消操作(cancel
)等。可以配合NSOperationQueue 来操作异步线程。
GCD: 基于C语言,由苹果开发的一个多核编程的解决方案,可以充分利用多核,更高效。
-
常用加密方式
Base64 加密后会变大 大1/3左右 可以反向解密 末位有个 = (常用于在网络传输中传递二进制数据。它将每三个字节转换成四个字符,其中用64个可打印字符表示。) 缺点是无法保证数据的机密性和完整性。
MD5加密 哈希值固定长度,校验数据完整性方便。 它通常用于验证数据的完整性 散列 长度为32位的字符串 128位(bit)。缺点是存在哈希碰撞攻击,安全性不高。
AES加密 安全性高,加密速度快 使用相同的密钥对数据进行加密和解密。 128/192/256 代表密钥长度16/24/32字节。缺点是需要共享密钥,密钥管理较为复杂。
RSA加密 非对称加密算法 公钥和私钥进行加密和解密 安全性高 (公钥加密 私钥解密 私钥加密用于签名防数据被篡改,公钥加密用于加密防敏感信息,防止泄露。) 缺点是加解密速度较慢,密钥长度过短容易被攻击。
Base64 URL编码(参数的 = ? 编码) 网络传输图片等
MD5 用户密码保护 文件完整性校验 数字签名(发布程序时同时发布其MD5,下载后比较MD5是否相同,就可知道程序是否被篡改。)
aes 用于加密明文 密文传输
-
weak和assign
weak
是一种弱引用, 引用计数不会加一 其实是runtime维护了一个hash表 key代表的是这个对象的地址 value代表的是所有指向这个地址的weak指针 当对象被回收的时候 会找到所有的weak指针并置为nil 涉及到一个hash的增删改查 所以有一定的性能消耗。(SideTable是个结构体)
assign
则是一种强引用,表示赋值操作不会对被引用对象的引用计数加 1,可以修饰对象 也可以修饰基本类型 但是不能修饰引用类型,修饰对象指针(引用类型)时,如果被引用的对象被释放后,指针将变成野指针。
-
strong和copy
strong指向的是相同对象地址, 仅仅是指针引用, 增加了引用计数, 这样源头改变的时候, 它也会跟着改变;
而copy声明的变量, 指向的是不同对象地址、它不会跟着源头改变, 实际上是深拷贝。
当原字符串是NSString时,字符串是不可变的,不管是strong还是copy属性的对象,都是指向原对象。
当原字符串是NSMutableString时,strong属性只是增加了原字符串的引用计数,而copy属性则是对原字符串做了次深拷贝,产生一个新的对象,且copy属性对象指向这个新的对象,且这个Copy属性对象的类型始终是NSString,而不是NSMutableString,因此其是不可变的。
-
指针
指针其实是一个内存地址,对于一个内存单元来说,单元的地址即为指针
常量指针本质是指针,常量修饰它表示这个指针是指向常量的 这个对象不能被修改
指针常量是一个常量,说明这个常量的值是指针 因为是常量 所以不能被赋值
指针函数 本质是一个函数 不过返回值是指针
函数指针 本质是一个指针 指向了一个函数
-
iOS 锁
OSSpinLock(自旋锁) while-do 忙等。一旦获取了自旋锁,线程就会一直保持该锁,直到显式释放自旋锁,对于线程只会阻塞很短时间的场合是有效的。
os_unfair_lock 在加锁时会处于休眠状态 是一种互斥锁 解锁后由内核唤醒
atomic 自带一把自旋锁。对于修饰的属性会用spinlock_t加锁,底层用os_unfair_lock。并不是线程安全的 只能保证get/set存取方法的线程安全,但是当一个线程在get/set时候 另一个线程在release 就可能crash
dispatch_semaphone(信号量) 信号量是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例,信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。create wait(-1) signal(+1) 大于0 wait 执行
pthread_mutex(互斥锁) pthread_mutex就是互斥锁本身,当锁被占用,其他线程申请锁时,不会一直忙等待,而是阻塞线程并睡眠
NSLock(互斥锁)是对pthread_mutex的封装
NSRecursiveLock(递归锁)NSRecursiveLock在底层也是对pthread_mutex的封装
NSCondition(条件锁)
NSConditionLock(条件锁)
@synchronized(互斥锁)走底层的objc_sync_enter和objc_sync_exit进出口方法,如果obj不存在,则调用objc_sync_nil,通过符号断点得知,这个方法里面什么都没做,直接return了,即 如果属性为空,则锁不住。底层维护了一个哈希表进行线程data的存储,通过链表表示可重入(即嵌套)的特性,虽然性能较低,但由于简单好用,使用频率很高( 没有锁,
threadCount = 1,lockCount = 1
, 存到tls_set_direct
链表中;
不是第一次进来,是在tls链表
进行快速缓存查找的,它们是在同一个线程进行lockCount
加,并且将result
存到tls_set_direct
。如果是exit就 减1。 如果链表中data为空就尝试解锁,成功返回数据, 如果失败或者不为空,返回错误,继续加锁。)
自旋锁:OSSpinLock, atomic
互斥锁:@synchronized, pthread_mutex, NSLock
条件锁:NSCondition, NSConditionLock 底层都是对pthread_mutex的封装,当满足某一个条件时才能进行操作
递归锁:pthread_mutex(recursive),NSRecursiveLock。递归锁就是同一个线程可以加锁N次而不会引发死锁。递归锁是特殊的互斥锁,即是带有递归性质的互斥锁
信号量:dispatch_semaphore
读写锁:读写锁实际是一种特殊的自旋锁
OSSpinLock被弃用原因是
当多个线程有优先级的时候,有一个优先级较低的线程先去访问了资源,并使用了OSSpinLock
对资源加锁,又来一个优先级较高的线程去访问了这个资源,这个时候优先级较高的线程就会一直占用cpu的资源,导致优先级较低的线程没办法与较高的线程争夺cpu的时间,最后导致最先被优先级较低的线程锁住的资源迟迟不能被释放,从而造成优先级反转的bug。
-
闭包和block
闭包是匿名函数 而block本质上就是个结构体struct,有一个isa指针。block是封装了函数调用(函数指针)以及函数调用环境(捕获到的参数)的OC对象。
闭包通过逃逸闭包修改内部变量 block 要__block 修饰符 复制引用地址 修改数据。
block捕获方式
全局的NSGlobalBlock
堆NSMallocBlock
栈NSStackBlock
Static 修饰的或者没有用到自定义局部变量的或者用到全局外部变量的 是全局的
栈 是访问自定义局部变量的 block 超过作用域就销毁
堆的 是copy过的block 自带一个引用计数 GCD的block 在堆上
ARC中 block只能存在堆或者全局。
1). 在block内部使用外部指针且会造成循环引用情况下,需要用__week修饰外部指针:
__weak typeof(self) weakSelf = self;
2). 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下。
__strong typeof(self) strongSelf = weakSelf;
3). 如果需要在block内部改变外部栈区变量的话,需要在用__block修饰外部变量。
block 本质上是一个OC对象,内部有个 isa 指针,可以用 retain/strong/copy 等修饰词修饰。但是 block 在创建的时候内存默认分配在栈上,而不是堆上的。所以它的作用域仅限创建时候的作用域内,当你在该作用域外调用该 block 时,程序就会崩溃。
一般情况下你不需要自行调用copy或者retain一个block. 只有当你需要在block定义域以外的地方使用时才需要copy. Copy将block从内存栈区移到堆区
其实block使用copy是MRC留下来的也算是一个传统吧, 在MRC下, 如上述, 在方法中的block创建在栈区, 使用copy就能把他放到堆区, 这样在作用域外调用该block程序就不会崩溃
但在ARC下, 使用copy与strong其实都一样, 因为block的retain就是用copy来实现的
-
设计模式
设计模式是为了可重用代码 让代码可读性更强 保证可靠性
设计模式是通过封装和隔离变化点来处理各种变化问题,隔离的好处在于增加复用性,降低耦合度
六个原则
单一职责原则 通俗地讲就是一个类只做一件事
开闭原则 对修改关闭,对扩展开放。要考虑到后续的扩展性,而不是在原有的基础上来回修改
接口隔离原则 使用多个专门的协议、而不是一个庞大臃肿的协议,如 UITableviewDelegate + UITableViewDataSource
依赖倒置原则 抽象不应该依赖于具体实现、具体实现可以依赖于抽象。调用接口感觉不到内部是如何操作的
里氏替换原则 父类可以被子类无缝替换,且原有的功能不受任何影响
迪米特法则 一个对象应当对其他对象尽可能少的了解,实现高聚合、低耦合
举例
代理模式 当我想让其他类去做某些事 但是我又不知道哪些类可以的时候 可以用代理模式 好处是 解耦合 通过@protocol方式实现,常见的有tableView,textField等。
单例模式 单例保证了应用程序的生命周期内仅有一个该类的实例对象,而且易于外界访问.在ios sdk中,UIApplication, NSBundle, NSNotificationCenter, NSFileManager, NSUserDefault, NSURLCache等都是单例
缺点是 可能造成责任过重 单例创建到程序结束一直存在 过多单例会影响性能
观察者模式 一个一对多的关系 当一个对象变化的时候 其他依赖都可以自动更新 好处是耦合性低 缺点是 可能会有循环
工厂模式 对实现了同一接口的一些类进行的实例创建 不需要知道具体类的名字只需要参数就可以创建
-
KVO和KVC
KVO: 允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,只针对属性才会发生作用, 一般继承自 NSObject 的对象都默认支持 KVO。
使用步骤
通过
addObserver:forKeyPath:options:context:
注册观察者,监听keypath变化观察者中实现
observeValueForKeyPath:ofObject:change:context:
方法,当keypath变化时 KVO会通过这个回调通知观察者不需要监听后需要
removeObserver:forKeyPath:
方法将KVO
移除。要在观察者消失前,否则Crash。
KVO实现 KVO是通过isa 混写(isa-swizzling)技术
实现的。 在运行时根据原类创建一个中间类,这个中间类是原类的子类(命名规则是NSKVONotifying_xxx的格式),并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class
首先调用willChangeValueForKey:方法。
然后调用set方法真正的改变属性的值。
开始调用didChangeValueForKey:这个方法,调用[super didChangeValueForKey:key]时会通知监听者属性值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context这个方法。
KVC: 可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定。
kvc可以 动态取值和设值、Model和字典转换setValuesForKeysWithDictionary、访问私有变量、修改控件内部属性
setValue:forKey: 赋值的原理
① 首先会查找setKey:、_setKey: (按顺序查找);
② 如果有直接调用,如果没有,先查看accessInstanceVariablesDirectly方法判断是否响应;
③ 如果可以,访问会按照 _key、_isKey、key、iskey的顺序查找成员变量,找到直接赋值;
④ 未找到报错NSUnkonwKeyException错误。
valueForKey: 取值的原理
① kvc取值按照 getKey、key、iskey、_key 顺序查找;
② 存在直接调用,如果没找到,同样会先查看accessInstanceVariablesDirectly方法;
③ 如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量,找到直接赋值
④ 未找到报错NSUnkonwKeyException错误。
KVC和KVO 它们的本质都一样,都会调用[self willChangeValueForKey:key]; 和 [self didChangeValueForKey:key]; 所以通过KVC修改属性会触发KVO
-
堆栈链表
栈:由系统编译器自动管理,不需要程序员手动管理。栈区的内存地址是从高到低的分配,存放局部变量,先进后出,一旦出了作用域就会被销毁。
堆:释放工作由程序员手动管理,不及时回收容易产生内存泄露。内存地址是从低到高分配,变量空间分配都是alloc,内存分配由系统来负责但是程序员需要管理堆区的内存释放。系统使用一个链表来维护所有已经分配过的内容空间。
堆是吃了拉 栈是吃了吐
内存从高到低顺序
栈:线性结构
存放临时变量,局部变量,函数的参数值等(函数和方法不一样)
存储的是指针,当然偶尔也会存储一些数据,比如当字符串很少时,会以TaggedPointer类型和指针存储到一起
非OC对象一般放在操作系统的栈里面(int、char、float、double、struct、enum等)
堆: 链表结构
由程序员管理的内存(MRC)使用alloc开辟堆内存,或者使用C语言中malloc等方法创建的
全局静态区
数据区: 存放已经初始化的全局变量,即静态分配(static)的变量和全局变量
BSS区: 存放程序中已经定义但是未初始化的数据
常量区
存放的是常量的值
NSString *str = @"测试数据";
str的指针:存储在栈中
str的值"测试数据"存储在常量区
static NSString * str0 = @"123";
str0存储在静态区
值存储在常量区
代码区
存放的是二进制的代码,需要保证二进制文件在运行时不能被修改,只读
链表: 一种线性表,是一种物理存储单元上非连续、非顺序的存储结构。
包含任意的实例数据datafields和一或两个用来指向上一个/或下一个节点的位置的链接links。
优势: 可以充分利用计算机内存空间,实现灵活的内存动态管理。允许插入和移除表上任意位置上的节点.
劣势:链表由于增加了节点的指针域,空间开销比较大;另外,链表失去了数组随机读取的优点,
-
内存管理
MRC ARC Autoreleasepool
MRC 手动管理内存
主要是引用计数器。创建时为1,每有人使用计数器+1(retain),当不使用后释放release -1。当没有人使用(计数为0)时系统会回收。对象即将被销毁时系统会自动给对象发送一条dealloc消息。
原则是谁添加谁释放,谁使用谁释放,有加就有减。
只要一个对象被释放了,我们就称这个对象为 "僵尸对象(不能再使用的对象)"
当一个指针指向一个僵尸对象(不可用内存),我们就称这个指针为野指针
只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS错误)
而没有指向存储空间的指针叫空指针,给空指针发消息不会有反应。
ARC 自动管理内存
编译器会管理好对象的内存,在App编译阶段会在合适的地方插入retain, release和autorelease。只要还有一个强指针变量指向对象,对象就会保持在内存中
Autoreleasepool
magic 用来校验autoreleasepage是否完整
id * next 下一个能存放autorelease对象的地址
thread 指向当前线程
depth代表深度 从0开始
hiwat代表high water mark
next == begin() 时,表示 AutoreleasePoolPage 为空;当 next == end() 时,表示 AutoreleasePoolPage 已满。
本质就是延迟调用release方法。是依赖AutoreleasePoolPage实现的。
AutoreleasePoolPage 本质是一个节点对象,大小是4096字(PAGE_MAX_SIZE:4096),前7个变量都是8字节,剩下的4040字节存储着autorelease对象地址
autoreleasepool本质上就是一个指针堆栈,内部结构是由若干个以AutoreleasePoolPage对象为结点的双向链表组成,系统会在需要的时候动态地增加或删除page节点
在运行循环(runloop)开始前,系统会自动创建一个autoreleasepool(一个autoreleasepool会存在多个AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush函数,runtime会向当前的AutoreleasePoolPage中add进一个POOL_BOUNDARY(哨兵对象),代表autoreleasepool的起始边界地址,并返回此哨兵对象的内存地址。
这时候next指针则会指向POOL_BOUNDARY(哨兵对象)后面的地址(对象地址1)。
后面我们创建对象,如果对象调用了autorelease方法(ARC编译器会给对象自动插入autorelease),则会被添加进AutoreleasePoolPage中,位置是在next指针指向的位置,如上面next指向的是对象地址1,这是后添加的对象地址就在对象地址1这里,然后next就会 指向到对象地址2 ,以此类推,每添加一个地址就会向前移动一次,直到指向end()表示已存满。
当不断的创建对象时,AutoreleasePoolPage不断存储对象地址,直到存满后,则又会创建一个新的AutoreleasePoolPage,使用child指针和parent指针指向下一个和上一个page,从而形成一个双向链表。
当调用objc_autoreleasePoolPop(哨兵对象地址)时,添加最后一个对象地址是8,那么这时候就会依次由对象地址8 -> 对象地址1,每个对象都会调用release方法释放,直到遇到哨兵对象地址为止。
当即将进入Loop时,会调用objc_autoreleasePoolPush创建自动释放池,优先级最高。
BeforeWaiting(即将休眠)的时候调用objc_autoreleasePoolPop和objc_autoreleasePoolPush释放旧池子和创建新池子。Exit(退出Loop) 使调用objc_autoreleasePoolPop释放自动释放池。优先级最低。
autorelaeasepool、NSRunLoop 、子线程三者的关系
主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理。
子线程默认不开启runloop,当产生autorelease对象时候,会将对象添加到最近一次创建的autoreleasepool中,一般是main函数中的autoreleasepool,由主线程runloop管理;也就是不用手动创建Autoreleasepool,线程销毁时在会在最近一次创建的autoreleasepool 中释放对象。
自定义的 NSOperation 和 NSThread 需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。 但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。NSThread和NSOperationQueue开辟子线程需要创建autoreleasepool GCD不需要 因为每个队列自行创建
AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程),每开一个线程,会有与之对应的AutoreleasePool。
autorelaeasepool释放:一是Autorelease对象是在当前的runloop即将进入休眠或者退出时时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。 二是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool
使用@autoreleasepool {}代码块手动创建的AutoreleasePool,当超出代码块的作用域时被销毁,这时会释放池中的对象,可以达到立即释放的效果。一般情况下,在编写循环时可能需要手动创建AutoreleasePool。(尽量避免对大内存使用该方法,对于这种延迟释放机制,还是尽量少用 不要把大量循环放到一个autoreleasepool里)
-
Runloop
Runloop 又叫运行循环 内部是一个 do- while循环,不断处理各种任务,保证程序持续运行。 目的就是当线程有任务的时候 执行任务 没任务的时候线程休眠 提高性能节省资源。
RunLoop与线程是一一对应关系,一个线程对应一个RunLoop,他们的映射存储在一个字典里,key为线程,value为RunLoop。主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
因为要处理app中各类的事件 比如事件响应 手势识别 界面刷新 autoreleasepool timer等 所以需要用到runloop保活
Mode
kCFRunLoopDefaultMode: 默认Mode,主线程通常在这个Mode下运行。
UITrackingRunLoopMode: 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
UIInitializationRunLoopMode: 启动App时第进入的第一个Mode,启动完成后不再使用,会切换到kCFRunLoopDefaultMode。
GSEventReceiveRunLoopMode: 接受系统事件的内部Mode,通常用不到。
kCFRunLoopCommonModes: 占位用Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode。
Source0: App内部事件,由App自己管理的,像UIEvent、CFSocket等 不能主动触发,需要调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop
source1:其他进程或者系统内核的任务。能主动唤醒 RunLoop 的线程
常用地方
子线程开启timer 通常用commonmode, 因为要考虑到runloop会由于界面滚动切换到trackingmode。
线程保活: 当有一个任务可能随时执行,为了避免多次任务多次创建销毁线程 减低性能消耗
tableview的滚动时图片不加载
监听卡顿
-
Runtime
在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才能根据函数的名称找到对应的函数来调用。
Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下面几个方面探寻Runtime的实现机制。
系统首先找到消息的接收对象 然后通过对象的isa指针找到他的类 (类存的实例方法 元类存的类方法)
首先先去找缓存 (method_name为key,method_imp是value)
在他的类中查找 method_list是否有selector
如果没找到 就去父类查找
如果找不到会有消息转发 在这里可以拦截crash 如不拦截会unrecognized selector sent to xxx crash
如果找到了 执行他的IMP(方法实现,指向最终实现程序的内存地址的指针)
转发IMP的return值
崩溃的三次补救
动态方法解析 resolveInstanceMethod 和 resolveClassMethod
重定向 forwardingTargetForSelector
完整转发 forwardInvocation
常用地方
关联对象(Objective-C Associated Objects)给分类增加属性
方法魔法(Method Swizzling)方法添加和替换和KVO实现
消息转发(热更新)解决Bug(JSPatch)
实现NSCoding的自动归档和自动解档
实现字典和模型的自动转换(MJExtension)
通用埋点
-
@property 的本质是什么
成员变量前加@property 系统就会自动帮我们生成基本的setter/getter方法
ivar(实例变量)、getter+setter(存取方法)
-
isa指针
是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。
对象 的类是 类(对象) ;
类(对象) 的类是 元类 ;和类同名
元类 的类是 根元类 NSObject;
根元类 的类是 自己 ,还是NSObject。
isa指针的作用就是描述oc对象的所有信息, 可以通过isa指针知道父类是哪个,实例变量有哪些,实例方法有哪些,遵守的协议等。
-
为什么swift将Array String 等设计为值类型
值类型和引用类型相比 最大的优势 就是更高效的使用内存, 值类型是在栈上, 栈上的操作只是单个指针的移动 而引用类型是在堆上 涉及到合并 位移 重链接等等, 这样设计是为了减少堆内存的分配和回收 并且用 copy-on-write 将值传递和复制开销降到最低
copy-on-write 当你传递一个值类型的变量的时候(给一个变量赋值,或者函数中的参数传值),它会拷贝一份新的值让你进行传递。你会得到拥有相同内容的两个变量,分别指向两块内存。频繁操作占用内存比较大的变量的时候就会带来严重的性能问题。所以
对于Int、String等基本类型的值类型,它们在赋值的时候就会发生拷贝,它们没有Copy-on-Write这一特性(因为Copy-on-Write带来的开销往往比直接复制的开销要大)。
对于Array、Dictionary、Set类型,当它们赋值的时候不会发生拷贝,只有在修改的之后才会发生拷贝,即Copy-on-Write。
-
存储属性和计算属性
存储属性 类似于成员变量 存储在实例对象的内存中(堆) 结构体和类可以定义存储属性 枚举不可以
计算属性 本质就是一个func 不会占用实例对象的内存 struct class enum 都可以定义
-
UIView和CALayer的关系
UIView继承于UIResponder 这个是响应者对象 可以对iOS中事件响应及传递
CALayer没有继承自UIResponder 所以 CALayer不能处理事件。 CALayer是来绘制UI的
UIView 是对CALayer的封装 例如frame center bounds 之类的其实都是在操控CALayer 但是有些 圆角 阴影等属性 只能用CALayer去设置
UIView 是CALayer 的代理 UIView持有一个CALayer属性 用来动画和绘制等
为什么提供提供基于UIView和CALayer两个平行的层级关系呢? 答:原因在于要做职责分离,避免重复代码。iOS系统中我们使用的是UIKit和UIView,而在MacOS系统使用的是AppKit和NSView,所以在这种情况下将展示部分使用CALayer分离出来会给苹果的多平台系统开发带来便捷。
-
setNeedsDisplay setNeedsLayout layoutIfNeeded
setNeedsDisplay 会自动调用 drawRect 进行绘制
setNeedsLayout 会默认调用 layoutSubViews 给当前视图做标记
layoutIfNeeded是查找标记 有的话立刻刷新
-
self 和 super的区别
self 是调用自己的方法 super是调用父类的
Self是类 super是预编译的指令
[self class] 和 [super class] 输出一样
super并不是父类的意思,它只是一个编译符号,只会去父类中寻找对应的方法和参数
-
类簇
类簇在在公共抽象超类下对多个私有的具体子类进行分组 比如 NSArray 不管创建可变不可变 alloc后都是 __NSPlaceholderArray 当init不可变数组后得到的是 __NSArray0, 一个元素 就是 __NSSingleObjectArrayI,多个元素就是 __NSArrayI, 可变的init出来就是 __NSArrayM。
优点就是
可以将抽象基类背后的复杂细节隐藏起来
不需要记住各种创建对象具体实现,简化开发成本
便于封装和组件化
减少if-else
新增功能不影响老代码
缺点是
已有类簇不好扩展
-
block和delegate区别
Block运行成本高 block出栈时候要将数据从栈copy到堆上,使用完成后才置为nil消除 delegate只是一个指针 直接调用 没有额外消耗,只多做了一个查表动作
-
动态库和静态库
动态库 .tbd (.dylib) .framework
静态库 .a .framework (自己生成的是静态库 系统的framework 是动态库)
.a 是纯二进制文件 .framework = .a + .h + 资源文件
静态库 链接时会被完整的复制到可执行文件中
动态库 链接时不复制 运行时由系统动态加到内存 只加载一次
动态库的好处是 可执行文件会变小 多个应用程序共享系统里的动态库
静态库的好处是模块化 避免少量改动导致大量重复编译 方便代码共享 不想别人看到内部实现
-
hitTest 和 pointInside
hitTest是寻找最合适的view 只要有事件传递就会调用这个
pointInside 判断触摸的点在不在控件内 hittest底层会用
-
启动优化
启动大概步骤为:
- 打开app,内核初始化跳转到dyld执行
- 分配虚拟内存空间
- fork进程
- 加在MachO到进程空间
- 动态加载链接器dyld并将控制权交给dyld,这个过程会产生ASLR随机数,为MachO起始地址在内存的偏移,随机地址可以提升安全性。
- 进入dyld动态链接器将app处理为一个可运行状态
- 动态加载MachO
- 地址修正
- OC环境配置。 dyld处理完后会调用OC的runtime执行setup等相关工作
执行各模块的初始化
通过 runtime在dyld注册通知, 执行C/C++初始化构造器。 如果有C++会回调 libc++ 对全局静态变量隐式初始化等进行调用。
然后就是进入main() 函数
进入 main() -> UIApplicationMain -> 初始化回调 -> 显示****UI****。
main()之前的时间打印
在Run里配置一个DYLD_PRINT_STATISTICS 可以打印pre-main花费时间
减少动态库使用 不用的库及时删除
尽量不用内嵌的 dylib(embedded)
清理冗余的类 category
删减一些无用的静态变量,删减没有被调用到或者已经废弃的方法。
Timer工具分析耗时方法操作
关注首页的内容 尽快加载
本地缓存
合并多个动态库
优化类、方法、全局变量
优化首屏渲染前的功能初始化
对
application:didFinishLaunchingWithOptions:
里的任务尽量延迟加载或懒加载。不要在 NSUserDefaults 中存放太多的数据,NSUserDefaults 是一个 plist 文件,plist 文件被反序列化一次。
主要是main阶段, 三方库的注册配置,引导页的显示,版本更新逻辑。把不影响的三方库注册可以延后等。
-
TCP 和 UDP
TCP 是面向链接的 是一个可靠的传输 链接需要进行三次握手 开销大 不会主动断开 适用于用户传输数据较大 可靠性高的应用 断开链接需要四次握手 就像打电话
UDP 是面向非连接的 不可靠的 UDP一次只能传送少量数据 可靠性低 但是传输经济
音视频是UDP 因为 TCP开销大 丢包会重传 但是音视频的话 丢一两个包 也没关系 网络不好也能保证播放
三次握手
第一次握手: 客户端给服务器发送一个 SYN(synchronous) 报文。 // 客户端发送网络包,服务端收到了。服务端得知客户端的发送能力、服务端的接收能力是正常的。
第二次握手: 服务器收到 SYN 报文之后,会应答一个 SYN+ACK(acknowledgement 确认) 报文。// 服务端发包,客户端收到了。客户端就能得知:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手: 客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。// 客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。 服务器收到 ACK 报文之后,三次握手建立完成。
好处是
确认双方的接受能力、发送能力是否正常。
指定自己的初始化序列号,为后面的可靠传送做准备。
如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成。
四次挥手
客户端发送一个 FIN(finish结束) 报文,报文中会指定一个****序列号****。此时客户端处于CLOSED_WAIT1状态。
服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的****序列号****值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT2状态。
如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个****序列号****。此时服务端处于 LAST_ACK 的状态。
客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的****序列号****值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态。
为什么客户端发送 ACK 之后不直接关闭,而是要等2MSL(最大报文段生存时间)才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 FIN 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。 至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。
-
HTTP和HTTPS
http是明文传输 https 有安全性的 ssl加密传输协议
Https需要到证书认证机构申请证书 一般需要费用 (公钥放在 <u>CA</u> 颁发的数字证书中)
http页面响应快 因为http使用tcp 三次握手建立连接,交换三个包 而https 除了tcp三个包 还要加上ssl握手的9个包 一共12个包
https 比http更安全
-
为什么说http是无状态的
无状态指的是协议对于事物处理没有记忆能力,服务器不知道客户端是什么状态。
-
DNS
DNS是进行主机名和ip地址转换的目录服务
一个由分层的DNS服务器实现的分布式数据库
解析过程
输入一个域名比如 www.163.com 客户端会发一个DNS请求到本地DNS服务器 本地DNS一般由网络计入服务商提供(移动 电信)
本地DNS会根据缓存查一次 如果有就返回 没有就向DNS根服务器去查询
根DNS服务器没有记录对应关系 会返回一个域服务器地址
本地DNS向域服务器发起请求(.com 域服务器) 然后域服务器会告诉本地DNS服务器域名解析服务器的地址
最后 本地DNS服务器会向解析服务器发出请求 这时候能收到一个和域名对应的IP地址 然后返回给客户端并且保存在缓存中
-
关键字
@escaping 逃逸闭包 当闭包作为一个参数传递的时候 就是逃逸了 就是调用逃离了函数的作用域范围 有可能会在函数的作用域范围之外调用
@autoclosure 关键字能简化闭包调用形式
convenience 便利构造器 必须用当前类的构造函数完成初始化 不能被子类重写或者子类用super调用
required 首先只能修饰类的初始化方法 当子类含有异于父类的初始化方法的时候 子类必须实现父类的 required初始化方法 当子类没有初始化方法的时候 默认用父类的
-
面向对象
封装 提高了代码复用性,隐藏了实现细节 便于调用
继承 可以实现代码复用 子类可以继承父类的所有成员变量和方法
多态 不同对象对响应同一个方法时做出的不同反应 子类的指针可以赋值给父类(父类sing()方法子类实现的时候可以是hiphop,可以是流行等) 父类指针指向子类指针,父类指针调用子类对象方法时,首先在子类里查找,没找到就到父类里找。
-
数据库中的事务是什么意思?
事务就是访问并操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。如果其中一个步骤出错就要撤销整个操作,回滚到进入事务之前的状态。
-
category能否添加属性,为什么?能否添加实例变量,为什么?
可以添加属性 但是实例变量+get+set都需要手动实现 可以通过关联对象实现
在category里实现 方法
(NSString *) property {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setProperty:(NSString *)categoryProperty {
objc_setAssociatedObject(self, @selector(property), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
不能 分类是运行时被编译的 这时候类结构已经固定 所以无法添加实例变量
category实际上是Category_t结构体,运行时,新添加的方法会倒序插入到原有方法的最前边,所以不同的category实现了同一个方法,最终执行的是最后一个。编译完成时category和原类是分开的,当运行时通过runtime才合并到一起。
-
编译过程做了哪些事
预处理、编译、汇编、链接。
先通过编译器生成机器码 机器码可以直接在CPU执行 所以效率高 编译依赖 clang 和 LLVM
Clang 作为编译器前端 llvm作为编译器后端
Clang 预处理 --> 词法分析 --> 语法分析 --> 生成IR(Clang Code Generator)
LLVM 对IR优化 --> 目标代码--> 汇编器 --> 机器码(LLVM Code Generator)--> 链接 --> Mac-O文件
编译器前端的任务是 语法分析 语义分析 生成中间代码 会进行类型检查
编译器后端任务是 进行无关代码优化 生成机器语言。 LLVM会进行BitCode的生成 链接期优化等等
-
指针
指针常量和常量指针
常量指针本质是指针,常量修饰它表示这个指针是指向常量的 这个对象不能被修改
指针常量是一个常量,说明这个常量的值是指针 因为是常量 所以不能被赋值
指针函数 和 函数指针
指针函数 本质是一个函数 不过返回值是指针
函数指针 本质是一个指针 指向了一个函数
-
iOS 内省和反射
iOS内省的方法
xxx是不是什么类型,是不是什么子类,能不能响应什么方法,是不是遵守某个协议
isMemberOfClass isKindOfClass isSubclassOfClass respondsToSelector conformsToProtocol
反射
类名 方法名 属性名 和字符串在运行时互相转化的能力
NSStringFromSelector NSSelectorFromString
NSClassFromString
代码模块化 解耦代码
后端动态下发类名方法名 动态调用
反射使用: 可以通过后台推过来的数据 来进行动态的页面跳转或者方法执行。
-
值类型和引用类型区别,swift中值类型有哪些,引用类型有哪些。
值类型,即每个实例保持一份数据拷贝。一般将其视为储存在栈
引用类型,即所有实例共享一份数据拷贝。变量与其分配的数据(实例)是分离的,引用类型的实例分配在堆上,变量分配在栈上
Swift 里struct,enum、 tuple 、Int、Double、Float、String、Array、Dictionary、Set都是值类型
Class 是引用类型。
-
Optional可选类型属于引用类型还是值类型?如何实现的
Optional是一个枚举类型。故是值类型。
Optional是一个enum枚举,里面有两个case,none和some(Wrapped),Optional.none就是nil,Optional.some(Wrapped)就是一个包装值,Wrapped就是存储的值。
-
MVVM
为了给MVC中的VC瘦身,衍生出了MVVM(Model-View-ViewModel)
Model 模型对象,用于保存数据,通常定义为只有数据并没有方法的结构体
View 呈现 UI(用户界面)并响应用户的事件,通常是 ViewController 和 View
ViewModel 用于桥接 Model 和 View 两层。准备好 Model 提供给 View 以呈现 UI,并把 View 层的用户交互数据更新到 Model 中。
可以理解为 Model层处理数据 使用didSet来处理数据变化后的操作。比如使用block来回调View层绑定的block方法。可以通过delegate、block、kvo、notification等实现数据绑定。
好处:
高内聚,低耦合。 Controller中只需要处理基本业务逻辑,其余状态以及模型的更新都由viewModel处理。也方便构造单元测试。解耦方便复用
代码清晰 所有逻辑处理都在ViewModel中,Controller层只负责展示view和生命周期。
开发解耦 由于模块分离 可以由不同的开发人员负责view层和viewmodel层。
坏处:
代码增多。每个Controller都会分出来一个ViewModel实现绑定。
定位原始出问题的地方较难。由于数据绑定,数据会快速通过绑定方式传递。
-
iOS程序开发引用的第三方库之间出现冲突的处理方法
通过xcrun 把.a 包解开 并把冲突文件剔除 再合并成新的.a 包放入工程。
-
怎么保存登录信息 如token之类的
如果是长期存储可以保存到keychain。 使用keychain需要导入Security框架,每个iOS程序都有一个独立的keychain存储,keychain里保存的信息不会因App被删除而丢失,所以在 重装App后,keychain里的数据还能使用。
-
iOS中常见的内存问题
一般app占用系统内存20%+ 就有内存警告,超过50%就很容易crash。
循环引用。
delegate 要用weak声明,否则会循环引用
Block 当block赋值的时候 又调用了持有的对象就会循环引用。解决方法是 __weak
NSNotification 在addObserver后要remove
可以通过Instruments 来查看 leaks。
-
SDWebImage
- setImageWithURL 会把占位图先显示出来。然后根据URL来找内存有没有,如果内存有图片直接展示。如果没有图片就加入队列去硬盘查找图片是否缓存,方式是通过URLKey去尝试读取文件。如果找不到就说明没有下载过,开始下载,生成或共享一个SDWebImageDownloader,下载使用由NSURLConnection来处理。下载结束后 内存缓存和硬盘缓存都保存一份。内存警告的时候清理内存缓存
-
App启动完成过程
解析info.plist 加载相关信息如闪屏,沙盒建立,权限检查
MachO加载 加载依赖、定位内外部指针引用 例如字符串函数等、执行C/C++初始化、加载类扩展中的方法、C++静态对象加载,调用OC的load函数
main函数 -> 创建UIApplication对象 -> 创建UIApplicationDelegate并复制 -> 读取 info.plist设置程序启动的属性 -> 创建main 的runloop -> 启动成功后调用 application: didfinish 方法 -> 如果info.plist配置了启动 storyboard文件名则加载sb文件夹 ->如果没配置就直接走rootViewController -> 显示
-
数据结构存储分为几种
两种 顺序存储结构和链式存储结构
顺序存储比如栈和队列
链式存储比如链表。
-
数组和链表的区别
数组元素在内存上连续,可以通过下标去查找。插入删除需要移动大量元素。适用于元素裱花少的情况
链表在内存上不连续存储,查找慢,但是插入和删除效率很高。
-
排序算法
选择排序:将已排序部分定义在左端,选择未排序部分最小元素和未排序部分第一个元素交换
冒泡排序: 将已排序部分定义在右端,遍历未排序部分的过程执行交换,将最大元素交换到右端
插入排序:将已排序定义在左端,将未排序部分元素第一个元素插入的哦啊已排序部分合适位置。
-
组件化
组件化好处是
业务分层、解耦。
有效拆分、组织庞大的工程代码,使工程目录可维护
便于各业务功能拆分抽离,实现功能复用
业务隔离,跨团队开发代码控制和版本风险控制
模块化对代码封装性合理性有要求。
随意组合 满足不同需求。
-
事件响应过程
当一个硬件事件(触摸、锁屏、摇晃等)发生后,首先由IOKit生成一个IOHIDEvent,随后用mach port转发给app 进程。随后注册的source1就会触发回调,内部会触发source0,并用_UIApplicationHandleEventQueue()于app内分发。 此方法会把IOEvent包装成UIEvent分发给UIWindow。
-
手势识别过程
当_UIApplicationHandleEventQueue()识别了一个手势时,首先会调用Cancel将当前的touchesBegin/Move/End 等打断,然后系统将对应的UIGestureRecognizer标记为待处理。苹果注册了一个Observer监测BeforeWaiting事件,回调函数会获得所有被标记为待处理的GestureRecognizer, 并执行gesture的回调。当有Gesture的变化时,这个回调会相应处理。
-
死锁
多个线程执行过程中,因为争夺资源而造成互相等待。
-
Tableview 卡顿
cell重用
避免cell 重新布局
提前计算并缓存cell属性及内容
减少cell中控件数量,尽量使cell之间布局大致相同,不同风格的可以使用不同的重用标志符。
不用离屏渲染,比如clearColor
局部reload。reload 单独section或者某几个cell。
异步加载网络数据图片等。
少使用addView给cell动态添加view
按需加载cell, 滚动很快时,只加载范围内的cell
缓存行高。
-
降低包大小
编译器优化:Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES,去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO, Other C Flags 添加 -fno-exceptions 利用 AppCode 检测未使用的代码:菜单栏 -> Code -> Inspect Code 编写LLVM插件检测出重复代码、未被调用的代码
去除无用资源
-
UIViewController生命周期
load
initialize
Init (xib, sb, 代码)
loadView
viewDidLoad
viewWillAppear
updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews
viewDidAppear
viewWillDisappear
viewDidDisappear
Dealloc
-
UIButton继承链,怎么给Button加点击区域
事件响应是从下往上传的,事件处理是从上往下传的。
UIButton-UIControl-UIView-UIResponder-NSObject
pointInside 修改区域范围
使用关联对象 扩大范围objc_setAssociatedObject
-
为什么一定要在主线程操作UI
因为UIKit不是线程安全的,所以在主线程串行执行就是人为加锁,高效也保证了线程安全。
-
Post请求和Get请求区别
post请求更安全(不会作为url一部分,不会缓存,不会保存在服务器日志以及浏览器浏览记录里。)
post请求可以发送大的数据
post请求能发送其他数据类型
传参方式不同,一个是在url一个是request body
get产生一个TCP包,post产生俩。(get请求 会把header和data一起发过去,服务器响应200。post会先发header,服务器响应100 continue后再发送data,服务器响应200)
-
SEL原理
SEL类型是方法指针。@selector语法取到的就是方法的指针。
-
drawrect & layoutsubviews调用时机
layoutsubviews
init初始化不会触发layoutSubviews。
addSubview会触发layoutSubviews。
设置view的Frame会触发layoutSubviews (frame发生变化触发)。
滚动一个UIScrollView会触发layoutSubviews。
旋转Screen会触发父UIView上的layoutSubviews事件。
改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
直接调用setLayoutSubviews。
drawrect
drawrect:
是在UIViewController的loadView:
和ViewDidLoad:
方法之后调用.当我们调用
[UIFont的 sizeToFit]
后,会触发系统自动调用drawRect:
当设置UIView的contentMode或者Frame后会立即触发触发系统调用
drawRect:
直接调用
setNeedsDisplay
设置标记 或setNeedsDisplayInRect:
的时候会触发drawRect:
-
block可以用Strong修饰吗
MRC 不可以,因为要有拷贝过程,如果用strong去做copy会崩溃。strong是ARC引入的关键字,如果用retain相当于忽略了block的copy过程。
ARC可以,因为ARC中 block只能存在堆或者全局,因此不涉及栈拷贝到堆的过程。
-
通知NSNotification
通知是结构体通过双向链表进行数据存储
通知以 name和 object两个维度来存储相关通知内容
iOS9以后 通知中心观察者引用是weak所以界面销毁时不移除会崩溃
多次添加一个通知,会导致接到通知的时候回调多次。多次移除不会产生crash
-
Method swizzle注意事项
避免使用 method_exchangeImplementations(), 使用method_setImplementation()
避免交换父类方法。
交换方法应该在+load方法内。
交换的分类方法应该添加自定义前缀,避免冲突
-
Hash算法
单向散列函数算法。将任意长度的消息压缩到固定长度的函数。过程不可逆,可用于数字签名,消息完整性检测,消息的起源认证检测等。常见的散列有 MD5 SHA N-Hash等。
-
PerformSelector
当调用performSelector的时候,内部会创建一个timer定时器添加到当前线程的runloop里,如果当前线程没有开启runloop,那该方法不会调用。
-
class_rw_t 和 class_ro_t
class_ro_t存储了当前类在编译期就已经确定的属性、方法以及遵守的协议,里边是没有分类的方法的。运行时添加的分类方法存储在class_rw_t中。ro代表 read only, 无法修改的。
class_rw_t生成在运行时,编译期间class_ro_t 结构体已经确定,objc_class中的bits的data部分存放着结构体地址。runtime 运行后会生成class_rw_t结构体,包含了class_ro_t 并更新data部分,换成class_rw_t结构体地址。
-
线程池
为什么需要线程池,线程的频繁创建和销毁不仅会消耗系统资源,而且会降低系统稳定性。
线程池预先创建空闲线程,线程池会启动空闲线程来执行任务,结束后会继续返回线程池成为空闲状态,等待下一个任务。
设计线程池
大概就是: 将任务写入阻塞队列,然后线程池中的空闲线程从队列中获取任务执行,执行完成后再从队列获取新的任务执行。
任务队列:
优先级队列: 支持有优先级的任务
延迟队列: 支持任务可以在某个时间点执行
先进后出队列: 队列类似栈,先进后出
同步队列: 不存储任务的队列,将任务push到队列中时阻塞,知道分配线程
线程创建与回收
线程池一般在初始化时创建空闲线程,或者需要时延迟创建。
当处于空闲状态一段时间后,要回收空闲线程以解约资源。
-
TLS和SSL的区别
a. 版本号。 TLS 的1.0版本号对标SSL v3.1
b. 报文鉴别码。TLS使用RFC-2104定义HMAC算法,SSL 使用类似算法,但是填充字节与秘钥之间采用的是连接运算,而HMAC采用异或运算。但是安全度相同
c. 为随机函数。TLS使用了PRF的为随机函数来将秘钥扩展成数据库,更安全
d. 报警代码。TLS支持了SSL几乎所有的报警代码,还补充了如解密失败,记录溢出,拒绝访问等。
e. 加密计算。在计算主密值时使用的方式不同。
f. 填充。填充后长度可以是密文块的任意整数倍。
-
Realm数据库底层实现原理?
Realm 是一个 MVCC 数据库,底层是用 C++ 编写的。MVCC 指的是多版本并发控制。
Realm是满足ACID的。原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
Realm底层是B+树实现的。B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一棵B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,也可能是一个包含两个或两个以上孩子节点的节点。
B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入。
Realm 采用了 zero-copy
架构,核心文件格式基于memory-mapped
,这样几乎就没有内存开销。这是因为每一个 Realm 对象直接通过一个本地 long
指针和底层数据库对应,这个指针是数据库中数据的钩子。数据库文件本身是映射到内存中的,Realm访问文件偏移就好比文件已经在内存中一样(这里的内存是指虚拟内存),提高了读取效率。
-
CoreData是数据库吗
Core Data本身并不是数据库,它是一个拥有多种功能的框架,其中一个重要的功能就是把应用程序同数据库之间的交互过程自动化了
-
WebView性能优化
在合适的时机初始化一个webView(可以是全局的),隐藏备用。当使用webView时,直接使用初始化过的,可以节约首次初始化的时间。
native开始网络请求数据,初始化完成后,webView获取代理请求的数据。
DNS和链接慢,想办法复用客户端使用的域名和链接。
webview缓存
-
class 方法和 objc_getClass区别
class方法为实例方法或者类方法。 objc_getClass是获取对象的指针,如传入实例对象返回的是类对象,如传入类对象则返回元类对象,如果是传入元类对象则返回根元类对象。
-
Java,python,OC运行效率孰高
OC > Java > python
Java编译后是字节码,需要在虚拟机上执行,OC编译出来是机器码,可以直接由硬件执行
python是动态变量类型的语言,程序运行时需要随时检查变量类型。
-
CADisplayLink
使用CADisplayLink获取FPS,displayLink的回调,1秒采样一次,计算间隔时间内的刷新频率,fps就是我们需要的数据。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
-
使用代码监听僵尸对象
系统是使用__dealloc_zombie 和 dealloc 做了方法交换。
__dealloc_zombie实现的大致流程是
获取当前类和类型 //const char *className = object_getClassName(self);
类名前拼上"NSZombie" //asprintf(&zombieClassName, "YHZombie%s", className);
获取zombiesClassName类 //Class zombieClass = objc_getClass(zombieClassName);
判断该类是否存在,如果不存在实例化一个 //zombieClass = objc_duplicateClass(objc_getClass("YHZombie"), zombieClassName, 0);
字符串变量释放
objc原本对象销毁方法 //objc_destructInstance(self); 内容主要是 结构解析、移除关联属性、清除弱引用和散列表。
将当前对象类修改为zombiesClass
判断是否开启了僵尸对象功能,如果没开启,释放当前内存。
zombie类可以实现forwardingTargetForSelector方法,打印内容。
-
UIImage解码时机
默认情况下CPU不会对图像进行解码,当CPU将图像数据给GPU时,GPU会判断数据格式并转换为Bitmap格式。
-
CPU和GPU
cpu 就像大脑,处理你对设备的所有操作。同时管理显卡如何显示。
GPU就是显卡,主要是创建动画渲染图像等。
CPU是任务并行,显卡是数据并行。 CPU主要负责复杂运算,而GPU负责简单而大量的工作。
-
NSDictionary字典的原理
字典是通过使用 setObject 方法,用hash表来实现key与value之间的映射和存储的。
哈希本质是一个数组,每个元素称为一个箱子,箱子里存的是键值对。哈希表又叫散列表,根据关键码直接访问的数据结构。通过把关键码映射到一个位置来访问记录,用来加快查找速度。映射函数叫散列函数,存放记录的数组叫散列表。
存储过程
根据key算出哈希值 h。
如果箱子个数为n,那么key应该是放在 h%n 个箱子中。
如果该箱子有值了,那么就用开放寻址或者拉链法进行冲突解决。
如果用拉链法,每个箱子都是一个链表,属于同一个箱子的所有键值都会排列在链表中。
-
NSArray存储形式
实际的数组元素被存在堆内存里,数组引用变量是一个引用类型的变量,存在栈内存里
数组就是在内存中开辟了一块连续的、大小相同的空间,用来存储数据。内存是连续的。
-
NSCache和NSMutableDictionary
NSCache是线程安全的,NSMutableDictionary是线程不安全的。当内存不足时,NSCache会自动释放内存。
-
NSArray和NSSet有什么区别
NSSet无序集合,不重复的,在内存中不连续的。
在搜索一个一个元素时,NSSet比NSArray效率高,主要是用到了hash算法。 NSSet通过anyobject访问,NSArray通过下标访问。
-
URL Scheme大概原理
苹果手机内的APP都有沙盒,APP之间是不可以互相通信。但是APP可以注册自己的URL Scheme,方便app之间相互调用。URL Scheme每个app必须唯一。
-
离屏渲染
消耗性能原因
- 需要新开一个缓冲区
- 渲染过程需要多次切换环境,先从当前屏幕切到离屏,等离屏渲染完毕后,将离屏渲染结果显示到屏幕上又要切换一次。
-
索引的优缺点
索引主要分为四种:普通索引、主键、唯一索引、复合索引
只包含一个字段的索引叫做单列索引,包含两个或以上字段的索引叫做复合索引
唯一索引是在表上一个或者多个字段组合建立的索引,这个(或这几个)字段的值组合起来在表中不可以重复。
主键是一种特殊的唯一索引,区别在于,唯一索引列允许null值,而主键列不允许为null值。
建立索引的优点:
- 索引能够提高数据检索的效率,降低数据库的IO成本。
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性,创建唯一索引
- 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间
- 加速两个表之间的连接,一般是在外键上创建索引
建立索引的缺点:
- 需要占用物理空间,建立的索引越多需要的空间越大
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加
一般需要建立索引的字段
- 经常用在where语句之后的字段
- 主键或者外键
- 字段具有唯一性的时候建立唯一性索引
- 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的