最近面试,碰到一个好玩的公司,出了一个面试题,ta给我了一张纸,让写出NSArray的enumerateObjectsUsingBlock内部怎么实现的。
NSArray * list = @[@"1",@"2",@"3"];
[list enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if([obj isEqualToString:@"2"]){
*stop = YES;
}
}];
这题目漂漂亮亮的写出来,起码需要以下要求:
1、对block有点理解,要不然写不出block,哈哈。
2、对指针有点理解,要不然写不出来。
3、对自己足够自信,对苹果sdk内部实现不能有恐惧。
block晚点再说,先看看指针。
1、指针基础:p、*p和&p三者的区别
指针四元素:
指针的类型
你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型
指针所指向的类型
把指针声明语句中的指针名字和名字左边的指针声明符去掉,剩下的就是指针所指向的类型*
指针的值
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。
指针本身所占据的内存区
指针本身占了多大的内存?用函数sizeof(指针的类型)测一下就知道了,不同位数的机器大小不同
NSInteger aaa = 3;
NSLog(@"aaa的内存地址====%p",&aaa); //aaa的内存地址0x111
NSInteger *bbb = &aaa;
NSLog(@"bbb变量存的内容====%p",bbb); //这获取的就是示意图中的0x111
NSLog(@"bbb的内存地址====%p",&bbb); //这获取的就是示意图中的0x222
变量内存示意.png
bbb是一个指针变量的名字,表示此指针变量指向的内存地址,如果使用%p来输出的话,它将是一个16进制数,从上面的结果可以看到打印bbb和&aaa的值是一样
*bbb表示此指针指向的内存地址中存放的内容,一般是一个和指针类型一致的变量或者常量
&是取地址运算符,&bbb就是取指针bbb的地址;
&bbb和bbb的区别在于:指针bbb同时也是个变量,既然是变量,编译器肯定要为其分配内存地址,&bbb就表示编译器为变量bbb分配的内存地址;而因为bbb是一个指针变量,这种特殊的身份注定了它要指向另外一个内存地址,程序员按照程序的需要让它指向一个内存地址,所以bbb表示它指向的内存地址。
2、基本数据类型、对象类型
char a = 10;
char *p = &a;
char value = *p;
printf("value的值:%d", value); //输出结果:value的值:10
NSString *name = @"solo";
NSLog(@"xxxx的内存地址====%p",name); //下图中solo的内存首地址,也是name指针在内存中存的内容
NSLog(@"name的内存地址===%p",&name); //name指针的内存地址
NSLog(@"name的description===%@",name); //name的description
对象类型,内存分布复杂,结构体是一片内存区域。
示意图1.png
NSString本身也是一个对象,它不止是char *这些基本类型这么简单。本质上OC的对象是一个结构体,是一片内存区域,我们并没有方法能直接完整打印出这个结构体。NSLog遇到%@格式和接收对象作为参数时,直接调用的是对象的description方法。这里与基本数据类型的处理是有区别的。
3、iOS中多级指针的应用
指针在iOS中运用十分广泛,只是太频繁没意识到而已,其实每个实例对象都是指针。这里说的应用是指多级指针的运用。下面这俩货我面写代码会经常看到:
NSArray * list = @[@"1",@"2",@"3"];
[list enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"enumerateObjectsUsingBlock===%@",obj);
if([obj isEqualToString:@"2"]){
*stop = YES;
}
}];
NSError *error = nil;
[[NSFileManager defaultManager] moveItemAtPath:@"" toPath:@"" error:& error];
if (error) {
NSLog(@"move failed:%@", [error localizedDescription]);
}
为什么这么写能修改函数外面的值?换个有函数实现简单的栗子:
- (void)testBaseData
{
NSInteger aaa = 3;
NSLog(@"函数前===%p",&aaa);//函数前===0x7ffeec80fba8
[self getNewCount:&aaa];
NSLog(@"aaa2===%ld",aaa);//aaa2===200
}
-(void)getNewCount:(NSInteger *)countaa
{
NSLog(@"函数里===%p",countaa);//函数里===0x7ffeec80fba8
*countaa = 200;
}
基本数据类型,会显得比较简单。可以看出函数前和函数里指针一样。
把aaa的内存地址,赋值给了指针countaa,countaa就是函数外面的aaa。修改countaa的值,就是修改aaa变量在内存中存的值。
- (void)showError
{
NSError *error = nil;
NSLog(@"函数前==%p", &error);//函数前==0x7ffee06dfd38
[self handleResponseCode:0 error:&error];
NSLog(@"函数后值==%@", error);//函数后值==Error Domain=NSCocoaErrorDomain Code=0 "(null)" UserInfo={code=0}
}
- (void)handleResponseCode:(NSInteger)code error:(NSError **)err
{
NSLog(@"函数里==%p", err);//函数里==0x7ffee06dfd30
if (code == 0) {
*err = [NSError errorWithDomain:NSCocoaErrorDomain code:code userInfo:@{@"code":@(code)}];
}
}
对着下面的内存示意图分析下:
示意图.pngerror是个指针,开始指向nil,在调用下面的函数的时候,通过取地符&,把error的内存地址(0x222)赋值给了新的指针变量err。
在函数中,*err就是err指针指向的变量(就是函数外面的error)。修改 *err的指针指向就是修改函数外error的指针指向。
注意:这里函数外的内存地址0x7ffee06dfd38和函数里面0x7ffee06dfd30会有略微不同,是__autoreleasing搞的鬼,暂且忽略,具体看这个吧
结论:我们通过一个指针参数作为桥梁,成功修改了函数外面变量的值。
这么写到底有啥好处?谁也不会没事撑的,搞这么个幺蛾子。
看个栗子:
- (void)testManyParameter
{
NSString * name = @"solo";
NSInteger age = 18;
NSString * money = @"100W";
BOOL isRichGuy = YES;
CGFloat degress = 0.55;
[self fucNewName:&name age:&age money:&money isRichGuy:&isRichGuy degress:°ress];
NSLog(@"name====%@",name);
}
-(void)fucNewName:(NSString **)newName
age:(NSInteger *)age
money:(NSString **)money
isRichGuy:(BOOL *)isRichGuy
degress:(CGFloat *)degress
{
*newName = @"fuck";
*age = 20;
*money = @"2000";
*isRichGuy = NO;
*degress = 0.66;
}
看出来了吧,返回参数可以不用放容器里返回,完全可以直接修改函数外面的值,也不用担心NSInteger这些变量不能直接放进容器里面的问题。
4、空指针nil、野指针、僵尸对象
1.空指针
值为nil,没有指向。由于iOS中采用的是对调用者发消息,如果消息的接受者为nil,对空指针发任何消息不起任何作用。
NSObject * object = nil;
NSLog(@"函数==%p", &object);//函数==0x7ffeeaba0ba8
NSLog(@"函数==%p", object);//函数==0x0
示意图.png
这里object存储的是0x0,表示object是一个空指针,空指针也是指针,也有四元素。
2.野指针
不是nil指针,是指向"垃圾"内存(不可用内存)的指针。当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称野指针
。如用assign修饰对象就出现导致野指针,因为对象创建出来没有任何强指针指向它,所以创建完以后立即会被释放了,这时候指针指向的地方已经不可用,所以就成了野指针。
3.僵尸对象
,在OC中,对象被释放后所占用的内存在没有被复写(重新分配给其他对象)前称为僵尸对象
,这是野指针是可以访问该内存的,因为对象的数据还在,所以程序不会报错。但是该内存一旦重新分配给其他对象就会出现问题。
最后给出最开始题目的答案: NSArray的enumerateObjectsUsingBlock实现类似下面:
- (void)enumTestBlock:(void (^)(id obj, NSUInteger index, BOOL * stop))enumBlock {
BOOL stopNow = NO;
for (int index = 0; index < self.count; index++ ) {
if (!stopNow) {
enumBlock(self[index], index, &stopNow);
} else {
break;
}
}
}
一些自己的理解记录下来,希望没有不对的地方。
参考1:https://www.jianshu.com/p/5b2c7bbc32d6
参考2:https://www.jianshu.com/p/c58e089ba219
参考3:https://blog.csdn.net/wnnvv/article/details/81144219
网友评论