Objective-C Runtime(运行时)

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_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"));

你可能感兴趣的:(Objective-C Runtime(运行时))