iOS 面试题(一)
1.instancetype
和id
的区别
区别一:
- 在ARC环境下:
instancetype
用来在编译期确定实例的类型,而使用id
的话,编译器不检查类型,运行时检查类型。 - 在MRC环境下:
instancetype
和id
一样,不做具体类型检查。
区别二:
-
id
可以作为方法的参数,但instancetype
不可以。instancetype
只适用于初始化方法和便利构造器的返回值类型。
2. @synthesize
和@dynamic
区别
在声明property
属性后,有2种实现选择:
@synthesize
编译器期间,让编译器自动生成getter
/setter
方法。
当有自定义的存或取方法时,自定义会屏蔽自动生成该方法。
@dynamic
告诉编译器,不自动生成getter
/setter
方法,避免编译期间产生警告,然后由自己实现存取方法。
或存取方法在运行时动态创建绑定:主要使用在CoreData的实现NSManagedObject
子类时使用,由Core Data框架在程序运行的时动态生成子类属性。
同时重写getter
/setter
方法
很少同时重写getter
/setter
方法,一般的话,大概都是使用懒加载方法,然后重写getter
方法,做一个非空判断。要同时重写属性的getter
/setter
,系统就会报错误:Use of undeclared identifier '_string'; did you mean 'string'?
OC最初设定@property
和@synthesize
的作用:
@property
的作用是定义属性,声明getter
/setter
方法。(注意:属性不是变量)
@synthesize
的作用是实现属性的,如getter
/setter
方法。
后来因为使用@property
灰常频繁,就简略了@synthesize
的表达。
从Xcode4.4以后@property
已经独揽了@synthesize
的功能。
主要有三个作用:
(1)生成了私有的带下划线的的成员变量因此子类不可以直接访问,但是可以通过getter
/setter
方法访问。那么如果想让定义的成员变量让子类直接访问那么只能在.h文件中定义成员变量了,因为它默认是@protected
。
(2)生成了getter
/setter
方法的实现当:用@property
声明的成员属性,相当于自动生成了getter
/setter
方法,如果重写了getter
/setter
方法,与@property
声明的成员属性就不是一个成员属性了,是另外一个实例变量,而这个实例变量需要手动声明。所以会报错误。
总结:一定要分清属性和变量的区别,不能混淆。@synthesize
声明的属性=变量。意思是,将属性的getter
/setter
方法,作用于这个变量。
同时重写解决办法如下:
@interface ViewController ()
@property (nonatomic, strong)NSString *string;
@end
@implementation ViewController
//添加以下代码解决
@synthesize string = _string;
-(NSString *)string{
return _string;
}
-(void)setString:(NSString *)string{
_string = string;
}
-(void)viewDidLoad{
[super viewDidLoad];
}
@end
3.NSProxy和NSObject
https://www.jianshu.com/p/0c31fed0a27a
4.传值通知和推送通知(本地&远程)
https://www.jianshu.com/p/d5011177ba90
5.imageName
和ImageWithContextOfFile
- 用
imageName
的方式加载时,图片使用完毕后缓存到内存中,内存消耗多,加载数速度快。即使生成的对象被autoReleasePool释放了,这份缓存也不释放,如果图像比较大,或者图像比较多,用这种方式会消耗很大的内存。imageName
采用了缓存机制,如果缓存中已加载了图片,直接从缓存读取,每次不需要再去读取文件,效率更高。
*ImageWithContextOfFile
图片时不会缓存的,加载速度慢。 - 大量使用
imageNamed
方式会在不需要缓存的地方额外增加开销CPU的时间。当应用程序需要加载一张比较大的图片并且使用一次性,那么使用imageWithContentsOfFile
是最合适的。
6.NSCache
和NSDcitionary
NSCache
使用很方便,提供了类似可变字典的实现方式,但它比可变字典更适用于实现缓存。
- 最重要的原因是
NSCache
是线程安全的,使用NSMutableDictionary
自定义实现缓存的时候需要考虑加锁和释放锁,NSCache
已经帮我们做好了这一步。 - 其次,内存不足时
NSCache
会自动释放存储的对象,不需要手动干预,如果是自定义实现需要监听内存状态然后做进一步删除对象的操作。 - 还有一点
NSCache
的键key不会被复制,所以key不需要实现NSCopying
协议。
以上三点就是NSCache
相比于NSMutableDictionary
实现缓存功能的优点,在需要实现缓存的时候应优先考虑使用NSCache
。
#import "DJViewController.h"
@interface DJViewController ()<NSCacheDelegate>
@end
@implementation DJViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSCache *cache = [[NSCache alloc]init];
//最大可缓存对象的个数为5个
[cache setCountLimit:5];
cache.delegate = self;
NSString *test = @"Hello World!";
[cache setObject:test forKey:@"Test"];
for (int i = 0; i < 10; i++) {
[cache setObject:[NSString stringWithFormat:@"world__%d",i] forKey:[NSString stringWithFormat:@"hello__%d",i]];
NSLog(@"add %d",i);
}
for (int i = 0; i < 10; i++) {
NSLog(@"get %d,%@",i,[cache objectForKey:[NSString stringWithFormat:@"hello__%d",i]]);
}
[cache removeAllObjects];
for (int i = 0; i < 10; i++) {
NSLog(@"get %d,%@",i,[cache objectForKey:[NSString stringWithFormat:@"hello__%d",i]]);
}
}
//当缓存的对象即将被删除时调用此方法
-(void)cache:(NSCache *)cache willEvictObject:(id)obj{
NSLog(@"remove %@",obj);
}
@end
//输出结果
2021-09-22 15:33:30.829312+0800 DJTestDemo[71357:240307] add 0
2021-09-22 15:33:30.829445+0800 DJTestDemo[71357:240307] add 1
2021-09-22 15:33:30.829540+0800 DJTestDemo[71357:240307] add 2
2021-09-22 15:33:30.829621+0800 DJTestDemo[71357:240307] add 3
2021-09-22 15:33:30.829729+0800 DJTestDemo[71357:240307] remove Hello World!
2021-09-22 15:33:30.829800+0800 DJTestDemo[71357:240307] add 4
2021-09-22 15:33:30.829900+0800 DJTestDemo[71357:240307] remove world__0
2021-09-22 15:33:30.829972+0800 DJTestDemo[71357:240307] add 5
2021-09-22 15:33:30.830055+0800 DJTestDemo[71357:240307] remove world__1
2021-09-22 15:33:30.830181+0800 DJTestDemo[71357:240307] add 6
2021-09-22 15:33:30.830484+0800 DJTestDemo[71357:240307] remove world__2
2021-09-22 15:33:30.830697+0800 DJTestDemo[71357:240307] add 7
2021-09-22 15:33:30.830922+0800 DJTestDemo[71357:240307] remove world__3
2021-09-22 15:33:30.831102+0800 DJTestDemo[71357:240307] add 8
2021-09-22 15:33:30.831310+0800 DJTestDemo[71357:240307] remove world__4
2021-09-22 15:33:30.831471+0800 DJTestDemo[71357:240307] add 9
2021-09-22 15:33:30.831675+0800 DJTestDemo[71357:240307] get 0,(null)
2021-09-22 15:33:30.842161+0800 DJTestDemo[71357:240307] get 1,(null)
2021-09-22 15:33:30.842600+0800 DJTestDemo[71357:240307] get 2,(null)
2021-09-22 15:33:30.842833+0800 DJTestDemo[71357:240307] get 3,(null)
2021-09-22 15:33:30.843000+0800 DJTestDemo[71357:240307] get 4,(null)
2021-09-22 15:33:30.843301+0800 DJTestDemo[71357:240307] get 5,world__5
2021-09-22 15:33:30.843423+0800 DJTestDemo[71357:240307] get 6,world__6
2021-09-22 15:33:30.843517+0800 DJTestDemo[71357:240307] get 7,world__7
2021-09-22 15:33:30.843688+0800 DJTestDemo[71357:240307] get 8,world__8
2021-09-22 15:33:30.844061+0800 DJTestDemo[71357:240307] get 9,world__9
2021-09-22 15:33:30.844323+0800 DJTestDemo[71357:240307] remove world__6
2021-09-22 15:33:30.844563+0800 DJTestDemo[71357:240307] remove world__7
2021-09-22 15:33:30.844952+0800 DJTestDemo[71357:240307] remove world__8
2021-09-22 15:33:30.845037+0800 DJTestDemo[71357:240307] remove world__9
2021-09-22 15:33:30.845546+0800 DJTestDemo[71357:240307] remove world__5
2021-09-22 15:33:30.845637+0800 DJTestDemo[71357:240307] get 0,(null)
2021-09-22 15:33:30.845800+0800 DJTestDemo[71357:240307] get 1,(null)
2021-09-22 15:33:30.846037+0800 DJTestDemo[71357:240307] get 2,(null)
2021-09-22 15:33:30.846305+0800 DJTestDemo[71357:240307] get 3,(null)
2021-09-22 15:33:30.846620+0800 DJTestDemo[71357:240307] get 4,(null)
2021-09-22 15:33:30.846852+0800 DJTestDemo[71357:240307] get 5,(null)
2021-09-22 15:33:30.847095+0800 DJTestDemo[71357:240307] get 6,(null)
2021-09-22 15:33:30.847342+0800 DJTestDemo[71357:240307] get 7,(null)
2021-09-22 15:33:30.847673+0800 DJTestDemo[71357:240307] get 8,(null)
2021-09-22 15:33:30.847882+0800 DJTestDemo[71357:240307] get 9,(null)
从输出可以看出,当我们要添加第六个对象时NSCache
自动删除了我们添加的第一个对象并触发了NSCacheDelegate
的回调方法,添加第七个时也是同样的,删除了缓存中的一个对象才能添加进去。
在第二个for循环中,我们通过key取出所有的缓存对象,前五个对象取出都为nil,因为在添加后面的对象时前面的被删除了,所以,当我们从缓存中获取对象时一定要判断是否为空,我们无法保证缓存中的某个对象不会被删除。
7.setNeedsDisplay
和setNeedsLayout
方法
https://www.jianshu.com/p/b296e03429d4
8. UILayer
和UiView
https://www.jianshu.com/p/708bee808dfc
9.layoutSubViews
和drawRect
layoutSubviews调用时机
-
init
初始化不会触发layoutSubviews
。 -
addSubview
会触发layoutSubviews
(frame
为0,是不调用的)。 - 设置view的frame会触发
layoutSubviews
(前提是frame的值设置前后发生了变化)。 - 滚动一个
UIScrollView
会触发layoutSubviews
。 - 旋转屏幕会触发父
UIView
上的layoutSubviews
事件(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview
中做相应的操作)。 - 改变一个
UIView
大小的时候也会触发父UIView
上的layoutSubviews
事件。 - 直接调用
setLayoutSubviews
(Apple是不建议这么做的)。
drawRect:
调用机制
- 调用时机:
loadView
->ViewDidload
->drawRect:
。 - 如果在
UIView
初始化时没有设置rect大小,将直接导致drawRect:
不被自动调用。 - 通过设置
contentMode
属性值为UIViewContentModeRedraw
。那么将在每次设置或更改frame的时候自动调用drawRect:
。 - 直接调用
setNeedsDisplay
,或者setNeedsDisplayInRect:
触发。drawRect:
,但是有个前提条件是:view当前的rect不能为nil。 - 该方法在调用
sizeThatFits
后被调用,所以可以先调用sizeToFit
计算出size。然后系统自动调用drawRect:
方法。
drawRect:
方法使用注意点:
- 若使用
UIView
绘图,只能在drawRect:
方法中获取相应的contextRef
并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。
drawRect:
方法不能手动显示调用,必须通过调用setNeedsDisplay
或者setNeedsDisplaynRect
,让系统自动调该方法。 - 若使用
CALayer
绘图,只能在drawInContext:
中(类似于drawRect:
)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay
等间接调用以上方法。 - 若要实时画图,不能使
gestureRecognizer
,只能使用touchbegan
等方法来掉用setNeedsDisplay
实时刷新屏幕。
10.CPU和GPU
https://www.jianshu.com/p/412d7cd68756
11.点(pt)和像素(px)
px:pixel像素,屏幕上显示的最小单位。在同一个屏幕尺寸,更高的PPI(pixel per inch像素密度,指每英尺的像素数),就能显示更多的像素,渲染的内容会更清晰。
pt:point点,是一个标准的长度单位,与分辨率无关,1pt=1/72inch。根据屏幕像素的密度,一个点可以包含多个像素。例如,在标准的Retina显示屏上1pt里有2*2个px。
12.int
和NSInteger
的区别
NSInteger
是一个封装,它会识别当前操作系统的位数,自动返回最大的类型。
-
NSInteger
与int
的区别是NSInteger
会根据系统的位数(32or64)自动选择int
的最大数值(int
orlong
)。 -
NSInteger
默认值是null
,而int
默认值是0。 - 声明为
NSInteger
的变量需要实例化,而声明为int
的变量不需要实例化。 -
NSInteger
是对象,用一个引用指向这个对象;而int
是基本类型,直接存储数值。
13.#import
和#include
在C和C++里是没有#import
的,只有#include
用来包含头文件。#include
就是将目标.h文件中的内容拷贝到当前文件中,并替换掉这句include。
但是这样做可能会因为重复引用带来编译错误,比如B和C都引用了A,D又同时引用了B和C,这样D引用了A两次。为了解决这个问题,OC加入了#import
,就是为了使得头文件只被引用一次。
当引用关系很复杂时,编译引用所占的代码量就会大幅上升,因为被引用的头文件在引用的地方都被拷贝了一次。
为了解决这个问题,C语言引入了预编译头文件(PreCompiled Header),将公用的头文件放入预编译头文件中预先进行编译,然后在真正编译工程时再将预先编译好的产物加入到所有待编译的Source中去,来加快编译速度。
@import
关于 @import
是iOS 7之后的新特性语法,这种方式叫Modules(模块导入) 或者 "semantic import(语义导入)" ,是一种更好的头部预处理的执行方式,这iOS 7之后你能通过@import
语法来导入任何的framework,Modules是一种将所有可执行的framework打包在一起,貌似这种方式比起传统的#import
更安全和更高效。
而且另外一个最大的改进就是使用@import
之后,你不用在project settings那里添加framework,系统会自动帮你加载上了,方便了很多,也避免了很多不必要的错误,例如忘记了加入framework而出现的 "Linker Error"。
@class
在.h文件进用@class
来声明,只告诉编译器,声明的类的名称。避免循环引用头文件。
14.全局和静态变量
static
(1)static
修饰局部变量
局部变量:在函数/方法/代码块内声明的变量。它的生命周期、作用域都是在这个代码块内。存储在栈区(stack)一旦出了这个代码块,存储局部变量的这个栈内存就会被回收,局部变量也就被销毁。
当用static
修饰局部变量时,变量被称为静态局部变量
,和全局变量,静态全局变量一样,是存储在静态存储区。存储在静态存储区的变量,其内存直到 程序结束 才会被销毁。即,生命周期是整个源程序。
(2)static
修饰全局变量
- 当全局变量没有使用
static
修饰时,其存储在静态存储区,直到程序结束才销毁。也就是其作用域是整个源程序。我们可以使用extern
关键字来引用这个全局变量。 - 当全局变量使用
static
修饰时,其生命周期依旧是在程序结束时才销毁。但是其作用域只限于申明它的这个文件才可见。使用extern
关键字无法引用这个全局变量。
const
主要强调变量是不可修改的。
const
修饰的是其右边的值,也就是const
右边的这个整体的值不能改变。
const
在*
前:
const
修饰*str
这个整体,所以这个整体不能改变,这个整体是str指向的内存中的值。
//值不变
static NSString const *name = @"abc";
static const NSString *name = @"bac"; //两种写法等价
// name是 指针变量, *name是 指针指向的变量的值
const
在*
后:
表示str指向的地址不能改变。
NSString * const str = @"abc"; // 地址不变
extern
主要是用来引用全局变量,它的原理是先在本文件中查找,查找不到再到其他文件中查找。
//.h中
@interface PDConst : NSObject
extern NSString *const appBaseURL;
@end
//.m中
@implementation PDConst
NSString *const currentBaseURL = @"http://192....";
@end
15.类
https://www.jianshu.com/p/40221ef8128e
16.字符串比较
OC中比较两个字符串是否相等时,应该用isEqualToString:
而不能仅仅只是比较字符串的指针值。
//比较内容
NSString *str1=@"hello 1";
NSString *str2;
str2=[NSString stringWithFormat:@"hello %d", 1];
if ([thing1 isEqualToString:thing2]){
NSLog(@"They are the same!")
}
//比较指针地址
if (str1==str2){
NSLog(@"They are the same!")
}
这是因为==
运算符只判断str1和str2的指针数值,而不是他们所指的对象。由于str1,str2是不同的字符串,所以第二种比较方式会认为它们是不同的。
有时我们想检查两个对象的标识:str1和str2是同一个对象吗?这时就应该使用运算符==
。如果是想查看等价性(即这两个字符串是否代表同一个事物吗),那么请使用isEqualToString:
。
17.NSNumber
和NSValue
作用
由于集合里只能存放对象,不可以存放基本数据类型,所以我们有时候需要讲一些对象比如基本数据类型,结构体等存到NSDictionary
、NSArray
中,我们就需要将这些数据类型或结构体包装成OC对象,以便集合能访问到。常用的用来包装这些类型的有NSNumber
、NSValue
。
区别
-
NSNumber
只能包装基本数据类型,比如int
、float
、char
、BOOL
等。 -
NSValue
可以包装任意一个对象,包括系统自定义的数据结构,结构体等等。 -
NSNumber
是NSValue
的一个子类。
NSNumber
//包装
int age = 20;
NSNumber *num = [NSNumber numberWithInt:age];//将基本数据类型int对象age 包装成NSNumber对象
@(age);//直接包装
//拆封
[num intValue];
NSValue
将结构体包装成OC对象
CGPoint p = CGPointMake(1,2);
NSValue *val = [NSValue valueWithPoint:p];//将结构体p包装成NSValue对象
//拆
[value pointValue]
18.nil
、Nil
、Null
、[NSNull null]
nil
就是空对象。把一个对象置成nil
之后,就不能对其进行retain
, copy
等引用计数相关的操作了。
在iOS中,Nil
完全等同于nil
。
NUll
就是C语言中的一个空指针,在Objective-C中也可以使用。
[NSNull null]
是值为空的对象。
空对象和值为空的对象的区别:
“空对象”是已经释放了内存地址的对象,即不存在的对象。
“值为空的对象”是分配了地址,但是没有值得对象,是实际存在的对象。
19.沙盒
(1)应用程序沙盒目录
- 应用程序包:和app同名,包含所有资源文件和可执行文件。
- Document:存放其中的数据会备份到iCloud,不允许放下载的数据。用户自行生成的文件放入其中。
- Library
caches:用于存放一些缓存数据,保存应用运行时生成的需要持久化的数据。
preference:存储偏好信息,苹果手机的设置应用会在该目录中查找应用的设置信息。 - tmp:临时文件夹,不定期删除。
(2)获取目录的方法
//Home目录
NSString *homeDirectory = NSHomeDirectory();
//Document目录 documents (Documents)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
NSString *path = [paths objectAtIndex:0];
//Libaray目录 various documentation, support, and configuration files, resources (Library)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,NSUserDomainMask,YES);
NSString *path = [paths objectAtIndex:0];
//Cache目录 location of discardable cache files (Library/Caches)
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES);
NSString *path = [paths objectAtIndex:0];
20.loadView
和viewDidLoad
(1)loadView
作用
每次访问UIViewController的view(比如controller.view
、self.view
)而且view为nil,loadView
方法就会被调用。loadView
方法是用来负责创建UIViewController的view。
默认实现
默认实现即[super loadView]
里面做了什么事情。
1)它会先去查找与UIViewController相关联的xib文件,通过加载xib文件来创建UIViewController的view。
如果在初始化UIViewController指定了xib文件名,就会根据传入的xib文件名加载对应的xib文件。
[[MJViewController alloc] initWithNibName:@"MJViewController" bundle:nil];
如果没有明显地传xib文件名,就会加载跟UIViewController同名的xib文件。
2)如果没有找到相关联的xib文件,就会创建一个空白的UIView,然后赋值给UIViewController的view属性。
使用
大家都知道UIViewController的view可以通过xib文件来创建,但是在某些情况下,xib不是那么地灵活,所以有时候我们想通过代码来创建UIView。 如果想通过代码来创建UIViewController的view,就要重写loadView
方法,并且不需要调用[super loadView]
,因为在第3点里面已经提到:若没有xib文件,[super loadView]
默认会创建一个空白的UIView。我们既然要通过代码来自定义UIView,那么就没必要事先创建一个空白的UIView,以节省不必要的开销。
(2)viewDidLoad
不管是通过xib文件还是重写loadView
创建UIViewController的view,在view创建完毕后,最终都会调用viewDidLoad
方法。
一般我们会在这里做界面上的初始化操作,比如往view中添加一些子视图、从数据库或者网络加载模型数据装配到子视图中。