iOS 面试题个人解答
最近也在找工作,看到简书上某位大神给出了这份面试题,根据自己的理解整理了一份答案,不保证完全正确,是我个人的理解,如有错误欢迎指正。
这里只给出答案,面试题部分可以移步http://www.jianshu.com/p/56e40ea56813
初级
1. 运行时和多态决定了OC动态语言的特性,运行时机制将数据类型的确定由编译时推迟到了运行时,使我们可以直到运行时才去决定一个对象的类型以及调用该类型对象指定的方法;多态:不同对象以自己的方式响应同一消息的能力叫做多态,体现在代码中就是协议代理、子类重写父类方法;假设动物类有一个相同的方法-shout,狗属于动物,猫也属于动物,继承动物类后实现了自己叫的方法,它们的叫声是不一样的,也就是不同的对象以自己的方式响应了-shout,因此也可以说运行时机制是多态的基础。
2. 设计模式
MVC:Model(处理数据模型),View(处理数据显示),Controller(处理用户交互)
MVVM:Model(处理数据模型),View(处理数据显示),ViewModel(处理业务逻辑),解决MVC中Controller过于臃肿的问题
MVP:Model(处理数据模型),View(处理数据显示),Presenter(处理业务逻辑),与MVC的区别是,MVP中View并不直接访问Model,他们直接通信是通过Presenter来进行的,所有交互都发生在Presenter中,而MVC中View是可以访问Model的。
3. 代理使用weak(MRC中使用assign),避免发生循环引用造成内存泄漏,delegate控制的是UI,是上层的东西;而dataSource控制的是数据,它们本质都是回调,只是回调的对象不同(参考UITableViewDateSource & Delegate中的方法)
4. property 的本质=ivar(实例变量)+ setter(存方法)+ getter(取方法);
默认情况下的关键字:
基本数据类型(atomic,readwrite,assign)
对象(atomic,readwrite,strong)(MRC中strong改为retain)
@dynamic告诉编译器不要自动生成属性的setter & getter方法,由我们手动实现
@synthesize让编译器自动生成属性的setter & getter方法(Xcode4.5以后不需要再写,系统会自动生成)
需要注意的是,如果同时重写了某个属性的setter & getter而没有声明对应的ivar,需要手动声明,否则编译器将找不到属性的实例变量。
5. 见4
6. 一般情况下,不可变对象(NSString,NSArray,NSDictionary等)使用copy修饰,可变对象(NSMutableString,NSMutabeArray,NSMutableDictionary等)使用strong修饰;
不可变对象可以接受子类对象,也就是说NSString可以接受NSMutableString,如果使用strong修饰,当传入的string是可变对象且发生改变时,会导致属性跟着改变造成数据错乱,如果可以确定传入的数据是不可变的,可以用strong。
注意,可变对象一定要用strong修饰,因为使用copy修饰后对象是不可变的,如果这时对可变对象进行修改会导致崩溃。
7. 实现NSCopying协议,如果对象分为可变与不可变版本,需要同时实现NSCopying与NSMutableCopying协议。
8. 不可变容器类:copy=浅拷贝,mutablecopy=单层深拷贝,拷贝后的对象可变
可变容器类:copy=单层深拷贝,拷贝后对象不可变,mutablecopy=单层深拷贝,拷贝后对象可变。
单层深拷贝表示对象本身为深拷贝,内部元素仍然是浅拷贝。
9. 因为父控件的subviews数组已经对他有一个强引用,不需要再强引用一次。
10. nonatomic:非原子操作,atomic:原子操作,使用atomic会给属性的setter & getter方法加锁,防止读写的时候被另外一个线程读取造成数据错乱,atomic只保证属性的存取方法是线程安全的,并不保证整个对象都是线程安全,需要使用线程锁(这里不太了解)。
11.
(1). 覆写prepareLayout方法,并在里面事先就计算好必要的布局信息并存储起来。
(2). 基于prepareLayout方法中的布局信息,使用collectionViewContentSize方法返回UICollectionView的内容尺寸。
(3). 使用layoutAttributesForElementsInRect:方法返回指定区域cell、Supplementary View和Decoration View的布局属性。
12. 没有用过storyboard,常用的是xib加代码布局。
13. 进程有独立的地址空间,一个进程崩溃不会对其他进程产生影响,而线程是进程中不同的执行路径,线程有自己的栈和局部变量,没有单独的地址空间,一个线程死掉就等于整个进程死掉
同步,一个线程等待上一个线程执行完了再执行当前的线程
异步,多个线程同时执行任务
并行,两个或多个事件在同一时刻发生
并发,两个或多个事件在同一时间间隔发生
14. 例子:使用GCD创建一个全局并发队列异步下载图片,下载完成后回到主队列刷新UI(异步任务+并发队列->回主线程刷新UI)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//下载图片
dispatch_async(dispatch_get_main_queue(), ^{
//刷新UI
});
});
15. GCD常用函数
dispath_get_main_queue()主队列 串行
dispath_get_global_queue()全局队列 并行
dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)自定义队列 可自定义并行DISPATH_QUEUE_CONCURRENT串行DISPATH_QUEUE_SERIAL
dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>) 同步添加任务到队列
dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>) 异步添加任务到队列
dispatch_suspend(<#dispatch_object_t _Nonnull object#>)挂起队列
dispatch_resume(<#dispatch_object_t _Nonnull object#>)恢复队列
dispatch_semaphore_create(<#long value#>)创建信号量
dispatch_semaphore_wait(<#dispatch_semaphore_t _Nonnull dsema#>, <#dispatch_time_t timeout#>)等待信号量
dispatch_semaphore_signal(<#dispatch_semaphore_t _Nonnull dsema#>)发出信号量
dispatch_group_create() 创建队列组
dispatch_group_async(<#dispatch_group_t _Nonnull group#>, <#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)异步添加队列到组中
dispatch_group_notify(<#dispatch_group_t _Nonnull group#>, <#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)队列组中任务完成时回调
dispatch_barrier_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
dispatch_barrier_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)等待所有位于barrier前的函数执行完毕后执行,并在barrier函数执行完毕后执行后面的函数,只在并发自定义队列中可用(区别:dispatch_barrier_sync将自己的任务插入后需要等待自己的任务完成才会执行后面的函数,dispatch_barrier_async不需要等待自己的任务完成,插入后就可以执行后面的函数)
16. 使用信号量机制(没怎么研究过)
17. plist,NSUserDefault(本质也是plist),归档解档,sqlite(FMDB),CoreData
18. 首次运行:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions —程序启动
- (void)applicationDidBecomeActive:(UIApplication *)application —-程序进入活跃状态
首次进入后台(home):
- (void)applicationWillResignActive:(UIApplication *)application —-程序结束活跃状态
- (void)applicationDidEnterBackground:(UIApplication *)application -—程序已经进入后台
再次运行:
- (void)applicationWillEnterForeground:(UIApplication *)application -—程序将要进入前台
- (void)applicationDidBecomeActive:(UIApplication *)application —-程序进入活跃状态
再次进入后台:
- (void)applicationWillResignActive:(UIApplication *)application —-程序结束活跃状态
- (void)applicationDidEnterBackground:(UIApplication *)application -—程序已经进入后台
19. 没有用过NSCache
20. 便利初始化函数只能调用自己类中的其他初始化方法,指定初始化函数才有资格调用父类的指定初始化函数。当我们为自己创建的类添加指定初始化函数时,必须准确的识别并覆盖直接父类所有的指定初始化函数,这样才能保证整个子类的初始化过程可以覆盖到所有继承链上的成员变量得到合适的初始化。(不是很理解,没有在初始化上出过问题)
21. NSLog中(或lldb中使用po obj)输出一个对象本质就是输出[obj description]的返回值,重写该方法可以改变输出内容便于调试。
22. 引用计数,分MRC(手动引用计数)和ARC(自动引用计数),对象被强引用时引用计数+1,对象的引用计数为0时释放对象。
中级
Block
1. OC中的block可以看作一个对象,因为block中含有isa指针,这个isa指针被初始化_NSConcreateStackBlock或者NSConcreateGlobalBlock类的地址;block根据在内存中的位置分为三种:NSGlobalBlock,NSStackBlock,NSMallocBlock。block中没有用到局部变量会初始化为NSConcreateGlobalBlock,如果用到局部变量,在MRC中会初始化为NSConcreateStackBlock,ARC中会初始化为NSConcreateMallocBlock。block作为属性时使用copy修饰以保证MRC下将block拷贝到堆中,ARC下不使用copy修饰也会自动拷贝到堆中
2. block 在实现时就会对它引用到的它所在方法中定义的栈变量进行一次只读拷贝,然后在 block 块内使用该只读拷贝。因为是只读的,所以在block内无法改变变量的值。由于block捕获了自动变量的瞬时值,所以在执行block语法后,即使改写block中使用的自动变量的值也不会影响block执行时自动变量的值。使用__block修饰的变量可以在block中修改、重新赋值,使用__block修饰的对象在block内不会被强引用一次,从而不会出现循环引用的问题。至于为什么加上__block就可以做到这些不太清楚。
3. 某个类使用block作为属性,然后再block内使用了self;delegate使用strong或retain修饰(一般来讲,只要出现self->成员变量->block->self的闭环就会导致循环引用)
block反向传值:
A页面进入B页面,在B中声明一个block属性,当B页面要消失的时候调用block传值给A,A在创建B页面实例时,实现b.block接收传入的参数进行处理。
Runtime
1. 方法调用的本质就是向对象发消息,runtime中,[obj xxx]会被转化为objc_msgSend(obj,xxx),这个函数做了如下几件事情:
根据对象的isa指针找到对象所属的类,去对应的类中寻找方法;
先找缓存,找不到再去找方法列表;
如果仍然找不到,就去父类中寻找方法,如此向上传递;
如果在最顶层的父类中还是找不到,程序会崩溃并抛出unrecognized selector异常。
2. 接1,不过在这之前,runtime会给出三次拯救程序崩溃的机会(Method resolution提供函数实现,Fast forwarding快速转发,转发给其他对象,Normal forwarding一般转发,转发给NSInvocation对象)
3. 不能向编译后得到的类添加实例变量,可以向运行时创建的类中添加实例变量;原理不明
4. runtime对注册的类,会进行布局,将 weak 对象放入一个 hash 表中。用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。
5. 不知道
RunLoop
1. runloop是用来管理线程的,它是一个事件处理循环,用来不停的调配工作以及处理输入事件;当线程的runloop开启后,线程就会在执行完任务后处于休眠状态,随时等待接受新的任务,而不是退出。每个线程都有一个runloop对象,主线程的runloop是默认开启的,子线程的runloop需要手动创建并开启。
2. runloop的mode用来指定事件在运行循环中的优先级,分为以下几种
公开提供:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
非公开:
UITrackingRunLoopMode:ScrollView滑动时
UIInitializationRunLoopMode:启动时
3. RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响scrllView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。
4. 不知道,印象中没有手动调用过@autoreleasepool。(这个题放在这里肯定是与runloop有关系的)
类结构
1. 对象的isa指针指向它的类对象,类对象中存放对象方法列表,成员变量列表和属性列表,根据这个指针可以知道将来调用哪个方法
类对象的isa指针指向它的元类(meta class),即类对象是元类的实例,元类内部存放类方法列表
元类的isa指针指向根元类,根元类的isa指针指向自己。
2. 类方法(+开头的方法)属于类对象,只能通过类对象调用,self是类对象,类方法中可以调用其他类方法,不能访问成员变量,不能直接调用实例方法。
实例方法(-开头的方法)属于实例对象,只能通过实例对象调用,self是实例对象,实例方法中可以访问成员变量,可以调用其他实例方法,可以调用类方法(通过类名)。
3. Category可以在不知道某个类的源码情况下,向类中添加扩展方法,catrgory的实现原理不明,至于覆盖原有方法,不是很推荐在category中重写原有方法,因为CocoaFramework有很多是用Category实现的,重写之后,会导致在Runtime的时候,只有一个方法会被执行,而哪个会被执行无法确定。category的诞生只是为了让开发者更加方便的去拓展一个类,它的初衷并不是让你去改变一个类。
4. runtime可以动态添加成员变量和属性,一般只有在为系统类添加属性时会用到,如果是自定义类的话直接添加属性即可,添加方法为,创建对应系统类的category,声明一个属性,实现settet & getter方法,使用runtime函数objc_setAssociatedObject(),objc_getAssociatedObject()为该类关联某个对象。
5. objc中向nil发送消息是完全有效的,只是在运行时不会有任何作用;
如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)
如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*)
float,double,long double 或者long long的整型标量,发送给nil的消息将返回0
如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0
高级
1. 行高计算放在ViewModel中,保证只计算一次,代理方法中直接获取已经计算好的行高;
绘图部分放在子线程中处理,绘制完成后回到主线程刷新UI(GCD);
减少层级,hide,不太明白什么意思;
离屏渲染,这个最常见的就是设置圆角图片,最近研究了一下关于圆角的各种实现方式,个人观点:所有针对UIView的layer所做的圆角处理都会导致离屏渲染,所有针对UIImage所做的圆角处理会导致混合图层。
离屏渲染影响滑动流畅度的因素和设置圆角数量有关,与大小无关,因此有了下面的测试
以下是我对各种圆角方法测试的结果(测试环境为iOS10.3,iPhone6s,同屏圆角图片数量在30个以上):
直接设置cornerRadius,最原始的方法,网上说这种方法会影响性能,但是根据测试结果来看平均fps在50+;
利用UIBezierPath+CAShapeLayer设置layer.mask,测试结果平均fps只有20+;
以上两种使用instruments检测都触发了离屏渲染
对UIImage进行裁剪,测试结果平均fps在45~55左右;
使用圆形镂空图片做遮罩,测试结果平均fps和上面的差不多;
对图片本身做圆角,不进行任何处理,测试结果平均fps在55+;
以上几种使用instruments检测都触发了混合图层;
由于没有其他设备,无法对低版本的系统及设备进行测试。
令人意外的是使用最简单的那种方式测试出来的效果是最好的,很多人都说这种方式对性能影响很大,但是结果并非如此,个人认为对UIkit的性能优化要以instrument的测试数据为准,不要盲目听信网上的观点。
测试demo可以在我的github中找到(https://github.com/JiYuwei/test)
2. 可以做的事情太多了,替换两个方法的实现,动态添加类,动态添加方法,成员变量,属性等,但是实际开发中直接用到runtime的地方很少。
3 4. SDWebImage用到了内存和磁盘双缓存,sd_setImageWithURL方法调用时会先从内存中查询图片缓存,如果找不到就会从磁盘中查找缓存,如果找到了会把图片再次设置到内存缓存中以提升效率,缓存查询成功就直接返回缓存数据,查询失败则发起网络请求,请求成功后会返回图片数据并写入缓存。
5. AFNetworking内部创建了一个单例线程。这个线程将会常驻内存,用来处理AFN发起的所有请求任务。当然,线程也跟随着一个runloop,AFN将这个 runloop的模式设置为NSDefaultRunLoopMode,不会在这个线程处理connection完成后的UI刷新等工作,而是会将数据抛给主线程,让主线程去完成UI的刷新。
至于为什么要这么做没有去想过。
6. KVO的使用前提是对象用KVC来赋值,使用方法向需要监听的对象添加观察者,然后在代理中实现监听回调。
原理:KVO是基于runtime实现的。某个类的属性对象第一次被观察时,runtime会动态创建该类的一个派生类,在派生类中重写被观察属性的setter方法实现通知机制,系统还会偷偷将对象isa指针指向该派生类,从而给被监听属性执行的是派生类的setter方法。
7. KVC就是间接通过字符串类型的key取出对应属性的值,原理:(记不清了,可能有误)
setValue: forKey:
拿到key之后,先去查找set方法,如果找到就使用set方法赋值,
如果找不到,就去寻找属性,如果有,直接对属性赋值,
如果找不到,就去寻找成员变量,如果有,直接对成员变量赋值,
如果找不到,就会调用setValue: forUndefinedKey 。
valueForKey:原理类似
可以访问私有属性,可以访问成员变量
答案暂时先整理到这里,其中不太清楚的将来了解了会不定期更新。
另:上海地区如果有工作机会的话欢迎介绍,e-mail:jishuzhong@163.com