Runtime Summary
作为一个 iOS 程序员,了解 runtime 是必须的。学习过程中也看过很多很多分析 runtime 的文章,halfrost、杨萧玉等大佬的文章更是读了好几遍。然后一段时间后,对 runtime 的印象还是模模糊糊。runtime 会用吗?会用。动态绑定属性,hook 交换方法,利用反射写路由…够吗?不够。
目录
- oc 中的类与对象
- oc 中的方法
- runtime 应用
- 总结
源码阅读
objc4-723
oc 中的类与对象
首先,了解一下基本的 oc 对于 objc_class 和 objc_object 的定义.
1. 来自runtime.h
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
这是很经典的一段,网上很多分析都基于这个定义。然而实际上这个片段已经不适用了。
在这里可以看到,在一个类中,有超类的指针,类名,版本的信息。 ivars是objc_ivar_list成员变量列表的指针;methodLists是指向objc_method_list指针的指针。methodLists是指向方法列表的指针。这里如果动态修改methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。
2. 来自objc-runtime-old.h
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
struct old_protocol_list *protocols;
// CLS_EXT only
const uint8_t *ivar_layout;
struct old_class_ext *ext;
...
}
3. 来自objc-runtime-new.h
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
}
4. 来自objc.h
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
实不相瞒,看下来云里雾里,所幸有很多前辈已经发表了很多自己的见解,顺着他们的思路,再回头看看就容易明白很多了。
来一波关于对象跟类的总结。
Objective-C里面,所有的对象底层都是 c 的结构体objc_object- 类也是对象,可以说包含
isa指针的就是对象 - 类里面有什么?
isa、superclass、cache、bits… isa指向元类,superclass指向父类,cache缓存指针和vtable(其实就是用来缓存常用的方法),bits包含了methods、properties、protocols等信息(在class_rw_t里)。- 对象如何调用方法?不可能每个对象都保存他们可以调用的方法,这样性能太差了。一个实例对象调用实例方法,通过 isa 指针找到它的类,在类的bits里可以找到方法调用。一个类调用类方法也是一样,通过 isa 指针找到这个类的元类,从元类中获取可执行的方法。
- 对象的内存管理。oc 通过引用计数管理对象的内存,引用计数保存在isa 指针中,isa 是一个isa_t类型的联合体,内部有一个bits,里面的extra_rc来表示引用计数。
oc 中的方法
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
runtime 的源码看起来真的有些吃不消,不像第三方库的源码,还看的懂一些。自己的确离大神还有很远的距离,他们早在15年、16年就详细得分析了这些源码。废话不多说,继续写一下 oc 中的方法,加深自己的理解。
IMP 是指向方法实现的一个指针,提到 imp 就不得不讲一下 oc 中的消息转发机制。
objc_msgSend 可以说是整个 oc 的核心。此函数是消息发送必经之路。简单的描述就是就是获取 IMP 并调用。它的源码是由汇编实现了,目前还看不太懂,现阶段只能站在前人的肩膀上加深自己的理解。
objc_msgSend 分为好几个阶段:
NilTest
判断被发送消息的对象是否为 nil 的。如果为 nil,那就直接返回 nil。因此在 oc 中 nil 调用方法并不会导致程序崩溃。search cache
前文了解到,在 objc_class 中有一个cache,这里缓存着常用的方法。如果每次调用方法都去所有的方法列表找显然很不明智,cache 是比较好的处理方式。MethodTableLookup
在缓存没有命中的情况下,我们就只能在方法表中找 IMP_class_lookupMethodAndLoadCache3->lookUpImpOrForward
递归向父类查找,方式跟前面一样,先查 cache 再查方法表。_class_resolveMethod
如果还是没有找到,就调用此方法,查找是否有开发人员动态添加的方法。id _objc_msgForward
如果还是没有,那就只能转发了。forwardingTargetForSelector
这一步是转发的补救措施之一:重写- (id)forwardingTargetForSelector:(SEL)aSelector如果 A 不能执行此方法,就用能执行此方法的 B 来替换,也就是替消息找备援接收者。methodSignatureForSelector->forwardInvocation
补救措施二:重写- (void)forwardInvocation:(NSInvocation *)anInvocation,消息手动重定向
2-8 的每一步只要找到了 imp 就直接执行,还会将 imp 存入 cache,方便下次调用。8后还未找到就程序崩溃。抛出经典的 unrecognized selector sent to instance xxxx 错误。
runtime 应用
1.Method Swizzling
这算是最常用的一个应用,比如要对一个应用的所有 controller 进行埋点,一个一个在代码里写那还算是程序员吗?利用 runtime,只要新建一个 controller 的 category,在 load 方法里面交换 viewWillAppear 实现。 Over
还有,我们经常做有关下标的操作(数组取值,字符串截取),容易产生越界。也只要 Method Swizzling 就可以做好防范。
2.Isa Swizzling
前面已经提到,有 isa 的就是对象,那么 Isa Swizzling 要用来干嘛呢?这个虽然自己用的不多,但是官方的 KVO 却是一个典型案例。
// 此处 p 的 isa 还是指向 Person
Person *p = [[Person alloc] init];
// 此处 p 的 isa 已经指向 NSKVONotifying_Person
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
这样乱交换不会出事吗?万一代码中调用object_getClass不是会出错?然而并没有,KVO 还在新类中重写了class,setter,dealloc,_isKVOA.
重写 class 方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。
重写 setter 方法显然就是为了监听,KVO 会在 setter 方法中新增另外两个方法的调用。
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
然后在didChangeValueForKey中调用
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
是不是豁然开朗,整个 KVO 就这么走通了。
3.Associated Object
这个用的也比较多,比如我们有时候需要 button 有一个属性 param ,让它能够携带参数,该怎么处理呢?我们可以写一个子类继承 UIButton,然后声明一个 param 属性就 ok,但是这样比较麻烦。最好可以在原生 button 上就能取到这个参数。这个时候我们就可以利用 runtime 关联一个属性。
- (void)setParam:(NSString *)param
{
objc_setAssociatedObject(self, ¶mKey,param,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)param
{
return objc_getAssociatedObject(self, ¶mKey);
}
4.反射
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class _Nullable NSClassFromString(NSString *aClassName);
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
FOUNDATION_EXPORT Protocol * _Nullable NSProtocolFromString(NSString *namestr) API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
应用在启动时,会调用每个类的 load 方法,并将类的信息与对应的字符串存储在一个 map 中,由此我们可以利用这些方法搞点事情。
比如路由:我们可以存储一份 Router : ControllerName 的 map,服务器下发路由后,我们找到对应的ControllerName,再通过NSClassFromString实例化对应的 controller,至此就完成了一个简单的路由。
总结
其实一些心得已经写在文中了,有一些是引用前辈的,有一些是自己的理解。对 runtime 老是一种似懂非懂的感觉,本篇算是好好整理了一下。