Objective-C runtime运行时详解

最近看了一下runtime运行时方面的文章,总结一下,加上自己的一下理解,如果文章有未全和不足的地方,欢迎各位在下方留言补充和指正。

image.png

目录:

  1. runtime 消息机制
  2. runtime 添加属性
  3. runtime 交换方法
  4. runtime 动态添加方法
  5. runtime Class的常见方法

1.获取成员变量列表
2.获取属性列表
3.获取方法列表
4.获取协议列表
5.获得类方法
6.获得实例方法
7.添加方法
8.替换原方法实现
9.交换两个方法

  1. runtime method swizzling 黑魔法

runtime 消息机制

对于OC代码,调用方法的实质就是一个消息发送,OC底层通过runtime实现
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。

  • 首先必须要导入头文件 #import

  • 在使用objc_msgSend方法编译时可能出现报错的情况,对应的解决办法如下
    image.png
  • 新建一个Person类 Person.h中代码

/** 实例方法*/
// 无参数无返回值
- (void)run1;
// 无参数有返回值
- (NSString *)run2;
// 有参数无返回值
- (void)run3:(NSString *)string;
// 有参数有返回值
- (NSString *)run4:(NSString *)string;

/** 类方法*/
// 无参数无返回值
+ (void)run1;
// 无参数有返回值
+ (NSString *)run2;
// 有参数无返回值
+ (void)run3:(NSString *)string;
// 有参数有返回值
+ (NSString *)run4:(NSString *)string;
  • Person.m中代码
- (void)run1 {
    NSLog(@"实例方法 :run1");
}

- (NSString *)run2 {
    return @"实例方法 :run2";
}

- (void)run3:(NSString *)string {
    NSLog(@"run3 --- 参数:%@", string);
}

- (NSString *)run4:(NSString *)string {
    return [NSString stringWithFormat:@"run4 --- 参数:%@", string];
}

+ (void)run1 {
    NSLog(@"run1 classMethod");
}

+ (NSString *)run2 {
    return @"run2 classMethod";
}

+ (void)run3:(NSString *)string {
    NSLog(@"run3 classMethod --- 参数:%@", string);
}

+ (NSString *)run4:(NSString *)string {
    return [NSString stringWithFormat:@"run4 classMethod --- 参数:%@", string];
}
  • 利用objc_msgSend调用上面的这些方法
