0x00
在OC中, isa是一个很重要的结构体(参考文章), OC runtime正是通过isa来确定object的类型。跟method-swizzling一样, 在程序运行的过程中, 我们可以通过runtime方法来改变object的isa, 这种技术叫isa-swizzling。
0x01
isa-swizzling最著名的应用场景, 就是KVO。 当class A的某个实例的属性被监听, 系统会自动创建class A的子类NSKVONotifying_A
, 重写对应属性的setter方法,加入对外通知属性变化的代码。但是这个子类NSKVONotifying_A
对外界是不可见的, 我们通过运行时看到的也只是class A, 这是怎么实现的呢? 我们可以参考一下这篇文章. 摘录主要部分如下:
///------ "addObserver:forKeyPath:options:context:"方法的实现 -----
/// 动态创建子类
NSString *newName = [@"PYKVONotifying_" stringByAppendingString:NSStringFromClass(object_getClass(self))];
NSString *setterName = [[@"set" stringByAppendingString:[[[keyPath uppercaseString] substringToIndex:1] stringByAppendingString:[keyPath substringFromIndex:1]]] stringByAppendingString:@":"];
Class subCls = objc_allocateClassPair(object_getClass(self), [newName UTF8String], 0);
class_addMethod(subCls, NSSelectorFromString(setterName), (IMP)DefaultSetterForKVO, "v@:@");
objc_registerClassPair(subCls);
/// 替换子类的isa
object_setClass(self, subCls);
更详细的信息可以参考苹果官方文档.
0x02
一般的文章, 对于isa-swizzling的介绍, 也就仅仅限于KVO的实现原理, 貌似这种技术,也就是苹果为了实现KVO而搞出来的一个补丁而已。 事实上, 在实际app开发中, isa-swizzling也可以有用武之地。
考虑这样一种情况:
如果你开发了一个业务组件, 需要给外部提供服务, 但是你又希望尽可能的对外隐藏具体业务实现, 不希望外界可以通过runtime方式获取内部实现, 那么isa-swizzling就是一个不错的选择方案。
举例如下:
/// ========= 模拟业务组件内部逻辑 =========
/// 定义公开给外部组件的protocol
@protocol MyBusinessModel
@property(nonatomic, assign) NSInteger modelID;
@property(nonatomic, copy) NSString *modelName;
@end
/// 定义业务protocol实现类,
/// 该类并不会公开给外部组件, 但是外部组件通过runtime可以发现。
@interface MyBusinessModel : NSObject
@end
@implementation MyBusinessModel
@synthesize modelID, modelName;
@end
/// 定义业务protocol的内部实现类。
/// 该类并不公开给外部组件, 并且也不希望外部组件通过runtime获取到。
/// 组件内部业务逻辑都隐藏在这个类中。
@interface MyBusinessModelInternal : MyBusinessModel
- (void)internalBusiness;
@end
@implementation MyBusinessModelInternal
/// 模拟需要隐藏的组件内部业务逻辑
- (void)internalBusiness {
NSLog(@"%@; id: %ld, name: %@", self, self.modelID, self.modelName);
}
@end
///========== 模拟外部组件 =========
void thirdpartyComponent(id param) {
///模拟外部组件正常业务逻辑
NSLog(@"param: %@", param);
///模拟外部组件试图通过运行时调用组件内部私有逻辑
@try {
[param performSelector:@selector(internalBusiness)];
} @catch (NSException *exception) {}
}
/// ========== 测试代码 =========
/// 组件内部业务逻辑
MyBusinessModelInternal *model = [[MyBusinessModelInternal alloc] init];
model.modelID = 1024;
model.modelName = @"foo model";
[model internalBusiness]; ///组件内部业务逻辑调用
/// 模拟组件和外部组件相互调用。
/// 需要给外部组件传递业务信息,但是希望能够防范外部组件非正常调用组件内部业务逻辑。
/// 因此通过isa-swizzling隐藏内部类信息。
object_setClass(model, MyBusinessModel.class);
thirdpartyComponent(model); //向外部发起消息
/// 模拟外部组件处理处理完成之后, 返回相应业务信息,
/// 这个时候, 内部组件可以把传递回来的参数恢复成内部实现类。
object_setClass(model, MyBusinessModelInternal.class);
[model internalBusiness];///组件内部业务逻辑调用
运行结果如下:
可以看出, 当外部组件试图调用被隐藏的内部方法时, 就会抛出错误。 但是外部组件把这个对象返回来之后, 内部还是可以恢复成内部实现, 正常调用内部逻辑。
那么, 如果外部组件回传的并非我们之前传给它的object, 而是外部组件自己创建的object呢? 收到这个object之后, 还能不能把它"恢复"成内部实现呢?
///模拟外部组件自己创建一个内部业务model
MyBusinessModel *model = [[MyBusinessModel alloc] init];
model.modelID = 1024;
model.modelName = @"foo model";
///模拟外部组件把自己创建的业务model回传给内部组件。
///然后内部组件把它照常"恢复"成内部model
object_setClass(model, MyBusinessModelInternal.class);
[(MyBusinessModelInternal *)model internalBusiness]; ///模拟调用内部逻辑
运行结果如下:
可以看出来, 这个丝毫不受影响。
其实如果明白了isa的作用, 这里边的原因就很好理解了。举个不是特别准确的例子:runtime可以类比为警察, 每个object就是普通民众, isa就是身份证。 警察判断你的身份, 靠的就是你的身份证。 但是现在身份证可以被伪造, 而警察又没有鉴定身份证真假的措施, 那么用伪造的身份证骗过警察也就不奇怪了。
0x03
延伸一下:
一般情况下, 我们如果需要把某个类型转换成另外一个类型, 会使用强制转换的方式:
NSArray *obj = [NSArray arrayWithObject:@"Hi, this is isa-swizzling test!"];
MyBusinessModelInternal *model = (MyBusinessModelInternal *)obj;
@try {
[model internalBusiness];
} @catch (NSException *exception) {}
运行结果:
可以看出, 如果不小心强制转换两个不兼容的类型, 肯定会报错的。编译器比较傻, 很容易骗过, 但是runtime可没那么好骗。 那么如果我们动些手脚呢?
NSTextField *obj = [[NSTextField alloc] init];
MyBusinessModelInternal *model1 = (MyBusinessModelInternal *)obj;
object_setClass(model1, MyBusinessModelInternal.class);
NSLog(@"model1: %@", model1);
[model1 internalBusiness];
object_setClass(obj, NSTextField.class);
是不是感觉runtime也是可以被欺骗的呢?(不过经过对其他类型的测试, 不一定100%都可以。 这个跟两个类的ivars结构是否接近有关系, ivars结构越接近, 那么越容易成功。)