第一关:笔试题
A1. 打印结果
main() {
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf(“%d,%d”,*(a+1),*(ptr-1));
}
答案:2,5
解析:
这里数组a初始化为{1,2,3,4,5},ptr是一个指向int *类型的指针,其初始值为(int *)(&a+1)。实际上,&a+1不是首地址+1,系统会认为加数组a的偏移,是偏移了一个数组的大小(本例是5个int)
a+1是指针+1,就指向了数组a的第二个元素,而当于ptr当前所指向的值。所以,*(a+1)的值就是2。
ptr-1是二维指针-1操作,因此指向了数组a最后一个元素的地址,所以*(ptr-1)的值为数组第一个元素值5。
A2. 输出结果
void Func (char str[100]) {
sizeof(str ) = ?
}
void *p = malloc(100); sizeof(p) = ?
答案:4
解析:
这道题很常见,Func(char str[100])函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。32位平台下,指针的长度(占用内存的大小)为4字节,所以sizeof(str) 和sizeof(p) 都为4。
A3. 用预处理指令#define声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
答案:#define SECONDSINA_YEAR (60 * 60 * 24 * 365)UL
解析:
其实这道题主要是考查是否够细心,因此类型为int的话,会overflow的。因此这里在后面添加了UL,表示无符号长整形。#define不能以分号结束,而且这里还要注意括号。
A4. 写”标准”宏MIN ,这个宏输入两个参数并返回较小的一个
答案:#define MIN(A,B) ((A) <= (B) ? (A) : (B))
解析:
这里也是考查是否够细心的细节。一定要注意,宏只是单纯的展开,因此一定要注意展开后是否正确。所以无论何时,都加上括号以明确其意途是很好的。
这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
A5. 数组和指针的区别
答案:
数组可以申请在栈区和数据区;指针可以指向任意类型的内存块
sizeof作用于数组时,得到的是数组所占的内存大小;作用于指针时,得到的都是4个字节的大小
数组名表示数组首地址,是常量指针,不可修改指向。比如不可以将++作用于数组名上;普通指针的值可以改变,比如可将++作用于指针上
用字符串初始化字符数组是将字符串的内容拷贝到字符数组中;用字符串初始化字符指针是将字符串的首地址赋给指针,也就是指针指向了该数组
A6. static的作用
函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值
在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问
在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝
在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
A7. 简述内存分区情况
代码区:存放函数二进制代码
数据区:系统运行时申请内存并初始化,系统退出时由系统释放,存放全局变量、静态变量、常量
堆区:通过malloc等函数或new等操作符动态申请得到,需程序员手动申请和释放
栈区:函数模块内申请,函数结束时由系统自动释放,存放局部变量、函数参数
A8. #include <filename>和#include “filename”有什么区别?
答案:
#include <filename>直接在库文件目录中搜索所包含的文件而#include “filename”在当前目录下搜索所包含的文件,如果没有的话再到库文件目录搜索。
A9. 下面四个修饰指针有什么区别?
const char *p;
char const *p;
char * const p;
const char * const p;
参考答案:
const char *p定义了一个指向不可变的字符串的字符指针,可以这么看:const char *为类型,p是变量。
char const *p与上一个是一样的。
char * const p定义了一个指向字符串的指针,该指针值不可改变,即不可改变指向。这么看:char *是类型,const是修饰变量p,也就是说p是一个常量
const char * const p定义了一个指向不可变的字符串的字符指针,且该指针也不可改变指向。这一个就很容易看出来了。两个const分别修饰,因此都是不可变的。
A10. 用过哪些排序算法?
冒泡排序、插入排序。在开发中最常用的就是冒泡排序和插入排序了,不用说那么多高深算法,在平常的工作中,若非BAT,也没有这么严格要求什么多高的效率。能掌握这两种最常用的就基本可以了,搞App开发,若非大数据,并没有什么太高的要求。
冒泡排序: 双重循环就可以实现,在工作中常应用于模型排序。
void bubbleSort(int[] unsorted)
{
for (int i = 0; i < unsorted.Length; i++)
{
for (int j = i; j < unsorted.Length; j++)
{
if (unsorted[i] > unsorted[j]) {
int temp = unsorted[i];
unsorted[i] = unsorted[j];
unsorted[j] = temp;
}
}
}
}
插入排序:
void insertSort(int *a,int n)
{
int i,j,key;
// 控制需要插入的元素
for (i = 1; i < n; i++)
{
// key为要插入的元素
key = a[i];
// 查找要插入的位置,循环结束,则找到插入位置
for (j = i; j > 0 && a[j-1] > key; j–)
{
// 移动元素的位置,供要插入元素使用
a[j] = a[j-1];
}
// 插入需要插入的元素
a[j] = key;
}
}
A11.堆与栈的区别
参考答案:
栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放。 栈空间是有限的,而堆是很大的自由存储区。
C中的malloc函数分配的内存空间是在堆上的,C++中对应的是new操作符。 程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也在栈上。
A12. 链表
链表是非常爱问的。这里不写出如何创建链表,如何合并链表的代码了,只是会给出思路。
如何合并两张有序的链表?回答时不用说得那么详细,一般来说,面试官也不会记得那么清楚,简单说到点上就可以了。既然两张表都是有序的,那么就可以先找到哪个作为主链。然后将副链每个元素按照顺序接入到主链上就可以了。大家可以百度找一些文章阅读,以了解更深一些。
A以上来源《iOS的C/C++笔试题集锦》 http://www.henishuo.com/ios-c-cpp-interview/
B1. #import与#include的区别
参考答案:
#import是Objective-C导入头文件的语法,可保证不会重复导入。
#include是C/C++导入头文件的语法,如果是Objective-C与C/C++混编码,对于C/C++类型的文件,还是使用#include来引入,这种写法需要添加防重复导入的语法。
B2. @class的作用
@class一般用于头文件中通过前向声明,就可以声明了,但是在.m文件中还是需要使用#import进来的。它的作用只是前向声明。
B3. 用NSLog函数输出一个浮点类型,结果四舍五入,并保留一位小数
参考答案:
float money = 1.011;
NSLog(@”%.1f”, money);
B4. property属性的修饰符有什么样的作用
参考答案:
property是属性访问声明,扩号内支持以下几个属性:
getter=getName、setter=setName:设置setter与getter的方法名
readwrite、readonly:设置可供访问级别
assign:方法直接赋值,不进行任何retain操作,为了解决原类型与环循引用问题
retain:其setter方法对参数进行release旧值再retain新值,所有实现都是这个顺序
copy:其setter方法进行copy操作,与retain处理流程一样,先对旧值release,再copy出新的对象,retainCount为1。这是为了减少对上下文的依赖而引入的机制。
nonatomic:非原子性访问,不加同步, 多线程并发访问会提高性能。注意,如果不加此属性,则默认是两个访问方法都为原子型事务访问。
B5. self.name=@object和name=@object有什么不同?
参考答案:
self.name =”object”:会调用对象的setName()方法;name = “object”:会直接把”object”字符串赋值给当前对象的name属性。
B6. viewDidLoad、loadView和viewDidUnload何时调用
参考答案:
viewDidLoad在view加载完成时调用,loadView在controller的view为nil时调用。对于viewDidUnload现在已经不能直接调用了。
B7. objective-c中的可变与不可变词典
参考答案:
可变字典就是可以增、删、改操作的字典,对应于NSMutableDictionary类型。
不可变字典就是不能执行增、删、改操作的字典,对应于NSDictionary类型。
B8. Objective-C的内存管理
参考答案:
现在内存管理几乎都采用ARC,也就是Automatic Reference Counting,意思是自动引用计数。由编译器在编译时自动为添加retain、release等代码。
如果问的MRC,也就是Manual Reference Counting,意思是手动内存管理。
黄金法则:谁使对象的引用计数+1,不再使用该对象时,谁就应该使该对象的引用计数-1。
B9. 自动生成getter/setter方法
参考答案: 对于以前的代码,那时还没有property,使用这样的方法来创建:
– (void)setName:(NSString *)aName;
– (NSString *)name;
在后面有了property,直接使用@property (nonatomic, copy) NSString *name这样的方法来声明,编译器会自动生成getter/setter方法并生成一个_name成员变量。
B10. 什么是MVC
参考答案:
我相信大部分人在被问到这个问题时,都会回答M就是Model,V就是View,C就是Controller。这都是停留在概念上的回答,明显没有什么工作经验。对于一个对框架和架构有一定的思想的人,回答时会从项目的耦合度、团队开发如何减少冲突、如何降低团队与团队之间的沟通成本、如何将M、V、C之间按照既定的标准建立沟通的桥梁。
Model用于处理数据,通常来说,Model中会包含多个字段,用于存储数据。但是,Model还会有一部分逻辑,比如说:
@interface TestModel: HYBBaseModel
// 这个是接口返回的字段,1表示XXX,2表示YYY,3表示ZZZ
@property (nonatomic, assign) NSUInteger type;
// 这个不是接口返回的字段,但是由于`type`字段是一个数值,不是`view`需要显示的数据
// 所以我们最好将逻辑统一放到这里来,外部只管获取最终显示需要的值即可。即使哪天接口
// 返回的字段变化或者增加什么新的值,只需要处理这个模型内部就好了。
@property (nonatomic, copy, readonly) NSString relationship;
@end
对于View,不应该包含逻辑,应该根据模型直接获取数据。
对于Controller,大部分交互逻辑都集中到了这里,所有View需要的数据,都是通过Controller提取Model然后交给view去显示数据。
B11. 重写getter/setter方法
假设声明属性:@property (nonatomic, copy) NSString *blogName;
重写这个属性的getter/setter方法:
参考答案:
这里一旦连getter方法也重写,编译器不会给我们自动生成成员变量_blogName,因此我们需要在类的声明中添加一个成员变量_blogName:
@interface Demo () {
NSString *_blogName;
}
@end
在自动内存管理下(ARC):
– (void)setBlogName:(NSString *)aName {
if (_blogName != aName) {
_blogName = nil;
_blogName = [aName copy];
}
}
– (NSString *)blogName {
return _blogName;
}
对于手动内存管理(MRC):
– (void)setBlogName:(NSString *)aName {
if (_blogName != aName) {
[_blogName release];
_blogName = nil;
_blogName = [aName copy];
}
}
– (NSString *)blogName {
return _blogName;
}
B12. obj在编译时和运行时分别是什么类型的对象
如下面的代码,obj在编译时和运行时分别时什么类型的对象:
NSString *obj = [[NSData alloc] init];
参考答案:
在编译时,我们所声明的obj是NSString *类型,因此是NSString类型对象。在运行时,由于指针obj所指向的是NSData类型对象的内存,因此实际上是NSData类型的对象。在编译时,这一行代码会转换成类似这样:
NSString *obj = ((id (*)(id, SEL))objc_msgSend)([NSData class], @selector(alloc));
obj = ((id (*)(id, SEL))objc_msgSend)((id)obj, @selector(init));
由于在编译时,转换成id,因此可以用NSString *指向NSData对象,而id是具备运行时特性的,因此在链接时,通过id的isa指针可以找到其所属的类,因此最终类型还是通过isa确定其所属类型。
B13. id声明的对象有什么特性?
id类型可以指向任何类型的对象。
B14. iOS设备性能测试
在实际开发中,我们经常需要对应用瘦身,因此对性能的检测是很重要的。
参考答案:
使用Profile-> Instruments ->Time Profiler可以检测性能。
B15. Objective-C中有私有方法、私有变量么?
参考答案:
在类的.m实现文件内声明,就可以作为私有方法、私有变量。但是,并不是绝对的私有,如果外部知道有这么个方法,一样可以调用,而且不会报错。就像苹果公司没有公开出来的API,只要我们通过其它方式了解到api就可以调用。于是苹果审核时经常由于使用了私有api而打回来了。
B16. 简述tableview的重用机制
参考答案:
[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
这个方法就是重用机制的核心了。比如,有一个界面可显示10个cell,那么创建10次cell,并给cell指定同样的重用标识(当然,可以为不同显示类型的cell指定不同的标识)并且10个cell将全部都加入到visiableCells数组,reusableTableCells为空.
滚动tableView,当有一个cell完全移出屏幕时,这个cell就会被加入到reusableTableCells。而新出现的那个cell将加入到visiableCells,而这个cell就是被重用的。
如果要让tableview不重用,不设置reuseIdentifier就可以了。
B17. nil、Nil、NULL和NSNull区别
参考答案:
nil:
nil和C语言的NULL相同,在objc/objc.h中定义。nil表示Objective-C对象的值为空。在C语言中,指针的空值用NULL表示。在Objective-C中,nil对象调用任何方法表示什么也不执行,也不会崩溃。
Nil:
那么对于我们Objective-C开发来说,Nil也就代表((void *)0)。
但是它是用于代表空类的. 比如:Class myClass = Nil;
NULL:
在C语言中,NULL是无类型的,只是一个宏,它代表空. 这就是在C/C++中的空指针。对于我们Objective-C开发来说,NULL就表示((void*)0).
NSNull:
NSNull是继承于NSObject的类型。它是很特殊的类,它表示是空,什么也不存储,但是它却是对象,只是一个占位对象。
使用场景就不一样了,比如说服务端接口中让我们在值为空时,传空。
NSDictionry *parameters = @{@”arg1″ : @”value1″,
@”arg2″ : arg2.isEmpty ? [NSNull null] : arg2};
区别:
NULL、nil、Nil这三者对于Objective-C中值是一样的,都是(void *)0,那么为什么要区分呢?又与NSNull之间有什么区别:
NULL是宏,是对于C语言指针而使用的,表示空指针
nil是宏,是对于Objective-C中的对象而使用的,表示对象为空
Nil是宏,是对于Objective-C中的类而使用的,表示类指向空
NSNull是类类型,是用于表示空的占位对象,与JS或者服务端的null类似的含意
B18. Category是什么,何时使用?
参考答案:
Category就是所谓的扩展。
有时我们需要在一个已经定义好的类中增加一些方法,而不想去重写该类,这时候使用扩展就很好。比如,当工程已经很大,代码量比较多,或者类中已经有很多方法,已经有其他代码调用了该类创建对象并使用该类的方法时,可以使用类别对该类扩充新的方法。
B19. 什么是Delegate?常用场景?
参考答案:
Delegate就是所谓的代理,代理是一种设计模式。在iOS开发中,会使用到大量的代理,而代理设计模式是苹果中的标准设置模式。
常用场景有反向传值。比如:苹果的蓝牙,我们进入到下一个界面去打开或者关闭蓝牙,当操作之后需要将状态反馈到前一个界面,并更新显示。对于这种状态,使用代理设计模式是很标准的模式。
B20. 什么是单例,如何设计单例?
参考答案:
单例就是全局都只有一个对象存在,而且是在整个App运行过程中都存在。每个App都会有单例,比如UIApplication。而我们在做用户数据存储时,通常都会用单例存储,因为应用在所有操作中,经常要求先登录。
下面这种写法是最常用的写法,这个是线程安全的。
+ (instancetype)shared {
static HYBUserManager *sg_userManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (sg_userManager == nil) {
sg_userManager = [[HYBUserManager alloc] init];
}
});
return sg_userManager;
}
B21. 什么是通知?
在iOS中,通知是非常常用的设计模式。它是多对多的关系。
1)获取通知中心对象
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
2)通知对象主要属性
@property (readonly, copy) NSString *name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
其中,第一个就是通知的名称,第二个是通知发布者,而第三个就是一些额外的信息。比如第三个可以存储一些数据,这样接收者就可以接收到这些数据了。
3)通知对象初始化方法
+ (instancetype)notificationWithName:(NSString *)aName
object:(nullable id)anObject
userInfo:(nullable NSDictionary *)aUserInfo;
4)发布通知
通知的发布,需要通过通知中心NSNotificationCenter来发布。
– (void)postNotificationName:(NSString *)aName
object:(nullable id)anObject
userInfo:(nullable NSDictionary *)aUserInfo;
5)注册通知
要想能够接收到通知,就得提前注册通知到通知中心,否则不会接收到。
– (void)addObserver:(id)observer
selector:(SEL)aSelector
name:(nullable NSString *)aName
object:(nullable id)anObject;
6)取消注册通知
通知注册了,在不需要时还需要移除注册。通知中心不会保留监听器对象, 在通知中心注册过的对象,必须在该对象释放前取消注册. 否则,当相应的通知再次出现时, 通知中心仍然会向该监听器发送消息. 因为, 相应的监听器对象已经被释放了, 所以可能会导致应用崩溃,这种崩溃很常见。
苹果提供了两个方法,可以移除注册:
– (void)removeObserver:(id)observer;
– (void)removeObserver:(id)observer
name:(nullable NSString *)aName
object:(nullable id)anObject;
通常,我们会在控制器的dealloc之前,先取消注册通知:
– (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
如果我们只是想取消某一个通知的注册,而不是全部,那么可以使用第二个方法,将通知名称传过去,就会只取消注册该通知。
B以上来源《iOS基础笔试题集锦一》 http://www.henishuo.com/objective-c-base-interview/
C1. 即时聊天App不会采用的网络传输方式
A UDP
B TCP
C HTTP
D FTP
参考答案:D
理由:FTP是文件传输协议,是File Transfer Protocol的简称,它的作用是用于控制互联网上文件的双向传输,因此一定不会是即时聊天使用的;UDP是面向无连接的传输层协议,数据传输是不可靠的,它只管发,不管收不收得到;TCP是面向连接的,可靠的传输层协议;HTTP是超文本传输协议,对应于应用层,而HTTP是基于TCP的。
C2. 下列技术不属于多线程的是
A Block
B NSThread
C NSOperation
D GCD
参考答案:A
理由:苹果提供了NSThread、NSOperation、GCD这三种技术用于处理多线程,对于NSThread是需要自己管理其生命周期的;对于NSOpeartion也是常用的技术之一,通常与NSOperationQueue一起配合使用;对于GCD是平时见到最多的,使用起来很方便,而它与block配合起来使用,简单而且简洁。Block不是一项技术,只是代码段,但是具备自动捕获上下文信息的功能,与函数指针有点类似,其实全局函数就是特殊的block。
C3. 线程和进程的区别不正确的是
A 进程和线程都是由操作系统所提供的程序运行的基本单元
B 线程之间有单独的地址空间
C 进程和线程的主要差别在于它们是不同的操作系统资源管理方式
D 线程有自己的堆栈和局部变量
参考答案:D
理由:这是学习操作系统知识的时候经常会考试的内容,但是在工作中经常会遇到多线程处理问题。通常来说,一个进程就代表着一个应用程序,而操作系统为了更好的利用资源,提供了线程用于处理并发。每个线程都有单独的地址空间,处理完成之后还得回到主线程。进程和线程都是操作系统的基本单元,只是分工不同,是两种不同的资源管理方式。线程所需要的资源都来自于进程,它没有自己独立的资源,也就没有自己的堆栈和局部变量。
C4. 堆和栈的区别正确的是
A 对于栈来讲,我们需要手工控制,容易产生memory leak
B 对于堆来说,释放工作由编译器自动管理,无需我们手工控制
C 在Windows下,栈是向高地址扩展的数据结构,是连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的
D 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低
参考答案:D
理由:栈是由编译器管理的,不是我们手动控制,但是栈所能分配的内存是比较少的,如果要处理大数据,则需要在堆上分配,因此在栈上比较容易出现Memory Leak;对于堆,需要我们自己申请内存,同时也需要我们自己手动释放,否则会造成内存泄露;对于堆,如果过多地申请内存空间,会导致内存空间不连接,从而造成内存碎片,使程序效率降低。
C5. 下列回调机制的理解不正确的是
A target-action:当两个对象之间有⽐较紧密的关系时,如视图控制器与其下的某个视图。
B delegate:当某个对象收到多个事件,并要求同一个对象来处理所有事件时。委托机制必须依赖于某个协议定义的⽅法来发送消息。
C NSNotification:当需要多个对象或两个无关对象处理同一个事件时。
D Block:适⽤于回调只发⽣生一次的简单任务。
参考答案:B
理由:对于Target-Action机制,要求两个对象之间有比较紧密的联系,比如在控制器与cell之间,可通过设置target为控制器对象,而action则为控制器中的某个回调方法;对于Delegator机制,它是苹果提供的标准回调机制,通常会提供一个标准的协议,然后由代理类遵守协议,最常用的用法是反向传值,比如打开蓝牙后要反馈给前一个界面蓝牙的开关状态;对于通知,通常是多对多的关系,它并不关心是谁要处理消息,任意对象都可以注册通知到通知中心,当发送通知时,所有注册了该通知的对象都可以收到消息,最常用的场景是跨模块,比如登录模块与其它模块有着非常紧密的联系,但是登录成功后各个地方可能需要做一些处理,因此通常会在登录成功或者登出成功后发送通知,以便各个需要处理的模块得到正确的处理;对于Block是相当简单的,它只适用于一对一的关系,比如在做某个操作成功或者失败后回调。
C6. 对于runloop的理解不正确的是
A 每一个线程都有其对应的RunLoop
B 默认非主线程的RunLoop是没有运行的
C 在一个单独的线程中没有必要去启用RunLoop
D 可以将NSTimer添加到runloop中
参考答案:C
理由:说到RunLoop,它可是多线程的法定。通常来说,一个线程一次只能执行一个任务,执行完任务后就会退出线程。但是,对于主线程是不能退出的,因此我们需要让主线程即时任务执行完毕,也可以继续等待是接收事件而不退出,那么RunLoop就是关键法宝了。但是非主线程通常来说就是为了执行某一任务的,执行完毕就需要归还资源,因此默认是不运行RunLoop的。NSRunLoop提供了一个添加NSTimer的方法,这个方法是在应用正常状态下会回调。
C7. 断点续传需要在请求头中添加的控制续传最重要的关键字
A range
B length
C type
D size
参考答案:A
理由:对于实现文件的断点续传和断点下载,需要设置请求头中的Range实体头,指定第一个字节的位置和最后一个字节的位置,而与之对应的响应头有Content-Range,指示了整个实体的长度及部分插入位置,如Content-Range: bytes 0-500/801,指定了范围为当前范围与文件总大小。
C8. MVC优点不正确的是
A 低耦合性
B 高重用性和可适用性
C 较低的生命周期成本
D 代码高效率
参考答案:D
理由:MVC只是一种设计模式,它的出现有比较久的历史了。Model-Controller-View是在开发中最常见到的设计模式,通过将Model、View、Controller三者相互联系,以Model作为数据加工厂,以Controller作为桥梁,处理业务,而View只是数据展示层,理应与业务无关。MVC设计模式降低了耦合性,提供了重用性和适用性,可有效地提高开发效率。
C9. 混编ObjC和C++的源码文件需要将文件格式的后缀改为
A .c
B .cpp
C .mm
D .m
参考答案:C
理由:ObjC要想与C++源代码文件混编,那么就需要将文件后缀改为.mm。这个没有什么可细说了,记住就好了!
C10. ObjC声明一个类所要用到的编译指令是
A @interface SomeClass
B @protocol SomeClass
C @implementation SomeClass
D @autorelease SomeClass
参考答案:A
理由:A是声明类的指令;B是声明协议的指令;C是实现类的定义的指令;D是声明自动释放池的指令。
C11. MRC文件在ARC工程混合编译时,需要在文件的Compiler Flags上添加什么参数
A -shared
B -fno-objc-arc
C -fobjc-arc
D -dynamic
参考答案:B
理由:对于ARC工程中,如果要混编MRC文件,需要在工程的Compiler Flags添加-fno-bojc-arc;对于MRC工程中,如果要混编ARC文件,需要设置为-fobjc-arc。
C12. 下面关于Objective-C内存管理的描述错误的是
A 当使用ARC来管理内存时,代码中不可以出现autorelease
B autoreleasepool 在 drain 的时候会释放在其中分配的对象
C 当使用ARC来管理内存时,在线程中大量分配对象而不用autoreleasepool则可能会造成内存泄露
D 在使用ARC的项目中不能使用NSZone
参考答案:A
理由:ARC只是在大多时候编译自动为我们添加上内存管理的代码,只是我们的源代码看不到而已,但是在编译时,编译器会添加上相关内存管理代码。对于自动释放池,在drain时会将自动释放池中的所有对象的引用计数减一,若引用计数为0,则会自动释放掉其内存。如果在线程中需要大量分配内存,我们理应添加上自动释放池,以防内存泄露。比如在for循环中要分配大量的内存处理数据,那么我们应该在for循环内添加自动释放池,在每个循环后就将内存释放掉,防止内存泄露。在ARC项目中,自然不能手动使用NSZone,也不能调用父类的dealloc。
C13. 下面哪个不属于对象数据序列化方法
A JSON
B Property List
C XML
D HTTP
参考答案:D
理由:数据序列化是将对象的数据转化成某一种格式的数据,在ios开发中最常用的就是JSON,部分公司会采用XML,笔者从未遇到过使用Property List来传输数据的,但是我们通过会将一些小量的数据存储到Property List中,比如NSUserDefaults就是操作plist文件的。而HTTP是超文件传输协议,只是一种协议。
C14. 在UIKit中,frame与bounds的区别是
A. frame 是 bounds 的别名
B. frame 是 bounds 的继承类
C. frame 的参考系是父视图坐标,bounds 的参考系是自身的坐标
D. frame 的参考系是自身坐标,bounds 的参考系是父视图的坐标
参考答案:C
理由:frame的参考系是父视图的坐标系,而bounds是参考自我的坐标系,bounds的原点是(0,0)点(就是view本身的坐标系统,默认永远都是0,0点,除非认为setbounds),而frame的原点却是任意的(相对于父视图中的坐标位置)。
C15. 下面关于线程管理错误的是
A. GCD所用的开销要比NSThread大
B. 可以在子线程中修改UI元素
C. NSOperationQueue是比NSthread更高层的封装
D. GCD可以根据不同优先级分配线程
参考答案:B
理由:首先,UI元素的更新必须在主线程。GCD与Block配合使用,block需要自动捕获上下文变量信息等,因此需要更多的资源,故比NSThread开销要大一些。NSOperationQueue与NSOperation配合使用,比NSThread更易于操作线程。GCD提供了多个优先级,我们可以根据设置优先级,让其自动为我们分配线程。
C以上来源《iOS基础笔试题集锦二》 http://www.henishuo.com/objc-interview-two/
D1. 对数组中的元素去重复
例如:NSArray *array = @[@”12-11″, @”12-11″, @”12-11″, @”12-12″, @”12-13″, @”12-14″];
第一种方法:
开辟新的内存空间,然后判断是否存在,若不存在则添加到数组中,得到最终结果的顺序不发生变化。效率分析:时间复杂度为O ( n2 ):
NSMutableArray *resultArray = [[NSMutableArray alloc] initWithCapacity:array.count];
// 外层一个循环
for (NSString *item in array) {
// 调用-containsObject:本质也是要循环去判断,因此本质上是双层遍历
// 时间复杂度为O ( n^2 )而不是O (n)
if (![resultArray containsObject:item]) {
[resultArray addObject:item];
}
}
NSLog(@”resultArray: %@”, resultArray);
补充:原来集合操作可以通过valueForKeyPath来实现的,去重可以一行代码实现:
array = [array valueForKeyPath:@”@distinctUnionOfObjects.self”];
NSLog(@”%@”, array);
但是返回的结果是无序的,与原来的顺序不同。
第二种方法:
利用NSDictionary去重,字典在设置key-value时,若已存在则更新值,若不存在则插入值,然后获取allValues。若不要求有序,则可以采用此种方法。若要求有序,还得进行排序。效率分析:只需要一个循环就可以完成放入字典,若不要求有序,时间复杂度为O(n)。若要求排序,则效率与排序算法有关:
NSMutableDictionary *resultDict = [[NSMutableDictionary alloc] initWithCapacity:array.count];
for (NSString *item in array) {
[resultDict setObject:item forKey:item];
}
NSArray *resultArray = resultDict.allValues;
NSLog(@”%@”, resultArray);
如果需要按照原来的升序排序,可以这样:
resultArray = [resultArray sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
NSString *item1 = obj1;
NSString *item2 = obj2;
return [item1 compare:item2 options:NSLiteralSearch];
}];
NSLog(@”%@”, resultArray);
第三种方法:
利用集合NSSet的特性(确定性、无序性、互异性),放入集合就自动去重了。但是它与字典拥有同样的无序性,所得结果顺序不再与原来一样。如果不要求有序,使用此方法与字典的效率应该是差不多的。效率分析:时间复杂度为O (n):
NSSet *set = [NSSet setWithArray:array];
NSArray *resultArray = [set allObjects];
NSLog(@”%@”, resultArray);
如果要求有序,那就得排序,比如这里要升序排序:
resultArray = [resultArray sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
NSString *item1 = obj1;
NSString *item2 = obj2;
return [item1 compare:item2 options:NSLiteralSearch];
}];
NSLog(@”%@”, resultArray);
补充:一直没有使用过有序集合,网友们反馈到可以直接使用有序集合,感谢大家:
NSOrderedSet *set = [NSOrderedSet orderedSetWithArray:array];
NSLog(@”%@”, set.array);
D2. NSArray、NSSet、NSDictionary与NSMutableArray、NSMutableSet、NSMutableDictionary的特性和作用
特性:
NSArray表示不可变数组,是有序元素集,只能存储对象类型,可通过索引直接访问元素,而且元素类型可以不一样,但是不能进行增、删、改操作;NSMutableArray是可变数组,能进行增、删、改操作。通过索引查询值很快,但是插入、删除等效率很低。
NSSet表示不可变集合,具有确定性、互异性、无序性的特点,只能访问而不能修改集合;NSMutableSet表示可变集合,可以对集合进行增、删、改操作。集合通过值查询很快,插入、删除操作极快。
NSDictionary表示不可变字典,具有无序性的特点,每个key对应的值是唯一的,可通过key直接获取值;NSMutableDictionary表示可变字典,能对字典进行增、删、改操作。通过key查询值、插入、删除值都很快。
作用:
数组用于处理一组有序的数据集,比如常用的列表的dataSource要求有序,可通过索引直接访问,效率高。
集合要求具有确定性、互异性、无序性,在iOS开发中是比较少使用到的,笔者也不清楚如何说明其作用
字典是键值对数据集,操作字典效率极高,时间复杂度为常量,但是值是无序的。在ios中,常见的JSON转字典,字典转模型就是其中一种应用。
D3. 简单描述一下XIB与Storyboards,说一下他们的优缺点。
优点:
XIB:在编译前就提供了可视化界面,可以直接拖控件,也可以直接给控件添加约束,更直观一些,而且类文件中就少了创建控件的代码,确实简化不少,通常每个XIB对应一个类。
Storyboard:在编译前提供了可视化界面,可拖控件,可加约束,在开发时比较直观,而且一个storyboard可以有很多的界面,每个界面对应一个类文件,通过storybard,可以直观地看出整个App的结构。
缺点:
XIB:需求变动时,需要修改XIB很大,有时候甚至需要重新添加约束,导致开发周期变长。XIB载入相比纯代码自然要慢一些。对于比较复杂逻辑控制不同状态下显示不同内容时,使用XIB是比较困难的。当多人团队或者多团队开发时,如果XIB文件被发动,极易导致冲突,而且解决冲突相对要困难很多。
Storyboard:需求变动时,需要修改storyboard上对应的界面的约束,与XIB一样可能要重新添加约束,或者添加约束会造成大量的冲突,尤其是多团队开发。对于复杂逻辑控制不同显示内容时,比较困难。当多人团队或者多团队开发时,大家会同时修改一个storyboard,导致大量冲突,解决起来相当困难。
D4. 请把字符串2015-04-10格式化日期转为NSDate类型
NSString *timeStr = @”2015-04-10″;
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @”yyyy-MM-dd”;
formatter.timeZone = [NSTimeZone defaultTimeZone];
NSDate *date = [formatter dateFromString:timeStr];
// 2015-04-09 16:00:00 +0000
NSLog(@”%@”, date);
D5. 在App中混合HTML5开发App如何实现的。在App中使用HTML5的优缺点是什么?
在iOS中,通常是用UIWebView来实现,当然在iOS8以后可以使用WKWebView来实现.有以下几种实现方法:
通过实现UIWebView的代理方法来拦截,判断scheme是否是约定好的,然后iOS调用本地相关API来实现:
– (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在iOS7以后,可以直接通过JavaScripteCore这个库来实现,通过往JS DOM注入对象,而这个对象对应于我们iOS的某个类的实例。更详细请阅读:
可以通过WebViewJavascriptBridge来实现。具体如何使用,请大家去其它博客搜索吧!
优缺点:
iOS加入H5响应比原生要慢很多,体验不太好,这是缺点。
iOS加入H5可以实现嵌入别的功能入口,可随时更改,不用更新版本就可以上线,这是最大的优点。
D6. 请描述一下同步和异步,说说它们之间的区别。
首先,我们要明确一点,同步和异步都是在线程中使用的。在iOS开发中,比如网络请求数据时,若使用同步请求,则只有请求成功或者请求失败得到响应返回后,才能继续往下走,也就是才能访问其它资源(会阻塞了线程)。网络请求数据异步请求时,不会阻塞线程,在调用请求后,可以继续往下执行,而不用等请求有结果才能继续。
区别:
线程同步:是多个线程访问同一资源时,只有当前正在访问的线程访问结束之后,其他线程才能开始访问(被阻塞)。
线程异步:是多个线程在访问竞争资源时,可以在空闲等待时去访问其它资源(不被阻塞)。
D7. 请简单描述一下队列和多线程的使用原理。
在iOS中队列分为以下几种:
串行队列:队列中的任务只会顺序执行
dispatch_queue_t q = dispatch_queue_create(“…”, DISPATCH_QUEUE_SERIAL);
并行队列: 队列中的任务通常会并发执行
dispatch_queue_t q = dispatch_queue_create(“……”, DISPATCH_QUEUE_CONCURRENT);
全局队列:是系统的,直接拿过来(GET)用就可以;与并行队列类似
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
主队列:每一个应用程序对应唯一一个主队列,直接GET即可;在多线程开发中,使用主队列更新UI
dispatch_queue_t q = dispatch_get_main_queue();
上面这四种是针对GCD来讲的,串行队列中的任务只能一个个地执行,在前一个没有执行完毕之前,下一个只能等待。并行队列可以并发地执行任务,因此多个任务之间执行的顺序不能确定,当添加一个新的任务时,交由GCD来判断是否要创建新的新的线程。
D8. 描述一下iOS的内存管理,在开发中对于内存的使用和优化包含哪些方面。我们在开发中应该注意哪些问题。
内存管理准则:谁强引用过,谁就在不再使用时使引用计数减一。
对于内存的使用和优化常见的有以下方面:
重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用。
尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能。
不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多。
选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。
延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。
数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。
处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。
避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。
使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存。
正确选择图片加载方式:详情阅读细读UIImage加载方式
D9. plist文件是用来做什么的。一般用它来处理一些什么方面的问题。
plist是iOS系统中特有的文件格式。我们常用的NSUserDefaults偏好设置实质上就是plist文件操作。plist文件是用来持久化存储数据的。
我们通常使用它来存储偏好设置,以及那些少量的、数组结构比较复杂的不适合存储数据库的数据。比如,我们要存储全国城市名称和id,那么我们要优先选择plist直接持久化存储,因为更简单。
D10. iOS中缓存一定量的数据以便下次可以快速执行,那么数据会存储在什么地方,有多少种存储方式?
偏好设置(NSUserDefaults)
plist文件存储
归档
SQLite3
Core Data
D11. 请简单写出增、删、改、查的SQL语句。
数据库的简单操作,还是会的,大学可没白学。
insert into tb_blogs(name, url)
增:
values(‘标哥的技术博客’,’http://www.henishuo.com’);
删:
delete from tb_blogs where blogid = 1;
改:
update tb_blogs set url = ‘www.henishuo.com’ where blogid = 1;
查:
select name, url from tb_blogs where blogid = 1;
D12. 在提交苹果审核时,遇到哪些问题被拒绝,对于被拒绝的问题是如何处理的。
这里只列出几种最常见的被拒原因:
最常见到的就是app中有虚拟物品交易,但是不是走内购导致被拒。
音频类App或者使用到音频相关的app,因为版权问题而被拒
App出现必闪退而被拒
D13. 细讲UIIamge的加载方式和区别
-imageNamed: 默认加载图片成功后会内存中缓存图片,这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象。如果缓存中没有找到相应的图片对象,则从指定地方加载图片然后缓存对象并返回这个图片对象。通常是加载bundle中的图片资源!
-initWithContentsOfFile: 仅仅加载图片而不在内存中缓存下来,那么每次获取时都会重新去加载。
使用场景
-imageNamed: 是读取到内存后会缓存下来,下次再读取时直接从缓存中获取,因此访问效率要比较高。对于图片资源比较小,使用比较频繁的图片,通常会选择使用此种方式来加载。当然,若不需要考虑性能时,直接使用此种方式也是可以的。
-initWithContentsOfFile: 当图片资源比较大,或者图片资源只使用一次就不再使用了,那么使用此种方式是最佳方式。当应用程序需要加载一张比较大的图片并且是一次性使用的,那么是没有必要去缓存这个图片的,用-imageWithContentsOfFile:是最为经济的方式,这样不会因为UIImage元素较多情况下,CPU会被逐个分散在不必要的缓存上而浪费过多CPU时间。另外,当我们的图片不是PNG图片时,我们通常会选择此种方式来加载。
大量使用-initWithContentsOfFile:方式来加载图片,会增加CPU的开销,所以我们需要根据特定场景慎重选择图片加载的方式。即使UIImage较小,但使用UIImage元素较多时,问题会有所凸显哦!
代码使用
对于-imageNamed: 这个API的调用就非常简单了,直接就是:
UIImage *image = [UIImage imageNamed:@”logo”];
// 在开发中,通常都定义了快捷调用的宏
#define kImgName(name) [UIImage imageNamed:name]
// 使用时就更简化了
UIImage *image = kImgName(@”logo”);
对于-initWithContentsOfFile:的使用就相对复杂了一点点:
NSString *filePath = [[NSBundle mainBundle] pathForResource:@”logo” ofType:@”png”];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:filePath];
// 但是在开发中,笔者通常会定义成宏,简化调用
#define kResourcePath(name, type) ([[NSBundle mainBundle] pathForResource:name ofType:type])
#define kImgFromFile(name, type) [[UIImage alloc] initWithContentsOfFile:kResourcePath(name, type)]
// 然后,调用也变得很简化了~
UIImage *image = kImgFromFile(@”logo”, @”png”);
D以上来源《宝库iOS开发笔记试题》 http://www.henishuo.com/ios-c-cpp-interview/
E1. 请写出UIViewController的完整生命周期
参考答案:
下面是笔者通过打印,先出现ViewController,然后在点击ViewController上的按钮时,模态弹出了一个纯代码HYBViewController,其打印如下:
-[ViewController initWithCoder:]
-[ViewController loadView]
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]
// present HYBViewController
-[HYBViewController initWithNibName:bundle:]
-[HYBViewController init]
-[HYBViewController loadView]
-[HYBViewController viewDidLoad]
-[ViewController viewWillDisappear:]
-[HYBViewController viewWillAppear:]
-[HYBViewController viewDidAppear:]
-[ViewController viewDidDisappear:]
E2. 给UIImageView添加圆角
参考答案:
1)最直接的方法就是使用如下属性设置:
imgView.layer.cornerRadius = 10;
// 这一行代码是很消耗性能的
imgView.clipsToBounds = YES;
好处是使用简单,操作方便。坏处是离屏渲染(off-screen-rendering)需要消耗性能。对于图片比较多的视图上,不建议使用这种方法来设置圆角。通常来说,计算机系统中CPU、GPU、显示器是协同工作的。CPU计算好显示内容提交到GPU,GPU渲染完成后将渲染结果放入帧缓冲区。
简单来说,离屏渲染,导致本该GPU干的活,结果交给了CPU来干,而CPU又不擅长GPU干的活,于是拖慢了UI层的FPS(数据帧率),并且离屏需要创建新的缓冲区和上下文切换,因此消耗较大的性能。
2)给UIImage添加生成圆角图片的扩展API:
– (UIImage *)hyb_imageWithCornerRadius:(CGFloat)radius {
CGRect rect = (CGRect){0.f, 0.f, self.size};
UIGraphicsBeginImageContextWithOptions(self.size, NO, UIScreen.mainScreen.scale);
CGContextAddPath(UIGraphicsGetCurrentContext(),
[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
[self drawInRect:rect];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
然后调用时就直接传一个圆角来处理:
imgView.image = [[UIImage imageNamed:@”test”] hyb_imageWithCornerRadius:4];
这么做就是on-screen-rendering了,通过模拟器->debug->Color Off-screen-rendering看到没有离屏渲染了!(黄色的小圆角没有显示了,说明这个不是离屏渲染了)
3)在画之前先通过UIBezierPath添加裁剪,但是这种不实用
– (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];
[self.image drawInRect:bounds];
}
4)通过mask遮罩实现
E3.请描述事件响应者链的工作原理
参考答案:
iOS使用hit-testing寻找触摸的view。 Hit-Testing通过检查触摸点是否在关联的view边界内,如果在,则递归地检查该view的所有子view。在层级上处于lowest(就是离用户最近的view)且边界范围包含触摸点的view成为hit-test view。确定hit-test view后,它传递触摸事件给该view。
官方小例子事件响应者链如下图所示:
触摸点在view A中,所以要先检查子view B和C。
触摸点不在view B中,但在C中,所以检查C的子view D和E。
触摸点不在D中,但在E中。View E是这个层级上处于lowest的view的边界范围包含触摸点,所以它成为了hit-test view。
Hit-test view是处理触摸事件的第一选择,如果hit-test view不能处理事件,该事件将从事件响应链中寻找响应器,直到系统找到一个处理事件的对象。若不能处理,则就有事件传递链了,继续看下面的事件传递链。
事件传递链如下图所示:
左半图:
initial view若不能处理事件,则传到其父视图view
view若不能处理,则传到其父视图,因为它还不是最上层视图
这里view的父视图是view controller的view,因为这个view也不能处理事件,因此传给view controller
若view controller也不能处理此事件,则传到window
若window也不能处理此事件,则传到app单例对象Application
若UIApplication单例对象也不能处理,则表示无效事件
右半图:
initial view一直传递直到最上层view(原话:A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.)
topmost view传递事件到它所在的控制器(原话:The topmost view passes the event to its view controller.)
view controller传递事件到topmost view的父视图,重复前三步,走到到达root controller(原话:passes the event to its topmost view’s superview. Steps 1-3 repeat until the event reaches the root view controller.)
由root控制器传递事件到window(原话:The root view controller passes the event to the window object.)
若window也不能处理此事件,则传到app单例对象Application
若UIApplication单例对象也不能处理,则表示无效事件
E4. 如何避免使用block时发生循环引用
参考答案:
关于block循环引用问题是非常常见的,但是很多人没有深入研究过,xcode没有提示警告就以为没有形成循环引用了。笔者也见过很多高级iOS开发工程师的同事,使用block并不会分析是否形成循环引用。
在ARC下,要避免block出现循环引用 __weak typedof(self)weakSelf = self;
推荐阅读iOS Block循环引用精讲http://www.henishuo.com/ios-block-memory-cycle/
E5. 请比较GCD与NSOperation的异同
参考答案:
相同点:GCD和NSOperation都是苹果提供的多线程实现方案。
不同点:GCD是轻量级的纯C写的多线程实现方案,使用起来非常方便,在开发中大量使用,但是对于取消和暂停线程就比较麻烦些。而NSOperation是面向对象的,兼容KVO,对于取消和暂停任务是非常容易的。
E6. 请写出NSTimer使用时的注意事项(两项即可)
说到NSTimer这个定时器类,要使用好它,还得了解Run Loop,因为在不同的run loop mode下,定时器不都会回调的。
mode主要是用来指定事件在运行循环中的优先级的,分为:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode
- UIInitializationRunLoopMode:run loop启动时,会切换到该mode
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的Mode有两个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。
如果想更深入地了解RunLoop,请参考iOS之Run Loop详解
如果想要销毁timer,则必须先将timer置为失效,否则timer就一直占用内存而不会释放。造成逻辑上的内存泄漏。该泄漏不能用xcode及instruments测出来。 另外对于要求必须销毁timer的逻辑处理,未将timer置为失效,若每次都创建一次,则之前的不能得到释放,则会同时存在多个timer的实例在内存中。
参考答案:
- 注意timer添加到runloop时应该设置为什么mode
- 注意timer在不需要时,一定要调用invalidate方法使定时器失效,否则得不到释放
E7.说说Core Animation是如何开始和结束动画的
笔者不是很清楚题目的真正要求,是想知道核心动画的哪些知识点。如何开始和结束动画,这核心动画有很多种,每种动画还有很大的区别。
参考答案:
动画的开始和结束都可以通过CAMediaTiming协议来处理,核心动画的基类是遵守了CAMediaTiming协议的,可以指定动画开始时间、动画时长、动画播放速度、动画在完成时的行为(停留在结束处、动画回到开始处、动画完成时移除动画)。
E以上来源《要出发公司iOS笔试题》 http://www.henishuo.com/ios-needgo-interview/
F1. 请写出以下代码输出
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf(“%d, %d”, *(a + 1), *(ptr + 1));
参考答案: 2, 随机值
这种类型题好像挺常见的。考的就是C语言上的指针的理解和数组的理解。
分析:
a代表有5个元素的数组的首地址,a[5]的元素分别是1,2,3,4,5。接下来,a + 1表示数据首地址加1,那么就是a[1],也就是对应于值为2.但是,这里是&a + 1,因为a代表的是整个数组,它的空间大小为5 * sizeof(int),因此&a + 1就是a+5。a是个常量指针,指向当前数组的首地址,指针+1就是移动sizeof(int)个字节。
因此,ptr是指向int *类型的指针,而ptr指向的就是a + 5,那么ptr + 1也相当于a + 6,所以最后的*(ptr + 1)就是一个随机值了。而*(ptr – 1)就相当于a + 4,对应的值就是5。
F2.写一个标准宏Max,并给出以下代码的输出
int array[5] = {1, 2, 3, 4, 5};
int *p = &array[0];
int max = Max(*p++, 1);
printf(“%d %d”, max, *p);
参考答案: 1,2
#define Max(X, Y) ((X) > (Y) ? (X) : (Y))
当看到宏时,就会想到宏定义所带来的副作用。对于++、–,在宏当中使用是最容易产生副作用的,因此要慎用。
分析:
p指针指向了数组array的首地址,也就是第一个元素对应的地址,其值为1.
宏定义时一定要注意每个地方要加上圆括号
*p++相当于*p, p++,所以Max(*p++, 1)相当于:
(*p++) > (1) ? (*p++) : (1)
=>
(1) > (1) ? (*p++) : (1)
=>
第一个*p++的结果是,p所指向的值变成了2,但是1 > 1为値,所以最终max的值就是1。而后面的(*p++)也就不会执行,因此p所指向的地址对应的值就是2,而不是3.
扩展:如果上面的*p++改成*(++p)如何?
(*++p) > (1) ? (*++p) : (1)
=>
(2) > (1) ? (*++p) : (1)
=>
max = *++p;
=>
*p = 3,max = 3;
F3. 在一个对象的方法里:self.name=@object;和name=@object有什么不同
参考答案:
这是老生常谈的话题了,实质上就是问setter方法赋值与成员变量赋值有什么不同。通过点语法self.name实质上就是[self setName:@object];。而name这里是成员变量,直接赋值。
一般来说,在对象的方法里成员变量和方法都是可以访问的,我们通常会重写Setter方法来执行某些额外的工作。比如说,外部传一个模型过来,那么我会直接重写Setter方法,当模型传过来时,也就是意味着数据发生了变化,那么视图也需要更新显示,则在赋值新模型的同时也去刷新UI。这样也不用再额外提供其他方法了。
F4. 怎样使用performSelector传入3个以上参数,其中一个为结构体
参考答案:
– (id)performSelector:(SEL)aSelector;
– (id)performSelector:(SEL)aSelector withObject:(id)object;
– (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
因为系统提供的performSelector的api中,并没有提供三个参数。因此,我们只能传数组或者字典,但是数组或者字典只有存入对象类型,而结构体并不是对象类型,那么怎么办呢?
没有办法,我们只能通过对象放入结构作为属性来传过去了:
ypedef struct HYBStruct {
int a;
int b;
} *my_struct;
@interface HYBObject : NSObject
@property (nonatomic, assign) my_struct arg3;
@property (nonatomic, copy) NSString *arg1;
@property (nonatomic, copy) NSString *arg2;
@end
@implementation HYBObject
// 在堆上分配的内存,我们要手动释放掉
– (void)dealloc {
free(self.arg3);
}
@end
测试:
my_struct str = (my_struct)(malloc(sizeof(my_struct)));
str->a = 1;
str->b = 2;
HYBObject *obj = [[HYBObject alloc] init];
obj.arg1 = @”arg1″;
obj.arg2 = @”arg2″;
obj.arg3 = str;
[self performSelector:@selector(call:) withObject:obj];
// 在回调时得到正确的数据的
– (void)call:(HYBObject *)obj {
NSLog(@”%d %d”, obj.arg3->a, obj.arg3->b);
}
F5. UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动cell过程中,label是否刷新,为什么?
这是否刷新取决于timer加入到Run Loop中的Mode是什么。Mode主要是用来指定事件在运行循环中的优先级的,分为:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode
- UIInitializationRunLoopMode:run loop启动时,会切换到该mode
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的Mode有两个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。
对于这道题,如果要cell滚动过程中定时器正常回调,UI正常刷新,那么要将timer放入到CommonModes下,因为是NSDefaultRunLoopMode,只有在空闲状态下才会回调。
F6. 有a、b、c、d 4个异步请求,如何判断a、b、c、d都完成执行?如果需要a、b、c、d顺序执行,该如何实现?
参考答案:
-
对于这四个异步请求,要判断都执行完成最简单的方式就是通过GCD的group来实现:
- 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, ^{ /*任务a */ });
-
dispatch_group_async(group, queue, ^{ /*任务b */ });
- dispatch_group_async(group, queue, ^{ /*任务c */ });
-
dispatch_group_async(group, queue, ^{ /*任务d */ });
-
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
- // 在a、b、c、d异步执行完成后,会回调这里
-
});
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
当然,我们还可以使用非常老套的方法来处理,通过四个变量来标识a、b、c、d四个任务是否完成,然后在runloop中让其等待,当完成时才退出run loop。但是这样做会让后面的代码得不到执行,直到Run loop执行完毕。
- 要求顺序执行,那么可以将任务放到串行队列中,自然就是按顺序来异步执行了。
F7. 使用block有什么好处?使用NSTimer写出一个使用block显示(在UILabel上)秒表的代码。
参考答案:
说到block的好处,最直接的就是代码紧凑,传值、回调都很方便,省去了写代理的很多代码。
对于这里根本没有必要使用block来刷新UILabel显示,因为都是直接赋值。当然,笔者觉得这是在考验应聘者如何将NSTimer写成一个通用用的Block版本。
NSTimer封装成Block版: http://www.henishuo.com/nstimer-block/
使用起来像这样:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
callback:^() {
weakSelf.secondsLabel.text = …
}
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
F8. 一个view已经初始化完毕,view上面添加了n个button(可能使用循环创建),除用view的tag之外,还可以采用什么办法来找到自己想要的button来修改Button的值
参考答案:
这个问题有很多种方式,而且不同的使用场景也不一样的。比如说:
- 第一种:如果是点击某个按钮后,才会刷新它的值,其它不用修改,那么不用引用任何按钮,直接在回调时,就已经将接收响应的按钮给传过来了,直接通过它修改即可。
- 第二种:点击某个按钮后,所有与之同类型的按钮都要修改值,那么可以通过在创建按钮时将按钮存入到数组中,在需要的时候遍历查找。
F9. tableview在滑动时,有时候会大量加载本地图片,这时候会很卡,如何解决加载耗时过长导致不流畅的问题
参考答案:
这是优化tableview的相关专题,如果只是处理图片加载问题,那可以通过异步读取图片然后刷新UI。当然,我们也可以在取数据时,在模型中提前准备好需要显示的图片资源,这样在cell只就不需要操作图片读取,而是直接显示。
F10. 给定一个如下的字符串(1,(2,3),(4,(5,6)7))括号内的元素可以是数字,也可以是括号,请实现一个算法清除嵌套的括号,比如把上面的表达式的变成:(1,2,3,4,5,6,7),表达式有误时请报错。
参考答案:
如果只是判断整个表达式是否有错误,然后去掉里面的圆括号,那么一个循环就可以了。不过我们只需要加两个变量分别来记录左圆括号和右圆括号的个数。这里假设逗号总是正确的情况下,伪代码如下:
left = 0;
rigt = 0;
for i = 0; i < str.length; ++i {
if 是左括号 {
left++;
continue;
}
if 是右括号 {
right++;
// 处理(1,)这样的结构
if 前一个是逗号 {
error;
}
continue;
}
[newStr append:str[i]];
}
if left != right {
error;
}
F以上来源《南京JingDong笔试题》http://www.henishuo.com/nanjing-jingdong-interview/
第二关:面试题
A1. 什么是arc?(arc是为了解决什么问题诞生的?)
首先解释ARC: automatic reference counting自动引用计数。
ARC几个要点:
在对象被创建时 retain count +1,在对象被release时 retain count -1.当retain count 为0 时,销毁对象。
程序中加入autoreleasepool的对象会由系统自动加上autorelease方法,如果该对象引用计数为0,则销毁。
那么ARC是为了解决什么问题诞生的呢?这个得追溯到MRC手动内存管理时代说起。
MRC下内存管理的缺点:
1.当我们要释放一个堆内存时,首先要确定指向这个堆空间的指针都被release了。(避免提前释放)
2.释放指针指向的堆空间,首先要确定哪些指针指向同一个堆,这些指针只能释放一次。(MRC下即谁创建,谁释放,避免重复释放)
3.模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁去释放。
4.多线程操作时,不确定哪个线程最后使用完毕
A2.请解释以下keywords的区别: assign vs weak, __block vs __weak
assign适用于基本数据类型,weak是适用于NSObject对象,并且是一个弱引用。
assign其实也可以用来修饰对象,那么我们为什么不用它呢?因为被assign修饰的对象在释放之后,指针的地址还是存在的,也就是说指针并没有被置为nil。如果在后续的内存分配中,刚好分到了这块地址,程序就会崩溃掉。
而weak修饰的对象在释放之后,指针地址会被置为nil。所以现在一般弱引用就是用weak。
首先__block是用来修饰一个变量,这个变量就可以在block中被修改(参考block实现原理)
__block:使用__block修饰的变量在block代码快中会被retain(ARC下,MRC下不会retain)
__weak:使用__weak修饰的变量不会在block代码块中被retain
同时,在ARC下,要避免block出现循环引用 __weak typedof(self)weakSelf = self;
是不一样的。
在MRC中__block variable在block中使用是不會retain的
但是ARC中__block則是會Retain的。
取而代之的是用__weak或是__unsafe_unretained來更精確的描述weak reference的目的
其中前者只能在iOS5之後可以使用,但是比較好 (該物件release之後,此pointer會自動設成nil)
而後者是ARC的環境下為了相容4.x的解決方案。
所以上面的範例中
__block MyClass* temp = …; // MRC環境下使用 __weak MyClass* temp = …; // ARC但只支援iOS5.0以上的版本 __unsafe_retained MyClass* temp = …; //ARC且可以相容4.x以後的版本
不是的。
atomic原子操作,系统会为setter方法加锁。 具体使用 @synchronized(self){//code }
nonatomic不会为setter方法加锁。
atomic:线程安全,需要消耗大量系统资源来为属性加锁
nonatomic:非线程安全,适合内存较小的移动设备
使用atomic并不能保证绝对的线程安全,对于要绝对保证线程安全的操作,还需要使用更高级的方式来处理,比如NSSpinLock、@syncronized等
block中的循环引用:一个viewController
@property (nonatomic,strong)HttpRequestHandler * handler; @property (nonatomic,strong)NSData *data;
_handler = [httpRequestHandler sharedManager];
[ downloadData:^(id responseData){
_data = responseData;
}];
self 拥有_handler, _handler 拥有block, block拥有self(因为使用了self的_data属性,block会copy 一份self)
解决方法:
__weak typedof(self)weakSelf = self
[ downloadData:^(id responseData){
weakSelf.data = responseData;
}];
A6.+(void)load; +(void)initialize;有什么用处?
在Objective-C中,runtime会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。
共同点:两个方法都只会被调用一次。
A7.为什么其他语言里叫函数调用, objective c里则是给对象发消息(或者谈下对runtime的理解)
先来看看怎么理解发送消息的含义:
曾经觉得Objc特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把[receiver message]当成简单的方法调用,而无视了”发送消息”这句话的深刻含义。于是[receiver message]会被编译器转化为:
objc_msgSend(receiver, selector)
如果消息含有参数,则为:
objc_msgSend(receiver, selector, arg1, arg2, …)
如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。
现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。
Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。
Objc Runtime使得C具有了面向对象能力,在程序运行时创建,检查,修改类、对象和它们的方法。可以使用runtime的一系列方法实现。
顺便附上OC中一个类的数据结构 /usr/include/objc/runtime.h
struct objc_class { Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,r untime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识 long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 #endif } OBJC2_UNAVAILABLE;
OC中一个类的对象实例的数据结构(/usr/include/objc/objc.h):
typedef struct objc_class *Class; /// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id;
向object发送消息时,Runtime库会根据object的isa指针找到这个实例object所属于的类,然后在类的方法列表以及父类方法列表寻找对应的方法运行。id是一个objc_object结构类型的指针,这个类型的对象能够转换成任何一种对象。
然后再来看看消息发送的函数:objc_msgSend函数
在引言中已经对objc_msgSend进行了一点介绍,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数了。
检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
如果 cache 找不到就找一下方法分发表。
如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
如果还找不到就要开始进入动态方法解析了,后面会提到。
后面还有:
动态方法解析resolveThisMethodDynamically
消息转发forwardingTargetForSelector
A8.什么是method swizzling?
Method Swizzling 原理(方法搅拌?)
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
……
归根结底,都是偷换了selector的IMP,如下图所示:
A9.UIView和CALayer是啥关系?
UIView是CALayer的delegate就及格了,能说出UIView主要处理事件,CALayer负责绘制。
1.UIView是iOS系统中界面元素的基础,所有的界面元素都继承自它。它本身完全是由CoreAnimation来实现的 (Mac下似乎不是这样)。它真正的绘图部分,是由一个叫CALayer(Core Animation Layer)的类来管理。 UIView本身,更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame,bounds等 等,实际上内部都是在访问它所包含的CALayer的相关属性。
2.UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的 类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示,例如通过
– (class) layerClass { return ([CAEAGLLayer class]); }
=使某个UIView的子类使用GL来进行绘制。
3.UIView的CALayer类似UIView的子View树形结构,也可以向它的layer上添加子layer,来完成某些特殊的表 示。例如下面的代码
grayCover = [[CALayer alloc] init]; grayCover.backgroundColor = [[[UIColor blackColor] colorWithAlphaComponent:0.2] CGColor]; [self.layer addSubLayer: grayCover];
会在目标View上敷上一层黑色的透明薄膜。
A10. 如何高性能的给UIImageView加个圆角?(不准说layer.cornerRadius!)
我觉得应该是使用Quartz2D直接绘制图片,得把这个看看。
步骤:
a、创建目标大小(cropWidth,cropHeight)的画布。
b、使用UIImage的drawInRect方法进行绘制的时候,指定rect为(-x,-y,width,height)。
c、从画布中得到裁剪后的图像。
– (UIImage*)cropImageWithRect:(CGRect)cropRect { CGRect drawRect = CGRectMake(-cropRect.origin.x , -cropRect.origin.y, self.size.width * self.scale, self.size.height * self.scale); UIGraphicsBeginImageContext(cropRect.size); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextClearRect(context, CGRectMake(0, 0, cropRect.size.width, cropRect.size.height)); [self drawInRect:drawRect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
A11. 使用drawRect有什么影响?(这个可深可浅,你至少得用过。。)
drawRect方法依赖Core Graphics框架来进行自定义的绘制,但这种方法主要的缺点就是它处理touch事件的方式:每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例。
A12. ASIHttpRequest或者SDWebImage里面给UIImageView加载图片的逻辑是什么样的?
ASIHttpRequest或者SDWebImage里面给UIImageView加载图片的逻辑是什么样的?(把UIImageView放到UITableViewCell里面问更赞) 很多同学没有读源码的习惯,别人的轮子拿来只是用用却不知道真正的营养都在源代码里面。这两个经典的framework代码并不复杂,很值得一读。能对一个UIImageView怎么通过url展示一张图片有完整的理解。涉及到的知识点也非常多,UITableViewCell的复用,memory cache, disk cache, 多线程切换,甚至http协议本身都需要有一定的涉及。
A13. 麻烦你设计个简单的图片内存缓存器(移除策略是一定要说的)
图片的内存缓存,可以考虑将图片数据保存到一个数据模型中。所以在程序运行时这个模型都存在内存中。
移除策略:释放数据模型对象。
A14. 讲讲你用Instrument优化动画性能的经历吧(别问我什么是Instrument)
A15. loadView是干嘛用的?
当你访问一个ViewController的view属性时,如果此时view的值是nil,那么,ViewController就会自动调用loadView这个方法。这个方法就会加载或者创建一个view对象,赋值给view属性。
loadView默认做的事情是:如果此ViewController存在一个对应的nib文件,那么就加载这个nib。否则,就创建一个UIView对象。
如果你用Interface Builder来创建界面,那么不应该重载这个方法。
如果你想自己创建view对象,那么可以重载这个方法。此时你需要自己给view属性赋值。你自定义的方法不应该调用super。如果你需要对view做一些其他的定制操作,在viewDidLoad里面去做。根据上面的文档可以知道,有两种情况:
1、如果你用了nib文件,重载这个方法就没有太大意义。因为loadView的作用就是加载nib。如果你重载了这个方法不调用super,那么nib文件就不会被加载。如果调用了super,那么view已经加载完了,你需要做的其他事情在viewDidLoad里面做更合适。
2、如果你没有用nib,这个方法默认就是创建一个空的view对象。如果你想自己控制view对象的创建,例如创建一个特殊尺寸的view,那么可以重载这个方法,自己创建一个UIView对象,然后指定 self.view = myView; 但这种情况也没有必要调用super,因为反正你也不需要在super方法里面创建的view对象。如果调用了super,那么就是浪费了一些资源而已
A16. viewWillLayoutSubView你总是知道的。
controller layout触发的时候,开发者有机会去重新layout自己的各个subview。
横竖屏切换的时候,系统会响应一些函数,其中 viewWillLayoutSubviews 和 viewDidLayoutSubviews。
– (void)viewWillLayoutSubviews { [self _shouldRotateToOrientation:(UIDeviceOrientation)[UIApplication sharedApplication].statusBarOrientation]; } -(void)_shouldRotateToOrientation:(UIDeviceOrientation)orientation { if (orientation == UIDeviceOrientationPortrait ||orientation == UIDeviceOrientationPortraitUpsideDown) { // 竖屏 } else { // 横屏 } }
通过上述一个函数就知道横竖屏切换的接口了。
注意:viewWillLayoutSubviews只能用在ViewController里面,在view里面没有响应。
A17. GCD里面有哪几种Queue?你自己建立过串行queue吗?背后的线程模型是什么样的?
1.主队列 dispatch_main_queue(); 串行 ,更新UI
2.全局队列 dispatch_global_queue(); 并行,四个优先级:background,low,default,high
3.自定义队列 dispatch_queue_t queue ; 可以自定义是并行:DISPATCH_QUEUE_CONCURRENT或者串行DISPATCH_QUEUE_SERIAL
A18. 用过coredata或者sqlite吗?读写是分线程的吗?遇到过死锁没?咋解决的?
没用过sqlite是说不过去的。用过CoreData的肯定有很多血泪史要说。多谢线程模型你肯定做过比较选择。死锁是啥肯定也是要知道的,没遇到过至少能举个简单的例子来说明。单个线程可以死锁(main thread里dispatch_sync到main queue),多个线程直接也可以死锁(A,B线程互相持有对方需要的资源且互相等待)。
A19. http的post和get啥区别?(区别挺多的,麻烦多说点)
A20. 什么是Binary search tree? search的时间复杂度是多少?
Binary search tree:二叉搜索树。
主要由四个方法:(用C语言实现或者Python)
1.search:时间复杂度为O(h),h为树的高度
2.traversal:时间复杂度为O(n),n为树的总结点数。
3.insert:时间复杂度为O(h),h为树的高度。
4.delete:最坏情况下,时间复杂度为O(h)+指针的移动开销。
可以看到,二叉搜索树的dictionary operation的时间复杂度与树的高度h相关。所以需要尽可能的降低树的高度,由此引出平衡二叉树Balanced binary tree。它要求左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这样就可以将搜索树的高度尽量减小。常用算法有红黑树、AVL、Treap、伸展树等。
A以上来源《iOS中级面试题一》http://www.henishuo.com/ios-middle-interview-questions-one/
B1. iOS数据持久化存储方案有哪些?
参考答案:
- plist属性列表存储(如NSUserDefaults)
- 文件存储(如二进制数据写入文件存储,通过NSFileManager来操作将下载起来的二进制数据写一篇文件中存储)
- NSKeydeArchiver归档存储,常见的是自动化归档/解档处理,想要学习如何通过runtime实现自动化归档/解档,可以阅读文章:
- 数据库SQLite3存储(如FMDB、Core Data)
B2. 沙盒的目录结构是怎样的?各自一般用于什么场合?
参考答案:
- Application:存放程序源文件,上架前经过数字签名,上架后不可修改
- Documents: 保存应⽤运行时生成的需要持久化的数据,iTunes同步设备时会备份该目 录。例如,游戏应用可将游戏存档保存在该目录
- tmp: 保存应⽤运行时所需的临时数据,使⽤完毕后再将相应的文件从该目录删除。应用 没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时 不会备份该目录
- Library/Caches: 保存应用运行时⽣成的需要持久化的数据,iTunes同步设备时不会备份 该目录。⼀一般存储体积大、不需要备份的非重要数据,比如网络数据缓存存储到Caches下
- Library/Preference: 保存应用的所有偏好设置,如iOS的Settings(设置) 应⽤会在该目录中查找应⽤的设置信息。iTunes同步设备时会备份该目录
B3. #define定义的宏和const定义的常量有什么区别?
参考答案:
- #define定义宏的指令,程序在预处理阶段将用#define所定义的内容只是进行了替换。因此程序运行时,常量表中并没有用#define所定义的宏,系统并不为它分配内存,而且在编译时不会检查数据类型,出错的概率要大一些。
- const定义的常量,在程序运行时是存放在常量表中,系统会为它分配内存,而且在编译时会进行类型检查。
#define定义表达式时要注意”边缘效应”,例如如下定义:
#define N 2 + 3 // 我们预想的N值是5,我们这样使用N
int a = N / 2; // 我们预想的a的值是2.5,可实际上a的值是3.5
B4. 常见的出现内存循环引用的场景有哪些?
参考答案:
- 定时器(NSTimer):NSTimer经常会被作为某个类的成员变量,而NSTimer初始化时要指定self为target,容易造成循环引用(self->timer->self)。 另外,若timer一直处于validate的状态,则其引用计数将始终大于0,因此在不再使用定时器以后,应该先调用invalidate方法
- block的使用:block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。在ARC与非ARC环境下对block使用不当都会引起循环引用问题, 一般表现为,某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身,简单说就是self.someBlock =Type var{[self dosomething];或者self.otherVar = XXX;或者_otherVar = …};出现循环的原因是:self->block->self或者self->block->_ivar(成员变量)
- 代理(delegate):在委托问题上出现循环引用问题已经是老生常谈了,规避该问题的杀手锏也是简单到哭,一字诀:声明delegate时请用assign(MRC)或者weak(ARC),千万别手贱玩一下retain或者strong,毕竟这基本逃不掉循环引用了!
B5. block中的weak self,是任何时候都需要加的么?
参考答案:
不是什么任何时候都需要添加的,不过任何时候都添加似乎总是好的。只要出现像self->block->self.property/self->_ivar这样的结构链时,才会出现循环引用问题。好好分析一下,就可以推断出是否会有循环引用问题。
B6. GCD的queue、main queue中执行的代码一定是在main thread么?
参考答案:
- 对于queue中所执行的代码不一定在main thread中。如果queue是在主线程中创建的,那么所执行的代码就是在主线程中执行。如果是在子线程中创建的,那么就不会在main thread中执行。
- 对于main queue就是在主线程中的,因此一定会在主线程中执行。获取main queue就可以了,不需要我们创建,获取方式通过调用方法dispatchgetmain_queue来获取。
B7. 头文件中声明的成员变量(不是属性),外部可直接访问么?
参考答案:
外部不能直接访问头文件所声明的成员变量,需要提供成员变量的getter方法才能在外部访问。而属性已经直接给我们自动生成了getter方法,因此外部可以直接访问属性。
B8. TCP和UDP的区别是什么?
参考答案:
- TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序传输)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。
-
UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快,传输的是报文。
B9. MD5和Base64的区别是什么,各自使用场景是什么?
参考答案:
做过加密相关的功能的,几乎都会使用到MD5和Base64,它们两者在实际开发中是最常用的。
- MD5:是一种不可逆的摘要算法,用于生成摘要,无法逆着破解得到原文。常用的是生成32位摘要,用于验证数据的有效性。比如,在网络请求接口中,通过将所有的参数生成摘要,客户端和服务端采用同样的规则生成摘要,这样可以防篡改。又如,下载文件时,通过生成文件的摘要,用于验证文件是否损坏。
-
Base64:属于加密算法,是可逆的,经过encode后,可以decode得到原文。在开发中,有的公司上传图片采用的是将图片转换成base64字符串,再上传。在做加密相关的功能时,通常会将数据进行base64加密/解密。
B10. 发送10个网络请求,然后再接收到所有回应之后执行后续操作,如何实现?
参考答案:
从题目分析可知,10个请求要全部完成后,才执行某一功能。比如,下载10图片后合成一张大图,就需要异步全部下载完成后,才能合并成大图。
做法:通过dispatch_group_t来实现,将每个请求放入到Group中,将合并成大图的操作放在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, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
B11. __block和__weak修饰符的区别?
参考答案:
- __block不管是ARC还是MRC模式下都可以使用,它的作用是标识block外的变量在block内使用可以修改变量的值。
- __weak只能在ARC模式下使用,表示弱引用,主要是用于防止循环引用而引入的。
B12. 常见的Http状态码有哪些?
参考答案:
- 302是请求重定向。
- 500及以上是服务器错误,如503表示服务器找不到、3840表示服务器返回无效JSON。
- 400及以上是请求链接错误或者找不到服务器,如常见的404。
- 200及以上是正确,如常见的是200表示请求正常。
- 100及以上是请求接受成功。
B13. iOS与H5交互的方式有哪些?
参考答案:
- 通过协议在shouldStartRequest中捕获请求,获取scheme来判断预先定义的功能,然后调用native代码。比如,定义点击图片调用native来展示大图,那么js接收到点击时,重定向将图片的url添加上自定义scheme,如HYBImagePreview://这样。更详细请阅读我的文章:Swift版、Objective-C版本
- 通过iOS7以后引入的JavascriptCore框架来实现,通过注入对象的方式来实现,维护起来更简单。更详细请阅读我的文章:Oc版本与JS交互、Swift版与JS交互、如何使用的是WKWebView,可阅读WKWebView与JS交互
- 通过WebViewJavascriptBridge这个第三方库来实现,具体百度吧!
B14. 如何浅拷贝与深拷贝的理解?
所谓浅拷贝是指只复制指向对象的指针,而不复制引用对象本身(同一份内存),而所谓深拷贝是指复制引用对象本身(新创建了一份内存)。
打个比方:
浅复制好比是你的影子,当你挂了,影子也消失了。 深复制好比克隆你,即使你挂了,新克隆出来的人是新的生命,不会因为你挂了也跟着挂。
B15. 使用是懒加载?常见场景?
参考答案:
所谓懒加载是指在真正需要到的时候才真正载入内存。常见的场景就是重写属性的getter方法,在getter方法中判断是否创建过或者加载过,若未创建过或者未加载过,则创建或者加载。
懒加载可以优化性能,有的功能一般情况下不会使用到,但是这些功能一使用就会占较大的内存,此时使用懒加载就非常的好了。
常见的写法:
– (NSMutableArray *)dataSource {
if (_dataSource == nil) {
_dataSource = [[NSMutableArray alloc] init];
// 添加默认数据等
// …
}
}
B16. 一个tableView是否可以关联两个不同的数据源?
参考答案:
当然是可以关联多个不同的数据源,但是不能同时使用多个数据源而已。比如,一个列表有两个筛选功能,一个是筛选城市,一个是筛选时间,那么这两个就是两个数据源了。当筛选城市时,就会使用城市数据源;当筛选时间时,就会使用时间数据源。
B17.对象添加到通知中心中,当通知中心发通知时,这个对象却已经被释放了,可能会出现什么问题?
参考答案:
其实这种只是考查对通知的简单应用。通知是多对多的关系,主要使用场景是跨模块传值。当某对象加入到通知中心后,若在对象被销毁前不将该对象从通知中心中移除,当发送通知时,就会造成崩溃。这是很常见的。所以,在添加到通知中心后,一定要在释放前移除。
B18. 实现过框架或者库以供他人使用么?如果有,请谈一谈构建框架或者库时候的经验;如果没有,请设想和设计框架的public的API,并指出大概需要如何做、需要注意哪些问题,以使人人更容易地使用你的框架。
从以下角度出发来思考和设计公共框架:
- 确保外部调用简单,且保证有详细的头文件注释说明。
- 确保API编码规范,保证风格统一。
- 确保API易扩展,可以考虑预留参数
- 确保没有外部依赖或者依赖要尽可能的少,以保证公共库的纯洁(原则上不能有外部依赖)
- 确保易维护,不存在冗余API
B19.如何自动计算cell的高度?
实现原理:通过数据模型的id作为key,以确保唯一,如何才能保证复用cell时不会出现混乱。在配置完数据后,通过更新约束,得到最后一个控件的frame,就只可以判断cell实际需要的高度,并且缓存下来,下次再获取时,判断是否存在,若存在则直接返回。因此,只会计算一遍。
B20.UITableView是如何计算内容高度的?为什么初始化时配置数据时,获取行高的代理方法会调用数据条数次?
参考答案:
UITableView是继承于UIScrollView的,因此也有contentSize。要得到tableview的contentsize,就需要得到所有cell的高度,从而计算出总高度,才能得到contentsize。因此,在reloadData时,就会调用该代理方法数据条数次。
B以上来源《iOS中级面试题二》 http://www.henishuo.com/ios-interview-middle-two/
C1. 什么情况使用weak关键字,相比assign有什么不同?
使用weak关键字的主要场景:
- 在ARC下,在有可能出现循环引用的时候往往要通过让其中一端使用weak来解决,比如: delegate代理属性,通常就会声明为weak。
- 自身已经对它进行一次强引用,没有必要再强引用一次时也会使用weak。比如:自定义 IBOutlet控件属性一般也使用weak,当然也可以使用strong。
相比assign不同之处:
- weak关键字只能用于对象,对于基本类型不能使用
- assign既可以用于对象,也可以用于基本类型,但是只是简单地进行赋值操作而已
C2. 怎么用copy关键字?
分析:
copy关键字只能应用于对象,不能用于基本类型。copy属性会复制一份,并且强引用之,但是对于集合类型,通常并不能达到深拷贝的目的。NSString、NSArray、NSDictionary等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,当然很多时候都使用了strong来声明。block也使用copy关键字来声明。
参考答案:
- copy关键字只能应用于对象,不能用于基本类型
- 对于字符串,理应始终使用copy,虽然使用strong一般情况下也没有关系
- 对于不可变集合类型,有可变和不可变类型,若要防止外部的修改影响所传过来的值,应该使用copy来声明,虽然大多情况下使用strong一定问题都没有。不过,实际开发中,我见到的几乎都是使用strong来声明的,包括笔者在内。
- 对于可变集合类型,都应该使用strong来声明,不能使用copy,因为copy会生成一个不可变的类型,而不是可变的。
- 对于block,都应该使用copy来声明,原因是block来捕获上下文的信息。
C3. 这个写法会出什么问题:@property (copy) NSMutableArray *array;
参考答案:
- 没有指明为nonatomic,因此就是atomic原子操作,会影响性能。该属性使用了同步锁,会在创建时生成一些额外的代码用于帮助编写多线程程序,这会带来性能问题,通过声明nonatomic可以节省这些虽然很小但是不必要额外开销。在我们的应用程序中,几乎都是使用nonatomic来声明的,因为使用atomic并不能保证绝对的线程安全,对于要绝对保证线程安全的操作,还需要使用更高级的方式来处理,比如NSSpinLock、@syncronized等
- 因为使用的是copy,所得到的实际是NSArray类型,它是不可变的,若在使用中使用了增、删、改操作,则会crash
C4. @property的本质是什么?ivar、getter、setter是如何生成并添加到这个类中的?
@property的本质:
@property = ivar(实例变量) + getter(取方法) + setter(存方法)
“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)
ivar、getter、setter如何生成并添加到类中:
这是编译器自动合成的,通过@synthesize关键字指定,若不指定,默认为@synthesize propertyName = _propertyName;若手动实现了getter/setter方法,则不会自动合成。
现在编译器已经默认为我们添加@synthesize propertyName = _propertyName;因此不再需要手动添加了,除非你真的要改成员变量名。
生成getter方法时,会判断当前属性名是否有_,比如声明属性为@property (nonatomic, copy) NSString *_name;那么所生成的成员变量名就会变成__name,如果我们要手动生成getter方法,就要判断是否以_开头了。
不过,命名都要有规范,是不允许声明属性是使用_开头的,不规范的命名,在使用runtime时,会带来很多的不方便的。
C5. @protocol和category中如何使用 @property
参考答案:
- 在protocol中使用@property只会生成setter和getter方法声明,我们使用属性的目的是希望遵守我协议的对象能实现该属性
-
category使用@property也是只会生成setter和getter方法的声明,如果我们真的需要给category增加属性的实现,需要借助于运行时的两个函数:
- objc_setAssociatedObject
-
objc_getAssociatedObject
- objc_setAssociatedObject
C6. runtime如何实现weak属性?
参考答案:
-
通过关联属性来实现:
- // 声明一个weak属性,这里假设delegate,其实weak关键字可以不使用,
-
// 因为我们重写了getter/setter方法
- @property (nonatomic, weak) id delegate;
-
- – (id)delegate {
-
return objc_getAssociatedObject(self, @”__delegate__key”);
- }
-
- // 指定使用OBJC_ASSOCIATION_ASSIGN,官方注释是:
-
// Specifies a weak reference to the associated object.
- // 也就是说对于对象类型,就是weak了
-
– (void)setDelegate:(id)delegate {
- objc_setAssociatedObject(self, @”__delegate__key”, delegate, OBJC_ASSOCIATION_ASSIGN);
-
}
- 通过objc_storeWeak函数来实现,不过这种方式几乎没有遇到有人这么使用过,因为这里不细说了。
- // 声明一个weak属性,这里假设delegate,其实weak关键字可以不使用,
C7. @property中有哪些属性关键字,后面可以有哪些修饰符?
属性可以拥有的特质分为四类:
- 原子性:nonatomic声明为非原子操作,atomic声明为原子操作。在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用同步锁。请注意,尽管没有名为”atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是”原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。
- 读/写权限:readwrite(读写)、readonly (只读)
- 内存管理相关:assign、strong、 weak、unsafe_unretained、copy
- 方法名:getter= 、setter=set。getter=的样式:
@property (nonatomic, getter=isOn) BOOL on;
- 不常用的:nonnull、null_resettable、nullable
C8.weak属性需要在dealloc中置nil么?
参考答案:
对于weak声明的属性,都不需要在dealloc中指定为nil,在ARC下,编译器会自动帮助我们处理。即使编译器不帮助我们处理,我们也不需要手动在dealloc中设置为nil。
C9. @synthesize和@dynamic分别有什么作用?
分析:
@property有两个对应的词,一个是@synthesize,,另一个是@dynamic。如果 @synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;这两个关键字都是为@property关键字工作的。
参考答案:
- @synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
-
@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成。假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺setter方法会导致程序崩溃;或者当运行到someVar = var 时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
C10. ARC下不显式指定任何属性关键字时,默认的关键字都有哪些?
参考答案:
- 对于基本数据类型默认关键字是:atomic,readwrite,assign
-
对于普通的Objective-C对象:atomic,readwrite,strong
C11. objc中向一个nil对象发送消息将会发生什么?
参考答案:
在Objective-C中向nil发送消息是完全有效的,只是在运行时不会有任何作用,因为在运行时调用时,objc_msgSend函数传过去的receiver是nil,而内部会判断receiver是否为nil,若为nil则什么也不干。同样,若cmd也就是selector为nil,也是什么也不干。
C12. objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?
参考答案:
实际上,编译器在编译时会转换成objc_msgSend,大概会像这样:
((void (*)(id, SEL))(void)objc_msgSend)((id)obj, sel_registerName(“foo”));
也就是说,[obj foo];在objc动态编译时,会被转换为:objc_msgSend(obj, @selector(foo));这样的形式,但是需要根据具体的参数类型及返回值类型进行相应的类型转换。
C13. 什么时候会报unrecognized selector的异常?
参考答案:
下面只讲述对象方法的解析过程:
- 第一步:+ (BOOL)resolveInstanceMethod:(SEL)sel实现方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过class_addMethod函数动态地添加方法,消息得到处理,此流程完毕。
- 第二步:在第一步返回的是NO时,就会进入- (id)forwardingTargetForSelector:(SEL)aSelector方法,这是运行时给我们的第二次机会,用于指定哪个对象响应这个selector。不能指定为self。若返回nil,表示没有响应者,则会进入第三步。若返回某个对象,则会调用该对象的方法。
- 第三步:若第二步返回的是nil,则我们首先要通过- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector指定方法签名,若返回nil,则表示不处理。若返回方法签名,则会进入下一步。
- 第四步:当第三步返回方法方法签名后,就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等
- 第五步:若没有实现- (void)forwardInvocation:(NSInvocation *)anInvocation方法,那么会进入- (void)doesNotRecognizeSelector:(SEL)aSelector方法。若我们没有实现这个方法,那么就会crash,然后提示打不到响应的方法。到此,动态解析的流程就结束了。
C14. 一个objc对象的isa的指针指向什么?有什么作用?
一个对象的isa指针指向的是他所属的类(class),用于查找对象上的方法。而类对象的isa指针又指向它的元类(meta class),无类的isa也指向根类的元类,而根类的元类的isa又指向根类本身。
C15. 下面的代码输出什么?
@implementation Son : Father
– (id)init {
self = [super init];
if (self) {
NSLog(@”%@”, NSStringFromClass([self class]));
NSLog(@”%@”, NSStringFromClass([super class]));
}
return self;
}
@end
// 输出
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
这个题目主要是考察关于Objective-C中对self和super的理解。我们都知道:self是类的隐藏参数,指向当前调用方法的这个类的实例。那super呢?
很多人会想当然的认为”super和self类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实 super是一个 Magic Keyword,它本质是一个编译器标示符,和self 是指向的同一个消息接受者!他们两个的不同点在于:super会告诉编译器,调用class 这个方法时,要去父类的方法,而不是本类里的。
上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。
当使用self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用super时,则从父类的方法列表中开始找。然后调用父类的这个方法。
C16. runtime如何通过selector找到对应的IMP地址?
如下图所示,每个selector都与对应的IMP是一一对应的关系,通过selector就可以直接找到对应的IMP:
C17. objc中的类方法和实例方法有什么本质区别和联系?
类方法:
- 类方法是属于类对象的(所谓的类对象,不是class instance)
- 类方法只能通过类对象调用
- 类方法中的self是类对象
- 类方法可以调用其他的类方法
- 类方法中不能访问成员变量
- 类方法中不定直接调用对象方法
实例方法:
- 实例方法是属于实例对象的
- 实例方法只能通过实例对象调用
- 实例方法中的self是实例对象
- 实例方法中可以访问成员变量
- 实例方法中直接调用实例方法
- 实例方法中也可以调用类方法(通过类名)
C18. _objc_msgForward函数是做什么的,直接调用它将会发生什么?
参考答案:
_objc_msgForward是IMP类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
IMP msgForward = _objc_msgForward;
如果手动调用objcmsgForward,将跳过查找IMP的过程,而是直接触发”消息转发”,进入如下流程:
- 第一步:+ (BOOL)resolveInstanceMethod:(SEL)sel实现方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过class_addMethod函数动态地添加方法,消息得到处理,此流程完毕。
- 第二步:在第一步返回的是NO时,就会进入- (id)forwardingTargetForSelector:(SEL)aSelector方法,这是运行时给我们的第二次机会,用于指定哪个对象响应这个selector。不能指定为self。若返回nil,表示没有响应者,则会进入第三步。若返回某个对象,则会调用该对象的方法。
- 第三步:若第二步返回的是nil,则我们首先要通过- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector指定方法签名,若返回nil,则表示不处理。若返回方法签名,则会进入下一步。
- 第四步:当第三步返回方法方法签名后,就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等
- 第五步:若没有实现- (void)forwardInvocation:(NSInvocation *)anInvocation方法,那么会进入- (void)doesNotRecognizeSelector:(SEL)aSelector方法。若我们没有实现这个方法,那么就会crash,然后提示打不到响应的方法。到此,动态解析的流程就结束了。
C19. runtime如何实现weak变量的自动置nil?
参考答案:
runtime对注册的类会进行布局,对于weak对象会放入一个hash表中。 用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc。假如weak指向的对象内存地址是a,那么就会以a为键,在这个 weak 表中搜索,找到所有以a为键的weak对象,从而设置为nil。
weak修饰的指针默认值是nil(在Objective-C中向nil发送消息是安全的)
C20. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量
因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表 和instance_size实例变量的内存大小已经确定,同时runtime会调用class_setIvarLayout或 class_setWeakIvarLayout来处理strong和weak引用。所以不能向存在的类中添加实例变量;
能向运行时创建的类中添加实例变量
运行时创建的类是可以添加实例变量,调用class_addIvar函数。但是得在调用 objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
C以上来源《招聘一个靠谱的iOS(上)》 http://www.henishuo.com/interview-part-one/
D1. runloop和线程有什么关系?
正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loop是线程的基础架构部分, Cocoa 和 CoreFundation都提供了方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程都有与之相应的run loop。
参考答案:
-
主线程的run loop默认是启动的。iOS的应用程序里面,程序启动后会有一个如下的main()函数
- int main(int argc, char * argv[]) {
-
@autoreleasepool {
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
-
}
- }
- int main(int argc, char * argv[]) {
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
- 对非主线程来说,run loop默认是没有启动的。如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
-
在任何一个Cocoa程序的线程中,都可以通过以下代码来获取到当前线程的run loop:
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
D2. runloop的mode作用是什么?
参考答案:
mode主要是用来指定事件在运行循环中的优先级的,分为:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
- UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode
- UIInitializationRunLoopMode:run loop启动时,会切换到该mode
- NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的Mode有两个:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。
D3. 在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
参考答案:
RunLoop只能同时运行在一种mode下。因此,如果要换mode,那么当前的loop也需要暂停,并重启成新的mode。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动,只能在NSDefaultRunLoopMode模式下处理的事件会影响scrllView的滑动。
原因:如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的,所以Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:
// 将timer添加到NSDefaultRunLoopMode中
// 默认会自动添加到Run Loop中,但是mode为NSDefaultRunLoopMode
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
// 先添加定时器
// 默认会自动添加到Run Loop中,但是mode为NSDefaultRunLoopMode
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
// 手动修改其mode为forMode:NSRunLoopCommonModes
// 在滚动时,定时器也可以正常回调了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
D4. objc使用什么机制管理对象内存?
参考答案:
通过引用计数器(retainCount)的机制来决定对象是否需要释放。 每次runloop完成一个循环的时候,都会检查对象的 retainCount,如果retainCount为0,说明该对象没有地方需要继续使用了,可以释放掉了。
D5. ARC通过什么方式帮助开发者管理内存?
参考答案:
ARC相对于MRC,不是在编译时添加retain/release/autorelease这么简单。应该是编译期和运行期两部分共同帮助开发者管理内存。
- 在编译期,ARC用的是更底层的C接口实现的retain/release/autorelease,这样做性能更好,也是为什么不能在ARC环境下手动retain/release/autorelease,同时对同一上下文的同一对象的成对retain/release操作进行优化(即忽略掉不必要的操作)
- ARC也包含运行期组件,这个地方做的优化比较复杂,但也不能被忽略,手动去做未必优化得好,因此直接交给编译器来优化,相信苹果吧!
D6. BAD_ACCESS在什么情况下出现?
参考答案:
这种问题是经常遇到的,在开发时经常会出现BAD_ACCESS。原因是访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。
D7. 苹果是如何实现autoreleasepool的?
参考答案:
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.
- objc_autoreleasepoolPush
- objc_autoreleasepoolPop
- objc_autorelease
看函数名就可以知道,对autorelease分别执行push、pop操作。销毁对象时执行release操作。
D8. 使用block时什么情况会发生引用循环,如何解决?
参考答案:
一个对象中强引用了block,在block中又使用了该对象,就会发生循环引用。 解决方法是将该对象使用_weak或者_block修饰符修饰之后再在block中使用。
__weak __typeof(self) weakSelf = self;
比如,controller中有成员变量_name:
__weak __typeof(_name) weakName = _name;
self.vc = [[HYBController alloc] init];
vc.successBlock = ^(NSString *name) {
weakName = name;
// 如果直接使用_name,则会造成循环引用
// _name = name;
};
D9. 在block内如何修改block外部变量?
参考答案:
默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上__block来让其写操作生效,示例代码如下:
__block int a = 0;
void (^foo)(void) = ^{
a = 1;
}
foo();
D10. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?
参考答案:
系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api需要考虑。所谓”引用循环”是指双向的强引用,
所以那些”单向的强引用”(block 强引用 self )没有问题,比如这些:
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }];
[[NSNotificationCenter defaultCenter] addObserverForName:@”someNotification”
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * notification) {
self.someProperty = xyz;
}];
这些情况不需要考虑”引用循环”。
但如果你使用一些参数中可能含有成员变量的系统api,如GCD、NSNotificationCenter就要小心一点。比如GCD内部如果引用了 self,而且GCD的其他参数是成员变量,则要考虑到循环引用:
__weak __typeof(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^{
__typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
});
类似的:
__weak __typeof(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@”testKey”
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
__typeof__(self) strongSelf = weakSelf;
[strongSelf dismissModalViewControllerAnimated:YES];
}];
self –> _observer –> block –> self 显然这也是一个循环引用。
D11. GCD的队列(dispatch_queue_t)有哪此类型?
参考答案:
只有两种类型:
- 串行队列Serial Dispatch Queue
- 并行队列Concurrent Dispatch Queue
D12. 如何用GCD同步若干个异步调用?
参考答案:
举例:根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
当放到group中的所有请求都完成时,才会回调dispatch_group_notify的block:
ispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
D13. dispatch_barrier_async的作用是什么?
参考答案:
在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。
barrier是屏障的意思,就是说在处理竞争资源时,使用dispatch_barrier_async函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等dispatch_barrier_async追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。
打个比方:比如你们公司周末跟团旅游,高速休息站上,司机说:大家都去上厕所,速战速决,上完厕所就上高速。超大的公共厕所,大家同时去,程序猿很快就结束了,但程序媛就可能会慢一些,即使你第一个回来,司机也不会出发,司机要等待所有人都回来后,才能出发。 dispatch_barrier_async 函数追加的内容就如同 “上完厕所就上高速”这个动作。
注意:使用 dispatch_barrier_async ,该函数只能搭配自定义并行队列 dispatch_queue_t 使用。不能使用: dispatch_get_global_queue ,否则 dispatch_barrier_async 的作用会和 dispatch_async 的作用一模一样。
D14. 苹果为什么要废弃dispatch_get_current_queue?
参考答案:
dispatch_get_current_queue容易造成死锁。详情点击该API查看官方注释。
D15. 以下代码运行结果如何?
– (void)viewDidLoad {
[super viewDidLoad];
NSLog(@”1″);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@”2″);
});
NSLog(@”3″);
}
参考答案:
只输出:1,然后主线程锁死。也就是所谓的界面卡死,点击都没有反映,但是又不会闪退。在使用dispatch_sync时一定要小心再小心。
D16. 若一个类有实例变量NSString *_foo,调用setValue:forKey:时,可以以foo还是_foo作为key?
参考答案:
两者都可以。
D17. 如何调试BAD_ACCESS错误?
参考答案:
出现BAD_ACCESS错误,通常是访问了野指针,比如访问了已经释放了的对象。快速定位问题的步骤有:
- 重写对象的respondsToSelector方法,先找到出现EXECBADACCESS前访问的最后一个object
- 设置Enable Zombie Objects
- 设置全局断点快速定位问题代码所在行,接收所有的异常
- Xcode7已经集成了BAD_ACCESS捕获功能:Address Sanitizer,与步骤2一样设置
D18. lldb(gdb)常用的调试命令?
参考答案:
- breakpoint 设置断点定位到某一个函数
- n 断点指针下一步,猜测是next的缩写
- po打印对象,笔者猜测po是print object的缩写
- p与po类似,只是打印出来的是地址
D19. Apple用什么方式实现对一个对象的KVO?
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。我画了一张示意图,如下所示:
KVO 确实有点黑魔法:
Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。
下面做下详细解释:
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们只在希望能控制回调的调用时机时才会这么做。大部分情况下,改变通知会自动调用。
比如调用 setNow: 时,系统还会以某种方式在中间插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的调用。大家可能以为这是因为 setNow: 是合成方法,有时候我们也能看到人们这么写代码:
– (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@”now”]; // 没有必要
_now = aDate;
[self didChangeValueForKey:@”now”];// 没有必要
}
这是完全没有必要的代码,不要这么做,这样的话,KVO代码会被调用两次。KVO在调用存取方法之前总是调用 willChangeValueForKey: ,之后总是调用 didChangeValueForkey: 。怎么做到的呢?答案是通过 isa 混写(isa-swizzling)。第一次对一个对象调用 addObserver:forKeyPath:options:context: 时,框架会创建这个类的新的 KVO 子类,并将被观察对象转换为新子类的对象。在这个 KVO 特殊子类中, Cocoa 创建观察属性的 setter ,大致工作原理如下:
– (void)setNow:(NSDate *)aDate {
[self willChangeValueForKey:@”now”];
[super setValue:aDate forKey:@”now”];
[self didChangeValueForKey:@”now”];
}
这种继承和方法注入是在运行时而不是编译时实现的。这就是正确命名如此重要的原因。只有在使用KVC命名约定时,KVO才能做到这一点。
KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。这在Apple 的文档可以得到印证:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
然而 KVO 在实现中使用了 isa 混写( isa-swizzling) ,这个的确不是很容易发现:Apple 还重写、覆盖了 -class 方法并返回原来的类。 企图欺骗我们:这个类没有变,就是原本那个类。。。
但是,假设”被监听的对象”的类对象是 MYClass ,有时候我们能看到对 NSKVONotifying_MYClass 的引用而不是对 MYClass 的引用。借此我们得以知道 Apple 使用了 isa 混写(isa-swizzling)。
D以上来源《招聘一个靠谱的iOS(下)》 http://www.henishuo.com/ios-interview-part-two/
E1. 自我介绍
自我介绍时,一定要简洁明了,不要长篇大论。以我个人而言,最不喜欢自我介绍说了一大堆,最后连她/他叫什么名字都没记住。
参考答案:
自我介绍时,突出重点,说话慢一些,在关键点声音大一点。本人回答时,就简单地说: 我叫某某某,做iosX年了,曾在XX公司担任过XX职务,在YY公司担任过XX职务,主要负责ZZ工作。业余喜欢做NN(要说积极点的),擅长LL(把自己的特长说明白)等。
E2. 最近这两天你有学到什么知识/技能么?
对于这个问题,面试官肯定知道作为求职者,这两天肯定是在忙于找工作、面试。那么,面试官问出这样的问题的目的是什么?如果我是面试官,我最想了解的是这两天你为此次面试准备了什么而不仅仅是告诉面试官这两天学习了某一方面的知识。
参考答案:
这两天为了准备面试,整理了以前所做过的一些项目的笔记,回头看了看以前的工作日志。一来是整理一些在工作中经常遇到的坑,比如cell重用问题、ios6适配问题等;二来是回头告别过去的自己,在思想上、技术上迎来全新的自我;三来定位自己下一个目标:往架构师方向深入研究。
E3. 最近有做过比较酷或者比较有挑战的项目么?
这个问题的关键是酷和挑战。其实这里所说的酷对应于开发中的动画,而挑战则对应于开发中的冲刺。对于笔者而言,其实并没有做过特别酷的项目,但是做过有挑战性的项目。但是没有做过并不是就不用回答,面试官想看到的是你的学习能力、应用能力以及解决问题的能力,而不是一句没做过或者没什么挑战性这样的话语。
参考答案:
我之前所负责的项目大多是电商项目,因此并不会特别酷,但是业务比较多,很有技术挑战性。不过,平时我也深入研究过ios核心动画相关知识,对于常用的动画是很熟悉的。在我看来,用户体验并不是所谓的酷,而是简单、方便且明了。我很在意用户体验问题,在开发中会不断地站在用户的角度地问自己用户讨厌什么、喜欢什么和怎样才能让用户感觉容易上手且使用简单等问题。比如,我会很在意网络状态的变化给用户的提示、请求网络时右上角的转圈圈是否开启、滚动cell时是否有卡顿的问题等。
我待过几家公司,从一个人开发到带领团队,从小公司到大公司,因此对于不同的公司对项目的要求完全不一样。对于大公司,一般项目管理机制相对比较完善,而且会有比较多经验丰富的技术VP,因此对于工作的要求比较高,对于用户的体验及反馈会非常地关注;而对于一些小公司,可能就一个人在开发,而这个人往往是菜鸟的多,因此都是东拼西凑而形成的项目,技术不成熟、水平不够,而且还被压着不断加班,因此几乎不会过多关注用户体验问题,当然这样项目也不会有什么好的构架(初创技术合伙人除外)。
现在我所在的公司不算大,也就1000+号人,而做ios也才40号人左右。本公司是按业务方向划分成多个团队,不同团队开发不同的业务需求,因此这样就面临技术架构问题、安全问题、团队开发如何做到互不干扰等问题了。而我在团队中的主要职责是处理团队之间冲突的问题、如何代码模块化以减少团队之外的依赖问题、移动端安全通信问题、项目存储安全问题、公共框架等问题,这一系列都是非常有技术挑战的,需要花费很多非工作时间去调研、写demo、写文档等。
关于动画的学习,笔者的博客有相关专题:iOS Core Animation
E4. 最近看过的书/文章有哪些?
询问最近看过的书或者文章,其实通过所回答的书的性质差不多就可以猜出当前状态下应聘者的技术水平大致处于什么样的水准了。下面的参考答案是笔者的常态。
参考答案:
最近在看《iOS应用逆向工程》、《The Swift Programming Language》。不过本人更喜欢的是阅读博客文章和官方文档,虽然官方文档是英文的,阅读起来相对要费劲一些,但是一方面可以提高英文阅读能力,另一方面英文原版表达的语义才是最准确的,其他翻译过来的文章会有一些变味之处。
E5. 为什么要学习编程,编程对你而言的乐趣在哪儿?
这样的话题在很多社区都出现过,其实问这样的问题只是想知道应聘者的态度而已。通过应聘者的回答,一方面可初步了解应聘者对编程的认知程度,另一方面可从应聘者口出得出编程对于应聘者而言是什么样的态度。下面是结合笔者的事迹写下的参考答案,仅供参考。
E6. 如果一个函数10次中有7次正确,3次错误,问题可能出现在哪里?
这样的问题通过应聘者的分析,可以知道应聘者的功底如何。很多人的回答会是很简单的,没有从多方面去分析。这样的问题也是很有意义的,在项目开发中所产生的bug,有的时候会出现这样的情况,而代码量比较大且业务比较复杂时,通过其他工具并不能分析出来是什么bug,但是我们却可以根据出现的频率推测。笔者把这个问题当作测试部反馈过来的bug描述问题来分析一下。
参考答案:
从问题描述可知,bug不会必现的,因此无法直接定位出错之处。从以下角度出现来分析可能出错之处:
- 因出错并不是崩溃,因此没有错误日志可看。第一步就是分析函数中的所有分支,是否在语法上存在可能缺少条件的问题。所以,检查所有的分支,确保每个分支执行的结果的正确的
- 检测函数的参数,保证必传参数不能为空,若为空应该抛出异常。因此,用断言检测参数的正确性是很重要的。
- 检测函数中每个分支所调用的函数返回结果是正确的,其实就是一个递归的过程(步骤1、2)
E7. 自身最大优点是什么,怎么证明?
人最大的敌人不是别人,而是自己。战胜自己,才是最大的胜利。很多人不清楚自己的优点是什么,甚至很多朋友喜欢说我最大的优点是没有缺点。如果是对面试官说这一句话,那么你可能被pass掉了。
参考答案:
我也不清楚我最大的优点是什么,但是我知道我有很多优点。
- 我学习能力特别强,接受新事物的能力也特别强。比如,我在工作之余还会去学习swift、PHP、js等。
- 我喜欢写博客、写总结、分享技术、帮助他人等。我觉得写博客的过程,既让自己对相关知识有更深刻的认识,更是帮助到他人。每做一期需求,我都会写一份总结,记录那些坑。在公司每个季度都会做几次技术分享,带动团队的技术氛围。我也喜欢帮助他人,我创建了自己的技术群,短短1个月群就满500人了,在群里通过回答大家问题,也让我了解到很多知识。笔者有好几个博客,不过现在自己搭建了个博客,以后会专门维护标哥的技术博客
- 我支持开源、喜欢开源。我写了几个开源库,大家若是觉得有价值,请随手给个star:标哥的GITHUB
- 我开发过多款App,解决问题的能力很强。在团队中充当技术主心骨,任何队员解决不好的问题,我都会帮助一起解决掉。
- 我对技术构架、团队如何解藕方面都有所研究。在团队开发中,因为经常面临团队开发存在交差的问题,导致需求变动引起很多问题,因此研究过如何让团队之间减少依赖的问题。
- 我活跃于GITHUB、CocoaChina、CSDN等,对于iOS相关技术知识比较熟悉。
就说这么多吧!(因为面试高级人员通常会交谈3个小时左右,所以尽可能地说吧,不要害怕时间过长)
E8. 有没有在 GitHub 上发布过开源代码,参与过开源项目?
github上的开源项目可以体现应聘者的水平以及对编程的热爱程度。一个不足够热爱编程的人,业余时间是不会花在编程上的,因此更不会有什么开源项目了。
参考答案:
这里我的开源库的地址标哥的GITHUB,里面除了一些开源库之外,还有很多的demo,每个demo都有对应的博客文章讲解,那都是我感觉学习的成果。
我在GITHUB上发布过很多开源代码,也提供了支持cocoapods的开源项目,现在也有不少人在使用,当然我也会一直维护着,不过我并没有参与过其他人发起的开源项目。
E9. 你最近遇到过的一个技术挑战是什么?怎么解决的?
通过应聘者回答所遇到过的技术挑战,其实从侧面就可以看出这个人的水平如何了。如果回答的技术挑战是个简单的问题,而在应聘者这里却是技术挑战,那么就可以知道这水平是初级的。然后应聘者针对这个技术挑战所给出的解决方案也可以看出面对技术挑战,可以看出应聘者处理问题的能力。
参考答案:
最近公司项目中的用户账号出现被盗现象,原因是通信安全问题处理不好。因为公司的项目已经是好几年的老项目了,包括服务端的接口好多是老接口,原来是没有处理任何加密的,因此很容易被盗取账号。现在我们的技术VP要求针对这个问题,做一个版本。因为主动接受挑战,所以这个重任落在了我的身上,由我来牵头做好这个需求。
这真的是一个很有挑战性的技术项目。步骤如下:
- 需要调研市场上比较有名的App,他们是如何做好安全通信问题的;
- 写好技术文档,将调研结果反馈出来并写出自己的技术方案;
- 开初步技术方案评审会,会有VP及各组Leader参与,会上会提出各种问题,并给予一一解答,然后做会议记录,会后继续完善文档;
- 开跨部分评审会,只有所有都通过了,才能立项。
- 技术立项,然后写好各方向所需要做的工作文档
为什么要这么麻烦?因为我们既要兼容以前的所有版本,又要保证技术安全,那就不会自己就能说了算的,而且也不仅仅是客户端的问题。
E10. 开发常用的工具有哪些?
通过回答这个问题,一方面可以看出这个应聘者在iOS开发领域的深入程度。如果只知道Xcode,Cocoapods,说明是初级或者根本不愿意在业余时间花费精力去扩展。
参考答案:
常用的iOS开发工具有:
- Xcode开发工具及配套的Instruments工具
- Xcode常用的插件
- Cocoapods第三方库管理依赖工具
- SourceTree是git版本管理工具
- CornerStone是SVN版本管理工具
- 友盟统计BUG日志分析工具
E11. 熟悉CocoaPods么?能大概讲一下工作原理么?
这个问题不会回答也没有关系,因为很多老项目是不使用CocoaPods的,因此不一定会了解。 回答说使用过Cocoapods写过demo,但是不太懂工作原理是没有关系的。因为在我看到这个问题之前,我也没有深入了解过其工作原理,只是熟悉如何使用而已。
参考答案:
关于其原理,大家百度一下或者谷歌一下吧!因为笔者对其工作原理也不会很清楚,只知道它会为我们创建一个工作区间,然后将所有在cocoapods中的引入的第三方库以libPods.a这样的方式引入到我们的工程中,这样就可以直接访问第三方库了。但是,更具体的细节就不了解了,大家想要深入了解的话, 还得找谷歌或者百度。
E12. 最常用的版本控制工具是什么,能大概讲讲原理么?
关于这个版本控制工具的工作原理,其实也就是对这此命令的操作而已。
参考答案:
最常用的版本控制工具有SourceTree(GIT)和CornerStone(SVN):
E13. 今年你最想掌握的一门技术是什么?为什么?目前已经做到了哪个程度?
既然是技术,那么就要说明是什么技术,至于为什么想要掌握,当然是想要在技术上更上一层楼。
参考答案:
我现在一直在研究runtime相关知识。掌握runtime相关技术,可以做很多正常状态下做不到的事、可以让做一些自动化处理工作、解决代码依赖问题等。目前已经对runtime中的成员变量、属性、消息转发、Swizzling等可以熟练使用。关于runtime专题,大家可以阅读我的博客专题:iOS Runtime相关知识点
E14. 你一般是怎么用Instruments的?
这个就是工作经验的问题了。Instruments工具里面有很多个选项,没有必要每个都答,其实笔者也只用过里面的几个而已。
参考答案:
- 使用Allocations来检测内存和堆栈信息
- 使用Leaks检测内存的使用情况,包括内存泄露问题
- 使用Zombies来检测过早释放的僵尸对象,通过它可以检测出在哪里崩溃的。
- 使用Time Profiler来检测CPU内存使用情况
E15. 你一般是如何调试Bug的?
这个问题看起来很笼统,但又一针见血。通过应聘者的回答,可很直观地看出这个应聘者的处理bug的能力,以及其解决问题的思维。
参考答案:
Bug分为测试中的Bug和线上的Bug:
- 线上Bug:项目使用了友盟统计,因此会有崩溃日志,通过解析dYSM可以直接定位到大部分bug崩溃之处。解决线上bug需要从主干拉一个新的分支,解决bug并测试通过后,再合并到主干,然后上线。若是多团队开发,可以将fix bug分支与其他团队最近要上线的分支集成,然后集成测试再上线。
- 测试Bug:根据测试所反馈的bug描述,若语义不清晰,则直接找到提bug人,操作给开发人员看,最好是可以bug复现。解决bug时,若能根据描述直接定位bug出错之处,则好处理;若无法直观定位,则根据bug类型分几种处理方式,比如崩溃的bug可以通过instruments来检测、数据显示错误的bug,则需要阅读代码一步步查看逻辑哪里写错。
对于开发中出现的崩溃或者数据显示不正常,那就需要根据经验或者相关工具来检测可能出错之处。当然,团队内沟通解决是最好的。
E16. 你在你的项目中用到了哪些设计模式?
项目中使用了很多的设计模式,我相信面试官最好听到的不仅仅是设计模式的名字,更想听到的是这些设计模式在项目中如何应用。因此,笔者认为这个问题隐式地说明了应该回答设计模式及其在项目中的应用。
参考答案:
- 单例设计模式:在项目中,单例是必不可少的。比如UIApplication、NSUserDefaults就是苹果提供的单例。在项目中经常会将用户数据管理封装成一个单例类,因此用户的信息需要全局使用。
- MVC设计模式:现在绝大部分项目都是基于MVC设计模式的,现在有一部分开发者采用MVVM、MVP等模式。
- 通知(NSNotification)模式:通知在开发中是必不可少的,对于跨模块的类交互,需要使用通知;对于多对多的关系,使用通知更好实现。
- 工厂设计模式:在我的项目中使用了大量的工厂设计模式,特别是生成控件的API,都已经封装成一套,全部是扩展的类方法,可简化很多的代码。
- KVC/KVO设计模式:有的时候需要监听某个类的属性值的变化而做出相应的改变,这时候会使用KVC/KVO设计模式。在项目中,我需要监听model中的某个属性值的变化,当变化时,需要更新UI显示,这时候使用KVC/KVO设计模式就很方便了。
就说这么多吧,还有很多的设计模式,不过其它并不是那么常用。
E17. 如何实现单例,单例会有什么弊端?
单例在项目中的是必不可少的,它可以使我们全局都可共享我们的数据。这只是简单的问题,大家根据自己的情况回答。
参考答案:
- 首先,单例写法有好几种,通常的写法是基于线程安全的写法,结合dispatch_once来使用,保证单例对象只会被创建一次。如果不小心销毁了单例,再调用单例生成方法是不会再创建的。
- 其次,由于单例是约定俗成的,因此在实际开发中通常不会去重写内存管理方法。
单例确实给我们带来的便利,但是它也会有代价的。单例一旦创建,整个App使用过程都不会释放,这会占用内存,因此不可滥用单例。
E18. iOS是如何管理内存的?
我相信很多人的回答是内存管理的黄金法则,其实如果我是面试官,我想要的答案不是这样的。我希望的回答是工作中如何处理内存管理的。
参考答案:
- Block内存管理:由于使用block很容易造成循环引用,因此一定要小心内存管理问题。最好在基类controller下重写dealloc,加一句打印日志,表示类可以得到释放。如果出现无打印信息,说明这个类一直得不到释放,表明很有可能是使用block的地方出现循环引用了。对于block中需要引用外部controller的属性或者成员变量时,一定要使用弱引用,特别是成员变量像_testId这样的,很多人都没有使用弱引用,导致内存得不到释放。
- 对于普通所创建的对象,因为现在都是ARC项目,所以记住内存管理的黄金法则就可以解决。
E19. 使用过哪些第三方库
开发过App,如果回答说没有使用过第三方库,那么这个人一定是刚入门。如果回答者能够说出很多有名的第三方库,并且能说明使用场景,那么可以突出这个面试者的知识面还是很广的,这是可以加分的。
参考答案:
关于常用的第三方库,笔者整理了一下自己常用的库,但并不是全部:http://www.henishuo.com/ios-thirdparty/
E20. 对多线程了解吗
对GCD熟悉吗?iOS中实现多线程有哪些方式?与这些问题一样,都是考查多线程的知识。
参考答案: