由objectAtIndex引发的数组越界的思考

本文探讨了在iOS开发中,因`objectAtIndex`导致的数组越界问题及其解决方法。通过分类重写该方法时遇到的警告,解释了类目重写与方法冲突的问题,并介绍了类族的概念。文章提到了子类化NSArray的挑战,以及为何在NSArray的分类中直接重写方法无效,最后提出了利用方法交换(method swizzling)来实现安全的`objectAtIndex`覆写策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一句平常得不能再平常得代码

NSArray *array = @[@1,@2,@3];
NSNumber *num = [array objectAtIndex:3];

很容易看出,要取出的值已经超过了数组最大长度,出现数组越界的情况。在平常开发中,这种错误还是比较容易定位,但是crash机制还是比较让人头痛,而且如果在项目中多处使用到这个,发生崩溃后也难以一下子找出发生崩溃的数组。所以自然想改造一下objectAtIndex这个方法。

首先使用分类。然后再分类里重写objectAtIndex方法。这时候xcode发出警告,

Category is implementing a method which will also be implemented by its primary class

这个警告的意思是, 在category中重写了原类的方法。原因是类目中添加的这个方法和原类的方法名一致, 运行的时候会执行这个方法, 而且也会执行原类中的方法. 苹果官方文档中又说明,如果在类别中声明的方法的名称与原始类中的方法相同,或者在同一类(或甚至超类)上的另一类中的方法相同,那么该行为对于使用哪种方法实现是未定义的运行。 如果您使用自己的类使用类别,那么这不太可能成为问题,但是在使用类别添加标准Cocoa或Cocoa Touch类的方法时可能会导致问题。

这时候解决的方法有:

  1. 用继承的方式重写父类方法
  2. 用类目重写原类的方法, 需要通过runtime的method swizzling来进行方法IMP的交换处理.
  3. 忽略警告处理

由于可能在项目中大量使用到了原系统方法,所以不考虑重写父类方法。我们直接重写category中的方法:

-(id)objectAtIndex:(NSUInteger)index{
    if (index>self.count-1) {
        NSLog(@"__%@__数组越界",self);
        return nil;
    }else{
        return self[index];
    }
}

往事具备,直接跑起来。然后果不其然直接crash。

Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'

我们在我们重写的函数打断点,发现根本没有走我们重写的那个方法。

这时候我们注意到一个东西:

-[__NSArrayI objectAtIndex:]

为了解释NSArrayI,我们引入了一个概念,类族。

Effective Objective-C中可以了解到这一概念,实际就是工厂方法的一个运用。

系统框架中有许多类族.大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray.

id maybeAnArray = /* ... */;
if ([maybeAnArray class] == [NSArray class]) {
  // Will never be hit
}
上面这段代码if语句永远不可能为真.[maybeAnArray class]所返回的类绝不可能是NSArray本身,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口后面的某个内部类型.
如果我们想判断某个对象是否位于类族中,不要直接检测两个"类对象"是否相同,而应该采用下面的代码:
id maybeAnArray = /* ... */;
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
  // Will be hit
}

我们经常需要向类族中新增实体子类,不过在Employee这个例子中,若是没有"工厂方法"的源代码,那就无法向其中新增雇员类别了.
然而对于Cocoa中NSArray这样的类族来说,还是有办法新增子类的,但是要遵守几条规则

  • 子类应该继承自类族中的抽象基类
    若要编写NSArray类族的子类,则需令其继承自不可变数组的基类或可变数组的基类.
  • 子类应该定义自己的数据存储方式
    开发者编写NSArray子类时,经常在这个问题上受阻.子类必须用一个实例变量来存放数组中的对象.我们以为NSArray自己肯定会保存那些对象,所以在子类中就无须再存一份了.但是NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需要具备的一些接口.对于这个自定义的数组子类来说,可以用NSArray来保存其实例.
  • 子类应当覆写超类文档中指明需要覆写的方法.
    在每个抽象基类中,都有一些子类必须覆写的方法.比如说,想要编写NSArray的子类,就需要实现count及"objectAtIndex:"方法.像lastObject这种方法则无需实现,因为基类可以根据前两个方法实现出这个方法.
    在类族中实现子类时所需遵守的规范一般都会定义于基类的文档之中,编码前应该先看看.
可见,NSArrayI是NSArray的一个隐藏起来的子类,而NSArray则是一个抽象基类。
关于NSArray的其它一些类族:
    NSArray *array0 = [NSArray new];
    NSArray *array = @[@1,@2,@3];
    NSMutableArray *array2 = [[NSMutableArray alloc]init];
    NSMutableArray *array3 = [NSMutableArray arrayWithObjects:@2,@3, nil];
    Class z = object_getClass(array0);    //输出__NSArrayO
    Class a = object_getClass(array);    //输出__NSArrayI
    Class b = object_getClass(array2);    //输出__NSArrayM
    Class c = object_getClass(array3);    //输出__NSArrayM

既然NSArray是一个抽象基类,那么势必在NSArray中覆写函数是无效的(在它的子类中会被重新实现),而NSArrayI又是一个被隐藏起来的类,所以无法直接在它的分类里面进行修改。

所以我们考虑method swizzling去实现方法的覆写。

+(void)load{
    Method m1 = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
    Method m2 = class_getInstanceMethod(self, @selector(pp_objectAtIndex:));
    method_exchangeImplementations(m1, m2);
}
-(id)pp_objectAtIndex:(NSUInteger)index{
    if (index>self.count-1) {
        NSLog(@"__%@__数组越界",self);
        return nil;
    }else{
        return [self pp_objectAtIndex:index];
    }
}
这样子,当我们发生数组越界的时候,就可以在控制台的地方得到
__(
    1,
    2,
    3
)__数组越界
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值