Runtime是什么?
Runtime 是一个运行时系统,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。和静态编译器共同构成了Objective-C 语言,平时编写的Objective-C 代码,最终都转化为runtime的C语言代码。
为什么学习Runtime?
了解Runtime可以更好的开发的高效可用的代码。用最少的代码,最简单的逻辑实现想要的功能,可以突破系统级Api限制实现一些看似“非法”的功能,当然这不会影响App正常上架。
怎么应用Runtime?
-
基本概念
类和对象
Objective-C 是一个面向对象的语言。需要了解根元类(root meteClass)、元类(meta-class)、类(Class)和对象(Object) 的基本概念,Objective-C 中的类(Class) 和 对象(Object) 都是一个指向相应结构体的指针。
类是一个指向objc_class结构体的指针,对象是一个指向objc_object结构体的指针。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
//isa指向对象所属的类。任何类都是对象,此处isa 指向元类,元类isa指向根元类,
//根元类的isa指向自己(形成一个闭环,避免无限循环下去)
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//父类,如果该类已经是最顶层的根类,那么它为NULL
Class super_class OBJC2_UNAVAILABLE;
//类名
const char *name OBJC2_UNAVAILABLE;
//类的版本信息,默认为0
long version OBJC2_UNAVAILABLE;
//类信息,供运行期使用的一些位标识
long info OBJC2_UNAVAILABLE;
//该类的实例变量大小
long instance_size OBJC2_UNAVAILABLE;
//该类的成员变量链表(数组),所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
//方法链表(数组),类的所有方法都存放在这个methodlists 链表中
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
//缓存最近使用的方法。无缓存的情况下methodLists里查找,找到后加入cache。未找到搜索父类methodLists,找到调用,找不到进入消息转发(后面讲)。
struct objc_cache *cache OBJC2_UNAVAILABLE;
//代理协议链表(数组),delegates。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;//分配的缓存bucket的总数
unsigned int occupied OBJC2_UNAVAILABLE;//实际的占用的bucket的总数
Method buckets[1] OBJC2_UNAVAILABLE;//指向Method数据结构指针的数组,就是缓存的方法指针列表。
};
/// A pointer to an instance of a class.
/// objc_object是表示一个类的实例的结构体
typedef struct objc_object *id;
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;//isa指向对象所属的类。这里指向本对象所属类
};
举个栗子
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
第一步:执行alloc方法。UILabel方法列表里找不到 +alloc的方法,于是逐层去父类方法列表
methodLists中查找,最终在NSObject找到并响应 +alloc方法。
第二步:根据UILabel所需内存空间大小开始分配内存空间,然后把isa指针指向UILabel类。同时,+alloc也被加进cache列表里面。
第三步:执行-initWithFrame方法,如果UILabel响应该方法,则直接将其加入cache;如果不响应,则去父类查找
第四步:下次再次执行该代码,则会直接从cache中取出相应的方法,直接调用。
补充:在IOS项目代码中我们所写的方法可能会包含参数和返回值,这些参数和返回值Runtime是无法直接识别的,所以编译器会进行一个类型编码Type Encoding的操作,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。可以使用@encode编译器指令来获取它。传入一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。
int num = 2;
NSLog(@"int encoding type: %s", @encode(typeof(num)));
成员变量和属性
Objective-C 中创建变量有几种方式。.h 或 .m中直接在@interface 内创建、全局变量 和 关联对象
成员变量的结构体如下:
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE; // 变量名。例如:name
char *ivar_type OBJC2_UNAVAILABLE; // 变量类型 。例如:NSString
int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节
#ifdef __LP64__
int space OBJC2_UNAVAILABLE; //占用空间大小
#endif
}
关联对象通常用于解决无法为Category 添加属性的案例。只会在运行时被添加,关联对象类似于一个字典。
static char testKey;
NSString *name = @"xiaobai";
objc_setAssociatedObject(self, &testKey, name, OBJC_ASSOCIATION_RETAIN);
参数第一个:关联给哪个对象(也可以说关联到哪个宿主)
参数第二个:给name对象添加一个key,通过这个key可以获取到name对象。
参数第三个:被关联的对象。这里是name。
参数第四个内存管理规则,有以下几种
OBJC_ASSOCIATION_ASSIGN //宿主释放,关联对象不释放
OBJC_ASSOCIATION_RETAIN_NONATOMIC //宿主释放,关联对象会被释放。含有NONATOMIC不需要保护线程安全性
OBJC_ASSOCIATION_COPY_NONATOMIC //宿主释放,关联对象会被释放。含有NONATOMIC不需要保护线程安全性
OBJC_ASSOCIATION_RETAIN //宿主释放,关联对象会被释放。保护线程安全性
OBJC_ASSOCIATION_COPY //宿主释放,关联对象会被释放。保护线程安全性
//读取关联对象
id objc_getAssociatedObject (self, &testKey);
参数第一个:宿主
参数第二个:关联对象key
//移除宿主身上所有关联对象
objc_removeAssociatedObjects (self);
补充:同一个关联对象key关联不同的对象,会移除之前该key所关联的对象。
方法与消息
Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
SEL sel = @selector(method);
NSLog(@"sel : %p", sel);
输出: sel : 0x100002d72
在Objective-C 同一个类中,SEL 是唯一的,所以方法名也是唯一的。不像java一样可以同一个方法名,不同的参数。SEL只是一个指向方法的指针(一个根据方法名hash化了的KEY值,能唯一代表一个方法,可以加快查找速度,仅仅是一个名字),通过SEL 可以查询到其对应的IMP函数指针,IMP函数指针指向真正执行方法的地址。
objc_method 结构体如下
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。
在程序运行时,编译器会将[receiver message] 转化为 objc_msgSend函数调用
objc_msgSend(receiver, selector, arg1, arg2, ...)
第一个参数:接受者
第二个参数:SEL
其余参数:参数
当消息发送给一个对象时,有以下几个流程
1 objc_msgSend通过对象的isa指针获取到类的结构体。
2 在类的cache里面查找方法的selector。如果 没有找到selector。在类的methodlists里寻找。
3 类的methodlists未找到,通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的methodlists里面查找方法的selector,会一直沿着类的继承体系到达NSObject类。
4 一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实 现,最后没有定位到selector,则会走消息转发流程。
Method Swizzling
Method Swizzling被普遍认为是一种黑魔法,使用得当可以起到举一反三的效果,反正则可能引起灾难。前面讲到通过方法名调用方法,最终实现取决于对应的IMP,只要交换IMP就可以得到替换方法实现的作用,这就是所谓的Method Swizzling
Method oldMethod = class_getInstanceMethod(class, @selector(oldMethod));
Method newMethod = class_getInstanceMethod(class, @selector(newMethod));
//交换IMP
method_exchangeImplementations(oldMethod, newMethod)
对于Method Swizzling ,通常会写到+load方法里,+load能够保证在类初始化的时候就会被加载,这为改变系统行为提供了一些统一性。另外会用dispatch_once保障其原子性,确保代码即使在多线程环境下也只会被执行一次。
为了避免出现灾难,需要注意几点,始终调用方法的原始实现、使用不同的方法命名,避免冲突和 理解执行原理
协议与分类
协议也就是我们经常写在@protocol 里的那些接口方法,在另外一个类中去实现它
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}
Runtime并没有在<objc/runtime.h>头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class中,我们可以通过针对objc_class的操作函数来获取分类的信息。
Runtime 提供了很多有关protocol操作函数,这里列出几个
// 返回指定的协议
OBJC_EXPORT Protocol *objc_getProtocol(const char *name)
__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
// 创建新的协议实例(如果同名的协议已经存在,则返回nil)
OBJC_EXPORT Protocol *objc_allocateProtocol(const char *name)
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);
// 在运行时中注册新创建的协议(新建的协议注册后才可以使用。协议只有在未注册前可以编辑。)
OBJC_EXPORT void objc_registerProtocol(Protocol *proto)
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);
// 为协议添加方法
OBJC_EXPORT void protocol_addMethodDescription(Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod)
__OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);
// 获取运行时所知道的所有协议的数组(获取到的数组要记得使用free来释放)
OBJC_EXPORT Protocol * __unsafe_unretained *objc_copyProtocolList(unsigned int *outCount)
__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
-
实践案例
No.1 统计用户操作行为,用户页面的访问轨迹。
正常情况下会在每个VC页面的viewWillAppear 里去做当前页面信息的上报,不利于维护,写起来又比较麻烦。Method Swizzling 可以解决这个问题
//+ load 保证代码被有效加载
+ (void)load
{
static dispatch_once_t onceToken;
//dispatch_once保证原子性
dispatch_once(&onceToken, ^{
Method systemMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method customMethod = class_getInstanceMethod(self, @selector(jk_viewWillAppear:));
method_exchangeImplementations(systemMethod, customMethod);
});
}
- (void)jk_viewWillAppear:(BOOL)animation
{
//一定要调用方法的原始实现。由于交换了IMP,所以不会导致死循环。
[self jk_viewWillAppear:animation];
NSString *currentVCName = [[self class] description];
//上报currentVCName
//upload info
}
No.2 获取一个类的所有方法
class_copyMethodList 获取到的方法列表包括protocol定义的协议方法
unsigned int count;
Method *methods = class_copyMethodList([self class], &count);
for (int i = 0; i < count; i++)
{
Method method = methods[i];
SEL selector = method_getName(method);
NSString *name = NSStringFromSelector(selector);
NSLog(@"%@", name);
}
No.3 获取一个类的所有属性(ALAssetsLibrary所得到的相册播放路径是Asset:// 开头的一个视频链接,AVPlayer 是无法直接播放的,所以需要转化为file:// 开头的视频链接才可以播放,通过对ALAsset的属性进行分析,可以得到一个AVPlayer 可以播放的 file://开头的视频链接)
// 获取当前类的所有属性
unsigned int count;// 记录属性个数
objc_property_t *properties = class_copyPropertyList(self, &count);
// 遍历
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
// An opaque type that represents an Objective-C declared property.
// objc_property_t 属性类型
objc_property_t property = properties[i];
// 获取属性的名称 C语言字符串
const char *cName = property_getName(property);
// 转换为Objective C 字符串
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
}
No.4 为分类添加属性
static char flashColorKey;
- (void)setName:(UIColor *)color
{
objc_setAssociatedObject(self, &flashColorKey, color, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIColor *)color
{
objc_getAssociatedObject(self, &flashColorKey);
}
No.5 动态地为某个类添加属性方法, 修改属性值方法
//创建一个用户信息model
Class UserInfoModel = objc_allocateClassPair([NSObject class], "Widget", 0);
//参数第一个: 添加到哪里,宿主是谁
//参数第二个:SEL,只是一个名字,相当于key,通过该key可以找到imp
//参数第三个:方法的真正实现IMP。
//参数第四个:方法的返回值类型,有几种,可以参加上面的type enocode
class_addMethod(UserInfoModel, @selector(jk_description), (IMP)jk_description, "v@:");
// 添加一个实例变量(不能在已存在的class中添加Ivar,所有说必须通过objc_allocateClassPair动态创建一个class,才能调用class_addIvar创建Ivar,最后通过objc_registerClassPair注册class。)
//参数第一个:即将注册的class
//参数第二个:变量名字
//参数第三个:内存大小
//参数第四个:内存对齐方式
//参数第五个:变量类型
class_addIvar(UserInfoModel, "userName", sizeof(id), rint(log2(sizeof(id))), @encode(id));
class_addIvar(UserInfoModel, "delegate", sizeof(id), rint(log2(sizeof(id))), @encode(id));
// 注册这个类
objc_registerClassPair(UserInfoModel);
// 创建一个model实例
id userInfoModel = [[UserInfoModel alloc] init];
//设置实例变量的值
[userInfoModel setValue:"lihua" forKey:[NSString stringWithUTF8String:"userName"]];
//创建一个protocol
Protocol *protocol = objc_allocateProtocol("UserInfoModelDelegate");
//参数第一个:协议
//参数第二个:SEL
//参数第三个:返回值类型
//参数第四个:是否为optional
//参输第五个:是否为实例方法
protocol_addMethodDescription(protocol, @selector(didSelectedItem), "v@:", NO, YES);
//注册protocol
objc_registerProtocol(protocol);
// 向类实例发送一条消息
objc_msgSend(userInfoModel, NSSelectorFromString(@"jk_description"));