0x00 前言
这篇文章是自己在学习 CFHipsterRef Chapter 2 Objective-C Runtime 时做的笔记。
0x01 libobjc
libobjc
是 Objective-C 2.0 运行时的共享库。
要想使用 runtime,可以导入 <objc/runtime.h>
头文件。
0x02 发送消息(Message Sending)
在 Objective-C 中,调用一个对象的某个方法,一般的写法是这样的:
然后编译器最后会将该语句翻译成:
1
| objc_msgSend(object, @selector(message));
|
objc_msgSend
有几种形式:
objc_msgSend
:向一个类发送简单返回值类型的消息
objc_msgSend_stret
:向一个类发送返回值类型为结构体的消息
objc_msgSendSuper
:向父类发送简单返回值类型的消息
objc_msgSendSuper_stert
:向父类发送返回值类型为结构体的消息
属性定义了对象状态的公共接口。
例如,访问一个对象的属性列表,我们可以通过属性元编程来避免手动实现 NSCoding
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #pragma mark - NSCoding
- (id)initWithCoder:(NSCoder *)decoder { if (self = [super init]) { unsigned in count; objc_property_t *properties = class_copyPropertyList([self class], &count); for (NSUInteger i = 0; i < count; i++) { objc_property_t property = properties[i]; NSString *key = [NSString stringWithUTF8String:property_getName(property)]; [self setValue:[decoder decodeObjectForKey:key] forKey:key]; } free(properties); } return self; }
- (void)encodeWithCoder:(NSCoder *)coder { unsigned int count; objc_property_t *properties = class_getPropertyList([self class], &count); for (NSUInteger i = 0; i < count; i++) { objc_property_t property = properties[i]; NSString *key = [NSString stringWithUTF8String:property_getName(property)]; [coder encodeObject:[self valueForKey:key] forKey:key]; } free(properties); }
|
0x04 关联对象(Associated Objects)
关联对象是 Objective-C 运行时的一个特性,允许开发者对已经存在的类在扩展中添加自定义的属性,这非常有用。
关联对象使用非常简单,用 objc_setAssociatedObject
存储一个关联值,用 objc_getAssociatedObject
获取这个关联值。
任何关联对象都是通过一个 key 来关联的,这个 key 可以是任何常量值。最简单的方法就是传入 getter 方法选择器(方法选择器在运行时保证是唯一且不变的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @interface NSObject (AssociatedObject)
@property (strong, nonatomic) id associatedObject;
@end
@implementation NSObject (AssociatedObject)
- (void)setAssociatedObject:(id)object { objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
- (id)associatedObject { return objc_getAssociatedObject(self, @selector(associatedObject)); }
|
0x05 动态添加方法(Dynamically Adding a Method)
正如属性描述了一个对象的状态,方法则构成了对象的行为。
在 Objective-C 中,方法以 +
或 -
前置声明,表示该方法是类方法还是实例方法(也就是说是通过类调用的还是类的对象来调用的)。
一个类可以通过创建一个 category 来添加新的方法,然后可以在包含了这个 category 的地方使用这些新的方法。
不过,方法也可以在运行时创建:
1 2 3 4 5 6 7
| Class c = [NSObject class]; IMP addingMethodIMP = imp_implementationWithBlock((NSString *)^(id self, id arg1) { return [NSString stringWithFormat:@"arg1 = %@", arg1]; });
const char *addingMethodTypes = [[NSString stringWithFormat:@"%s%s%s", @encode(id), @encode(id), @encode(SEL)] UTF8String]; class_addMethod(c, @selector(addingMethodWithArg:), addingMethodIMP, addingMethodTypes);
|
0x06 Method Swizzling
Method Swizzling 是改变一个已存在的方法的实现的过程。这个技术是改变一个映射到一个类的调度表中的底层函数的方法,在运行时修改为调用自己定义的方法。
比如在某些时候想要跟踪一个应用里面的每个 ViewController 在一次生命周期内打开的次数。想要实现这个功能有几种方法:
- 每个 ViewController 都添加跟踪代码到重载的
viewDidAppear
方法里面,但是这样会有很多重复的代码。
- 子类化 UIViewController,但是这样的话,还要子类化
UITableViewController
、UINavigationController
等其他用到的 ViewController,还是要添加很多重复的代码。
有了 Method Swizzling,解决办法就非常优雅了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewDidAppear:); SEL swizzledSelector = @selector(xxx_viewDidAppear:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMthod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); }
#pragma mark - Method Swizzling
- (void)xxx_viewDidAppear:(BOOL)animated { [self xxx_viewDidAppear:animated]; NSLog(@"viewDidAppear: %@", self); }
@end
|
现在,当任何 UIViewController 对象或者 它的子类对象调用 viewDidAppear
方法时,都会打印出一个 log。
因为 Method Swizzling 是影响全局状态的,所以尽可能减少竞态条件的可能性非常重要。
+ load
方法保证在类初始化的时候执行,相比之下,+ initialize
不保证什么时候它会被执行(事实上,除非 app 直接对一个类发送消息,否则这个类的 + initialize
方法不会被调用)。
0x07 动态创建一个类(Dynamically Creating a Class)
结合之前提到的动态属性和方法定义,libobjc
的终极大招是运行时创建一个类。
创建一个 Product
类,有两个属性:name
和 price
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @interface Product : NSObject
@property (readonly) NSString *name; @property (readonly) double price;
- (instancetype)initWithName:(NSString *)name price:(double)price;
@end
@implementation Product
- (instancetype)initWithName:(NSString *)name price:(double)price { if (self = [super init]) { self.name = name; self.price = price; } return self; }
|
运行时创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| Class c = objc_allocateClassPair([NSObject class], "Product", 0); class_addIvar(c, "name", sizeof(id), log2(sizeof(id)), @encode(id)); class_addIvar(c, "price", sizeof(double), log2(sizeof(double)), @encode(double));
Ivar nameIvar = class_getInstanceVariable(c, "name"); ptrdiff_t priceIvarOffset = ivar_getOffset(class_getInstanceVariable(c, "price"));
IMP initIMP = imp_implementationWithBlock(^(id self, NSString *name, double price) { object_setIvar(self, nameIvar, name); char *ptr = ((char *)(__bridge void *)self) + priceIvarOffset; memcpy(ptr, &price, sizeof(price)); return self; });
const char *initTypes = [[NSString stringWithFormat:@"%s%s%s%s%s%s", @encode(id), @encode(id), @encode(SEL), @encode(id), @encode(id), @encode(NSUInteger)] UTF8String]; class_addMethod(c, @selector(initWithFirstName:lastName:age), initIMP, initTypes);
IMP nameIMP = imp_implementationWithBlock(^(id self) { return object_getIvar(self, nameIvar); }); const char *nameTypes = [[NSString stringWithFormat:@"%s%s%s", @encode(id), @encode(id), @encode(SEL)] UTF8String]; class_addMethod(c, @selector(name), nameIMP, nameTypes);
IMP priceIMP = imp_implementationWithBlock(^(id self) { char *ptr = ((char *)(__bridge void *)self) + priceIvarOffset; double price; memcpy(&price, ptr, sizeof(price)); return price; }); const char *priceTypes = [[NSString stringWithFormat:@"%s%s%s", @encode(double), @encode(id), @encode(SEL)] UTF8String]; class_addMethod(c, @selector(age), priceIMP, priceTypes);
objc_registerClassPair(c);
|
这里代码很多,我们来一点一点说。
首先,一个类用 objc_allocateClassPair
被分配了内存空间,指定了这个类的父类和类名。
然后,使用 class_addIvar
给这个类添加了实例变量。第四个参数用于确定变量的最小对齐方式(variable’s minimum alignment),这取决于 ivar 的类型和目标平台架构。
接下来是用 imp_implementationWithBlock:
定义初始化函数(initializer)的实现。调用 object_setIvar
设置 name
。price
是通过之前计算的 offset 执行 memcpy
来设置的。
为了添加初始化函数,需要计算每个参数的类型编码。@encode
和 字符串插值有点尴尬混乱,但是就是这样的。
添加 ivar 的 getter 方法类似。
最后,一旦所有的方法都被添加,类就被注册到运行时。从那时起,Product
就可以像其他任何 Objective-C 类一样交互了:
1 2
| Product *widget = [[Product alloc] initWithName:@"Widget" price:50.00]; NSLog(@"%@: %g", widget.name, widget,price);
|