又到了跳槽季,这几天公司web的兄弟在准备招人,准备了一大堆的问题等着虐来面试的人,我说这些问题换成你在提前不做准备的情况下也够呛能回答得很准确。web兄弟也很认同。但是面试不就是这样吗,不问点有料的东西怎么能表现出面试官很有水准呢?
所以赶紧理了理iOS的基础问题,帮助自己复习一遍平时开发中不常关注的点,如果同时能帮到别人的话更好。另外博主水平有限,如果下面的内容中有错误还请提点。
以下问题不分主次,内容会慢慢更新。
关于block
1、block的类型
在block runtime中定义了6种类。
_NSConcreteStackBlock 栈上创建的block
_NSConcreteMallocBlock 堆上创建的block
_NSConcreteGlobalBlock 作为全局变量的block
_NSConcreteWeakBlockVariable
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
其中只有前三种能够用得到。在ARC下只有_NSConcreteMallocBlock和_NSConcreteGlobalBlock,因为在ARC开启时,ARC会自动处理block的内存管理操作。(所以ARC下block属性声明成copy 或 strong都可以,而MRC下必须是copy)。
2、block的变量捕获
block可以修改全局变量,方法内statice修饰的变量和有__block关键字的栈变量和传入block的参数。对于普通的栈变量只可访问不可修改。
这里需要注意的是下面这个例子:
NSInteger i = 1;
void (^aBlock)() = ^(void){
NSLog(@“i = %ld", i);
};
i = 2;
aBlock();
这时的输出结果为i = 1
而这个例子中
__block NSInteger i = 1;
void (^aBlock)() = ^(void){
NSLog(@“i = %ld", i);
};
i = 2;
aBlock();
输出结果为i = 2。
二者的区别是在变量i声明时增加了block关键字。导致输出结果不同的原因是在block访问外部变量时,默认会将变量copy到他自己的数据结构中(说人话就是把外部的变量复制一份给自己,对应到例子中就是把i=1拷贝一份自己存起来,i=2就不管了)。所以调用block时它输出的还是自己记住的i=1。而在声明了block关键字之后block就不会copy变量,而是copy变量的内存地址,所以在程序执行了i=2后再调用block,block会从相同的地址拿到数据,这时这里面的数据已经是2。
block的数据结构可以参考下面连接中巧神的讲解。(面个试连block的数据结构都要问一遍,哥哥你面的是BAT吗- -)。
block中的循环引用
要知道block本身也是一个对象,明白这一点就比较好理解为什么block会发生循环引用了。
比如经常会出现self引用了block,而block里面又引用了self的情况,上升到对象层面就是对象A引用了对象B,对象B又引用了对象A,这样就形成了一个标准的保留环。
解决方法当然是破坏掉这个环中的某一节,即用weak来避免双向的强引用。
参考:
谈Objective-C block的实现
objc 中的 block
关于runtime
OC的运行时机制由runtime库实现,runtime库做了两件事
1、封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
2、找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。
针对第一点,可以展开为类与对象的组成。首先要知道类的本质也是对象。因为class类型实际是一个指向objc_class结构体的指针。而在这个结构体中存在一个isa指针,凡是首地址是*isa的结构体指针,都可以被认为是objc中的对象(isa指针的作用:运行时可以通过isa指针,查找到该对象是属于什么类(Class))。
既然类也是对象,那么它是属于什么类的呢?答案是类的isa指针指向其元类(meta class)。元类的isa指向根元类(NSObject的元类),根元类的isa指向它自己。也就是下面这张被看烂了的关系图。
图片来源
当我们向一个对象发送消息时,runtime会在这个对象所属类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。比如[[objA alloc] init]这个行代码,alloc方法会保存在meta-class方法列表中,init方法会保存在class方法列表中。
关于第二点,要涉及到OC中的消息机制。我们通常所说的调用方法,在OC中准确的叫法应该是“消息传递”。在OC中,如果向对象发送消息,就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是C函数,对象收到消息后究竟要调用哪个方法完全在运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门动态语言。
[objA message:parameter]这行代码中,objA是接收者,message是选择子(selector),选择子和参数的组合叫做消息。当编译器看到这条消息,会把它转换成标准的c函数调用(objc_msgSend)。objc_msgSend函数会依据接收者和选择子的类型来调用适当的方法。
具体的操作是会在接收者所属的类对象中搜索其方法列表,如果能找到与选择子名称相符的方法,就跳至其实现代码,如果没找到就沿着该类的继承体系继续寻找,直到找到对应的实现。如果没找到的话就会执行消息转发操作(一会说)。
在每个类中都有一块缓存,用来存储“快速映射表”,快速映射表的存在是为了解决每次执行消息时都要走一次上面所述的繁琐流程。它将曾经被调用过的方法保存在表中。在objc_msgSend执行的时候会优先在“快速映射表”里面查找对应的方法,找到了就会立即返回,从而避免了再次逐个类搜索方法实现的繁琐流程。
关于消息转发,这是一个比较模板化的东西,这里只描述消息转发流程,不做具体使用讲解(写下来又是一大堆)。首先对象在收到未知的消息时会执行其所在类的+resolveInstanceMethod:方法(对应的还有+resolveClassMethod:方法,一个是在收到未知对象消息时,一个是收到未知类消息时)。使用这个方法的前提是解决此未知消息的相关方法实现已经写好,只需要在执行+resolveClassMethod:/+resolveInstanceMethod:方法时利用runtime动态插入到类里面就可以。
如果上一步没有成功解决问题,还有第二次机会,这次尝试把该未知消息传递给其他接收者来处理。对应方法为- (id)forwardingTargetForSelector:(SEL)aSelector,如果该方法返回了一个非nil对象,则该未知消息会被发送给此对象。需要注意在这一步不可以对消息做任何操作。
如果在第二次机会仍然没有完成对消息的处理,则会触发完整的消息转发机制。首先创建NSInvocation对象,将未处理的消息的全部细节封装其中,此步骤会调用forwardInvocation:方法来转发消息。
参考:
Objective-C 中的类和对象
《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》
load和initialize的区别
1、运行时间的区别
当类文件被加载时load方法就会被调用,也就是说无论这个类在实际应用时是否被你调用到,load方法都会被执行。
而initialize是在第一次向该类发送消息时才会被调用,比如[ClassA alloc],当向ClassA类发送alloc消息时,initialize方法才会被执行。
2、关于父类的调用
首先两个方法都不需要手动调用父类方法([super load]/[super initialize]),区别是当子类没有实现load方法时,不会调用父类的load,而initialize则会。
3、关于线程安全
两个方法都是线程安全的,所以尽量避免执行阻塞现成的操作。
4、用途
load方法多用于Method swizzle操作,initialize多用于初始化全局变量或静态变量(博主水平有限,表示initialize从来没用过。)
关于内存管理
引用计数
OC语言使用引用计数来管理内存,在iOS系统上从来也没有垃圾回收机制。在引用技术架构下,每个对象都拥有一个计数器,用来表示当前有多少个事物想另此对象继续存活下去。即为引用计数。NSObject协议中有三个方法来操作引用计数。retain,release,autorelease。对象创建后retainCount为1,若有对象想让其继续存活,则引用计数+1(调用retain方法),若不想让其继续存活则引用计数-1(调用release方法)。当引用计数为0时,系统会将该对象所在的内存标记为“可重用”,即其他对象想要使用内存时可以占用这块地址。所有指向该地址的引用也将无效。
这里有一个问题,就是当某个对象的引用计数为0时再次访问此对象依然有效的问题。关于这个问题的解释是当引用计数为0后,该对象所占的内存只是被放回了可用内存区域,如果这时候这块内存尚未被覆写,那么该对象依然有效。这种访问方式极其的危险(只在MRC时代比较常见,ARC下很少会发生这种情况)。可以通过将指针置nil来避免误操作。
ARC
在ARC下,引用计数还是会执行的,只不过所有的操作由ARC代为完成。在ARC下,retain,release,autorelease和dealloc方法是不允许被手动调用的。
在使用ARC时必须遵循方法命名规则,若方法名以alloc,new,copy,mutableCopy开头,则返回的对象归调用者所有。人话版就是这些词语开头的方法返回的对象需要调用者自己release,否则不需要,因为以其他单词开头的方法返回对象时会被自动加上autorelease。
其实这些操作全部都由ARC去处理,但是原理是这样要清楚。
ARC下的所有权修饰符
ARC有效时,id类型和对象类型同C语言其他类型不同,其类型上必须附加所有权修饰符。包括以下四种:
strong
weak
unsafe_unretained
autoreleasing
strong是默认的所有权修饰符,表示对对象的强引用。也就是说当我们在ARC中写下这样的代码时:
id *obj =[ [NSObject alloc] init];
实际上是这样的
id __strong *obj =[ [NSObject alloc] init];
weak的出现是为了解决在引用计数中必然会出现的问题,就是循环引用问题。当两个对象互相都使用strong修饰符互相持有时,就发生了循环引用。weak提供了与strong相反的弱引用。弱引用不持有对象,也就是当对象不再被任何强引用持有时,即使存在弱引用也仍然会释放对象。这个弱引用会自动失效并置nil。
unsafe_unretained提供与weak类似的功能,不同点在于引用失效后不会自动设置为nil。它的存在是因为在iOS4以前并没有weak修饰符,只有unsafe_unretained。
autoreleasing修饰符替换了MRC下的autorelease方法。虽然在ARC下NSAutoreleasePool和autorelease方法不能被使用。但是ARC有效时autorelease还是起作用的。下面两端代码是等效的:
//MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id *obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
//ARC
@autoreleasepool{
id __autoreleasing *obj = [[NSObject alloc] init];//__autoreleasing不必要显示的写出来。
}
参考:
《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》
《Objective-C高级编程 iOS与OS X多线程和内存管理》
tableView的优化
1、避免出现透明的控件,也就是给控件一个backgroundColor。
2、避免发生过多的离屏渲染。
3、针对不定高度的cell,提前缓存高度。
4、异步加载图片并解码(这里有一篇我写的关于图片加载的文章)。
copy
copy方法实现
想让自定义的类实现copy操作,需要实现NSCopying协议。
- (id)copyWithZone:(NSZone *)zone
协议中只有一个方法,关于NSZone是一个历史问题,以前的程序开发会根据这个参数把内存分区,即zone,创建的对象会在区里面。现在只有一个默认分区。所以不用管它。比如要将一个model类实现copy操作:
- (id)copyWithZone:(NSZone *)zone{
NELMineModel *model = [[[self class] allocWithZone:zone] init];
return model;
}
如果类中还有其他的属性需要一并拷贝,那就要都写上,比如这样:
- (id)copyWithZone:(NSZone *)zone{
NELMineModel *model = [[[self class] allocWithZone:zone] init];
model.datas = [self.datas mutableCopy];
return model;
}
对应的如果要实现mutableCopy,就要实现NSMutableCopying协议。
集合的copy
Foundation框架中的所有集合类默认都执行浅拷贝,也就是之拷贝容器本身,对容器内的存储对象不做拷贝(因为对象未必都支持拷贝操作)。也就是说拷贝的集合与原集合内的对象共用同一块内存空间,修改拷贝集合中的对象(注意是修改集合中的对象,不是对这个集合做操作)同时也会对原集合中的对象产生影响。
如果要使集合进行深拷贝,需要使用下面这个方法:
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
如果copyItems参数为YES,则该方法会向array里面的每个元素发送copy消息,用拷贝好的元素创建新的集合返回给调用者。
修改上面的代码:
- (id)copyWithZone:(NSZone *)zone{
NELMineModel *model = [[[self class] allocWithZone:zone] init];
model.datas = [[NSMutableArray alloc] initWithArray:self.datas copyItems:YES];
return model;
}
重写一个带copy关键字属性的setter
@property (nonatomic, copy) NSString *title;
- (void)setTitle:(NSString *)title{
_title = [title copy];
}
为什么这么写,和strong有什么区别?用_代替self避免递归调用。使用copy后无论传入的是可变(NSString)还是不可变类型(NSMutableString)使用copy后都会变为不可变类型,方式数据被无意间变动。而使用strong则会直接将指针指向原地址,如果传入的NSString没问题,如果是可变类型一旦数据发生改动将会被一并改动。
如何手动通知KVO
首先要在被观察对象的类里面重写+ (BOOL)automaticallyNotifiesObserversForKey:方法。然后在需要调用KVO的地方增加willChangeValueForKey:和didChangeValueForKey:两个方法。具体看这里
SEL和IMP的区别
SEL(选择器),是表示一个方法的selector的指针,每一个方法都有一个对应的SEL。SEL只是一个指向方法的指针,用来查找对应的IMP。
IMP实际上是一个函数指针,指向方法实现的首地址,定义如下:
id (*IMP)(id, SEL, ...)
第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址,如果是类方法,则是指向元类的指针),第二个参数就是用来查找到对应IMP的SEL。
Autorelease对象什么时候释放
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
Autoreleasepool的使用场景
当需要在循环中大量创建临时变量时,为了避免内存峰值过高,需要手动写上@autoreleasepool。还有其他的使用场景求告知。
网友评论