iOS面试杂记
1. 函数局部变量的return
R:一般的来说,函数是可以返回局部变量的。 局部变量的作用域只在函数内部,在函数返回后,局部变量的内存已经释放了。因此,如果函数返回的是局部变量的值,不涉及地址,程序不会出错。但是如果返回的是局部变量的地址(指针)的话,程序运行后会出错。因为函数只是把指针复制后返回了,但是指针指向的内容已经被释放了,这样指针指向的内容就是不可预料的内容,调用就会出错。准确的来说,函数不能通过返回指向栈内存的指针(注意这里指的是栈,返回指向堆内存的指针是可以的)。不管是返回指针还是返回值,return将return之后的值存到eax寄存器中,回到父函数再将返回的值赋给变量。
2. NSString地址
@“hello”和[NSString stringWithFormat:@"hello"]有何区别?
NSString *A = @"hello";
NSString *B = @"hello";
NSString *C = [NSString stringWithFormat:@"hello"];
NSString *D = [NSString stringWithFormat:@"hello"];
@“hello”位于常量池,可重复使用,其中A和B指向的都是同一份内存地址。
而[NSString stringWithFormat:@"hello"]是运行时创建出来的,保存在运行时内存(即堆内存),其中C和D指向的内存地址不同,与A、B也不相同。
2. 手写单例
单例用途:限制创建,提供全局调用,节约资源和提高性能
static的作用:防止外部访问
@synchronized的作用:为了防止多线程同时访问对象,造成多次分配内存空间,所以要加上线程锁。单线程操作可以不用。系统单例有: UIApplication、NSNotificationCenter、NSFileManager、NSUserDefaults、NSURLCache 、NSHTTPCookieStorage等
参考: iOS Principle:Singleton
#import "SingleInstance.h"
@implementation SingleInstance
// 类方法命名规范 share类名|default类名|类名
+(id)sharedInstance {
// 创建静态对象 防止外部访问
static SingleInstance *instance;
// @synchronized (self) {
// // 为了防止多线程同时访问对象,造成多次分配内存空间,所以要加上线程锁。
// // 相比于GCD,@synchronized比较耗性能
// if (instance == nil) {
// instance = [[super allocWithZone:NULL] init];
// }
// return _instance;
// }
// 也可以使用一次性代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// super 避免循环调用
instance = [[super allocWithZone:NULL] init];
});
return instance;
}
// 为了严谨,也要重写allocWithZone 和 copyWithZone
+(id)allocWithZone:(struct _NSZone *)zone {
// 最好用self 用SingleInstance他的子类调用时会出现错误
return [self sharedInstance];
}
-(id)copyWithZone:(nullable NSZone *)zone {
return self;
}
@end
延伸问题:单例模式如何销毁?
有时候会有一种场景,需要销毁单例进行重建。
但多数需要销毁的单例实际上可能不适用于单例这种模式,可能需要重新考虑架构设计问题。
// SingleInstance *instance; dispatch_once_t onceToken; 声明为全局静态变量
// 了解到 dispatch_once_t参数的初始值就是0l,只需要重置dispatch_once_t参数及实例参数即可
+ (void)tearDown{
instance=nil;
onceToken=0l;
}
3. 代码执行顺序
// 对象 A
- (void)_aLog {
NSLog(@"1");
// 发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"NSLog(2)" object:nil userInfo:nil];
NSLog(@"3");
}
// 观察者对象B 接收通知去执行selector(log)
-(void)_bLog {
NSLog(@"2");
}
执行对象A的_aLog方法,打印结果: 1 2 3
通知的响应回调是同步执行的。
NSNotification接受线程是基于发送消息的线程的,即发送消息和接收消息 是在同一个线程
参考:iOS多线程中使用NSNotification
// 对象 A
- (void)_aLog {
NSLog(@"1");
// 执行performSelector相关方法
[self performSelector:@selector(log2) withObject:nil afterDelay:0];
[self performSelector:@selector(log3) withObject:nil];
NSLog(@"4");
}
-(void)log2 {
NSLog(@"2");
}
-(void)log3 {
NSLog(@"3");
}
执行对象A的_aLog方法,打印结果: 1 3 4 2
[self performSelector:@selector(test) withObject:nil afterDelay:0] 是NSRunLoop.h的方法,是单线程的也就是说只有当前调用此方法的函数执行完毕后,selector方法才会被调用。
[self performSelector:@selector(log3) withObject:nil];是NSObject.h文件下的方法,会走正常调用顺序。
- (void)_aLog {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(log2) withObject:nil afterDelay:0.0];
NSLog(@"3");
});
}
-(void)log2 {
NSLog(@"2");
}
执行对象A的_aLog方法,打印结果: 1 3
因为[self performSelector:@selector(test) withObject:nil afterDelay:.0]实际在runloop里面是一个定时器,但是因为在子线程,runloop是默认没有开启的。所以该方法不会被调用。
参考:关于performSelector:afterDelay:的一个坑及思考
4. CoreFundation 和 Fundation
Fundation 是 CoreFundation 一种包装,在MRC底层数据结构是一样的.
在MRC情况下 CF对象和NS对象是一个东西.当在ARC情况下需要用到CF对象时可以利用__bridge来转换对象:
__bridge ->内存管理者不切换 ,即 用CF类构造函数创造的对象仍然需要手动release , OC类构造函数创建的对象可以自动释放.下面同理.
__bridge_transfer/CFBridgingRelease ->把CF对象转换成NS对象,并且内存管理者切换 , 即 CF对象转换为OC对象后,可以被自动释放.
__bridge_retained/CFBridgingRetain -> 把NS对象转换成CF对象,并且内存管理者切换.
5. 关于alloc 、init和new
alloc: 为对象的所有成员变量分配内存空间,并且为各个成员变量重置为默认值,如int型为0,BOOL型为NO,指针型变量为nil。使对象不被释放,将地址返回给指针。
init: 为分配好的内存进行初始化。好比拿到地皮要规划盖楼
new:等同[alloc]init,实际上[alloc]init在alloc的时候调用allocWithZone分配了内存,然后调用init初始化。而new在分配内存后直接调用init进行初始化。
new不常使用因为它只能init,而alloc init却有更多实用和可定制的初始化方法。
alloc ,init分配的内存会和相关联的对象在内存地址中相靠近,利于内存读取,调用时消耗很少的内存,提升了程序处理速度
alloc & allocwithZone :alloc会默认调用allocWithZone方法
6. 沙盒
每个iOS程序都有一个独立的文件系统(存储空间),而且只能在对应的文件系统中进行操作,此区域被称为沙盒。应用必须待在自己的沙盒里,其他应用不能访问该沙盒。所有的非代码文件都要保存在此,例如属性文件plist、文本文件、图像、图标、媒体资源等。
Documents/ 保存应用程序的重要数据文件和用户数据文件等。用户数据基本上都放在这个位置(例如从网上下载的图片或音乐文件),该文件夹在应用程序更新时会自动备份,在连接iTunes时也可以自动同步备份其中的数据。
Library:这个目录下有两个子目录,可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份.
Library/Caches: 保存应用程序使用时产生的支持文件和缓存文件(保存应用程序再次启动过程中需要的信息),还有日志文件最好也放在这个目录。iTunes 同步时不会备份该目录并且可能被其他工具清理掉其中的数据。
Library/Preferences: 保存应用程序的偏好设置文件。NSUserDefaults类创建的数据和plist文件都放在这里。会被iTunes备份。tmp/: 保存应用运行时所需要的临时数据。不会被iTunes备份。iPhone重启时,会被清空。
7.关于一个int *p=(int *)(&a + 1);
#include <stdio.h>
int main ()
{
int a[5] = {1,2,3,4,5};
int *p = (int*)(&a + 1);//&a表示整个数组的地址
printf("%d %d" , *(a + 1), *(p - 1));
}
//输出结果为:2,5
*(a+1)其实很简单就是指a[1],输出为2.
问题关键就在于第二个点,*(p-1)输出为多少?
解释如下
&a+1不是首地址+1,系统会认为加了一个整个a数组,偏移了整个数组a的大小(也就是5个int的大小)。所以int*ptr=(int*)(&a+1);其实p实际是&(a[5]),也就是a+5.
原因为何呢?
&a是数组指针,其类型为int(*)[5];而指针+1要根据指针类型加上一定的值,不同类型的指针+1之后增加的大小不同,a是长度为5的int数组指针,所以要加5*sizeof(int),所以p实际是a[5],但是p与(&a + 1)类型是不一样的,这点非常重要,所以p - 1只会减去sizeof(int*),a,&a的地址是一样的,但意思就不一样了,a是数组首地址,也就是a[0]的地址,&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1],&a + 1是下一个对象的地址,即a[5]。
8. 关闭控件响应事件的方法
方法1. 属性userInteractionEnabled设成No
方法2. hidden属性设成yes
方法3. alpha在0~0.01之间
方法4. 重写控件将hitTest:withEvent:方法,将返回置为空(因为响应者链条是这个方法传递的)。
9. 防止UIButton重复点击的方法
方法1. 使用UIButton的enabled或userInteractionEnabled
比如:使用UIButton的enabled属性, 在点击后, 禁止UIButton的交互, 直到完成指定任务之后再将其enable即可
方法2. 使用performSelector:withObject:afterDelay:和cancelPreviousPerformRequestsWithTarget
使用这种方式, 会在连续重复点击UIButton的时候, 自动取消掉之前的操作, 延时1s后执行实际的操作.
方法3. 使用runtime来对sendAction:to:forEvent:方法进行hook
UIControl的sendAction:to:forEvent:方法用于处理事件响应.
如果我们在该方法的实现中, 添加针对点击事件的时间间隔相关的处理代码, 则能够做到在指定时间间隔中防止重复点击.