iOS面试题
runtime
参考文档链接
- runtime介绍
- runtime消息传递
- runtime消息转发
- runtime应用
介绍
OC是一门动态语言的根本是Runtime的存在,runtime可以提供得创建类和对象、进行消息传递和转发能力。
消息机制
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程是这样的:
- 首先,通过obj的isa指针找到它的 class;
- 在 class的 method list找 foo;
- 如果 class中没到 foo,继续往它的 superclass中找 ;
- 一旦找到 foo这个函数,就去执行它的实现IMP。
但这种实现有个问题,效率低。但一个class往往只有 20%的函数会被经常调用,可能占总调用次数的 80%。每个消息都需要遍历一次objc_method_list并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class中另一个重要成员objc_cache做的事情 - 再找到foo之后,把foo的method_name作为key,method_imp作为value给存起来。当再次收到foo消息的时候,可以直接在cache里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class结构体中的。
消息转发
- 动态函数解析
- 备用接收者
- 完成消息转发:返回一个signature对象喝invovation对象
runloop的原理以及核心源代码
参考链接,文章讲解了详细的原理和源码分析。
- 目的:省电提高响应速度,节省资源,避免琐碎线程开销。
- 原理:线程(创建)–>runloop将进入–>最高优先级OB创建释放池–>runloop将睡–>最低优先级OB销毁旧池创建新池–>runloop将退出–>最低优先级OB销毁新池–>线程(销毁)
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
autoreleasepool 原理
基本工作原理:
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
- 目的:改变变量作用域,延迟释放
- 作用:降低内存使用峰值,for()临时变量的内存回收;系统级优化,区别于java中的GC,定时内存回收,iOS使用pool可以做到及时内存回收,避免像java中时“卡”时“流畅”。
核心流程
- _objc_autoreleasePoolPush和_objc_autoreleasePoolPop 都是对AutoreleasePoolPage的封装;
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
- AutoreleasePoolPage 是一个 C++ 中的类,它在 NSObject.mm 中的定义是这样的:
class AutoreleasePoolPage {
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
- magic 检查校验完整性的变量
- next 指向新加入的autorelease对象
- thread page当前所在的线程,AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
- parent 父节点 指向前一个page
- child 子节点 指向下一个page
- depth 链表的深度,节点个数
- hiwat high water mark 数据容纳的一个上限
- EMPTY_POOL_PLACEHOLDER 空池占位
- POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是 POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界
- PAGE_MAX_SIZE = 4096, 为什么是4096呢?其实就是虚拟内存每个扇区4096个字节,4K对齐的说法。
- COUNT 一个page里对象数
小结
- 自动释放池是一个个 AutoreleasePoolPage 组成的一个page是4096字节大小,每个 AutoreleasePoolPage 以双向链表连接起来形成一个自动释放池
- 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
- pop 时是传入边界对象,然后对page 中的对象发送release 的消息
KVO
- 原理:
isa swizzling
方法,通过runtime
动态创建一个中间类,继承自被监听的类。- 使原有类的
isa
指针指向这个中间类,同时重写改类的Class
方法,使得该类的Class
方法返回自己而不是isa
的指向。 - 复写中间类的对应被监听属性的
setter
方法,调用添加进来的方法,然后给当前中间类的父类也就是原类的发送setter
消息。
NSCache
- 是线程安全的,Dictionary不是线程安全
- 系统资源将要耗尽时,它可以自动删减缓存。
- 可以设置最大缓存数量。
- 可以设置最大占用内存值。
load和initialize区别和源码
具体可参考链接
-
- initialize 方法:苹果官方对这个方法有这样的一段描述:这个方法会在 第一次初始化这个类之前 被调用,我们用它来初始化静态变量。
- load 方法会在加载类的时候就被调用,也就是应用启动的时候,在调用 main 函数之前会加载所有的类,也会调用每个类的 load 方法。
- initialize 方法类似一个懒加载,如果没有使用这个类,那么系统默认不会去调用这个方法,且默认只加载一次,但存在一个initialize的实现被多次调用的情况,因为如果当前类没有实现initializie,则对当前class的initialize会转发调用super class的initialize;
- initialize 的调用发生在 +init 方法之前,创建子类的时候,子类会去调用父类的 initialize 方法。
自定义对象的占用的空间
参考链接链接
- 系统分配了16个字节给 NSObject 对象(通过 malloc_size 函数获得)
- NSObject 对象内部只使用了8个字节的空间(64bit环境下,可以通过 class_getInstanceSize 函数获得)
GCD串行队列和并行队列
串行队列
2种获取方式:一个串行队列对应只有一个线程,因此同时只能执行一个操作,先追加的操作先执行。执行很多操作的时候就好像人们排队买东西一样,先来后到。
//1.手动创建串行队列
dispatch_queue_t mySerialQueue = dispatch_queue_create("com.gcd.queueCreate.mySerialQueue", NULL);
//2.主队列是串行队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
并行队列
一个并行队列可以有多个线程,同时可以执行多个操作。在执行多个操作的时候,执行顺序会根据操作内容和系统状态发生变化。
//1.手动创建并行队列
dispatch_queue_t myConcurrentQueue = dispatch_queue_create("com.gcd.queueCreate.myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//2.全局队列是并行队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dealloc 调用时机
- 这个类被release的时候会被调用;
- 这个对象的retain count为0的时候会被调用,或者说一个对象或者类被置为nil的时候;
- 父类会在子类调用完后自动调用
1.Dealloc 调用流程
- 1.首先调用 _objc_rootDealloc()
- 2.接下来调用 rootDealloc()
- 3.这时候会判断是否可以被释放,判断的依据主要有5个,判断是否有以上五种情况
- NONPointer_ISA
- weakly_reference
- has_assoc
- has_cxx_dtor
- has_sidetable_rc
- NONPointer_ISA
- 4-1.如果有以上五中任意一种,将会调用 object_dispose()方法,做下一步的处理。
- 4-2.如果没有之前五种情况的任意一种,则可以执行释放操作,C函数的 free()。
- 5.执行完毕。
2.object_dispose() 调用流程。
- 1.直接调用 objc_destructInstance()。
- 2.之后调用 C函数的 free()。
3.objc_destructInstance() 调用流程
- 1.先判断 hasCxxDtor,如果有 C++ 的相关内容,要调用 object_cxxDestruct() ,销毁 C++ 相关的内容。
- 2.再判断 hasAssocitatedObjects,如果有的话,要调用 object_remove_associations(),销毁关联对象的一系列操作。
- 3.然后调用 clearDeallocating()。
- 4.执行完毕。
4.clearDeallocating() 调用流程。
- 1.先执行 sideTable_clearDellocating()。
- 2.再执行 weak_clear_no_lock,在这一步骤中,会将指向该对象的弱引用指针置为 nil。
- 3.接下来执行 table.refcnts.eraser(),从引用计数表中擦除该对象的引用计数。
- 4.至此为止,Dealloc 的执行流程结束。
无重复字符的最长子串
定义:字串:连续无重复的字符集
例如:一个字符串 awbcdewgh
他的子串: awbc、awbcd、awbcde …很多个子串 ,但是都是连续在一起 。
他的子序列: abc 、abcd、 abcde … 很多个子序列 ,但是子序列中的字符在字符串中不一定是连在一起的,而是删除其中若干个, 但是子序列一定是单调的(即字符之间ASCII单调递增或单调递减,相对顺序不能改变)
- (void)lengthOfLongestSubstring:(NSString *)tmpString{
NSString *str = tmpString;
NSMutableSet *set = [NSMutableSet new];
long n = str.length;
//length:长度,index:起始位置
int length = 0, i = 0, j = 0 ,index = 0;
while (i < n && j < n) {
//截取单个字符判断
if (![set containsObject:[str substringWithRange:NSMakeRange(j,1)]]) {
//添加当前字符
[set addObject:[str substringWithRange:NSMakeRange(j++, 1)]];
if (j - i > length) {
length = j - i;
index = i;
NSLog(@"lenght:%d---j:%d---i:%d",length,j,i);
}
}else{
[set removeObject:[str substringWithRange:NSMakeRange(i++, 1)]];
}
}
NSString * result = [str substringWithRange:NSMakeRange(index, length)];
NSLog(@"%@",result);
}
内存循环引用,代码检测
产生的几种情况:
- 相互持有:你中有我,我中有你,无法释放,strong–weak
- block在copy时都会对block内部用到的对象进行强引用的
- delegate:weak 修饰属性代理变量
- NSTimer:可以强制invalidate
检测内存泄露的方法
block的内存管理
Block 的内存管理分两种情况,如果是 MRC 其实可以使用 __Block,这样 Block 内部截取的变量的引用计数不会 +1,也就无需程序员动手管理了。block不能修改局部变量的值,若修改需要加__block 修饰。
参考链接
- 1.堆 Block:__NSConcreteMallocBlock ————————堆中
- 2.栈 Block:__NSConcreteStackBlock ————————栈中
- 全局 Block:__NSConcreteGlobalBlock ————————数据区域中
为什么更新UI的操作必须放在主线程
参考链接
因为UIKit框架是线程不安全的。把UIKit设计成线程安全并不会带来太多的便利,也不会提升太多的性能表现,甚至会因为加锁解锁而耗费大量的时间。事实上并发编程也没有因为UIKit是线程不安全而变得困难,我们所需要做的只是要确保UI操作在主线程进行就可以了。所有UI操作在串行队列中就可以了,非主线程异步绘制UI无法满足60fps刷新率
autolayout与frame的性能比较,原理
- Auto Layout 是针对多尺寸屏幕的设计。其本质是通过线性不等式设置UI控件的相对位置,从而适配多种iPhone/iPad 屏幕的尺寸。
- Frame 是基于 XY 坐标轴系统的布局。它从数学上限定了 UI 控件的具体位置,是 iOS 开发中最底层、最基本的界面布局机制。
- Auto Layout 的性能比 Frame 差很多。Auto Layout 的布局过程是首先求解线性不等式,然后再转化为 Frame 进行布局。其中求解的计算量非常大,通常 Auto Layout 的性能损耗是 Frame 布局的10倍左右。
参考链接
NSCopy协议:
NSCopying是一个与对象拷贝有关的协议。如果想让一个类的对象支持拷贝,就需要让该类实现NSCopying协议。NSCopying协议中的声明的方法只有一个- (id)copyWithZone:(NSZone *)zone。当我们的类实现了NSCopying协议,通过类的对象调用copy方法时,copy方法就会去调用我们实现的- (id)copyWithZone:(NSZone *)zone方法,实现拷贝功能
Masonry 布局与基础的内存管理
基于XXX
riblet
参考链接
这个方案是Uber 骑手App模块化开发的一个方案。
swift和OC比较有哪些方面的优势
- Swift代码更好写:类型推断,多返回值,全面的ARC 去掉臃肿的API实现,结构体可以实现扩展和协议
- Swift更安全
- Swift更快
- Swift开源
- Swift跨平台
swift中struct、enum和类的区别
- 枚举、结构体、类的共同点:
1,定义属性和方法;
2,下标语法访问值;
3,初始化器;
4,支持扩展增加功能;
5,可以遵循协议; - 类特有的功能:
1,继承;
2,允许类型转换;
3,析构方法释放资源;
4,引用计数; - 类是引用类型
引用类型(reference types,通常是类)被复制的时候其实复制的是一份引用,两份引用指向同一个对象。所以在修改一个实例的数据时副本的数据也被修改了(s1、s2)。 - 枚举,结构体是值类型
值类型(value types)的每一个实例都有一份属于自己的数据,在复制时修改一个实例的数据并不影响副本的数据(p1、p2)。值类型和引用类型是这三兄弟最本质的区别。
RAC中有哪些操作符
throttle的作用是什么
参考链接
iOS中有Disk I/O Throttle,Memory I/O Throttle,和Network I/O Throttle
Global Queue有哪几种优先级:Default,Low,High,BACKGROUND(I/O Throttle)
应用场景:
- 网络限流:弱网情况下,减小
- 用户连续点击button,防止误操作连续pushVC,可以设置timestamp间隔解决,也可以采用RxSwift方法实现
button.rx_tap
.throttle(0.5, MainScheduler.instance)
.subscribeNext { _ in
print("Hello World")
}
.addDisposableTo(disposeBag)
自定义TabBar的高度遇到过哪些坑
可自行添加…
childController.tabBarItem.selectedImage = [[UIImage imageNamed:selectedImage] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
离屏渲染,有哪些方式会触发离屏渲染
Off-Screen Rendering意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
触发方式:
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
- 复杂形状设置圆角等
两个子试图最近公共父视图。
这个问的其实是数据结构中的二叉树,查找一个普通二叉树中两个节点最近的公共祖先问题
- 找出所有父类
- contain/emrate 集合,
App的签名原理
async非对称加密
哈希表的底层实现原理
响应式编程,RXSwift
app编译过程
lldb
- breakpoint 设置断点定位到某一个函数
- tb 输出详细线程信息
- po 打印
- n 断点指针下一步