1.Foundation对象与Core Foundation对象有什么区别
- Foundation对象是OC的,Core Foundation对象是C对象
- 数据类型之间的转换
- ARC:__bridge_retained、__bridge_transfer
- 非ARC: __bridge
2.KVO内部实现原理
- 1.KVO是基于runtime机制实现的
-
2.当某个类的对象第一次被观察时, 系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制(NSKVONotifying_Dog)。我们注册监听的时候,底层找方法的的isa指针就变成指向新创建的子类对象也就是NSKVONotifying_Dog这个新类。
派生出的问题:
1.为什么用的是子类而不是分类?
答:因为如果用分类的话,那么Dog的子类就不走了,肯定不行。
2.你可否自己实现kvo?
答:kvo是给予runtime机制的。具体的请点击
https://blog.csdn.net/u014247354/article/details/78403567
3.kvo的局限性?
答:kvo只能监听属性的改变;再一个,运行时时创建派生类比较耗性能。
3.手写单例
#import "MMTool.h"
@implementation MMTool
static MMTool *_mmTool = nil;
+(instancetype)allocWithZone:(struct _NSZone *)zone{
if (_mmTool == nil) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_mmTool = [super allocWithZone:zone];
});
}
return _mmTool;
}
+(instancetype)sharedMMTool{
return [[self alloc]init];
}
4.runtime
1.必备常识
- Ivar : 成员变量
- Method : 成员方法
- objc_msgSend : 给对象发送消息
- class_copyMethodList : 遍历某个类所有的方法
- class_copyIvarList : 遍历某个类所有的成员变量
5.关于block
下面是面试题:
1.
int a = 10;
void (^block)() =^{
NSLog(@"打印结果是 %d",a);
};
a = 20;
block(); //打印结果是 10
----------------------------------------------------------------------------
2.
static int a = 10;
void (^block)() =^{
NSLog(@"打印结果是 %d",a);
};
a = 20;
block(); //打印结果是 20
----------------------------------------------------------------------------
3.
__block int a = 10;
void (^block)() =^{
NSLog(@"打印结果是 %d",a);
};
a = 20;
block(); //打印结果是 20
----------------------------------------------------------------------------
4.
int a = 10;
- (void)tes{
void (^block)() =^{
NSLog(@"打印结果是 %d",a);
};
a = 20;
block(); //打印结果是 20
}
除了第一种是值传递之外,剩下的只要有修饰(__block、static)、或者全局变量修饰的都是指针传递。
用static修饰、和一个全局变量,本质是一份内存,所以是指针传递,也就是内存地址传递。前后的内存地址是一样的
用__block修饰,其本质是:会把“外部变量”在栈中内存地址放到堆中。前后的内存地址不一样
除了第一种不能在block中修改a的值,剩下的三种都是可以修改a的值。
1.在用__block修饰的时候,是把“外部变量”在栈中内存地址放到堆中,所以当我们修改a的值的时候,其实是在栈去进行修改。
2.而不用__block修饰的时候,再去修改a的值,那就是在栈中进行修改,然后就会报错。
所以:栈中的数据不能修改,内存由系统释放。堆中的数据可以修改,所以想修改,就用__block修饰。
关于block的循环引用问题:
block用从copy修饰。那么内存就在堆区,copy一次,就会retain,引用对象的计数器就会加1. 所以用block就用__block(MRC);
ARC :id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏。
就可以保证对象在block中引用的时候,引用计数器不会增加。
关于block【RAC中的一个例子】
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
RACDynamicSignal *signal = [[self alloc] init];
signal->_didSubscribe = [didSubscribe copy];
return [signal setNameWithFormat:@"+createSignal:"];
}
(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
RACDisposable * :是返回值类型
id<RACSubscriber> subscriber:指的是传入的参数
didSubscribe : block起的别名
6.关于MRC开发
原则:
- 有alloc\init\copy的,都要对应的使用release或者autorelease
- 有retain的都要使用一次release或者autorelease。
.h文件:
@property (retain, nonatomic) Dog * dog;
@end
@implementation Person
- (void)setDog:(Dog *)dog{
if (_dog != dog) {
[_dog release]; //开始指向空指针,给空指针发消息是OK的
[_dog retain]; //引用计数加一,然后_dog = dog
}
}
- (void)dealloc{
[self.dog release];
self.dog = nil;
[super dealloc];
}
@end
在dealloc方法里面为什么要进行release? 在set为什么要判断_dog!=dog?然后再release再retain?
答:在dealloc里面release 是因为,对dog对象进行了retain,所以要release。
解释set方法
如果没有if判断
p.dog = dog1;
p.dog = dog1;
[d release]
p.dog = dog1; //这句就会崩溃,因为dog1的引用计数为0。
加了判断后,意思就是:如果是同一对象,那么就不管。如果是新对象,那么就释放旧对象然后对新对象retain一次。
7.多线程的底层实现?
1.首先弄清楚什么是线程,什么是多线程。
2.Mach是第一个以多线程方式处理任务的系统,因此多线程的底层实现机制是基于Mach的线程
3.开发中很少用Mach级的线程,因为Mach级的线程没有提供多线程的基本特征,线程之间是独立的
开发中实现多线程的方案
1.C语言的POSIX接口:#include <pthread.h>
2.OC的NSThread
3.C语言的GCD接口(性能最好,代码更精简)
4.OC的NSOperation和NSOperationQueue(基于GCD)
线程间怎么通信?
1.performSelector:onThread:withObject:waitUntilDone:
2.NSMachPort
8.网络图片处理问题中怎么解决一个相同的网络地址重复请求的问题?
利用字典(图片地址为key,下载操作为value)
/**定义字典保存下载好的图片key 图片的地址 value 图片UIImage*/
@property (nonatomic, strong) NSMutableDictionary *imagesDic;
/**定义字典保存下载操作key 图片地址 value 下载操作*/
@property (nonatomic, strong) NSMutableDictionary *operationsDic;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
// 1.根据下载地址去字典中取图片
UIImage *image = self.imagesDic[@"http://www.baidu/example.png""];
// 2.判断字典中是否有图片(是否已经下载过图片)
if (image == nil) {
// 没有下载过(1.从来没有下载过, 2.没有下载完,正在下载中)
NSOperation *operation =self.operationsDic[@"http://www.baidu/example.png"];
if (operation == nil) {
// 1.从来没有下载过
// 创建操作
operation = [NSBlockOperation blockOperationWithBlock:^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.baidu/example.png"]];
UIImage *image = [UIImage imageWithData:imageData];
// 将下载好的图片存储到字典中
self.imagesDic[@"http://www.baidu/example.png""] = image;
// 将操作从字典中移除
self.operationsDic[@"http://www.baidu/example.png""] = nil;
// 回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
}];
// 将操作放入队列,会自动开辟一个线程
[self.queue addOperation:operation];
// 将操作添加到字典中,这里可以用来判断是不是正在下载
self.operationsDic[@"http://www.baidu/example.png"] = operation;
}else
{
// 2.没有下载完,正在下载中
cell.imageView.image = [UIImage imageNamed:@"占位图片"];
}
}else
{
// 已经下载过
cell.imageView.image = image;
}
return cell;
}
思路
9.你用过NSOperationQueue么?如果用过或者了解的话,你为什么要使用NSOperationQueue,实现了什么?请描述它和GCD的区别和类似的地方(提示:可以从两者的实现机制和适用范围来描述)。
1.GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本封装
2.GCD只支持FIFO的队列,NSOperationQueue可以很方便地调整执行顺序、设置最大并发数量、依赖、栅栏
3.NSOperationQueue可以在轻松在Operation间设置依赖关系,而GCD需要写很多的代码才能实现
4.NSOperationQueue支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)
5.GCD的执行速度比NSOperationQueue快
- 任务之间不太互相依赖:GCD
- 任务之间有依赖\或者要监听任务的执行情况:NSOperationQueue
10.使用 [UIImage imageNamed:@"example.png"];会有怎样的影响?
imageNamed加载图片
1.用 imageNamed:方法加载的图片不会随着UIImageView一起销毁,而是依旧在内存中。【用instrument实时看的】
2.加载时耗的内存比较大
3.相同的图片只会加载一份到内存中,如果使用多次或者同时使用,使用的是同一个对象。
imageWithContentsOfFile加载图片
1.用 imageWithContentsOfFile:方法加载的图片会随着UIImageView一起销毁
2.加载时耗的内存相对于imageNamed方法要小
3相同的图片会多次加载到内存中 ,如果同时使用,使用的是不同的对象。
- 总结:imageNamed:如果一些图片在多个界面会使用,并且图片较小、使用频率高。例如:图标和小的背景图。
- imageWithContentsOfFile:只在一个地方用、并且图片较大、使用频率不高。
11.关于self和super关键字
Self关键字
1.如果self在对象方法中,那么self就代表调用当前对象方法的那个对象
2.如果self在类方法中,那么self就代表调用当前类方法的那个类。
总结:我们只关注self 在哪个方法中。
Super关键字
1.如果想在子类方法中调用父类的方法,就可以用super。
2. super 是一个 Magic Keyword, 它本质是一个编译器标示符,super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。
- 一个面试题
下面的代码输出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
都输出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
解释:https://www.jianshu.com/p/10b65df06e44
1.[self class]的意思是先去子类中找class方法;[super class]的意思是先去Father父类中找class方法;
2.[self class]没有找到,最后找到NSObject里面找到class方法,而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。[super class]也是一样,最后也在NSObject里面找到class方法...
- 如果改成这样
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
-----------------------------------------
@implementation Father
- (Class)class{
[super class];
return [Father class];
}
@end
都输出 Father
NSStringFromClass([self class]) = Father
NSStringFromClass([super class]) = Father
但如果改成这样子:
@implementation Son
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
- (Class)class{
[super class];
return [Son class];
}
@implementation Father
//空的
@end
都输出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
12.New实现原理
New做了三件事情
1.开辟存储空间 +alloc方法
2.初始化所有的属性(成员变量)- init 方法
3.返回对象的地址
如图:
13.什么是构造方法?什么是类构造方法?;instancetype和id的区别?
构造方法
- (instancetype)init{
if (self = [super init]) {
}
类构造方法
+(instancetype)person{
return [[self alloc]init]; //这里一定要用self不要用类名。
}
//id 是动态类型,在运行的时候才会确定具体的类型;instancetype是静态类型,在编译时就确定具体的类型。
有了id动态类型,便于多态的使用。
14.类的本质和存储细节?
15.类的启动过程?
1.先调用+ (void)load{}
只要程序启动就会将类的代码加载到内存中,放到代码区【只要程序启动就加载而不是用到的时候加载】load方法会在当前类被加载到内存的时候调用,有且仅会调用一次
2.再调用+ (void)initialize{ },在当前类第一次被使用时就会调用(创建类对象的时候,类对象只会创建一次)。这个方法在整个程序中只调用一次。
注意点:
1.如果有继承关系,则先调用父类的load方法
2.如果有继承关系,则先调用父类的initialize方法
f#16.空指针和野指针以及僵尸对象?
即指向僵尸对象的指针就是野指针
@autoreleasepool {
Person * p = [[Person alloc]init];
[p release];//0 这个时候堆类的实例对象就是一个僵尸对象
[p release]; //p 这个时候调用release的时候,实例对象已经被销毁,这个时候会崩溃。这个时候p就是野指针【即指向僵尸对象的指针就是野指针】
[p release];
[p release];
}
上面代码会崩溃
改成
@autoreleasepool {
Person * p = [[Person alloc]init];
[p release];
p = nil; //让p指向空指针,给空指针发消息是OK的
[p release];
[p release];
[p release];
}
17.关于@class和import, 以及@class避免的循环拷贝?
1.import是一个预编译指令,它会将“.h”文件拷贝到import所在的位置,并且import有一个特点,只要".h"中的文件发生了变化,那么importj就会重新拷贝一次(更新操作)。【如果A中import了B类,B中import了C类,C类一动,B和A类都会改动】
2.@class仅仅只会告诉编译器,@class后面是一个类,所以编译器不知到这个类中有什么方法和参数。所以在.m文件里面需要import,这个时候如果改变导入的类,只会更新引用的这个类,而不是很多类。
18.关于循环retain
Person * p = [[Person alloc]init];
Dog * d = [[Dog alloc]init];
p.dog = d; //2
d.ownner = p; //2
[p release];//1
[d release]; //1
解决就让某一方不要进行retain,改成
'@property (strong, assign)Person * owner;'改成assign。
19.关于autorelease和autoreleasepool
1. [p autorelease] 引用计数是不变的。
这句话的意思是:系统会创建一个自动释放池。当自动释放池销毁的时候会给自动释放池里面的对象发送一次release。相当于延迟release
@autoreleasepool {
Person * p = [[Person alloc]init];
[p run];
[p release];
[p run]; //这句话崩溃
}
@autoreleasepool {
Person * p = [[Person alloc]init];
[p run];
[p autorelease]; //这句话的好处是,我们不用关心什么时候释放。会在最后统一发一次release
[p run]; 但是这个不会。
}
注意点:
1.一定要在 @autoreleasepool {
}里面进行autorelease。
2.在@autoreleasepool {
}不要进行消耗内存的操作
3.自动释放池是可以嵌套的。
20.关于Copy的深浅拷贝和copy的内存管理;以及property和copy;以及block为什么用copy修饰;以及自定义类实现copy。
原则:
- copy会产生一个新的副本,修改副本不会影响原来的值,修改原本也不会影响副本的值。
浅拷贝:就是指针拷贝。
深拷贝:就是创建新对象。
如果前后两个对象是不可变的,就是浅拷贝。其他的都是深拷贝。
举例:
NSString * str = @"qwe";
NSString * strCopy =[str copy]; //浅拷贝。要满足我们的原则。
总结:浅拷贝不生成新的对象。深拷贝会产生新的对象
所以浅拷贝会进行一次retain
property和copy
防止外界修改内部的值
@property (nonatomic, copy) NSString * str;
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString * s = [NSMutableString stringWithFormat:@"hello"];
_str = s;
[s appendString:@"world"];
NSLog(@"str = %@ , s = %@",_str,s);
}
打印结果:str = helloworld , s = helloworld
原因是copy执行的是深拷贝。生成了一个新的str对象。
所以字符串就用copy修饰
block为什么用copy修饰
因为block默认在栈中,就不会对外界对象进行retain ,若在block中访问对象,但是对象在访问前就被释放了就会崩溃。所以用copy,将block从栈中移到堆中,这样就可以retain一次,保要使用对象的命。
自定义类实现copy
1.遵守NSCopying协议
2.实现- (id)copyWithZone:(NSZone *)zone{
}
21.关于网络?
1.什么是URL?
URL的全称是Uniform Resource Locator(统一资源定位符)
通过1个URL,能找到互联网上唯一的1个资源
URL就是资源的地址、位置,互联网上的每个资源都有一个唯一的URL.
URL的基本格式 = 协议://主机地址/路径
http://202.108.22.5/img/bdlogo.gif
2.什么是请求头、请求体,什么是响应头、响应体 ?
请求头:包含了对客户端的环境描述、客户端请求信息等
22.小知识点
[UINavigationBar appearance];
[UITabBarItem appearance];
通过appearance统一设置所有UITabBarItem的文字属性
后面带有UI_APPEARANCE_SELECTOR的方法, 都可以通过appearance对象来统一设置
23.内存泄漏的种类,以及你是如何避免的?
参考文章1
参考文章2
1.网络请求管理者。保持是一个管理者,如果多个管理者就有多个任务队列。
2.Block的循环引用。
3.delegate的循环引用
4.#import的循环引用
5.无线for循环 最后内存飙升,APP崩溃
6.NSTimer不清除会内存泄露。手机发烫,内存飙升app崩溃。
7.局部变量是控制器,它被释放后,其View还在,这个时候也是内存泄露。
避免:
1.运用单例
2.__weak typeof(self) weakSelf = self;
3.weak 或者assign
4.用@class
5.用instruments看内存增长的情况
6.dealloc的时候清楚NSTimer
7.运用父子控制器,再一个用strong修饰,不用局部变量。
24.谈谈上拉下拉刷新的细节处理?
1.这里主要涉及的是多次请求问题
2.请求失败情况下的问题。
3.刷新控件的关闭和显示问题
4.分页处理的问题,page参数的恢复问题。
25.一些开发经验?
1.如果图片设置的frame与预设的不一样,要想到autoresizing
2.减轻第三方框架的风险。对第三方框架进行一次封装,面向自己的类开发。
26.frame和bounds,为什么要有bounds?
bounds的有以下两个特点,可以从bounds自身的来剖析:bounds是一个Rectangle,前半部分是point后半部分是view的size。这两个属性也预示着两个特点:
bounds像是浮于frame之上的。frame是一个框架,bounds是显示子view的东西,下面总结bounds的两个特征:
第一、 对于bound的point:它不会改变frame的原点,改变的是bounds自己的原点,进而影响到“子view”的显示位置。这个作用更像是移动bounds原点的意思。
第二、 对于bound的size:它可以改变的frame。如果bounds的size比frame的size大。那么frame也会跟着变大,那么frame的原点也会变,但是center不会变。这个作用更像边界的意思。
可以推测一下,setBound第一个特性可以用于view的滑动,手势动作,因为可以影响子view的显示位置。
工程中经常修改tablewview的contentInset值。该技巧常用于屏幕两边,上下头部的“留白”。修改contentInset的时候其实修改的也是bounds。
27.ARC的实现机制到底是什么?
ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:
编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。
28.SDWebimage的缓存策略是什么?
29.iOS开发中#import、#include和@class的区别解析
- 一般来说,导入objective c的头文件时用#import,包含c/c++头文件时用#include。
- ,#import 确定一个文件只能被导入一次,这使你在递归包含中不会出现问题。<标记>
所以,#import比起#include的好处就是不会引起交叉编译。
,#import && #class:
- import会包含这个类的所有信息,包括实体变量和方法(.h文件中),而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,后面会再告诉你。
- 在头文件中, 一般只需要知道被引用的类的名称就可以了。 不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。 而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
备注:#import 就是把被引用类的头文件走一遍,即把.h文件里的变量和方法包含进来一次,且仅一次,而@class不用,所以后者编译效率更高。 - 在编译效率方面考虑,如果你有100个头文件都#import了同一个头文件,或者这些文件是依次引用的,如A–>B, B–>C, C–>D这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用@class则不会。
- 如果有循环依赖关系,如:A–>B, B–>A这样的相互依赖关系,如果使用#import来相互包含,那么就会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。
备注:实践证明,A,B相互#import不会出现编译错误。因为<标记>处已经说明#import时文件只被导入一次,所以此条不成立。
总结: - 如果不是c/c++,尽量用#import。
- 能在实现文件中#import,就不在头文件中#import。
- 能在头文件中@class+实现文件中#import,就不在头文件中#import。
30.NSDictionary 的底层数据结构是什么?
NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率。数据在hash表中分布的越均匀,其访问效率越高。而在Objective-C中,通常都是利用NSString 来作为键值,其内部使用的hash函数也是通过使用 NSString对象作为键值来保证数据的各个节点在hash表中均匀分布。
参考文章