Runtime
是Object-C
的一种特性,本人并不感冒。不过这块内容却很流行,也是Object-C
动态特性的来源,被认为是比Swift
好的地方。
平时用不用是一回事,知道这些基础知识还是有好处的,至少跟人聊的时候能打上话。
动态特性带来了方便,但是同时也带来了很大的安全隐患,在使用的时候尽量谨慎一点。
另外,这是底层的函数,像ARC
这种偷懒用的好特性就没有了,要注意内存泄漏问题。(基本上无法避免)
Runtime全方位装逼指南
RuntimeLearn
这篇文章写得比较好,基础概念写得比较清晰,值得优先读
iOS动态性(二)可复用而且高度解耦的用户统计埋点实现
统计埋点确实是一个比较典型的应用,这篇文章写得比较清楚
对象、类、元类
Object-C
是一种面向对象的语言。“一切皆对象”是本质的一点。Object-C
就是借助实例,类,元类三级结构来实现这一点的。isa
指针是Object-C
采用c
语言的"结构体"来实现面向对象的方法。实例的
isa
指向对应的类,类的isa
指向元类。元类的
isa
都指向根元类,根元类的isa
指向自己。类和元类除了
isa
指针,还有一个super_class
指针,指向父类(父元类)。根类的父类指向nil
元类保存静态变量和静态类方法
跟元类的父类指向根类,根类的
isa
指针指向根元类
消息发送
Objective-C
中的方法调用,不是简单的方法调用,而是发送消息,也就是说,其实[receiver message]
会被编译器转化为:objc_msgSend(receiver, selector)
这个是
Objective-C
动态特性的本质;在函数调用之前插入一个消息转发,按照对象(id)
,函数(SEL)
,参数三级结构实现动态特性。一些函数定义
void objc_msgSend(void /* id self, SEL op, ... */ );
typedef struct objc_selector *SEL;
typedef struct objc_object *id;
// 下面几个都是将字符串转换为函数指针SEL;根据使用场景选择方便的
// 这个c字符串
SEL 变量名 = sel_registerName(const char *str); // 在c的模块中推荐用
// 下面两个是NSString
SEL 变量名 = NSSelectorFromString(NSString *aSelectorName); // 推荐用这个
SEL 变量名 = @selector(NSString *aSelectorName); // 这个用得比较多,不过难理解,不是很推荐
- 例子,有个类TestClass,有如下方法和调用
- (void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
}
TestClass *testObject = [[TestClass alloc] init];
[testObject showSizeWithWidth:110.5f andHeight:200.0f]
也可以用下面的调用方式:
((void (*) (id, SEL, float, float)) objc_msgSend) (testObject, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
这个就是
Object-C
动态特性的来源。id、SEL
都是一些指向结构体的指针,objc_msgSend
的类型是void
,在具体使用的时候需要强制转化为需要的类型。(参数类型,返回值类型都要考虑到)。这里有很大的安全隐患,代码难懂,很容易出错。所以本人一直不建议用。编译器会根据情况在
objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret或 objc_msgSend_fpret
五个方法中选择一个来调用。如果消息是传递给超类,那么会调用objc_msgSendSuper
方法,如果消息返回值是数据结构,就会调用objc_msgSendSuper_stret
方法,如果返回值是浮点数,则调用objc_msgSend_fpret
方法。
类的本质
-
Class
也是一个结构体指针类型
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
ivars:
指向该类的成员变量列表。大多数情况可以认为这个也是类的属性列表。不过有说法认为有另外的属性列表,只是这里看不出来。methodLists:
指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。这也是Category
实现的原理,同样解释了Category
不能添加属性的原因。protocols:
指向该类的协议列表。
这里没有单独的属性列表,给理解带来了困难。如果都是基本类型,可以认为成员变量列表就是属性列表,比如下面的“自动归档”部分处理的那样。
另外一种说法是有单独的属性列表,只是这里没有显示出来。比如“字典转模型”,就使用了属性列表。
class_copyPropertyList和class_copyIvarList的区别
远程调用
有些时候,比如首页,显示的内容由后台决定,实现所谓的“千人千面”
这里的实现基础,就是类
Class
、方法SEL
与字符串NSString
的互转
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);
有了
Class
之后,就可以通过id object = [[Class alloc] init];
得到对象有对象和
SEL
之后,就可以通过函数执行
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
客户端先把调用的类和方法在本地实现好,后台发送类名和方法名的字符串,然后根据字符串,实现动态调用。
本人经历过的四五个
App
中有一个是采用这种方案实现动态首页的。另外,网上也有对这个问题的详细描述。下面这篇链接是比较好的一篇。
- 关于组建化,本人更偏向于蘑菇街的方案。原因是这个方案
url
的编码更自由一点,对应关系可以自定义。另外,有蘑菇街的实践也是一个考虑原因。
对象关联
Category
可以添加方法,但是怎么样添加属性呢?答案是通过对象关联的方法。Category
中的属性,只会生成setter
和getter
方法,不会生成成员变量关联的属性和一个全局变量关联,那个
key
一般是一个静态全局变量相关函数:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object); // 移除所有关联属性,不要轻易使用
// 属性的修饰符,根据情况设置
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
这方面的资料很多,使用也相对简单,比如下面就有一篇:
iOS-OC-Runtime使用小谈(objc_setAssociatedObject)
自动归档
- 自动归档主要是实现
NSCoding
协议
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end
- 相关函数
// 把成员变量当做属性,获取列表
Ivar *class_copyIvarList(Class cls, unsigned int *outCount);
// 获得成员变量的名字,带_前缀;这是c字符串
const char *ivar_getName(Ivar v) ;
// 通过KVC获得成员变量的值
- (nullable id)valueForKey:(NSString *)key;
// 通过KVC设置成员变量的值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- 下面是一篇比较好的参考文章:
Runtime应用(三)实现NSCoding的自动归档和自动解档
字典与模型互转
在类的头文件中定义的属性,不包括额外定义的成员变量
使用属性列表函数,而不是成员列表函数
// 属性列表,不是成员变量列表
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
// 获取属性的名字
const char *property_getName(objc_property_t property) ;
- 可以使用
KVC
,也可以使用objc_msgSend
调用getter
和setter
函数进行值的读取和设置
字典转模型,自动归档等推荐的第三方库为YYModel
,实际用过,确实很方便
YYModel
方法动态解析
给一个对象发消息,就是执行objc_msgSend(id, SEL, ...)
函数。使用很小心,id、SEL
都正确的情况下,当然没问题。但是,如果出错了呢?
是的,崩溃,崩溃信息一般如下:
unrecognized selector sent to instance ...
- 不是
SEL
找不到吗?下面这个函数就是给机会,修改SEL
参数
+ (BOOL)resolveInstanceMethod:(SEL)sel;
- 这个对象没有,其他对象可能有啊。下面这个函数就是给机会,修改
id
参数
- (id)forwardingTargetForSelector:(SEL)aSelector;
- 其他对象也没有这个
SEL
,那么再给机会,id、SEL
都改,完全自定义。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- 最后,就会抛出异常,就是常说的崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector;
- 这块内容具体的应用场景在实际工作中还没有遇到过。上面这些函数都在
NSObject.h
文件中定义,是基类中的函数。
方法交换
- 这个是有使用场景的,最常见的场景是“统计埋点”
- 这项技术有一个专门的名字叫Method Swizzling,为什么这么叫,原因不清楚。
Objective-C的hook方案(一): Method Swizzling
- 主要用到的函数:
Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);
IMP method_getImplementation(Method m);
IMP method_setImplementation(Method m, IMP imp);
BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char *types);
- 可以考虑用来解决崩溃的问题,比如下面的文章
- 方法交换需要放在
+(void)load
方法中,并且要用dispatch_once
进行保护。道理很简单,交换偶数次不就被还原了吗?