/** 对象方法/实例方法 */
    // 底层的实际写法
    Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
    person = objc_msgSend(person, sel_registerName("init"));

    NSLog(@"无参数无返回值:");
    
    ((void (*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("run1")); 
    
    objc_msgSend(person, @selector(run1));
    
    NSLog(@"无参数有返回值:");
    
    NSString *run2Return1 = ((NSString *(*) (id, SEL)) (void *)objc_msgSend)(person, sel_registerName("run2"));
    NSLog(@"%@", run2Return1);
    
    NSString *run2Return2 = objc_msgSend(person, @selector(run2));
    NSLog(@"%@", run2Return2);
    
    NSLog(@"有参数无返回值:");
    
    ((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run3:"), @"3333");

    
    NSLog(@"有参数有反回值:");
    
    NSString *run4Return1 = ((NSString *(*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run4:"), @"4444");
    NSLog(@"%@", run4Return1);
    
    NSLog(@"--------------------------");
    
    /** 类方法 */
    
    NSLog(@"classMethod - 无参数无返回值:");
    
    ((void (*) (id, SEL)) (void *)objc_msgSend)(Person.class, sel_registerName("run1"));
    
    objc_msgSend(Person.class, @selector(run1));
    
    NSLog(@"classMethod - 无参数有返回值:");
    
    NSString *run2Return1Class = ((NSString *(*) (id, SEL)) (void *)objc_msgSend)(Person.class, sel_registerName("run2"));
    NSLog(@"%@", run2Return1Class);
    
    NSString *run2Return2Class = objc_msgSend(Person.class, @selector(run2));
    NSLog(@"%@", run2Return2Class);
    
    NSLog(@"classMethod - 有参数无返回值:");
    
    ((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(Person.class, sel_registerName("run3:"), @"3333");
    
    NSLog(@"classMethod - 有参数有反回值:");
    
    NSString *run4Return1Class = ((NSString *(*) (id, SEL, NSString *)) (void *)objc_msgSend)(Person.class, sel_registerName("run4:"), @"4444");
    NSLog(@"%@", run4Return1Class);
  • 注:
/**
   错误写法(arm64崩溃偶尔发生)
   */
    objc_msgSend(person, sel_registerName("run3:"), @"12345678");
    /**
     标准写法
     */
    ((void (*) (id, SEL, NSString *)) (void *)objc_msgSend)(person, sel_registerName("run3:"), @"不能直接写objc_msgSend,会出现崩溃的现象(正常应该是可以的)");
  • 解释一下标准写法前面参数
((void (*) (id, SEL)) (void *)objc_msgSend)
1、第一个void代表是否有返回值
   如果返回值是NSString类型的 例:无参数有返回值的方法
   写法:((NSString *(*) (id, SEL)) (void *)objc_msgSend)
2、(id, SEL)
   如果方法有参数,参数类型是NSString 例:有参数无返回值
   写法:((void *(*) (id, SEL, NSString *)) (void *)objc_msgSend)
3、关于离objc_msgSend最近的void从互联网上还没找到具体含义,还望知道的好友留言或私信告知
应用与注意

注:使用objc_msgSend()创建对象不能自动释放,对象需要手动release。使用runtime执行初始化方法创建的对象的时候是不在ARC控制之下的,所以在该类销毁的时候需要手动release
应用:使用objc_msgSend()创建对象时好处,应用之一就是在一个控制器要跳转多个控制器的时候,不再需要每个控制器都单独写一遍初始化,也不再需要每一个控制器单独写release方法,使用runtime的话,使用一个或者根据情况使用几个回调,返回控制器的类名以及相应的参数就好了。

Class class = objc_getClass(controllerName.UTF8String); //或者 NSStringFromClass(<#Class  _Nonnull __unsafe_unretained aClass#>)
        
id viewController = ((id(*)(id,SEL))objc_msgSend)(class,NSSelectorFromString(@"new"));

 [self xpz_pushViewController:viewController];
// 导航控制器获得控制权后进行release即可
- (void)xpz_pushViewController:(__kindof UIViewController *)viewController
{
    [self pushViewController:viewController];
    
    //release
    ((void(*)(id,SEL))objc_msgSend)(viewController,NSSelectorFromString(@"release"));
}

category添加属性

1.面试中经常会被问到如何给category添加属性,在平时我们偶尔也会遇到想要在分类中添加属性的情况,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成,其实可以使用runtime动态添加属性方法
2.还有另外一种方法: use @dynamic or provide a method implementation in this category

1.使用runtime动态添加属性

// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
  • 需求:在UIView上添加一个播放器
  • objc_getAssociatedObject有两个参数,第一个参数为从该object中获取关联对象,第二个参数为想要获取关联对象的key;

对于第二个参数const void *key,有以下四种推荐的key值:

  1. 声明 static char kAssociatedObjectKey;,使用 &kAssociatedObjectKey 作为 key 值;
  2. 声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey;,使用 kAssociatedObjectKey 作为key值;
  3. selector ,使用 getter 方法的名称作为key值;
  4. 而使用_cmd可以直接使用该@selector的名称,即hideButton,并且能保证改名称不重复。(与上一种方法相同)
static char playerViewKey;  // playerView
static void *playerLayerKey = &playerLayerKey; // playerLayer
/************************************************* playerView ********************************************************/
// getter
- (UIView *)playerView {
    UIView *_playerView = objc_getAssociatedObject(self, &playerViewKey);
    if (!_playerView) {
        objc_setAssociatedObject(self, &playerViewKey, _playerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _playerView;
}
// setter
- (void)setPlayerView:(UIView *)playerView {
    return objc_setAssociatedObject(self, &playerViewKey, playerView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}




/************************************************* avplayer ********************************************************/
//getter
- (AVPlayer *)avPlayer {
    AVPlayer *_avPlayer = objc_getAssociatedObject(self, @selector(avPlayer));
    if (!_avPlayer) {
        objc_setAssociatedObject(self, @selector(avPlayer), _avPlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _avPlayer;
}
//setter
- (void)setAvPlayer:(AVPlayer *)avPlayer {
    objc_setAssociatedObject(self, @selector(avPlayer), avPlayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}



/************************************************* playerLayer ********************************************************/
- (AVPlayerLayer *)playerLayer {
    return objc_getAssociatedObject(self, playerLayerKey);
}
- (void)setPlayerLayer:(AVPlayerLayer *)playerLayer {
    objc_setAssociatedObject(self, playerLayerKey, playerLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}



/************************************************* playerItem ********************************************************/
/**
 getter方法
 */
- (AVPlayerItem *)playerItem {
    return objc_getAssociatedObject(self, _cmd);
}
/**
 setter方法
 */
- (void)setPlayerItem:(AVPlayerItem *)playerItem {
    objc_setAssociatedObject(self, @selector(playerItem), playerItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

扩展:

objc_getAssociatedObject与objc_setAssociatedObject方法另一种用途,在tableViewCell中的btn点击事件中使用

在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;方法中使用:
if (self.GroupListArr.count > 0) {
            cell.hidden = NO;
            CB_ActivityGroupModel *model = self.GroupListArr[0];
            [cell setModel:model];
            [cell.joinGroupBtn addTarget:self action:@selector(joinGroupAction:) forControlEvents:(UIControlEventTouchUpInside)];
            objc_setAssociatedObject(cell.joinGroupBtn, &joinGroup, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
使用
- (void)joinGroupAction:(UIButton *)btn
{
    CB_ActivityGroupModel *model = objc_getAssociatedObject(btn, &joinGroup);
    CB_ActivityGroupDetailsVC *vc = [[CB_ActivityGroupDetailsVC alloc]init];
    vc.goodModel = self.detailsModel;
    vc.model = model;
    [self.navigationController pushViewController:vc animated:YES];
}

runtime 交换方法

当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

class_getInstanceMethod 得到类的实例方法
class_getClassMethod 得到类的类方法

  • 给系统的imageNamed添加额外功能
#import "UIImage+Image.h"
#import 

@implementation UIImage (Image)
+ (void)load
{
    Class class = [self class];
    // 类方法
    // 1.获取 imageNamed方法地址
    Method originalMethod = class_getClassMethod(class, sel_registerName("imageNamed:"));
    // 2.获取 xpz_imageNamed方法地址
    Method swizzledMethod = class_getClassMethod(class, @selector(xpz_imageNamed:));
    // 交换 imageNamed:
    method_exchangeImplementations(originalMethod, swizzledMethod);
    
}
/**
 看清楚下面是不会有死循环的
 调用 imageNamed => xpz_imageNamed
 调用 xpz_imageNamed => imageNamed
 */
+ (nullable UIImage *)xpz_imageNamed:(NSString *)name{
    UIImage *xpz_image = [UIImage xpz_imageNamed:name];
    if (xpz_image) {
        NSLog(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return xpz_image;
}
@end

/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {
 
 }
 */
  • 给UIViewController的viewWillAppear添加额外功能,就可以再控制台看到每次控制器的变化
#import "UIViewController+hook.h"
#import 

@implementation UIViewController (hook)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(hook_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        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)hook_viewWillAppear:(BOOL)animated {
    [self hook_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

打印

[控制器:UIViewController+hook.m -- line: 53行]  viewWillAppear: 
[控制器:UIViewController+hook.m -- line: 53行]  viewWillAppear: 
[控制器:UIViewController+hook.m -- line: 53行]  viewWillAppear: 
  • 为什么要添加didAddMethod判断?

先尝试添加原SEL其实是为了做一层保护,因为如果这个类没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是我们想要的。所以我们先尝试添加 orginalSelector,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。

大概的意思就是我们可以通过class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。

runtime动态地添加方法

float runtime_addMethod(id receiver, SEL sel, const void *arg1, int arg2)
{
    NSLog(@"方法名:%s, 参数1:%@, 参数2:%d", __FUNCTION__, [NSString stringWithUTF8String:arg1], arg2);
    return 1;
}

    Person_runtimeVC *vc = objc_msgSend(objc_getClass("Person_runtimeVC"), sel_registerName("alloc"));
    vc = objc_msgSend(vc, sel_registerName("init"));
    /** 添加方法*/
    // 动态添加run方法
    // class: 给哪个类添加方法
    // SEL: 添加哪个方法,即添加方法的方法编号
    // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
    // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
    class_addMethod([vc class], NSSelectorFromString(@"runtime_addMethod"), (IMP)runtime_addMethod, "f@:r^vd");
    int returnValue = ((float (*)(id, SEL, const void *, int))objc_msgSend)((id)vc, NSSelectorFromString(@"runtime_addMethod"), "参数1", 10086);
    NSLog(@"返回值:%d", returnValue);
    NSLog(@"%s", @encode(const void *));


/**打印结果*/
[控制器:ViewController.m -- line: 108行]    方法名:runtime_addMethod, 参数1:参数1, 参数2:10086
[控制器:ViewController.m -- line: 74行] 返回值:1
[控制器:ViewController.m -- line: 75行] r^v
  • 参数:"f@:r^vd"
  第1个字符:表示函数(方法)返回值类型,这里返回值类型是 `float` ,故为 `f`
  第2、3个字符:苹果解释是由于函数(方法)至少带有两个参数(self和_cmd)还记得之前的 (id,SEL) 么,所以第2、3个字符必须是 ‘@:’,其实我们当做固定写法就好了
  第4个字符根据NSLog(@"%s", @encode(const void *));打印结果可以看出来r^v是第三个参数的类型,第四个参数是int,所以是d

苹果官方其他类型对照表

runtime 常见方法

原著https://www.jianshu.com/p/46dd81402f63

  • 获取成员变量
/** 获取类中的所有成员变量*/
    Ivar *ivarList = class_copyIvarList([Person_runtimeVC class], &count);
    for(int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        NSLog(@"获取类成员变量:%@  ---  %@",ivarName,key);
    }
  • 获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
 for (unsigned int i=0; i%@", [NSString stringWithUTF8String:propertyName]);
 }
  • 获取方法列表
   Method *methodList = class_copyMethodList([self class], &count);
   for (unsigned int i; i%@", NSStringFromSelector(method_getName(method)));
   }
  • 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
   for (unsigned int i; i%@", [NSString stringWithUTF8String:protocolName]);
   }

现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法

  • 获得类方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 获得实例方法
Class PersonClass = object_getClass([xiaoming class]);
SEL oriSEL = @selector(test2);
Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
  • 添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
  • 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  • 交换两个方法
method_exchangeImplementations(oriMethod, cusMethod);

什么是 method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换
  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP


    image

selector --> 对应的IMP

  • 交换方法的几种实现方式

  • 利用 method_exchangeImplementations 交换两个方法的实现

  • 利用 class_replaceMethod 替换方法的实现

  • 利用 method_setImplementation 来直接设置某个方法的IMP。

    image

    这里可以参考简友这篇:【Runtime Method Swizzling开发实例汇总】http://www.jianshu.com/p/f6dad8e1b848

runtime知识很多,后期会慢慢补充,欢迎广大简友补充学习;刚学习使用Markdown,排版上做的不是太好。
本篇笔记部分参考自以下:

  • iOS 模块详解—「Runtime」
  • runtime - 消息发送(objc_msgSend)
  • iOS 给分类category添加属性

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