25、OC 你了解的锁有哪些?⾃旋和互斥对⽐?⽤ C/OC/C++,任选其⼀,实现⾃旋或互斥?
追问⼀:⾃旋和互斥对⽐?
追问⼆:⽤ C/OC/C++,任选其⼀,实现⾃旋或互斥?⼜述即可!
回答
在计算机科学中,锁是⼀种同步机制,⽤于在存在多线程的环境中实施对资源的访问限制。你可以理解成它⽤于排除并发的⼀种策略!
在 iOS 中,锁分为递归锁、条件锁、分布式锁、⼀般锁(根据 NSLock 类⾥⾯的分类进⾏划分)
1.@synchronized 关键字加锁
-
NSLock 对象锁
-
NSCondition (同步执⾏)
-
NSConditionLock 条件锁
-
NSRecursiveLock 递归锁(循环锁)
6.NSDistributedLock (分布式锁)
-
pthread_mutex 互斥锁(C 语⾔)
-
dispatch_semaphore 信号量实现加锁(GCD)
-
OSSpinLock
10.pthread_rwlock
11.POSIX Conditions
12.os_unfair_lock
⽤ NSCodition 同步执⾏的顺序
NSCodition 是⼀种特殊类型的锁,我们可以⽤它来同步操作执⾏的顺序。它与 mutex 的区别在于更加精准,等待某个 NSCondtion 的线程⼀直被 lock,直到其他线程给那个 condition 发送了信号。下⾯我们来看使⽤⽰例:
某个线程等待着事情去做,⽽有没有事情做是由其他线程通知它的。
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];
其他线程发送信号通知上⾯的线程可以做事情了:
[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
-1、自旋锁与互斥锁的区别
1、互斥锁:在⼀个双核的机器上有两个线程(线程 A 和线程 B),它们分别运⾏在 Core0 和 Core1 上。假设线程 A 想要通过 pthread_mutex_lock 操作去得到⼀个临界区的锁,⽽此时这个锁正被线程 B 所持有,那么线程 A 就会被阻塞 (blocking),Core0 会在此时进⾏上下⽂切换(Context Switch)将线程A 置于等待队列中,此时 Core0 就可以运⾏其他的任务(例如另⼀个线程 C)⽽不必进⾏忙等待。且保证共享数据操作的完整性。每个对象都对应于⼀个可称为" 互斥锁" 的标记,这个标记⽤来保证在任⼀时刻,只能有⼀个线程访问该对象
2、⾃旋锁:它属于 busy-waiting 类型的锁,如果线程 A 是使⽤ pthread_spin_lock 操作去请求锁,那么线程 A 就会⼀直在 Core0 上进⾏忙等待并不停的进⾏锁请求,直到得到这个锁为⽌。
区别:只是⾃旋锁不会引起调⽤者睡眠,如果⾃旋锁已经被别的执⾏单元保持,调⽤者就⼀直循环在那⾥看是否该⾃旋锁的保持者已经释放了锁。
⾃旋锁会忙等: 所谓忙等,即在访问被锁资源时,调⽤者线程不会休眠,⽽是不停循环在那⾥,直到被锁资源释放锁。
互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调⽤者线程会休眠,此时 cpu 可以调度其他线程⼯作。直到被锁资源释放锁。此时会唤醒休眠线程。
优缺点:
⾃旋锁的优点在于,因为⾃旋锁不会引起调⽤者睡眠,所以不会进⾏线程调度,cpu 时间⽚轮转等耗时操作。所有如果能在很短的时间内获得锁,
⾃旋锁的效率远⾼于互斥锁。
缺点在于,⾃旋锁⼀直占⽤ CPU,他在未获得锁的情况下,⼀直运⾏--⾃旋,所以占⽤着 CPU,如果不能在很短的时间内获得锁,这⽆疑会使
CPU 效率降低。⾃旋锁不能实现递归调⽤。
pthread_mutex 表⽰互斥锁。互斥锁可以传⼊不同参数,实现递归锁 pthread_mutex(recursive)。NSLock,NSCondition,NSRecursiveLock,
NSConditionLock 都是内部封装的 pthread_mutex,即都属于互斥锁。@synchronized 是 NSLock 的⼀种封装,牺牲了效率,简洁了语法。
OSSpinLock 表⽰⾃旋锁,从上图可以看到⾃旋锁的效率最⾼,但是现在的 iOS 因为优先级反转的问题,已经不安全,所以推荐使⽤ pthread_mutex 或者 dispatch_semaphore。
⾃旋锁和互斥锁
相同点:都能保证同⼀时间只有⼀个线程访问共享资源。都能保证线程安全。
不同点:
互斥锁:如果共享数据已经有其他线程加锁了,线程会进⼊休眠状态等待锁。⼀旦被访问的资源被解锁,则等待资源的线程会被唤醒。
⾃旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的⽅式等待锁,⼀旦被访问的资源被解锁,则等待资源的线程会⽴即执⾏。
⾃旋锁的效率⾼于互斥锁。
使⽤⾃旋锁时要注意:
由于⾃旋时不释放 CPU,因⽽持有⾃旋锁的线程应该尽快释放⾃旋锁,否则等待该⾃旋锁的线程会⼀直在哪⾥⾃旋,这就会浪费 CPU 时间。
持有⾃旋锁的线程在 sleep 之前应该释放⾃旋锁以便其他可以获得该⾃旋锁。内核编程中,如果持有⾃旋锁的代码 sleep 了就可能导致整个系统挂起。
使⽤任何锁都需要消耗系统资源(内存资源和 CPU 时间),这种资源消耗可以分为两类:
1.建⽴锁所需要的资源
2.当线程被阻塞时所需要的资源
两种锁的加锁原理:
互斥锁:线程会从 sleep(加锁)——>running(解锁),过程中有上下⽂的切换,cpu 的抢占,信号的发送等开销。
⾃旋锁:线程⼀直是 running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
在 Swift 中,不同的锁类型具有不同的特性,适用于不同的场景。以下表格总结了常用锁的优缺点对比:
| 锁类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
NSLock | 简单易用,适用于轻量级同步 | 性能开销略高,不适用于高并发场景 | 对共享资源的简单互斥访问 |
Mutex (pthread_mutex_t) | 低级控制,可与 C/C++ 代码集成 | 使用复杂,需要手动内存管理 | 需要细粒度控制或与 C/C++ 代码集成时 |
RecursiveLock | 可重入,适用于嵌套锁场景 | 性能略低于其他锁类型 | 在嵌套结构中需要对共享资源进行多次访问时 |
Semaphore | 控制并发数量,确保公平资源分配 | 编程复杂度略高 | 控制并发操作的数量或确保公平的资源分配 |
ReadWriteLock | 提高读写并发性,降低锁竞争 | 编程复杂度较高 | 存在读写操作且需要提高并发性时 |
drive_spreadsheet导出到 Google 表格
选择锁类型时,请考虑以下因素:
- 共享资源的访问模式: 如果是简单的互斥访问,可以使用
NSLock或Mutex。如果需要更复杂的访问模式,例如可重入锁或控制并发数量,可以使用其他类型的锁。 - 性能要求: 如果性能至关重要,需要考虑锁的开销。例如,
Mutex通常比NSLock更快。 - 编程复杂度: 一些锁类型,例如
Semaphore和ReadWriteLock,编程复杂度更高,需要更仔细地考虑。
以下是一些具体的应用场景示例:
- 保护共享变量: 使用
NSLock或Mutex保护对共享变量的访问。 - 实现线程安全的队列: 使用
RecursiveLock保护对队列的访问。 - 控制并发网络请求: 使用
Semaphore控制并发网络请求的数量。 - 读写共享数据结构: 使用
ReadWriteLock提高读写共享数据结构的并发性。
在 Swift 中,可以使用多种类型的锁来同步对共享资源的访问,并确保线程安全的操作。以下是一些常用的锁及其使用方法:
1. NSLock:
NSLock 是一种基本的互斥锁,可提供对共享资源的同步访问。它确保一次只能有一个线程访问临界区(受保护的代码块)。
Swift
let lock = NSLock()
func someMethod() {
lock.lock() // 进入临界区之前获取锁
// 临界区代码(共享资源访问)
lock.unlock() // 退出临界区后释放锁
}
请谨慎使用代码。
content_copy
2. Mutex (pthread_mutex_t):
Mutex (pthread_mutex_t) 是一种低级别的锁,可提供对同步的细粒度控制。它通常用于性能至关重要或与 C 或 C++ 代码集成的情况。
Swift
import Darwin
let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
pthread_mutex_init(mutex, nil, 0)
func someMethod() {
pthread_mutex_lock(mutex) // 进入临界区之前获取互斥锁
// 临界区代码(共享资源访问)
pthread_mutex_unlock(mutex) // 退出临界区后释放互斥锁
}
请谨慎使用代码。
content_copy
3. RecursiveLock:
RecursiveLock 是一种特殊的锁,允许线程多次获取锁而不会造成死锁。当线程需要在嵌套结构中多次访问共享资源时,它很有用。
Swift
let lock = RecursiveLock()
func someMethod() {
lock.lock() // 第一次获取锁
// 嵌套代码,包含共享资源访问
lock.lock() // 在嵌套代码中再次获取锁
// 更嵌套的代码,包含共享资源访问
lock.unlock() // 释放嵌套代码中获取的锁
lock.unlock() // 释放一开始获取的锁
}
请谨慎使用代码。
content_copy
4. Semaphore:
Semaphore 是一种允许有限数量的线程同时访问共享资源的锁。它用于控制并发操作的数量或确保公平的资源分配。
Swift
let semaphore = DispatchSemaphore(value: 3) // 创建一个最大计数为 3 的信号量
func someMethod() {
semaphore.wait() // 进入临界区之前等待信号量许可
// 临界区代码(共享资源访问)
semaphore.signal() // 退出临界区后发出许可证可用信号
}
请谨慎使用代码。
content_copy
5. ReadWriteLock:
ReadWriteLock 是一种特殊的锁,为读操作和写操作提供不同的访问权限。它允许多个线程并发读取共享资源,同时限制一次只能有一个线程写入共享资源。
Swift
let lock = ReadWriteLock()
func readMethod() {
lock.readLock() // 在读取共享资源之前获取读锁
// 读取共享资源
lock.unlock() // 读取后释放读锁
}
func writeMethod() {
lock.writeLock() // 在写入共享资源之前获取写锁
// 写入共享资源
lock.unlock() // 写入后释放写锁
}
请谨慎使用代码。
content_copy
请记住,根据您的特定同步需求和性能要求选择合适的锁类型。考虑以下因素:访问共享资源的线程数量、访问频率以及争用的可能性。
26、内存泄漏可能会出现的⼏种原因,聊聊你的看法?⾮ OC 对象如何处理?地图类内存若泄漏,如何处理?
追问⼀:⾮ OC 对象如何处理?
追问⼆:地图类内存若泄漏,如何处理?
第⼀种可能:第三⽅框架不当使⽤;
第⼆种可能:block 循环引⽤;
第三种可能:delegate 循环引⽤;
第四种可能:NSTimer 循环引⽤
第五种可能:⾮ OC 对象内存处理
第六种可能:地图类处理
第七种可能:⼤次数循环内存暴涨
追问⼀:⾮ OC 对象如何处理?
⾮ OC 对象,其需要⼿动执⾏释放操作例:CGImageRelease(ref),否则会造成⼤量的内存泄漏导致程序崩溃。
其他的对于 CoreFoundation 框架下的某些对象或变量需要⼿动释放、C 语⾔代码中的 malloc 等需要对应 free。
追问⼆:地图类内存若泄漏,如何处理?
地图是⽐较耗费 App 内存的,因此在根据⽂档实现某地图相关功能的同时,需要注意内存的正确释放,⼤体需要注意的有需在使⽤完毕时将地图、代理等滞空为 nil;
KVO 越界 signal 野指针 线程问题 内存 后台超时, 只有 exception&signal
注意地图中标注(⼤头针)的复⽤,并且在使⽤完毕时清空标注数组等
SEGV:(Segmentation Violation,段违例),⽆效内存地址,⽐如空指针,未初始化指针,栈溢出等;
SIGABRT:收到 Abort 信号,可能⾃⾝调⽤ abort()或者收到外部发送过来的信号;
SIGBUS:总线错误。与 SIGSEGV 不同的是,SIGSEGV 访问的是⽆效地址(⽐如虚存映射不到物理内存),⽽ SIGBUS 访问的是有效地址,但总线访问异常(⽐如地址对齐问题);
SIGILL:尝试执⾏⾮法的指令,可能不被识别或者没有权限;
SIGFPE:Floating Point Error,数学计算相关问题(可能不限于浮点计算),⽐如除零操作;
SIGPIPE:管道另⼀端没有进程接⼿数据; 常见的崩溃原因基本都是代码逻辑问题或资源问题,⽐如数组越界,访问野指针或者资源不存在,
或资源⼤⼩写错误等
27、我们说的 oc 是动态运⾏时指是什么意思?
多态。主要是将数据类型的确定由编译时,推迟到了运⾏时。这个问题其实浅涉及到两个概念,运⾏时和多态。简单来说,运⾏时机制使我们直到运⾏时才去决定⼀个对象的类别,以及调⽤该类别对象指定⽅法。多态:不同对象以⾃⼰的⽅式响应相同的消息的能⼒叫做多态。意思就是假设⽣物类(life)都⽤有⼀个相同的⽅法-eat;那⼈类属于⽣物,猪也属于⽣物,都继承了 life 后,实现各⾃的 eat,但是调⽤是我们只需调⽤各⾃的 eat ⽅法。也就是不同的对象以⾃⼰的⽅式响应了相同的消息(响应了 eat 这个选择器)。因此也可以说,运⾏时机制是多态的基础
描述 iOS 程序的运⾏流程?
-
系统调⽤ app 的 main 函数
-
main 函数调⽤ UIApplicationMain.
-
UIApplicationMain 创建 sharedapplication instance, UIApplication 默认的 instance.
-
UIApplicationMain 读取 Info.plist 找到主 nib ⽂件, 加载 nib,把 shared applicationinstance 设为 nib 的 owner.
-
通过 nib ⽂件,创建 app 的独⽴ UIWindows object.
-
通过 nib,实例化了程序的 AppDelegate object.
-
app 内部启动结束,application:didFinishLaunchingWith-Options: 被设定成 wAppDelegate instance.
-
AppDelegate 向 UIWindowinstance 发 makeKeyAndVisible 消息, app 界⾯展⽰给⽤户. app 准备好接收⽤户的操作指令.
Object-C 有多继承吗?没有的话⽤什么代替?
OC 本⾝是没有多继承的,但是我们可以通过协议来实现类似 C++中的多继承。
28、block 本质是什么?
block 是⼀个数据类型, 多⽤于参数传递, 代替代理⽅法, (有多个参数需要传递或者多个代理⽅法需要实现还是推荐使⽤代理⽅法), 少⽤于当做返回值传递. block 是⼀个 OC 对象, 它的功能是保存代码⽚段, 预先准备好代码, 并在需要的时候执⾏,/* 在 Block 中, 如果只使⽤全局或静态变量或不使⽤外部变量, 那么 Block 块的代码会存储在全局区;如果使⽤了外部变量, 在 ARC 中, Block 块的代码会存储在堆区;在 MRC 中, Block 快的代码会存储在栈区;block 默认情况下不能修改外部变量, 只能读取外部变量,在 ARC 中, 外部变量存在堆中, 这个变量在 Block 块内与在 Block 块外地址相同;外部变量存在栈中, 这个变量会被 copy 到为 Block 代码块所分配的堆中;在 MRC 中, 外部变量存在堆中, 这个变量在 Block 块内与 Block 块外相同;外部变量存在栈中,
这个变量会被 copy 到为 Block 代码块所分配的栈中;如果需要修改外部变量, 需要在外部变量前⾯声明 __block;
在 ARC 中, 外部变量存在堆中, 这个变量在 Block 块内与 Block 块外地址相同; 外部变量存在栈中, 这个变量会被转移到堆区, 不是复制, 是转移.。在 MRC 中, 外部变量存在堆中, 这个变量在 Block 块内与 Block 块外地址相同;外部变量存在栈中, 这个变量在 Block 块内与 Block 块外地址相同;
在 MRC 下⽤_block,在 ARC 下使⽤__weak;
关于 block 在内存中的位置
block 快的存储位置(block ⼊⼜的地址)可能存放在 3 个地⽅:代码区(全局区)、堆区、栈区(ARC 情况下回⾃动拷贝到堆区、因此 ARC 下只有两个地⽅:代码区和堆区)。
代码区:不访问栈区的变量(如局部变量),且不访问堆区的变量(如⽤ alloc 创建的对象)时,此时 block 存放在代码区;
堆区:如果访问了堆区的变量(如局部变量),或堆区的变量(如⽤ alloc 创建的对象),此时 block 存⽅在堆区;--需要注意,堆是向⾼地址扩展的数据结构,是不连续的内存区域,以链表的⽅式进⾏存储,堆是不连续的存储内存区域,是以链表的⽅式存储。
实际是放在栈区,在 ARC 情况下⾃动拷贝到堆区,如果不是 ARC 则存放在栈区,所在函数执⾏完毕就会释放,想再外⾯调⽤需要⽤ copy 指向它,
这样就拷贝到了堆区,strong 属性不会拷贝、会造成野指针错区。(需要理解 ARC 是⼀种编译器特性,即编译器在编译时在核实的地⽅插⼊ retain、release、autorelease,⽽不是 iOS 的运⾏时特性)。栈是向低地址扩展的数据结构,采⽤后进先出(LIFO ),栈是连续的存储空间,且栈的⼤⼩是有限的,
此外代码存在堆区时,需要注意,因为堆区不像代码区不变化,堆区是动态的(不断的创建销毁),当没有强指针指向的时候就会被销毁,如果再去访问这段代码时,程序就会崩溃!所以此种情况在定义 block 属性时需要指定为 strong or copy。block 是⼀段代码,即不可变,所以使⽤ copy 也不会深拷贝全局静态区:全局区又可分为未初始化全局区:.bss 段和初始化全局区:data 段。全局变量和静态变量的存储是放在⼀块的,初始化的全局变量和
静态变量在⼀块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另⼀块区域,在程序结束后有系统释放。
将内存区域分为五个区,由低地址向⾼地址分类分别是:代码区、常量区、全局静态区、堆、栈。⾃由存储区Block 中对全局变量、全局静态变量、局部静态变量的使⽤:
全局变量和静态全局变量的值改变,以及它们被 Block 捕获进去,因为是全局的,作⽤域很⼴
静态变量和⾃动变量,被 Block 从外⾯捕获进来,成为__main_block_impl_0 这个结构体的成员变量
⾃动变量是以值传递⽅式传递到 Block 的构造函数⾥⾯去的。Block 只捕获 Block 中会⽤到的变量。由于只捕获了⾃动变量的值,并⾮内存地址,所以 Block 内部不能改变⾃动变量的值。
Block 捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量
Block 就分为以下 3 种
_NSConcreteStackBlock:只⽤到外部局部变量、成员属性变量,且没有强指针引⽤的 block 都是 StackBlock。 StackBlock 的⽣命周期由系统控制的,⼀旦返回之后,就被系统销毁了,是不持有对象的
_NSConcreteStackBlock 所属的变量域⼀旦结束,那么该 Block 就会被销毁。在 ARC 环境下,编译器会⾃动的判断,把 Block ⾃动的从栈 copy 到堆。⽐如当 Block 作为函数返回值的时候,肯定会 copy 到堆上
_NSConcreteMallocBlock:有强指针引⽤或 copy 修饰的成员属性引⽤的 block 会被复制⼀份到堆中成为 MallocBlock,没有强指针引⽤即销毁,⽣命周期由程序员控制,是持有对象的
_NSConcreteGlobalBlock:没有⽤到外界变量或只⽤到全局变量、静态变量的 block 为_NSConcreteGlobalBlock,⽣命周期从创建到应⽤程序结束,也不持有对象
ARC 环境下,⼀旦 Block 赋值就会触发 copy,__block 就会 copy 到堆上,Block 也是__NSMallocBlock。ARC 环境下也是存在__NSStackBlock 的时
候,这种情况下,__block 就在栈上
ARC 下,Block 中引⽤ id 类型的数据有没有__block 都⼀样都是 retain,⽽对于基础变量⽽⾔,没有的话⽆法修改变量值,有的话就是修改其结构
体令其内部的 forwarding 指针指向拷贝后的地址达到值的修改
29、weak 为什么能解除循环引⽤?
block 是 oc 类。数据结构就是,有个 isa 指针结构体。如果 block 使⽤外部变量 a,结构体就会有个 a 的成员变量,结构体 a 会根据使⽤外部变量修饰符来修饰;
sidetable
weak 指针如何⾃动置为 nil
__weak 修饰表明⼀种关系“⾮拥有关系”。弱引⽤,不决定对象的存亡。即使⼀个对象被持有⽆数个弱引⽤,只要没有强引⽤指向它,那么还是会被销毁。
若附有__weak 修饰符的变量所引⽤的对象被废弃,则将 nil 赋值给该变量。假设变量 obj 附加__strong 修饰符且对象被赋值。
{
// 声明⼀个 weak 指针
id __weak obj1 = obj;
}
模拟编译器编译后的代码:
id obj1;
objc_initWeak(&obj1, obj);
objc_release(obj);
objc_destroyWeak(&obj1);
通过 objc_initWeak 函数初始化附有__weak 修饰符的变量:
/* 编译器的模拟代码 */
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak 函数把第⼆参数的赋值对象的地址作为键值,将第⼀参数的附有__weak 修饰符的变量的地址注册到 weak 表中。如果第⼆参数为0,则把变量的地址从 weak 表中删除。
weak 表与引⽤计数表相同,作为散列表被实现。如果使⽤ weak 表,将废弃对象的地址作为键值进⾏检索,就能⾼速地获取对应的附有 weak 修饰符的变量的地址。另外,由于⼀个对象可同时赋值给多个附有 weak 修饰符的变量中,所以对于⼀个键值,可注册多个变量的地址。
在变量作⽤域结束时通过 objc_destroyWeak 函数释放该变量:
/* 编译器的模拟代码 */
objc_storeWeak(&obj1, 0);
释放对象时,废弃谁都不持有的对象的同时,程序的动作是怎样的呢?下⾯我们来跟踪观察。对象将通过 objc_release 函数释放。
(1)objc_release
(2)因为引⽤计数为 0 所以执⾏ dealloc
(3)_objc_rootDealloc
(4)object_dispose
(5)objc_destructInstance
(6)objc_clear_deallocating
对象被废弃时最后调⽤的 objc_clear_deallocating 函数的动作如下:
(1)从 weak 表中获取废弃对象的地址为键值的记录。
(2)将包含在记录中的所有附有__weak 修饰符变量的地址,赋值为 nil。
(3)从 weak 表中删除该记录。
(4)从引⽤计数表中删除废弃对象的地址为键值的记录。
根据以上步骤,前⾯说的如果附有__weak 修饰符的变量所引⽤的对象被废弃,则将 nil 赋值给该变量这⼀功能即被实现。由此可知,如果⼤量使⽤附有__weak 修饰符的变量,则会消耗相应的 CPU 资源。良策是只在需要避免循环引⽤时使⽤__weak 修饰符。
以上就是⼀个 weak 指针从初始化到被置为 nil 的全过程,在写这篇⽂章之前我⼀直有疑惑,如果是 objc_clear_deallocating 函数进⾏了 weak 指针置为 nil 的操作,那 objc_destroyWeak 函数是⼲嘛的?我反复推敲,想起来⽂中早已说明了⽤途 “在变量作⽤域结束时通过 objc_destroyWeak 函数释放该变量”,也就是说 objc_destroyWeak 函数是在 weak 指针被置为 nil 后,⽤来将 weak 释放掉。
__weak ⽴即释放对象
使⽤__weak 修饰符时,以下源代码会引起编译器警告。
{
id __weak obj = [[NSObject alloc] init];
}
因为该源代码将⾃⼰⽣成并持有的对象赋值给附有__weak 修饰符的变量中,所以⾃⼰不能持有该对象,这时会被释放并被废弃,因此会引起编译器警告:warning: Assigning retained object to weak variable; object will be released after assignment
编译器如何处理该源代码呢?
/*编译器的模拟代码*/
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_initweak(&obj, tmp);
objc_destroyWeak(&object);虽然⾃⼰⽣成并持有的对象通过 objc_initWeak 函数被赋值给附有__weak 修饰符的变量中,但编译器判断其没有持有者,故该对象⽴即通过
objc_release 函数被释放和废弃。
这样⼀来,nil 就会被赋值给引⽤废弃对象的附有__weak 修饰符的变量中。下⾯我们通过 NSLog 函数来验证⼀下:
id __weak obj= [[NSObject alloc] init];
NSLog(@"obj=%@",obj);
以下为该源代码的输出结果,其中⽤%@输出 nil。
obj=(null)
如上所述,以下源代码会引起编译器警告。
id __weak obj= [[NSObject alloc] init];
这是由于编译器判断⽣成并持有的对象不能继续持有。附有__unsafe_unretained 修饰符的变量又如何呢?
id __unsafe_unretained obj=[[NSObject alloc] init];
与__weak 修饰符完全相同,编译器判断⽣成并持有的对象不能继续持有,从⽽发出警告:
Assigning retained object to unsafe_unretained variable; object will be released after assignment
该源代码通过编译器转换为以下形式。
/*编译器的模拟代码*/
id obj = objc_msgSend( NSObject, @selector(alloc));
objc_msgSend(obj,@selector(init));
objc_release(obj);
objc_release 函数⽴即释放了⽣成并持有的对象,这样该对象的悬垂指针被赋值给变量 obj 中。
那么如果最初不赋值变量又会如何呢?下⾯的源代码在 MRC 时必定会发⽣内存泄漏。
[[NSObject alloc] init];
由于源代码不使⽤返回值的对象,所以编译器发出警告。
warning:expression result unused [-Wunused-value]
[[NSObject alloc] init];
可像下⾯这样通过向 void 型转换来避免发⽣警告。
(void)[[NSObject alloc] init];
不管是否转换为 void,该源代码都会转换为以下形式
/* 编译器的模拟代码 */
id tmp = objc_msgSend( NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_release(tmp);
在调⽤了⽣成并持有对象的实例⽅法后,该对象被释放。看来“由编译器进⾏内存管理”这句话应该是正确的。
Runtime 维护了⼀个 weak 表,⽤于存储指向某个对象的所有 weak 指针。weak 表其实是⼀个 hash(哈希)表,key 是所指对象的地址,value 是 weak 指针的地址(这个地址的值是所指对象的地址)数组。为什么 value 是数组?因为⼀个对象可能被多个弱引⽤指针指向。
weak 的实现原理可以概括⼀下三步:
1、初始化时:runtime 会调⽤ objc_initWeak 函数,初始化⼀个新的 weak 指针指向对象的地址。
2、添加引⽤时:objc_initWeak 函数会调⽤ objc_storeWeak() 函数,objc_storeWeak() 的作⽤是更新指针指向,创建对应的弱引⽤表。
3、释放时,调⽤ clearDeallocating 函数。clearDeallocating 函数⾸先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的
数据设为 nil,最后把这个 entry 从 weak 表中删除,最后清理对象的记录。
30、Objective-C 的优缺点
优点:1).Cateogies 2).Posing 3).动态识别 4).指标计算 5).弹性讯息传递 6).不是⼀个过度复杂的 C 衍⽣语⾔ 7).Objective-C 与 C++ 可混合编程
缺点: 1).不⽀持命名空间 2).不⽀持运算符重载 3).不⽀持多重继承 4).使⽤动态运⾏时类型,所有的⽅法都是函数调⽤,所以很多编译时优化⽅法都⽤不到。(如内联函数等),性能低劣。
对于命名冲突可以使⽤长命名法或特殊前缀解决,如果是引⼊的第三⽅库之间的命名冲突,可以使⽤ link 命令及 flag 解决冲突
对于语句 NSString*obj = [[NSData alloc] init]; obj 在编译时和运⾏时分别时什么类型的对象?
1、编译时是 NSString 的类型;运⾏时是 NSData 类型的对象
-2、类别和类扩展的区别是什么?
category 和 extensions 的不同在于 后者可以添加属性,另外后者添加的⽅法是必须要实现的,extensions 可以认为是⼀个私有的 Category。
类别:给⼀个类添加⽅法,同时不让⼦类继承该⽅法,所以产⽣了类别(分类)(添加的属性是共有的,本类和⼦类都能访问), ⽅法没被实现编译器是不会有任何警告的,在运⾏时添加到类中
类扩展:⼀个结构体指针,原则上只能增加⽅法,不能添加属性 + , ⽅法没被实现,编译器会报警,类扩展是在编译阶段被添加到类中, 类扩展所声明的⽅法必须依托对应类的
分类 > 本类 > ⽗类
Category 是表示一个指向分类的结构体的指针
33、如何访问并修改⼀个类的私有属性
通过 KVC 来设置
通过 runtime 动态改变:class_copyIvarList
通过 msg_send() 设置:适⽤私有属性,不适⽤私有变量
Object-C 有私有⽅法吗?私有变量呢?
objective-c– 类⾥⾯的⽅法只有两种, 静态⽅法和实例⽅法. 这似乎就不是完整的⾯向对象了,按照 OO 的原则就是⼀个对象只暴露有⽤的东西.
如果没有了私有⽅法的话,对于⼀些⼩范围的代码重⽤就不那么顺⼿了. 在类⾥⾯声名⼀个私有⽅法
关键字 volatile 有什么含义?并给出三个不同例⼦?
⼀个定义为 volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在⽤到这个变量时必须每次都⼩⼼地重新读取这个变量的值,⽽不是使⽤保存在寄存器⾥的备份。
下⾯是 volatile 变量的⼏个例⼦:
1) 并⾏设备的硬件寄存器(如:状态寄存器)
2) ⼀个中断服务⼦程序中会访问到的⾮⾃动变量(Non-automatic variables)
3)多线程应⽤中被⼏个任务共享的变量⼀个参数既可以是 const 还可以是 volatile 吗?解释为什么。
是的,⼀个例⼦是只读的状态寄存器。它是 volatile 因为它可能被意想不到地改变。它是 const 因为程序不应该试图去修改它,
volatile 表⽰变量随时可以改变
⼀个指针可以是 volatile 吗?解释为什么。
可以是的,尽管这种情况并不常见,但它还是可以。⼀个例⼦就是:当⼀个中断服务⼦程序企图去修改⼀个指向⼀个 buffer 指针的时候。
static 有什么作⽤?
1)函数体内 static 变量的作⽤范围为该函数体,不同于 auto 变量,该变量的内存只被分配⼀次,因此其值在下次调⽤时仍维持上次的值;
2)在模块内的 static 全局变量可以被模块内所⽤函数访问,但不能被模块外其它函数访问;
3)在模块内的 static 函数只可被这⼀模块内的其它函数调⽤,这个函数的使⽤范围被限制在声明它的模块内;
4)在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有⼀份拷贝;
5)在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因⽽只能访问类的 static 成员变量。
34、线程和进程的区别?
进程和线程都是由操作系统所体会的程序运⾏的基本单元,系统利⽤该基本单元实现系统对应⽤的并发性。进程和线程的主要差别
在于它们是不同的操作系统资源管理⽅式。进程有独⽴的地址空间,⼀个进程崩溃后,在保护模式下不会对其它进程产⽣影响,⽽
线程只是⼀ 个进程中的不同执⾏路径。线程有⾃⼰的堆栈和局部变量,但线程之间没有单独的地址空间,⼀个线程死掉就等于
整个进程死掉,所以多进程的程序要⽐多线程的程 序健壮,但在进程切换时,耗费资源较⼤,效率要差⼀些。但对于⼀些要求同时
进⾏并且又要共享某些变量的并发操作,只能⽤线程,不能⽤进程。
1.
-3、堆和栈的区别?
管理⽅式:对于栈来讲,是由编译器⾃动管理,⽆需我们⼿⼯控制;对于堆来说,释放⼯作由程序员控制,容易产⽣ memoryleak。
申请⼤⼩:
栈:在 Windows 下,栈是向低地址扩展的数据结构,是⼀块连续的内存的区域。这句话的意思是栈顶的地址和栈的最⼤容量是系统预先规定好的,在 WINDOWS 下,栈的⼤⼩是 2M(也有的说是 1M,总之是⼀个编译时就确定的常数),如果申请的空间超过栈的余空间时,将提⽰ overflow。因 此,能从栈获得的空间较⼩。
堆:堆是向⾼地址扩展的数据结构,是不连续的内存区域。这是由于系统是⽤链表来存储的空闲内存地址的,⾃然是不连续的,⽽链表的遍历⽅向是由低地址向⾼地址。堆的⼤⼩受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间⽐较灵活,也⽐较⼤。
碎⽚问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从⽽造成⼤量的碎⽚,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的⼀⼀对应,以⾄于永远都不可能有⼀个内存块从栈中间弹出
分配⽅式:堆都是动态分配的,没有静态分配的堆。栈有 2 种分配⽅式:静态分配和动态分配。静态分配是编译器完成的,⽐如局部变量的分配,动态分配由 alloca 函数进⾏分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进⾏释放,⽆需我们⼿⼯ 实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供⽀持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令
执⾏, 这就决定了栈的效率⽐较⾼。堆则是 C/C++函数库提供的,它的机制是很复杂的。
35、成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项⽬中的应⽤
定义在类内部的⾮静态类,就是成员内部类;
定义在类内部的静态类,就是静态内部类;
定义在⽅法中的类,就是局部类。(局部内部类是嵌套在⽅法和作⽤于内的,对于这个类的使⽤主要是应⽤与解决⽐较复杂的问题,想创建⼀个类来辅助我们的解决⽅案,到那时又不希望这个类是公共可⽤的,所以就产⽣了局部内部类,局部内部类和成员内部类⼀样被编译,只是它的作⽤域发⽣了改变,它只能在该⽅法和属性中被使⽤,出了该⽅法和属性就会失效。)应⽤场景:如果⼀个类只在某个⽅法中使⽤,则可以考虑使⽤局部类;
匿名内部类是没有访问修饰符的:
new 匿名内部类,这个类⾸先是要存在的。
当所在⽅法的形参需要被匿名内部类使⽤,那么这个形参就必须为 final。
匿名内部类没有明⾯上的构造⽅法,编译器会⾃动⽣成⼀个引⽤外部类的构造⽅法。
匿名内部类使⽤⼴泛,⽐如我们常⽤的绑定监听的时候。怎么理解 MVC,在 OC 中 MVC 是怎么实现的?
MVC 设计模式考虑三种对象:模型对象、视图对象、和控制器对象。模型对象代表特别的知识和专业技能,它们负责保有应⽤程序的数据和定义操作数据的 逻辑。视图对象知道如何显⽰应 ⽤程序的模型数据,⽽且可能允许⽤户对其进⾏编辑。控制器对象是应⽤程序的视图对象和模型对象之间的协调者。
37、什么是深拷⻉和浅拷⻉
1.概念 浅拷贝:指针拷贝,不会创建⼀个新的对象。浅拷贝简单点说就是对内存地址的复制,让⽬标对象指针和源对象指针指向同⼀⽚内存空间
深拷贝: 内容拷贝,会创建⼀个新的对象。深拷贝就是拷贝地址中的内容,让⽬标对象产⽣新的内存区域,且将源内存区域中的内容复制到⽬标内存区域中。
2.各种类型的对象深拷贝,\ 1.⾮容器类对象(⽐如像 NSString,NSNumber 这些不能包含其他对象的叫做⾮容器类,如 NSArray 和 NSDictionary 可以容纳其他对象的叫做容器类对象) ...
⼀般来说,A copy 之后得到 copyA ,然后我们在修改 copyA 的值得时候 A 会跟着变化我们称作浅拷贝。
深拷贝就是 A copy 之后得到 copyA,在我们修改 copyA 的值的时候,两个实例不会相互影响.
在 runtime 下 NSString 的“真⾝”是__NSCFConstantString ⽽ NSMutableString 的“真⾝”是__NSCFString,然后我们就能很清楚的看到,只要是 copy 得到的值就是不可变类型,⽽ mutablecopy 得到的是可变类型对系统⾮容器类不可变对象调⽤ Copy ⽅法其实只是把当前对象的指针指向了原对象的地址,⽽调⽤ mutableCopy ⽅法则是新分配了⼀块内存区域并把新对象的指针指向了这块区域。对于可变对象来说调⽤ Copy 和 MutableCopy ⽅法都会重新分配⼀块内存。但是 copy 和 mutableCopy 的区别在于copy 在复制对象的时候其实是返回了⼀个不可变对象,因此当调⽤⽅法改变对象的时候会崩溃
深 copy 是重新开辟了内存 浅 copy 还是那个内存 只是创建了⼀个指针
copy 拷贝出来的对象类型总是不可变类型
mutableCopy 拷贝出来的对象类型总是可变类型
总结:使⽤ copy 的⽬的是,防⽌把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发⽣变化会⽆意间篡改不可变类型对象原来的值。
浅拷贝:只复制指向对象的指针,⽽不复制引⽤对象本⾝。深拷贝:复制引⽤对象本⾝。内存中存在了两份独⽴对象本⾝,当修改 A 时,A_copy 不变。
必须遵循 nscopying 协议,如果想实现可变和不可变拷贝时,必须同时遵循 nscoping 和 nsmutablecoping 协议。并且实现 - (id)copyWithZone:(NSZone *)zone;
可变使⽤ copy 表⽰深拷贝,不可变集合类使⽤ copy 的时候是浅拷贝。
可变集合类使⽤ mutablecopy 表⽰深拷贝,不可变集合类使⽤ copy 的时候是浅拷贝。
关于容器实现 copy 或 metableCopy ,容器内元素默认都是 指针拷贝,不是内容复制。
当原字符串是 NSString 类型时,由于它是不可变类型的,不管是使⽤ strong 特性,还是 copy 特性的对象,它们所指向的地址都跟原字符串是⼀样的,都指向原字符串对象。也就是说当原字符串是 NSString 类型时,copy 特性的操作,只是做了⼀次浅拷贝,只是增加了指针指向原字符串所指向的地址。
当原字符串是 NSMutableString 类型时,strong 特性对象只是增加了原字符串的引⽤计数,但是 copy 特性对象则是对原字符串进⾏了深拷贝,创建了⼀个新对象,并且指向了这个新对象。此时,copy 特性对象是 NSString 类型的不可变的,strong 特性对象是 NSMutableString 类型的可变的。关于在声明 NSString 属性时,我们是要选择 strong 特性,还是选择 copy 特性,是需要通过开发过程中的实际情况来选择的。但是我们在⼤多数情况下,在⽣命 NSString 属性时,都是希望其不被改变,防⽌数据出错。所以⼤多数情况下还是选择 copy 特性,从⽽来避免⼀些⽆法预估的 bug。在补充⼀下,当原字符串是 NSMutableString 类型,也就是可变类型的时候,strong 特性操作只是增加了原字符串的引⽤计数,⽽ copy 特性操作则是进⾏深拷贝,所以在 copy 会耗费更多的内存资源跟性能。
数组扩容流程:
申请⼀块新的空间
将原数据拷贝到新的地址中去
释放原数据存储空间,并将指针指向新的内存区域
__weak 和__block 区别,__block 的作用是什么?strong?block 捕捉变量
1、sidetable
2、默认情况下,block ⾥⾯的变量,拷贝进去的是变量的值,⽽不是指向变量的内存的指针。当使⽤__block 修饰后的变量,拷贝到 block ⾥⾯的就是指向变量的指针,所以我们就可以修改变量的值。
3、1__block 不管是 ARC 还是 MRC 模式下都可以使⽤,可以修饰对象,还可以修饰基本数据类型。
2.__weak 只能在 ARC 模式下使⽤,也只能修饰对象(NSString),不能修饰基本数据类型(int)。
3.__block 对象可以在 block 中被重新赋值,__weak 不可以。
4.__block 对象在 ARC 下可能会导致循环引⽤,⾮ ARC 下会避免循环引⽤,__weak 只在 ARC 下使⽤,可以避免循环引⽤。
strong 表⽰指向并拥有该对象。其修饰的对象引⽤计数会增加 1。该对象只要引⽤计数不为 0 则不会被销毁。当然强⾏将其设为 nil 可以销毁它。
weak 表⽰指向但不拥有该对象。其修饰的对象引⽤计数不会增加。⽆需⼿动设置,该对象会⾃⾏在内存中销毁。
assign 主要⽤于修饰基本数据类型,如 NSInteger 和 CGFloat,这些数值主要存在于栈上。
weak ⼀般⽤来修饰对象,assign ⼀般⽤来修饰基本数据类型。原因是 assign 修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃。⽽栈上的内存系统会⾃动处理,不会造成野指针。
copy 与 strong 类似。不同之处是 strong 的复制是多个指针指向同⼀个地址,⽽ copy 的复制每次会在内存中拷贝⼀份对象,指针指向不同地址。copy ⼀般⽤在修饰有可变对应类型的不可变对象上,如 NSString , NSArray , NSDictionary 。
Objective-C 中,基本数据类型的默认关键字是 atomic , readwrite , assign ;普通属性的默认关键字是 atomic , readwrite , strong 。
44、什么导致线程阻塞
代码 1
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
NSLog(@"为啥堵塞"); 1-会死锁,同步执⾏又调⽤主线程,造成相互执⾏任务得不到释放
});//
代码 2
dispatch_queue_t queue = dispatch_queue_create("abc", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"为啥不堵塞"); 2-同步和串⾏,不会开启线程,串⾏执⾏任务
});
1、dispatch_barrier_async 栅栏方法
有时需要异步执⾏两组操作,⽽且第⼀组操作执⾏完之后,才能开始执⾏第⼆组操作,
dispatch_barrier_async 函数会等待前边追加到并发队列中的任务全部执⾏完毕之后,再将指定的任务追加到该异步队列中。然后在dispatch_barrier_async 函数追加的任务执⾏完毕之后,异步队列才恢复为⼀般动作,接着追加任务到该异步队列并开始执⾏。
在执⾏完栅栏前⾯的操作之后,才执⾏栅栏操作,最后再执⾏栅栏后边的操作
数据多读单写 用 GCD 实现
多读单写 就是在写⼊的时候⽤栅栏函数做拦截操作,读取的时候⽤同步⽅式⽴马返回数据,多线程同事访问的时候可以并发读取数据
isolationQueue 创建时,参数 dispatch_queue_attr_t 的值必须是 DISPATCH_QUEUE_SERIAL(或者 0)self.isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_CONCURRENT);
- (void)setCount:(NSUInteger)count forKey:(NSString *)key{
key = [key copy];
dispatch_barrier_async(self.isolationQueue, ^(){
if (count == 0) {
[self.counts removeObjectForKey:key];
} else {
self.counts[key] = @(count);
}
});
}
锁竞争:⾸先,这⾥有⼀个警告:上⾯这个例⼦中我们保护的资源是⼀个 NSMutableDictionary,出于这样的⽬的,这段代码运⾏地相当不错。
但是在真实的代码中,把隔离放到正确的复杂度层级下是很重要的。
如果你对 NSMutableDictionary 的访问操作变得⾮常频繁,你会碰到⼀个已知的叫做锁竞争的问题。锁竞争并不是只是在 GCD 和队列下才变得特殊,任何使⽤了锁机制的程序都会碰到同样的问题——只不过不同的锁机制会以不同的⽅式碰到。
所有对 dispatch_async,dispatch_sync 等等的调⽤都需要完成某种形式的锁——以确保仅有⼀个线程或者特定的线程运⾏指定的代码。GCD 某些程序上可以使⽤时序(译注:原词为 scheduling)来避免使⽤锁,但在最后,问题只是稍有变化。根本问题仍然存在:如果你有⼤量的线程在相同时间去访问同⼀个锁或者队列,你就会看到性能的变化。性能会严重下降。你应该从直接复杂层次中隔离开。当你发现了性能下降,这明显表明代码中存在设计问题。这⾥有两个开销需要你来平衡。第⼀个是独占临界区资源太久的开销,以⾄于别的线程都因为进⼊临界区的操作⽽阻塞。第⼆个是太频繁出⼊临界区的开销。在 GCD 的世界⾥,第⼀种开销的情况就是⼀个 block 在隔离队列中运⾏,它可能潜在的阻塞了其他将要在这个隔离队列中运⾏的代码。第⼆种开销对应的就是调⽤ dispatch_async 和dispatch_sync 。⽆论再怎么优化,这两个操作都不是⽆代价的。
令⼈忧伤的,不存在通⽤的标准来指导如何正确的平衡,你需要⾃⼰评测和调整。启动 Instruments 观察你的 app 忙于什么操作。
如果你看上⾯例⼦中的代码,我们的临界区代码仅仅做了很简单的事情。这可能是也可能不是好的⽅式,依赖于它怎么被使⽤。
在你⾃⼰的代码中,要考虑⾃⼰是否在更⾼的层次保护了隔离队列。举个例⼦,类 Foo 有⼀个隔离队列并且它本⾝保护着对NSMutableDictionary 的访问,代替的,可以有⼀个⽤到了 Foo 类的 Bar 类有⼀个隔离队列保护所有对类 Foo 的使⽤。换句话说,你可以把类 Foo 变为⾮线程安全的(没有隔离队列),并在 Bar 中,使⽤⼀个隔离队列来确保任何时刻只能有⼀个线程使⽤ Foo 。
全部使⽤异步分发:
正如你在上⾯看到的,你可以同步和异步地分发⼀个 block,⼀个⼯作单元。但是我们需要正视⼀个⼀个⾮常普遍的问题——死锁。在 GCD 中,以同步分发的⽅式⾮常容易出现这种情况。见下⾯的代码:
dispatch_queue_t queueA; // assume we have this
dispatch_sync(queueA, ^(){
dispatch_sync(queueA, ^(){
foo();
});
});
⼀旦我们进⼊到第⼆个 dispatch_sync 就会发⽣死锁。我们不能分发到 queueA,因为当前线程正在队列中并且永远不会离开。但是有更隐晦的产⽣死锁⽅式:
dispatch_queue_t queueA; // assume we have this
dispatch_queue_t queueB; // assume we have this
dispatch_sync(queueA, ^(){
foo();
});
void foo(void){
dispatch_sync(queueB, ^(){
bar();
});
}
void bar(void){
dispatch_sync(queueA, ^(){
baz();
});
}
单独的每次调⽤ dispatch_sync() 看起来都没有问题,但是⼀旦组合起来,就会发⽣死锁。这是使⽤同步分发存在的固有问题,如果我们使⽤异步分发,⽐如:
dispatch_queue_t queueA; // assume we have this
dispatch_async(queueA, ^(){
dispatch_async(queueA, ^(){
foo();
});
});
⼀切运⾏正常。异步调⽤不会产⽣死锁。因此值得我们在任何可能的时候都使⽤异步分发。我们使⽤⼀个异步调⽤结果 block 的函数,来代替编写⼀个返回值(必须要⽤同步)的⽅法或者函数。这种⽅式,我们会有更少发⽣死锁的可能性。
异步调⽤的副作⽤就是它们很难调试。当我们在调试器⾥中⽌代码运⾏,回溯并查看已经变得没有意义了。
要牢记这些。死锁通常是最难处理的问题。
环形链表双指针法,二叉树的前中后序遍历,动态规划,贪心,分治法,回溯法
1.你是否接触过 OC 中的反射机制?简单聊一下概念和使用
动态的运⾏状态下我们可以构造任意⼀个类,然后我们通过这个类知道这个类的所有属性和⽅法,并且如果我们创建⼀个对象,我们也可以通过对象找到这个类的任意⼀个⽅法,这就是反射机制。
在 Objective-C 中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别。任何对象都有 isa 指针
GCD 线程加锁
1、semaphore,
2、NSLock,
3、@synchronized
防⽌两条线程同时对此任务进⾏编辑,每次只能有⼀条线程执⾏此任务。所以就⽤到了线程加锁
2、 dispatch_semaphore ⾃旋锁
3、 @synchronized 互斥锁
4、 NSLock [_lock lock];加锁 [_lock unlock];解锁
5、 同步:不具备开启线程的能⼒,⼀定串⾏执⾏任务
6、异步:具有开启线程的能⼒,但是在主队列⾥不会开启新的线程。如果在串⾏队列和并发队列⾥开启 n 个⼦线程,gcd 优化之后未必会真的有 n 个⼦线程。
GCD 中如何取消线程
在 iOS 开发中,常⽤ NSOperation 和 GCD 来做多线程的开发,NSOperation 有 cancel 可以取消还未执⾏的线程。但是没办法做到取消⼀个正在执⾏的线程。
GCD ⽬前有两种⽅式可以取消线程,如下
1、类似 NSOperation ⼀样,可以取消还未执⾏的线程。但是没办法做到取消⼀个正在执⾏的线程。可以取消还未执⾏的线程有两种⽅式:
1.1 iOS8 后采⽤提供的 API,通过 dispatch_block_cancel 可以 cancel 掉 dispatch_block_t,需要注意的是,未执⾏的可以⽤此⽅法 cancel 掉,若已经执⾏则 cancel 不掉;
1.2 ⾃⼰通过设置 BOOL 值来让线程不执⾏线程处理的逻辑。
2、iOS8 以后,如果想中断(interrupt)线程,也就是取消⼀个正在执⾏的线程,可以使⽤ dispatch_block_testcancel ⽅法; dispatch_block_create 或者 dispatch_block_create_with_qos_class 或者 dispatch_block_cancel,如果取消了分派块,则返回⼀个⾮零值,否则为零。
-4、NSString 为什么要用 copy 关键字,如果用 strong 会有什么问题?
1、可变类型(NSMutableArray,NSMutableString 等)是不可变类型(NSString,NSArray 等)的⼦类,因为多态的原因,我们可以使⽤赋值指向⼦类对
象,也就是我们可以使⽤不可变类型去接受可变类型。
1.当我们使⽤ strong 修饰 A 不可变类型的时候,并且使⽤ B 可变类型给 A 赋值,再去修改可变类型 B 值的时候,A 所指向的值也会发⽣改变。引
⽂ strong 只是让创建的对象引⽤计数器+1,并返回当前对象的内容地址,当我们修改 B 指向的内容的时候,A 指向的内容也同样发⽣了改变,因为他们
指向的内存地址是相同的,是⼀份内容。
2.当我们使⽤ copy 修饰 A 不可变类型的时候,并且使⽤ B 可变类型给 A 赋值,再去修改可变类型 B 值的时候,A 所指向的值不会发⽣改变。因为
当时⽤ copy 的修饰的时候,会拷贝⼀份内容出来,并且返回指针给 A,当我们修改 B 指向的内容的时候,A 指向的内容是没有发⽣改变的。因为 A 指向
的内存地址和 B 指向的内存地址是不相同的,是两份内容
3.copy 修饰不可变类型(NSString,NSArray 等)的时候,且使⽤不可变类型进⾏赋值,表⽰浅拷贝,只拷贝⼀份,和 strong 修指针饰⼀样,当修饰的
是可变类型(NSMutableArray,NSMutableString 等)的时候,表⽰深拷贝,直接拷贝新⼀份内容,到内存中。表⽰两份内容。
strong 是创建了新的对象 所以 B 修改 A 不会变
-5、浅谈iOS之weak底层实现原理
1、Runtime 会维护⼀个 Weak 表,⽤于维护指向对象的所有 weak 指针。Weak 表是⼀个哈希表,其 key 为所指对象的指针,vaue 为 Weak 指针的地址
数组。
具体过程如下
1、初始化时: runtime 会调⽤ objc_initWeak 函数初始化⼀个新的 weak 指针指向对象的地址。
2、添加引⽤时: objc_initWeak 函数会调⽤ objc_ storeWeak(0 函数,更新指针指向,创建对应的弱引⽤表。
3、释放时,调⽤ clearDeallocating 函数 clearDeallocating 函数⾸先根据对象地址获取所有 Weak 指针地址的数组,然后遍历这个数组把其中的数据
设为 n,最后把这个 enty 从 Weak 表中删除,最后清理对象的记录。
当 weak 引⽤指向的对象被释放时,又是如何去处理 weak 指针的呢?当释放对象时,其基本流程如下:
1、调⽤ objc_release
2、因为对象的引⽤计数为 0,所以执⾏ dealloc
3、在 dealloc 中,调⽤了_objc_rootDealloc 函数
4、在_objc_rootDealloc 中,调⽤了 object_dispose 函数5、
5、调⽤ objc_destructInstance
6、最后调⽤ objc_clear_deallocating,详细过程如下:
a. 从 weak 表中获取废弃对象的地址为键值的记录
b. 将包含在记录中的所有附有 weak 修饰符变量的地址,赋值为 nil
c. 将 weak 表中该记录删除
d. 从引⽤计数表中删除废弃对象的地址为键值的记录
-6、ARC 的原理是什么?使用了 ARC 后,还有哪些编码场景下会出现内存泄漏,请列举?
原理:
1、
ARC 的规则就是只要对象没有强指针引⽤,就会被释放掉,换⽽⾔之 只要还有⼀个强引⽤指针变量指向对象,那么这个对象就
会存在内存中。弱指针指向的对象,会被⾃动变成空指针(nil 指针),从⽽不会引发野指针错误。
2、
ARC 本质是 NSAutoreleasePool 的直接应⽤:
AutoreleasePoolPage 是⼀个 C++实现的类。
typedef struct{
magic_t const magic;
id* next;
pThread_t* thread;
AutoreleasePoolPage* Parent;
AutoreleasePoolPage* Child;
uint32_t const depth;
uint32_t hiwat;
}
其中:
1,magic 是魔术数字,划分内存边界,数据结构起始处;
2,next,是指向该 AutoreleasePool 的边界;
3,thread, 是该 AutoreleasePool 的线程,每个线程有⾃⼰的 AutoreleasePool;
4,Parent、child,⽤于建⽴链表,⼀个 AutoreleasePool 不⼀定⾜够;
每个 NSAutoreleasePool 都是 4096bytes,不够的就申请新的 NSAutoreleasePool,⽤ child, parent 连接起来
1,“类实例所占内存”就是数据接⼜ NSAutoreleasePool 的空间;
2,“id objx”, 就是各个申请的对象指针;
3,next,指的是堆栈顶处;
对象是如何申请的?
这⼀页再加⼊⼀个 autorelease 对象就要满了(也就是 next 指针马上指向栈顶),这时就要执⾏上⾯说的操作,建⽴下⼀页 page 对象,与这⼀页
链表连接完成后,新 page 的 next 指针被初始化在栈底(begin 的位置),然后继续向栈顶添加新对象。
所以,向⼀个对象发送- autorelease 消息,就是将这个对象加⼊到当前 AutoreleasePoolPage 的栈顶 next 指针指向的位置
对象是如何释放的?
每当进⾏⼀次 objc_autoreleasePoolPush 调⽤时,runtime 向当前的 AutoreleasePoolPage 中 add 进⼀个哨兵对象,值为 0(也就是个 nil),那么这
⼀个 page 就变成了下⾯的样⼦:
objc_autoreleasePoolPush 的返回值正是这个哨兵对象的地址,被 objc_autoreleasePoolPop(哨兵对象)作为⼊参,于是:
1.根据传⼊的哨兵对象地址找到哨兵对象所处的 page
2.在当前 page 中,将晚于哨兵对象插⼊的所有 autorelease 对象都发送⼀次- release 消息,并向回移动 next 指针到正确位置
3.补充 2:从最新加⼊的对象⼀直向前清理,可以向前跨越若⼲个 page,直到哨兵所在的 page
刚才的 objc_autoreleasePoolPop 执⾏后,释放。
当您向⼀个对象发送⼀个 autorelease 消息时,Cocoa 就会将该对象的⼀个引⽤放⼊到最新的⾃动释放池。它仍然是个正当的对象,因此⾃动释放
池定义的作⽤域内的其它对象可以向它发送消息。当程序执⾏到作⽤域结束的位置时,⾃动释放池就会被释放,池中的所有对象也就被释放。
object-c 是通过⼀种"referring counting"(引⽤计数)的⽅式来管理内存的, 对象在开始分配内存(alloc)的时候引⽤计数为⼀,以后每当碰到有
copy,retain 的时候引⽤计数都会加⼀, 每当碰到 release 和 autorelease 的时候引⽤计数就会减⼀,如果此对象的计数变为了 0, 就会被系统销毁.
NSAutoreleasePool 就是⽤来做引⽤计数的管理⼯作的,这个东西⼀般不⽤你管的.
autorelease 和 release 没什么区别,只是引⽤计数减⼀的时机不同⽽已,autorelease 会在对象的使⽤真正结束的时候才做引⽤计数减⼀.
泄漏情况
1,循环参照A 有个属性参照 B,B 有个属性参照 A,如果都是 strong 参照的话,两个对象都⽆法释放。
这种问题常发⽣于把 delegate 声明为 strong 属性了。
2,死循环
如果某个 ViewController 中有⽆限循环,也会导致即使 ViewController 对应的 view 关掉了,ViewController 也不能被释放。
这种问题常发⽣于 animation 处理。
3,强应⽤
例如在 block 中调⽤ self.语法某个类将 block 作为⾃⼰的属性变量,然后该类在 block 的⽅法体⾥⾯又使⽤了该类本⾝;相互持有,导致都释放不了;
4,转换问题
Objective-C 和 Core Foundation 对象相互转换时就可能出现内存泄漏的问题。
5,NSTimer 的使⽤
NSTimer,NSTimer 会对它的 target 持有强引⽤,如果 NSTimer 不释放掉,就会⼀直持有它的 target 的强引⽤,如果这个 NSTimer 在被 target 强
引⽤,会⼀直都释放不掉,造成内存泄露.
6,单例也会造成内存泄漏
如果⼀个单例持有⼀个 block,block 内又使⽤了当前这个 ViewController 类,会引起循环引⽤。所以单例持有的代码块中要⽤弱引⽤,原因是:
单例不会被释放掉,它会⼀直持有 block,导致该 block 所在的 ViewController 释放不掉。
7,performSelector afterDelay
关于内存管理的执⾏原理是这样的执⾏
[self performSelector:@selector(method1:) withObject:self.tableLayer afterDelay:3];
的时候,系统会将 tableLayer 的引⽤计数加 1,执⾏完这个⽅法时,还会将 tableLayer 的引⽤计数减 1,有时切换场景时延时函数已经被调⽤但
还没有执⾏,这时 tableLayer 的引⽤计数并没有减少到 0,也就导致了切换场景 dealloc ⽅法没有被调⽤,出现了内存泄露。
解决办法就是取消那些还没有来得及执⾏的延时函数,代码:
[NSObject cancelPreviousPerformRequestsWithTarget:self]
当然你也可以⼀个⼀个得这样⽤:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(method1:) object:nil]
加上了这个以后,切换场景后会顺利地执⾏了 dealloc ⽅法,⾄此内存泄漏问题解决。
8,循环未结束
如果某个 ViewController 中有⽆限循环,也会导致即使 ViewController 对应的 view 关掉了,ViewController 也不能被释放。
这种问题常发⽣于 animation 处理。
CATransition *transition = [CATransition animation]; transition.duration = 0.5; tansition.repeatCount = HUGE_VALL; [self.view.layer
addAnimation:transition forKey:"myAnimation"];
上例中,animation 重复次数设成 HUGE_VALL,⼀个很⼤的数值,基本上等于⽆限循环了。
解决办法是,在 ViewController 关掉的时候,停⽌这个 animation。
-(void)viewWillDisappear:(BOOL)animated {
[self.view.layer removeAllAnimations];
}
9,⼤数据循环创建临时变量造成内存暴涨问题
该循环内产⽣⼤量的临时对象,直⾄循环结束才释放,可能导致内存泄漏,解决⽅法为在循环中创建⾃⼰的 autoReleasePool,及时释放占⽤内存
⼤的临时变量,减少内存占⽤峰值。
单例的弊端:
优点:
1:⼀个类只被实例化⼀次,提供了对唯⼀实例的受控访问。
2:节省系统资源
3:允许可变数⽬的实例。
缺点:
1:⼀个类只有⼀个对象,可能造成责任过重,在⼀定程度上违背了“单⼀职责原则”。
2:由于单例模式中没有抽象层,因此单例类的扩展有很⼤的困难。
3:滥⽤单例将带来⼀些负⾯问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多⽽出现连接池
溢出;如果实例化的对象长时间不被利⽤,系统会认为是垃圾⽽被回收,这将导致对象状态的丢失。
AutoRelease 的理解
1、写基于命令⾏的程序时,就是没有 UI 框架时,如 AppKit 等 Cocoa 框架。
2、写循环,循环⾥⾯包含了⼤量临时创建的对象。
3、创建了新的线程。4、长时间在后台运⾏的任务。
5、MRC 下需要对象调⽤ autorelease 才会⼊池, ARC 下可以通过 __autoreleasing 修饰符,否则的话看⽅法名,通过调⽤
alloc/new/copy/mutablecopy 以外的⽅法取得的对象,编译器帮我们⾃动加⼊ autoreleasepool。 (使⽤ alloc/new/copy/mutablecopy ⽅法进⾏初始化
时,由系统管理对象,在适当的位置 release,不加⼊ autoreleasepool )。
6、使⽤ array 会⾃动将返回对象注册到 autoreleasepool。
7、__weak 修饰的对象,为了保证在引⽤时不被废弃,会注册到 autoreleasepool 中。
8、id 的指针或对象的指针,在没有显式指定时会被注册到 autoreleasepool 中。
9、ARC 下⾯,我们使⽤@autoreleasepool{}来使⽤⼀个 Autoreleasepool,实际上 UIKit 通过 RunLoopObserver 在 RunLoop ⼆次 Sleep 间
Autoreleasepool 进⾏ Pop 和 Push,将这次 Loop 产⽣的 autorelease 对象释放 对编译器会编译⼤致如下:
void *DragonLiContext = objc_ AutoreleasepoolPush();
// {} 的 code
objc_ AutoreleasepoolPop(DragonLiContext);
释放时机: 当前 RunLoop 迭代结束时候释放.
1、及时释放局部变量、对象调⽤使⽤
2、@autoreleasepool 是⾃动释放池,让我们更⾃由的管理内存
3、当我们⼿动创建了⼀个@autoreleasepool,⾥⾯创建了很多临时变量,当@autoreleasepool 结束时,⾥⾯的内存就会回收
4、ARC 时代,系统⾃动管理⾃⼰的 autoreleasepool,runloop 就是 iOS 中的消息循环机制,当⼀个 runloop 结束时系统才会⼀次性清理掉被 autorelease
处理过的对象,其实本质上说是在本次 runloop 迭代结束时清理掉被本次迭代期间被放到 autorelease pool 中的对象的。⾄于何时 runloop 结束并没有固定
的 duration。
5、⽐如⾃⼰写的命令⾏⼯具 for 循环⾥创建了很多的临时变量,如果不使⽤@autoreleasepool,那临时变量内存可能是爆发式的,但是使⽤了
@autoreleasepool,在每个@autoreleasepool 结束时,⾥⾯的临时变量都会回收,内存使⽤更加合理。
-7、ARC 的⼯作原理
Automatic Reference Counting,⾃动引⽤计数,即 ARC,ARC 会⾃动帮你插⼊ retain 和 release 语句,ARC 编译器有两部分,分别是前端编译器和优化器
前端编译器:前端编译器会为“拥有的”每⼀个对象插⼊相应的 release 语句。如果对象的所有权修饰符是__strong,那么它就是被拥有的。如果在某个⽅法内创建了⼀个对象,前端编译器会在⽅法末尾⾃动插⼊ release 语句以销毁它。⽽类拥有的对象(实例变量/属性)会在 dealloc ⽅法内被释放。事实上,你并不需要写 dealloc ⽅法或调⽤⽗类的 dealloc ⽅法,ARC 会⾃动帮你完成⼀切。此外,由编译器⽣成的代码甚⾄会⽐你⾃⼰写的 release 语句的性能还要好,因为编辑器可以作出⼀些假设。在 ARC 中,没有类可以覆盖 release ⽅法,也没有调⽤它的必要。ARC 会通过直接使⽤ objc_release 来优化调⽤过程。⽽对于 retain 也是同样的⽅法。ARC 会调⽤ objc_retain 来取代保留消息
ARC 优化器: 虽然前端编译器听起来很厉害的样⼦,但代码中有时仍会出现⼏个对 retain 和 release 的重复调⽤。ARC 优化器负责移除多余的retain 和 release 语句,确保⽣成的代码运⾏速度⾼于⼿动引⽤计数的代码
ARC 管理原则:只要⼀个对象没有被强指针修饰就会被销毁,默认局部变量对象都是强指针,存放到堆⾥⾯,只是局部变量的强指针会在代码块结束后释放,对应所指向的内存空间也会被销毁。
MRC 没有 strong,weak,局部变量对象就是相当于基本数据类型。MRC 给成员属性赋值,⼀定要使⽤ set ⽅法,不能直接访问下划线成员属性赋值,因为使⽤下划线是直接赋值(如_name = name),⽽ set ⽅法会多做影响引⽤计数⽅⾯的事情,⽐如 retain。
-8、介绍⼀下 Object-C 的内存管理机制?
1)当你使⽤ new,alloc 和 copy ⽅法创建⼀个对象时,该对象的保留计数器值为 1.当你不再使⽤该对象时,你要负责向该对象发送⼀条 release 或 autorelease 消息.这样,该对象将在使⽤寿命结束时被销毁.
2)当你通过任何其他⽅法获得⼀个对象时,则假设该对象的保留计数器值为 1,⽽且已经被设置为⾃动释放,你不需要执⾏任何操作来确保该对象被清理.如果你打算在⼀段时间内拥有该对象,则需要保留它并确保在操作完成时释放它.
3)如果你保留了某个对象,你需要(最终)释放或⾃动释放该对象.必须保持 retain ⽅法和 release ⽅法的使⽤次数相等.为什么很多内置的类,如 TableViewController 的 delegate 的属性是 assign 不是 retain。
循环引⽤
所有的引⽤计数系统,都存在循环应⽤的问题。例如下⾯的引⽤关系:
1)对象 a 创建并引⽤到了对象 b.
2)对象 b 创建并引⽤到了对象 c.
3)对象 c 创建并引⽤到了对象 b.
这时候 b 和 c 的引⽤计数分别是 2 和 1。当 a 不再使⽤ b,调⽤ release 释放对 b 的所有权,因为 c 还引⽤了 b,所以 b 的引⽤计数为 1, b 不会被释放。b 不释放,c 的引⽤计数就是 1,c 也不会被释放。从此,b 和 c 永远留在内存中。这种情况,必须打断循环引⽤通过其他规则来维护引⽤关系。⽐如,我们常见的 delegate 往往是 assign ⽅式的属性⽽不是 retain ⽅式 的属性,赋值不会增加引⽤计数,就是为了防⽌ delegation 两端产⽣不必要的循环引⽤。如果⼀个 UITableViewController 对象 a,通过 retain 获取了 UITableView 对象 b 的所有权,这个 UITableView 对象 b 的 delegate 又是 a,如果这个 delegate 是 retain ⽅式的,那基本上就没有机会释放这两个对象了。⾃⼰在设计使⽤ delegate 模式时,也要注意这点。
对象是什么时候被 release 的?
引⽤计数为 0 时。 autorelease 实际上只是把对 release 的调⽤延迟了,对于每⼀个 Autorelease,系统只是把该 Object 放⼊了当前的Autoreleasepool 中,当该 pool 被释放时,该 pool 中的所有 Object 会被调⽤ Release。对于每⼀个 Runloop,系统会隐式创建⼀个 Autoreleasepool,这样所有的 release pool 会构成⼀个象 CallStack ⼀样的⼀个栈式结构,在每⼀个 Runloop 结束时,当前栈顶的 Autoreleasepool 会被销毁,这样这 个 pool ⾥的每个 Object(就是 autorelease 的对象)会被 release。那什么是⼀个 Runloop 呢?⼀个 UI 事件,Timercall, delegate call,都会是⼀个新的 Runloop。
-9、谈谈对 KVO, 和 KVC 的理解
⼀个⽬标对象管理所有依赖于它的观察者对象,并在它⾃⾝的状态改变时主动通知观察者对象。这个主动通知通常是通过调⽤各观察者对象所提供的接⼜⽅法来实现的。观察者模式较完美地将⽬标对象与观察者对象解耦。
KVC:俗称“键值编码”,通过⼀个 key 来访问某个属性;⼀种间接访问对象属性的机制,甚⾄可以通过 KVC 来访问对象的私有属性!修改textField 的 placeholder 也是通过 KVC 修改的,KVC 对多种数据类型的⽀持,KVC 在某种程度上提供了替代存取⽅法(访问器⽅法)的⽅案,不过存取⽅法终究是个好东西,以⾄于只要有可能,KVC 也尽可能先尝试使⽤存取⽅法访问属性。
当使⽤ KVC 访问属性时,它内部其实做了很多事:
1.⾸先查找有⽆,set,is等 property 属性对应的存取⽅法,若有,则直接使⽤这些⽅法;
2.若⽆,则继续查找_,_get,set等⽅法,若有就使⽤;
3.若查询不到以上任何存取⽅法,则尝试直接访问实例变量,
4.若连该成员变量也访问不到,则会在下⾯⽅法中抛出异常。之所以提供这两个⽅法,就是让你在因访问不到该属性⽽程序即将崩掉前,供你重写,在内做些处理,防⽌程序直接崩掉。
5.利⽤ KVC 即键值编码来给对象的私有属性赋值。
6.如何⼿动触发 KVO,valueForUndefinedKey:和 setValue:forUndefinedKey:⽅法。
KVO:就是为对象添加⼀个观察者“Observer”,当其属性值发⽣改变时,就会调⽤"observeValueForKeyPath:"⽅法,为我们提供⼀个“对象值改变了!”的时机进⾏⼀些操作。Key-Value Obersver,即键值观察。它是观察者模式的⼀种衍⽣。基本思想是,对⽬标对象的某属性添加观察,当该属性发⽣变化时,会⾃动的通知观察者。这⾥所谓的通知是触发观察者对象实现的 KVO 的接⼜⽅法。
KVO 是解决 model 和 view 同步的好法⼦.
另外,KVO 的优点是当被观察的属性值改变时是会⾃动发送通知的,这⽐通知中⼼需要 post 通知来说,简单了许多。
KVO:当指定的对象的属性被修改了,允许对象接收到通知的机制。
利⽤ RuntimeAPI 动态⽣成⼀个⼦类,并且让 instance 对象的 isa 指向这个全新的⼦类,当修改 instance 对象的属性时,会调⽤ Foundation 的,
_NSSetXXXValueAndNotify 函数,此函数的内部实现为 调⽤ willChangeValueForKey,调⽤⽗类(原来)的 setter 实现,调⽤ didChangeValueForKey:,
didChangeValueForKey:内部会调⽤ observer 的 observeValueForKeyPath:ofObject:change:context:⽅法。
Apple 使⽤了 isa 混写(isa-swizzling)来实现 KVO。当观察对象 A 时,KVO 机制动态创建⼀个新的名为 NSKVONotifying_A 的新类,该类集成
⾃对象 A 的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter ⽅法,setter ⽅法会负责在调⽤元 setter ⽅法之前和之后,通知所有观察对象属
性值的更改情况。(备注:isa 混写(isa-swizzling)isa:is a kind of ; swizzling: 混合,搅合)
1、NSKVONotifying_A 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统创建的⾃雷
NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应⽤层⾯来看,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层想实现过程,让我们误以为还是原来的类。但是此时如果我们创建⼀个新的名为“NSKVONotifying_A”的类,就会发现系统运⾏到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了(isa 指针的作⽤:每个对象都有 isa 指针,指向该对象的类,他告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新⼦类,那么这个被观察的对象就神奇地变成新⼦类的对象(或实例)了。)因⽽在该对象上对 setter 的调⽤就会调⽤已重写的 setter,从⽽激活键值通知机制。
2、⼦类 setter ⽅法剖析:KVO 的键值观察通知依赖与 NSObject 的两个⽅法:willChangeValueForKey:和 didChangeValueForKey:,在存取数值的前后分别调⽤ 2 个⽅法:
被观察属性发⽣改变之前,willChangeValueForkey:被调⽤,通知系统该 keyPath 的属性值即将变更;当改变发⽣后,didChangeValueForkey:被调 ⽤,通知系统该 keyPath 的属性值已经变更;之后,observeValueForKey:ofObject:context:也会被调⽤。且重写观察属性的 setter ⽅法这种继承⽅式的注⼊是在运⾏时⽽不是编译时实现的。
通过 KVC 修改属性会触发 KVO。
-10、https 的原理是什么?AFNetworking 中是怎样处理 https 的?
1、AFNetworking 主要是对 NSURLSession 和 NSURLConnection(iOS9.0 废弃)的封装,其中主要有以下类: 1). AFHTTPRequestOperationManager:内部封装的是 NSURLConnection, 负责发送⽹⽹络请求, 使⽤⽤ 最多的⼀⼀个类。(3.0 废弃)
2). AFHTTPSessionManager:内部封装是 NSURLSession, 负责发送⽹⽹络请求,使⽤⽤最多的⼀⼀个类。 3). AFNetworkReachabilityManager:实时监测⽹⽹络状态的⼯⼯具类。当前的⽹⽹络环境发⽣⽣改变之后,这 个⼯⼯具类就可以检测到。
4). AFSecurityPolicy:⽹⽹络安全的⼯⼯具类, 主要是针对 HTTPS 服务。
5). AFURLRequestSerialization:序列列化⼯⼯具类,基类。上传的数据转换成 JSON 格式 (AFJSONRequestSerializer).使⽤⽤不不多。
6). AFURLResponseSerialization:反序列列化⼯⼯具类;基类.使⽤⽤⽐⽐较多:
7). AFJSONResponseSerializer; JSON 解析器器,默认的解析器器.
8). AFHTTPResponseSerializer; 万能解析器器; JSON 和 XML 之外的数据类型,直接返回⼆⼆进 制数据.对服务器器返回的数据不不做任何处理理.
9). AFXMLParserResponseSerializer; XML 解析器器;
10). AFN 会添加⼀条常驻线程⽬的是开辟线程请求⽹络数据。如果没有常住线程的话,就会每次请求⽹络就去开辟线程,完成之后销毁开辟线程,这样就造成资源的浪费,开辟⼀条常住线程,就可以避免这种浪费,我们可以在每次的⽹络请求都添加到这条线程。HTTPS 加密的原理:
1、服务器端⽤⾮对称加密(RSA)⽣成公钥和私钥
2、然后把公钥发给客户端, 服务器则保存私钥
3、客户端拿到公钥后, 会⽣成⼀个密钥, 这个密钥就是将来客户端和服务器⽤来通信的钥匙
4、然后客户端⽤公钥对密钥进⾏加密, 再发给服务器
5、服务器拿到客户端发来的加密后的密钥后, 再使⽤私钥解密密钥, 到此双⽅都获得通信的钥匙
HTTP 协议,1.0 2.0(HTTP 了解吗?HTTP2.0 介绍下)
0.9 版本中只接受 GET 请求,不⽀持请求头
1.0 代理服务器中
1.1:持久连接被默认采⽤,并能很好地配合代理服务器⼯作。还⽀持以管道⽅式在同时发送多个请求,以便降低线路负载,提⾼传输速度。
1.1 相较于 1.0 区别在于:缓存处理,带宽优化及⽹络连接的使⽤,错误通知的管理,消息在⽹络中的发送,互联⽹地址的维护,安全性及完整性。还要求更加严格以确保服务的可靠性,增强了在 HTTP/1.0 没有充分考虑到分层代理服务器、⾼速缓冲存储器、持久连接需求或虚拟主机等⽅⾯的效能;安全增强版的HTTP (即 S-HTTP 或 HTTPS),则是 HTTP 协议与安全套接⼜层(SSL)的结合,使HTTP 的协议数据在传输过程中更加安全
2.0 HTTP/1.x,HTTP/2.0 不会再⽤明⽂, 协议是明⽂的,存在很多缺点——⽐如传输内容会被偷窥(嗅探)和篡改,所以在 94 年修改了 HTTPS,加密协议叫 SSL,英⽂ Secure Sockets Layer 的缩写,HTTP over SSL。HTTPS 将 HTTP 协议数据包放到SSL/TSL 层加密后,在 TCP/IP 层组成 IP 数据报去传输,以此保证传输数据的安全;⽽对于接收端,在 SSL/TSL 将接收的数据包解密之后,将数据传给 HTTP 协议层,就是普通的 HTTP 数据。TCP/IP 协议是分层的,从底层⾄应⽤层分别为:物理层、链路层、⽹络层、传输层和应⽤层;从应⽤层⾄物理层,数据是⼀层层封装,封装的⽅式⼀般都是在原有数据的前⾯加⼀个数据控制头,HTTP 是⼀种请求/响应式的协议,HTTPS 将 HTTP 协议数据包放到 SSL/TSL 层加密后,在 TCP/IP 层组成 IP 数据报去传输,以此保证传输数据的安全;⽽对于接收端,在SSL/TSL 将接收的数据包解密之后,将数据传给 HTTP 协议层,就是普通的 HTTP 数据。
⾸先了解下对称加密和⾮对称加密,对称加密就是将你要传输的内容⽤⼀个密钥加密起来,发送⽅和都要知道这个密钥,⽤它来加密解密。但使⽤对称加密需要给对⽅传这个密钥,在互联⽹上传输这个密钥很容易被截取。所以就有了⾮对称加密,这种加密指的是可以⽣成⼀对密钥 (k1, k2)。凡是k1 加密的数据,k1 ⾃⾝不能解密,⽽需要 k2 才能解密;凡是 k2 加密的数据,k2 不能解密,需要 k1 才能解密。
-11、CA 证书包含哪些数据?
版本号(Version Number)
序列号(Serial Number)
签名算法:如 sha256-with-RSA-Encryption;ccdsa-with-SHA2S6;
颁发者
有效期
主体(Subject) : 证书拥有者的标识信息
主体的公钥信息(SubJect Public Key Info):公钥算法 (Public Key Algorithm)公钥采⽤的算法;主体公钥(Subject Unique Identifier):公钥的内容。
颁发者唯⼀号(Issuer Unique Identifier)
主体唯⼀号(Subject Unique Identifier):扩展(Extensions,可选): 消息转发机制原理
-12、响应者链的关系
-13、调⽤⽤类别的⽅⽅法:
-
从内存(字典)中找图⽚⽚(当这个图⽚⽚在本次使⽤⽤程序的过程中已经被加载过),找到直接使 ⽤⽤。
-
从沙盒中找(当这个图⽚⽚在之前使⽤⽤程序的过程中被加载过),找到使⽤⽤,缓存到内存中。
-
从⽹⽹络上获取,使⽤⽤,缓存到内存,缓存到沙盒。
sd 加载⼀张图⽚的时候,会先在内存⾥⾯查找是否有这张图⽚,如果没有会根据图⽚的 md5(url)后的名称去沙盒⾥⾯去寻找,是否有这张图⽚,如果没有会开辟线程去下载,下载完毕后加载到 imageview 上⾯,并 md(url)为名称缓存到沙盒⾥⾯。
-14、开发项⽬时你是怎么检查内存泄露
当⼀个 ViewController 被 pop 或 dismiss 之后,我们认为该 ViewController,包括它上⾯的⼦ ViewController,及它的 View,View 的 subView 等,都很快会被释放,如果某个 View 或者 ViewController 没释放,我们就认为该对象泄漏了。
为基类 NSObject 添加⼀个⽅法 -willDealloc ⽅法,该⽅法的作⽤是,先⽤⼀个弱指针指向 self,并在 3 秒后,通过这个弱指针调⽤ -assertNotDealloc,⽽ -assertNotDealloc 主要作⽤是直接中断⾔。
UIViewController 的分类中,使⽤ Method Swizzling,hook 掉了 viewDidDisappear:,viewWillAppear:,dismissViewControllerAnimated:completion: 等⽅法,让他们都执⾏ willDealloc ⽅法,这样,在不⼊侵开发代码的情况下,为 UIViewController 添加了检查内存泄露的功能(AOP), 释放前调⽤这个⽅法,如果 2 秒后它被释放成功,weakSelf 就指向 nil,不会调⽤ assertNotDealloc ⽅法. 构造堆栈信息的原理是:递归遍历⼦对象,然后将⽗对象 class name 加上⼦对象 class name,⼀步步构造出⼀个 view stack
-15、如何判断⽤户本次是否上传成功和下载成功了?
⽤ MD5 验证⽂件的完整性!(仅仅通过代码来判断当前次的请求发送结束或者收到数据结束不可以的)
当客户端上传⼀个⽂件的时候,在请求 body ⾥⾯添加该⽂件的 MD5 值来告诉服务器,服务器接受⽂件完毕以后通过校验收到的⽂件的 MD5 值与请求 body ⾥⾯的 MD5 值来最终确定本次上传是否成功
当客户端下载⼀个⽂件的时候,在响应头⾥⾯收到了服务器附带的该⽂件的 MD5 值,⽂件下载结束以后,通过获取下载后⽂件的 MD5 值与本次
请求服务器返回的响应头中的 MD5 值做⼀个⽐较,来最终判断本次下载是否成功
MD5,是⼀个将任意长度的数据字符串转化成短的固定长度的值的单向操作。任意两个字符串不应有相同的散列值
MD5 校验可以应⽤在多个领域,⽐如说机密资料的检验,下载⽂件的检验,明⽂密码的加密等。MD5 校验原理举例:如客户往我们数据中⼼同步⼀个⽂件,该⽂件使⽤ MD5 校验,那么客户在发送⽂件的同时会再发⼀个存有校验码的⽂件,我们拿到该⽂件后做 MD5 运算,得到的计算结果与客户发送的校验码相⽐较,如果⼀致则认为客户发送的⽂件没有出错,否则认为⽂件出错需要重新发送。
算法
1、哈希原理
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)⽽直接进⾏访问的数据结构。也就是说,它通过把关键码值映射到表中⼀个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表 M,存在函数 f(key),对任意给定的关键字值 key,代⼊函数后若能得到包含该关键字的记录在表中的地址,则称表 M 为哈希(Hash)表,函数 f(key)为哈希(Hash) 函数。
哈希概念:哈希表的本质是⼀个数组,数组中每⼀个元素称为⼀个箱⼦(bin),箱⼦中存放的是键值对。
2、哈希储存原理
1.根据 key 计算出它的哈希值 h。
2.假设箱⼦的个数为 n,那么这个键值对应该放在第 (h % n) 个箱⼦中。
3.如果该箱⼦中已经有了键值对,就使⽤开放寻址法或者拉链法解决冲突。
在使⽤拉链法解决哈希冲突时,每个箱⼦其实是⼀个链表,属于同⼀个箱⼦的所有键值对都会排列在链表中。
哈希表还有⼀个重要的属性: 负载因⼦(load factor),它⽤来衡量哈希表的空/满程度,⼀定程度上也可以体现查询的效率,计算公式为:
负载因⼦ = 总键值对数 / 箱⼦个数
负载因⼦越⼤,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,⼀般来说,当负载因⼦⼤于某个常数(可能是 1,或者 0.75 等)时,哈希表将⾃动扩容。
哈希表在⾃动扩容时,⼀般会创建两倍于原来个数的箱⼦,因此即使 key 的哈希值不变,对箱⼦个数取余的结果也会发⽣改变,因此所有键值对的存放位置都有可能发⽣改变,这个过程也称为重哈希(rehash)。
哈希表的扩容并不总是能够有效解决负载因⼦过⼤的问题。假设所有 key 的哈希值都⼀样,那么即使扩容以后他们的位置也不会变化。虽然负载因⼦会降低,但实际存储在每个箱⼦中的链表长度并不发⽣改变,因此也就不能提⾼哈希表的查询性能。
基于以上总结,细⼼的朋友可能会发现哈希表的两个问题:
1.如果哈希表中本来箱⼦就⽐较多,扩容时需要重新哈希并移动数据,性能影响较⼤。
2.如果哈希函数设计不合理,哈希表在极端情况下会变成线性表,性能极低。
3.block 和函数指针的理解;
相似点:
函数指针和 Block 都可以实现回调的操作,声明上也很相似,实现上都可以看成是⼀个代码⽚段。
函数指针类型和 Block 类型都可以作为变量和函数参数的类型。(typedef 定义别名之后,这个别名就是⼀个类型)
不同点:
函数指针只能指向预先定义好的函数代码块(可以是其他⽂件⾥⾯定义,通过函数参数动态传⼊的),函数地址是在编译链接时就已经确定好的。
Block 本质是 Objective-C 对象,是 NSObject 的⼦类,可以接收消息。
函数⾥⾯只能访问全局变量,⽽ Block 代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过__block 访问指⽰符修饰的局部变量还可以在 block 代码块⾥⾯进⾏修改)。
从内存的⾓度看,函数指针只不过是指向代码区的⼀段可执⾏代码,⽽ block 实际上是程序运⾏过程中在栈内存动态创建的对象,可以向其发送copy 消息将 block 对象拷贝到堆内存,以延长其⽣命周期。
-16、SVN 与 Git 的最主要的区别?
SVN 是集中式版本控制系统,版本库是集中放在中央服务器的,⽽⼲活的时候,⽤的都是⾃⼰的电脑,所以⾸先要从中央服务器哪⾥得到最新的版本,然后⼲活,⼲完后,需要把⾃⼰做完的活推送到中央服务器。集中式版本控制系统是必须联⽹才能⼯作,如果在局域⽹还可以,带宽够⼤,速度够快,如果在互联⽹下,如果⽹速慢的话,就纳闷了。
Git 是分布式版本控制系统, ,每个⼈的电脑就是⼀个完整的版本库,这样,⼯作的时候就不需要联⽹了,因为版本都是在⾃⼰的电脑上。既然每个⼈的电脑都有⼀个完整的版本库,那多个⼈如何协作呢?⽐如说⾃⼰在电脑上改了⽂件 A,其他⼈也在电脑上改了⽂件 A,这时,你们两之间只需把各⾃的修改推送给对⽅,就可以互相看到对⽅的修改了。
Git 把内容按元数据⽅式存储,⽽ SVN 是按⽂件,
Git 没有⼀个全局版本号,SVN 有,
Git 的内容的完整性要优于 SVN: GIT 的内容存储使⽤的是 SHA-1 哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和⽹络问题时降低对版本库的破坏,Git 下载下来后,在 OffLine 状态下可以看到所有的 Log,SVN 不可以,克隆⼀份全新的⽬录以同样拥有五个分⽀来说,SVN 是同时复製 5 个版本的⽂件,也就是说重复五次同样的动作。⽽ Git 只是获取⽂件的每个版本的 元素,然后只载⼊主要的分⽀(master)在我的经验,克隆⼀个拥有将近⼀万个提交(commit),五个分⽀,每个分⽀有⼤约 1500 个⽂件的 SVN,耗了将近⼀个⼩时!⽽ Git 只⽤了区区的 1 分钟,
版本库(repository):SVN 只能有⼀个指定中央版本库。当这个中央版本库有问题时,所有⼯作成员都⼀起瘫痪直到版本库维修完毕或者新的版本库设⽴完成。⽽ Git 可以有⽆限个版本库。或者,更正确的说法,每⼀个 Git 都是⼀个版本库,区别是它们是否拥有活跃⽬录(Git Working Tree)。如果主要版本库(例如:置於 GitHub 的版本库)发⽣了什麼事,⼯作成员仍然可以在⾃⼰的本地版本库(local repository)提交,等待主要版本库恢复即可。⼯作成员也可以提交到其他的版本库,分⽀(Branch)在 SVN,分⽀是⼀个完整的⽬录。且这个⽬录拥有完整的实际⽂件。如果⼯作成员想要开啟新的分⽀,那将会影响“全世界”!每个⼈都会拥有和你⼀样的分⽀。如果你的分⽀是⽤来进⾏破坏⼯作(安检测试),那将会像传染病⼀样,你改⼀个分⽀,还得让其他⼈重新切分⽀重新下载,⼗分狗⾎。⽽ Git,每个⼯作成员可以任意在⾃⼰的本地版本库开啟⽆限个分⽀。举例:当我想尝试破坏⾃⼰的程序(安检测试),并且想保留这些被修改的⽂件供⽇后使⽤, 我可以开⼀个分⽀,做我喜欢的事。完全不需担⼼妨碍其他⼯作成员。只要我不合并及提交到主要版本库,没有⼀个⼯作成员会被影响。等到我不需要这个分⽀时, 我只要把它从我的本地版本库删除即可。⽆痛⽆痒。
Git 有本地仓库和缓存区,Git 的分⽀名是可以使⽤不同名字的。例如:我的本地分⽀名为 OK,⽽在主要版本库的名字其实是 master。
我可以在 Git 的任意⼀个提交点(commit point)开启分⽀!(其中⼀个⽅法是使⽤ gitk –all 可观察整个提交记录,然后在任意点开啟分⽀。)
提交(Commit)在 SVN,当你提交你的完成品时,它将直接记录到中央版本库。当你发现你的完成品存在严重问题时,你已经⽆法阻⽌事情的发⽣了。如果⽹路中断,你根本没办法提交!⽽ Git 的提交完全属於本地版本库的活动。⽽你只需“推”(git push)到主要版本库即可。Git 的“推”其实是在执⾏“同步”(Sync)。
Git 的特点版本控制可以不依赖⽹络做任何事情,对分⽀和合并有更好的⽀持(当然这是开发者最关⼼的地⽅)
performSelector:withObject:afterDelay:inModes:的实现原理
1、 在⼦线程中执⾏不会调⽤其中的 SEL ⽅法,因为⼦线程中的 runloop 默认是没有启动的状态。使⽤[[NSRunLoop currentRunLoop] run]⽅法开启当前线程的 runloop;执⾏完 performSelector ⽅法后该 timer 事件会被添加到⼦线程的 runloop 中:
2、 在⼦线程中两者的顺序必须是先执⾏ performSelector 延迟⽅法之后再执⾏ run ⽅法。因为 run ⽅法只是尝试想要开启当前线程中的 runloop,但是如果该线程中并没有任何事件(source、timer、observer)的话,并不会成功的开启。
3、 performSelector:⽅法只是⼀个单纯的消息发送,和时间没有⼀点关系。所以不需要添加到⼦线程的 Runloop 中也能执⾏
-17、famework 和.a 文件 在编写 SDK 时 有什么区别吗?
.a 是静态库,只暴露.h ⽂件,Sdk 可以是静态也可以是动态,.h.m 都暴露可以看到,只暴露头⽂件供开发者使⽤
famework 可以是静态也可以是动态,framework 是⼆进制⽂件+头⽂件+bundle(如果有资源⽂件 像图⽚ nib 多语⾔⽂件)
-18、通过热点访问服务器,怎么保证数据传输安全,没有被挟持
FTP 协议+socket 传输
-19、NSDictionary 和 NSArray 区别, 构建缓存时选 NSCache 而非 NSDictionary?
1、数组 NSArray 的⼤多数⽅法使⽤ isEqual:来检查对象间的关系(例如 containsObject:)。有⼀个特别的⽅法:indexOfObjectIdenticalTo:⽤来检查指针相等,如果你确保在同⼀个集合中搜索,那么这个⽅法可以很⼤的提升搜索速度。
2、⼀个字典存储任意的对象键值对,初始化⽅法使⽤相反的对象到值的⽅法:[NSDictionary dictionaryWithObjectsAndKeys:object, key, nil]。
NSDictionary 中的键是被拷贝的并且需要是恒定的。如果在⼀个键在被⽤于在字典中放⼊⼀个值后被改变,那么这个值可能就会变得⽆法获取了。
⼀个有趣的细节,在 NSDictionary 中键是被拷贝的,⽽在使⽤⼀个 toll-free 桥接的 CFDictionary 时却只被 retain。CoreFoundation 类没有通⽤对象的拷贝⽅法,因此这时拷贝是不可能的(*)。这只适⽤于使⽤ CFDictionarySetValue()的时候。如果通过 setObject:forKey 使⽤ toll-free 桥接的 CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成⽴ — 转换为 CFDictionary 的 NSDictionary 对象,对其使⽤ CFDictionarySetValue()⽅法会调⽤回 setObject:forKey 并拷贝键。
3、1).当系统资源将要耗尽时,NSCache 可以⾃动删减缓存。如果采⽤普通的字典,那么就要⾃⼰编写挂钩,在系统通知时⼿动删减缓存,NSCache会先⾏删减 时间最久为被使⽤的对象
2).NSCache 并不会拷贝键,⽽是会保留它。此⾏为⽤ NSDictionary 也可以实现,但是需要编写⽐较复杂的代码。NSCache 对象不拷贝键的原因在于,很多时候键都是不⽀持拷贝操作的对象来充当的。因此 NSCache 对象不会⾃动拷贝键,所以在键不⽀持拷贝操作的情况下,该类⽐字典⽤起来更⽅便
3).NScache 是线程安全的,NSDictionary 不是。在开发者⾃⼰不编写加锁代码的前提下,多个线程可以同时访问 NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存⾥找不着指定的键,那么就要下载该键对应的数据了
-20、线程间怎么通信?
4.线程在运⾏过程中,可能需要与其它线程进⾏通信。我们可以使⽤ NSObject 中的⼀些⽅法:
在应⽤程序主线程中做事情:performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在指定线程中做事情:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在当前线程中做事情:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
取消发送给当前线程的某个消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
如在我们在某个线程中下载数据,下载完成之后要通知主线程中更新界⾯等等,可以使⽤如下接⼜:
- (void)myThreadMainMethod{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// to do something in your thread job
[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO];
[pool release];
}
-21、内存缓存和本地缓存、内存?
缓存分为内存缓存和磁盘缓存两种,其中内存是指当前程序的运⾏空间,缓存速度快容量⼩,是临时存储⽂件⽤的,供 CPU 直接读取,⽐如说打开⼀个程序,他是在内存中存储,关闭程序后内存就又回到原来的空闲空间;磁盘是程序的存储空间,缓存容量⼤速度慢可持久化与内存不同的是磁盘是永久存储东西的,只要⾥⾯存放东西,不管运⾏不运⾏ ,他都占⽤空间!磁盘缓存是存在 Library/Caches。
iOS 内存分为 5 个区:栈区,堆区,全局区,常量区,代码区。
栈区 stack:这⼀块区域系统会⾃⼰管理,我们不⽤⼲预,主要存⼀些局部变量,以及函数跳转时的现场保护。因此⼤量的局部变量,深递归,函数循环调⽤都可能导致内存耗尽⽽运⾏崩溃。
堆区 heap:与栈区相对,这⼀块⼀般由我们⾃⼰管理,⽐如 alloc,free 的操作,存储⼀些⾃⼰创建的对象。
全局区(静态区 static):全局变量和静态变量都存储在这⾥,已经初始化的和没有初始化的会分开存储在相邻的区域,程序结束后系统会释放。
常量区:存储常量字符串和 const 常量。
代码区:存储代码。
-22、简述 MVC,MVP,MVVP,VIPER?从 MVC 到 MVP,MVVM,VIPER,这些概念不断进化的底层原因是什么?
1、MVC:简单来说就是,逻辑、试图、数据进⾏分层,实现解耦。
2、MVVM:是 Model-View-ViewMode 模式的简称。由视图(View)、视图模型(ViewModel)、模型(Model)三部分组成.⽐ MVC 更加释放控制器臃肿,将⼀部分逻辑(耗时,公共⽅法,⽹络请求等)和数据的处理等操作从控制器⾥⾯搬运到 ViewModel 中
MVVM 的特点:
低耦合。View 可以独⽴于 Model 变化和修改,⼀个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
可重⽤性。可以把⼀些视图的逻辑放在 ViewModel ⾥⾯,让很多 View 重⽤这段视图逻辑。
独⽴开发。开发⼈员可以专注与业务逻辑和数据的开发(ViewModel)。设计⼈员可以专注于界⾯(View)的设计。
可测试性。可以针对 ViewModel 来对界⾯(View)进⾏测试
3、MVP:MVP 的 V 层是由 UIViewController 和 UIView 共同组成 view 将委托 presenter 对它⾃⼰的操作,(简单来说就是 presenter 发命令来控制 view 的交互,要你隐藏就隐藏,叫你 show 你就乖乖的 show)
presenter 拥有对 view 交互的逻辑(就是上⾯说的意思)
presenter 跟 model 层通信,并将数据转化成对适应 UI 的数据并更新 view
presenter 不需要依赖 UIKit
view 层是单⼀,因为它是被动接受命令,没有主动能⼒。
presenter 作为业务逻辑的处理者,⾸先要向 Service 层拿数据赋值给 model,所以它将可以向 model 层通信。其次,UI 的处理权移交给了它,所以它需要与 view 成通讯,发送命令更新 UI。同时,UI 的响应将触发业务逻辑的处理,所以 view 层向 presenter 层通讯,告诉他⽤户做了什么操作,需要你反馈对应的数据来更新 UI。这样就完成了从⽤户交互获得交互反馈到整个业务逻辑.
4、MVVP:就是 API 请求完数据,解析成 Model,之后在 ViewModel 中转化成能够直接被视图层使⽤的数据,交付给前端(View 层).
5、VIPER:VIPER ⾥的各部分正是存在着由外向内的依赖,从外向内表现为:View -> Presenter -> Interactor -> Entity,Wireframe 严格来说也是⼀类特殊的 Use Case,⽤于不同模块之间通信,连接了不同的 Presenter。必须要记住的是,VIPER 架构是根据由外向内的依赖关系来设计的。
事实上,它⽐使⽤了数据绑定技术的 MVVM 更加简单,就是因为它职责明确。从 MVC 转到 VIPER 的过程同样是很清晰的,它甚⾄把重构的思路都体现出来了。⽽ MVVM 则留下了许多尚未明确的责任,导致不同的⼈会在某些地⽅有不同的实现。
-23、什么是野指针?什么情况下会出现野指针?野指针问题如何调试?
1、指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址。
2、因为这些疏忽⽽出现的删除或申请访问受限内存区域的指针。指针变量未初始化,指针释放后之后未置空,指针操作超越变量作⽤域.
3、初始化时置 NULL,释放时置 NULL,所以动态分配内存后,如果使⽤完这个动态分配的内存空间后,必须习惯性地使⽤ delete 操作符去释放它。
-24、安装包⼤⼩如何优化,请列举你使⽤过的优化⼿段。
1、配置编译选项 (Levels 选项内)Generate Debug Symbols 设置为 NO,这个配置选项应该会让你减去⼩半的体积。注意这个如果设置成 NO就不会在断点处停下
2、舍弃架构 armv7,armv7 ⽤于⽀持 4s 和 4,4s 是 2011 年 11 ⽉正式上线,虽然还有⼩部分⼈在使⽤,但是追求包体⼤⼩的完全可以舍弃了。
3、去除⽆⽤的三⽅库、代码、readme
4、图⽚处理图⽚是安装包⾥占⽤空间最⼤的东西,我的项⽬中占⽤了⼀半的体积。
*⽤ imageoptim 压缩图⽚的⼤⼩
*⼀些⽐较⼤体积的背景图⽚压缩成.jpg 格式的。
*⽤ LSUnusedResource 这个软件查找项⽬中没有⽤到的图⽚,然后删除,当然不⼀定特别准确,有⼀些[UIImage imageNamed:[NSString
stringWithFormat:@"icon_%d",index]]这样使⽤的图⽚也会被列在未使⽤图⽚中。
*使⽤ Assets.xcassets 来管理图⽚也可以减⼩安装包的体积
5、build setting ⾥ DEAD_CODE_STRIPPING = YES(好像默认就是 YES)。 确定 dead code(代码被定义但从未被调⽤)被剥离,去掉冗余的代码,即使⼀点冗余代码,编译后体积也是很可观的。
6、编译器优化级别 Build Settings->Optimization Level 有⼏个编译优化选项,release 版应该选择 Fastest, Smalllest[-Os],这个选项会开启那些不增加代码⼤⼩的全部优化,并让可执⾏⽂件尽可能⼩。
7、去除符号信息 Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在 release 版本应该设为 yes,可以去除不必要的调试符号。
Symbols Hidden by Default 会把所有符号都定义成”private extern”,设了后会减⼩体积。
8、Strip Linked Product:DEBUG 下设为 NO,RELEASE 下设为 YES,⽤于 RELEASE 模式下缩减 app 的⼤⼩; 2018.7.17 新增
9、编译器优化,去掉异常⽀持。Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加-fno-exceptions Enable
C++ Exceptions Enable Objective-C Exceptions Other C Flags 添加-fno-exceptions
10、利⽤ AppCode 检测未使⽤的代码:菜单栏 ->Code->InspectCode 最后要说:xcode BulidSetting 中的设置都可以区分 debug 和 release,如果觉得在开发的时候还想⽤到这些,就把 debug 和 release 分开设置就可以了
-25、如何使⽤队列来避免资源抢夺?
当我们使⽤多线程来访问同⼀个数据的时候,就有可能造成数据的不准确性。这个时候我么可以使⽤线程锁的来来绑定。也是可以使⽤串⾏队列来完成。如:fmdb 就是使⽤ FMDatabaseQueue,来解决多线程抢夺资源。
-26、能否向编译后得到的类中增加实例变量?能否向运⾏时创建的类中添加实例变量?为什么?
1.不能向编译后得到的类增加实例变量
2.能向运⾏时创建的类中添加实例变量
1.编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存⼤⼩已经确定,runtime 会调⽤ class_setvarlayout 或 class_setWeaklvarLayout 来处理 strong weak 引⽤.所以不能向存在的类中添加实例变量
2.运⾏时创建的类是可以添加实例变量,调⽤ class_addIvar 函数.但是的在调⽤ objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
-27、如何控制⽹络顺序请求?
1、 回答有六:
2、
第⼀种 dispatch_group_async、dispatch_group_notify 配合使⽤:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"任务⼀完成");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务⼆完成");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任务三完成");
});//在分组的所有任务完成后触发
dispatch_group_notify(group, queue, ^{
NSLog(@"所有任务完成");
});
3、
dispatch_group_enter/leave
4、
信⾏量
5、
模拟循环⽹络请求 同时进⾏ 统⼀回调 (GCD + 信号量⽅式):dispatch_group_t和 dispatch_group_async
6、
模拟循环⽹络请求 顺序进⾏ (GCD + 信号量⽅式)
7、
模拟循环⽹络请求 顺序进⾏ (GCD + group enter/leave ⽅式)
dispatch_group_t group = dispatch_group_create();
for (int i = 0 ; i < 5;i++) {
dispatch_group_enter(group);
// 模拟请求 ↓
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(6 - i);
NSLog(@"任务%d完成",i);
dispatch_group_leave(group);
});
// 模拟请求 ↑
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // 顺序执⾏与同步执⾏的不同点
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"全部搞完了");
});
-28、pod install 运⾏原理分析
1、分析dependency
2、对⽐本地Pod和podfile.lock⽂件中的版本,如果不⼀致会提⽰存在风险。
3、对⽐podfile是否发⽣了变化,add/remove pod依赖
4、如果存在 ,会⽣成两个列表,⼀个是需要add的pods,⼀个是需要remove的pods。
5、如果存在remove的,删除remove的pods(会删除podfile.lock⾥的版本依赖)。
6、添加需要的pods依赖
7、此时,如果是常规的CocoaPods库(基于git),会先去:
8、Spec下查找对应的Pods⽂件
9、找到对应的tag
10、找到对应tag下⾯的podspec⽂件
11、git clone下来代码并copy到Pod⽬录下
12、运⾏pre-install hook
13、⽣成Pod Project
14、将该pod⽂件添加到⼯程中
15、添加对应的framework、.a库、bundle等
16、链接头⽂件,⽣成Target
17、运⾏post-install hook
18、⽣成podfile.lock ,之后⽣成⽂件副本mainfest.lock并将其放在Pod⽂件夹内。(如果出现 The sandbox is not sync with the podfile.lock这种错
误,则表⽰manifest.lock和podfile.lock⽂件不⼀致),此时⼀般需要重新运⾏pod install命令。
19、配置原有的project⽂件(add build phase)
20、添加了 Embed Pods Frameworks
21、添加了 Copy Pod Resources
22、其中,pre-install hook和post-install hook可以理解成回调函数,是在podfile⾥对于install之前或者之后(⽣成⼯程但是还没写⼊磁盘)可以执⾏的逻辑,逻辑为:
-29、Runtime如何通过selector找到对应的IMP地址?
⼀、概述
对于实例⽅法,每个实例的 isa 指针指向着对应类对象,⽽每⼀个类对象中都⼀个对象⽅法列表。
对于类⽅法,每个类对象的 isa 指针都指向着对应的元对象,⽽每⼀个元对象中都有⼀个类⽅法列表。
⽅法列表中记录着⽅法的名称,⽅法实现,以及参数类型,其实 selector 本质就是⽅法名称,通过这个⽅法名称就可以在⽅法列表中找到对应的⽅法实现。
当我们发送⼀个消息给⼀个 NSObject 对象时,这条消息会在对象的类对象⽅法列表⾥查找
当我们发送⼀个消息给⼀个类时,这条消息会在类的 Meta Class 对象的⽅法列表⾥查找
在寻找 IMP 的地址时,runtime 提供了两种⽅法:
IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m);
根据官⽅描述,第⼀种⽅法可能会更快⼀些
@note \c class_getMethodImplementation may be faster than \c method_getImplementation(class_getInstanceMethod(cls, name)).
2.1、 IMP class_getMethodImplementation(Class cls, SEL name)
对于第⼀种⽅法⽽⾔,类⽅法和实例⽅法实际上都是通过调⽤ class_getMethodImplementation()来寻找 IMP 地址的,不同之处在于传⼊的第⼀个
参数不同(假设有⼀个类 A
类⽅法
class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));
实例⽅法
class_getMethodImplementation([A class],@selector(methodName));
通过该传⼊的参数不同,找到不同的⽅法列表,⽅法列表中保存着下⾯⽅法的结构体,结构体中包含这⽅法的实现,selector 本质就是⽅法的名称,通过该⽅法名称,即可在结构体中找到相应的实现。
Selector、Method 和 IMP 的关系可以这样描述:
在运⾏期分发消息,⽅法列表中的每⼀个实体都是⼀个⽅法(Method),它的名字叫做选择器(SEL),对应着⼀种⽅法实现(IMP)。
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;
struct objc_method {
SEL method_name; // ⽅法选择器。
char *method_types; // 存储着⽅法的参数类型和返回值类型。
IMP method_imp; // 函数指针。
}
2.2、 IMP method_getImplementation(Method m);
⽽对于第⼆种⽅法⽽⾔,传⼊的参数只有 method,区分类⽅法和实例⽅法在于封装 method 的函数
类⽅法
Method class_getClassMethod(Class cls, SEL name)
实例⽅法
Method class_getInstanceMethod(Class cls, SEL name)
最后调⽤
IMP method_getImplementation(Method m)
获取 IMP 地址
调⽤ class_getClassMethod()的第⼀个参数⽆论传⼊ objc_getClass()还是 objc_getMetaClass(),最终调⽤ method_getImplementation()都可以成功的找
到类⽅法的实现。
调⽤ class_getInstanceMethod()的第⼀个参数如果传⼊ objc_getMetaClass(),再调⽤ method_getImplementation()时⽆法找到实例⽅法的实现,可以找到类⽅法的实现。
1、@class关键字介绍
(1)概念:只是声明是一个类,但是调用不了这个类里面的方法。
(2)作用:只是定义成员变量、属性。
(3)好处:当import导入的文件里面的方法变动了,引用的地方也要
跟着改变,而且还需要重新编译一次,影响程序效率。但是使用
@class关键字声明的类就不用跟着改变,效率比较高。
2、@class和import的区别
(1)import方式会导入被引用类的所有信息,包括被引用类的变量和方法,而且运行时也会对其编译,影响效率;@class方式只是告诉编译器,@class修饰的只是类的声明,具体这个类里面有什么信息,不需要知道,等实现文件中真正要用到时,才会真正去查看这个类中的信息。
(2)使用@class方式只需要知道被引用类的名称就可以了。
(3)import方式如果引入的文件中稍有改动,都要重新编译一次,这样造成效率低,使用@class就不会出现这样的问题。
3、Category(分类)
(1)概念:对类进行扩展,只能扩展方法,不能扩展成员变量,不需要创建子类,也不需要知道其源代码,分类实现了类方法模块化,把不同方法分配到不同的分类文件中。
(2)作用:可以为已存在的类添加新的方法,跟继承不一样,可以实现把不同的方法分割到不同的类中去编写,最终加载合并到一起运行。
(3)好处:当你在定义类的时候,突然发生需求变化,但是又不想改变原来的东西,那么可以使用分类进行扩展;使用分类可以进行分工合作,提高代码编写效率。
4、Protocol :协议介绍
(1)概念:就是一系列方法的列表,其中声明的方法可以被任何类实现。
(2)用法:里面的方法可以只用部分,这点跟java的接口不一样,里面方法可以不全部实现。一个协议可以遵守多个协议,多个协议之间用逗号隔开,相当于拥有了其他协议中的方法声明,只要父类遵守了某个协议,就相当于子类也遵守了。
(3)基协议:NSObject是一个基类,最根本最基本的类,任何其他类最终都要继承它。因为NSObject协议中声明很多OC框架中的基本方法 比如description,retain,release等
-30、IM 底层协议
答: IM 底层协议答:
- XMPP (Extensible Messaging and Presence Protocol)
XMPP 是一个开放标准的通信协议,它支持消息传递、状态显示和 XML 数据的实时交换。因为它是可扩展的,XMPP 通常用于即时通讯和其他实时互联网服务。
- MQTT (Message Queuing Telemetry Transport)
MQTT 是一个轻量级的、发布/订阅消息传递协议,它专为需要最小带宽和网络开销的情况设计,如物联网(IoT)。它也适用于移动设备和其他计算能力、电池寿命或网络带宽受限的场景。
- SIP (Session Initiation Protocol)
虽然 SIP 主要用于控制多媒体通讯会话,如视频和语音通话,但它也可用于即时消息传递。它定义了建立、管理和拆除通信会话的消息。
- WebSockets
WebSockets 是通过单个 TCP 连接提供双向通信管道的协议。它非常适合网页和服务器之间的即时通讯,并且已经成为许多现代即时通讯系统的底层技术。
- HTTP/3 and WebRTC
HTTP/3 是最新的 HTTP 版本,提供了更快的连接和传输。WebRTC 专为浏览器和移动应用程序提供实时通信能力,允许直接的点对点通讯。
- Signal Protocol
Signal 协议是一个用于端到端加密的通信协议。虽然它不是一个传输协议,但在隐私和安全性敏感的即时通讯应用中广泛使用。
- RTP/RTCP (Real-time Transport Protocol/Real-time Transport Control Protocol)
RTP 和 RTCP 是为传输音频和视频数据而设计的协议。它们常常在需要实时数据传输的通讯系统中使用,尤其在视频会议和流媒体服务上。
- STOMP (Simple (or Streaming) Text Orientated Messaging Protocol)
STOMP 是一个简易的文本协议,可以工作于其他传输协议(如 TCP 或 WebSocket)之上。它被用于在客户端与消息代理之间交换数据。
- AMQP (Advanced Message Queuing Protocol)
AMQP 是一个为处理消息导向的中间件设计的协议,它支持可靠的和不可靠的消息传递,并提供了队列、路由和事务等特性。
websocket 发送⼤⽂件
webocket 握⼿成功状态码
webocket 握⼿成功过程(协议⾥,收什么消息,发什么消息)
swift ⾥的多线程实现,三个线程同时消耗⼀个队列(任务队列)
相机⾥的图⽚⽤的什么协议实现的
soctet 怎么通信的?
HTTP 协议 请求 原始⽂本协议是什么样的
协议的明⽂
SQL 类型有哪些?连表操作查询