iOS-Runtime(运行时)使用及详解(全网最全)

1.objc_msgSend

objc_msgSend(消息发送): 给某个对象,发送方法编号消息,通过SEL可以找到对应的IMP

2.MethodSwizzling

  • MethodSwizzling: OC编译阶段,动态地进行操作method_exchangeImplementations
  • class_addMethod resolveInstanceMethod

3.RunTime常见用法

  • 动态方法解析(防止未实现的method运行Crash)
  • 拦截系统方法(setBackgroundColor:)
  • ③给Category添加属性 - ④Button设置时间间隔
  • ⑤数据埋点:减少埋点所带来的侵入性
  • ⑥字典、模型互转 - ⑦自动归结档
  • ⑧查找内存泄漏 - ⑨通过Ivar指针改变实例变量的值(object_setIvar)
  • 自定义KVO

4.消息发送机制和消息转发机制的区别

消息发送机制:
使用运行时,通过SEL快速去查找IMP的过程
消息转发机制:
IMP找不到的时候,通过一些方法做转发处理
eg:动态添加方法,在resolveInstanceMethod类方法里面通过class_addMethod实现消息的动态转发

5.runtime -消息发送的整个过程,怎么查找

  • runtime消息发送给类对象的时候
  • objc_msgSend() 会去它元类中查找能够响应消息的方法实现(先找子类,后找父类)
  • 如果找到了,就会对这个类对象执行方法调用
  • 找不到时会到resolveInstanceMethod方法中,通过class_AddMethod实现消息的动态转发

6.数据埋点:运行时Method Swizzling机制与AOP编程

场景需求

统计UIViewController加载次数
统计UIButton点击次数
统计自定义方法的执行
统计UITableView的Cell点击事件
需要监听不同类,不同按钮,系统方法,及表单元点击事件

7.objc_msg_Send解析

$阅读寄存器

$ register read x1
x1 = 0x00000001ded79b5f

$ 打印方法编号

po (SEL)0x00000001ded79b5f

objc_msg_Send(t,test)

* 方法缓存; 
* 对象 isa  isa_t (结构体)
  • 缓存没有命中,方法列表的遍历
  • cache_t 类对象
  • 缓存的命中,地址的运算拿到缓存, sel
  • 否则,objc_msgSend_uncached

8.lookUpImpOrForward

  • 1.从当前类对象的方法列表中遍历方法列表
  • 2会去它元类中查找能够响应消息的方法实现(先找子类,后找父类)
  • 3.如果找到了,就会对这个类对象执行方法调用
  • 4.找不到就会执行动态方法解析
动态方法解析
  • 动态的添加一个方法,方法列表中(SEL-IMP(你给的))
void sendMessage(id self, SEL _cmd,NSString *msg)
{
    // implementation ....
    NSLog(@"sendMessage%@",msg);
}
// 第一个阶段,动态的方法解析
+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSString * methodName = NSStringFromSelector(sel);
    if ([methodName isEqualToString:@"sendMessage:"]){
        // 动态的添加方法实现,sel -- IMP实现
        //v 返回值
        return class_addMethod(self, sel, (IMP)sendMessage, "v@:@");
    }
    return NO;
}
标准消息转发:objc_msgForward
  • 方法转发
  • forwardTargetForSelector 找一个备胎来接收你
  • methodSignature 方法签名
  • forwardInvocation消息转发
  • doesNotRecognizeSelector 未识别
/// 第二个阶段:找备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
    // 1. 判断方法名称
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]){
        return [SpareWheel new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
//1.方法签名
//2.消息转发
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSString * methodName = NSStringFromSelector(aSelector);
    if ([methodName isEqualToString:@"sendMessage:"]){
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    SpareWheel *sp = [SpareWheel new];
    if ([sp respondsToSelector:sel]){
        [anInvocation invokeWithTarget:sp];
    }else {
        [super forwardInvocation:anInvocation];
    }
}

-(void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"未知错误~");
}
效果图

9.拦截系统方法

修改setBackgroundColor:

创建扩展类UIView+Swizzing

#import "UIView+Swizzing.h"
#import <objc/runtime.h>

@implementation UIView (Swizzing)
+(void)load {
    Method m1 = class_getInstanceMethod(self, @selector(setBackgroundColor:));
    Method m2 = class_getInstanceMethod(self, @selector(hk_setBackgroundColor:));
    method_exchangeImplementations(m1, m2);
}
- (void)hk_setBackgroundColor:(UIColor *)background {
    if (background == [UIColor blueColor]){
        [self hk_setBackgroundColor:[UIColor yellowColor]];
    }
}
@end

分类里为什么不会生成实例变量?

  • 类的内存布局在编译的时候被确定,分类是在运行时加载。
  • 已经确定的内存布局不会被更改.

10.探索KVO实现原理

创建一个Person类,并实例化两个实例,p1和p2
对p1进行addObserver监听并添加回调方法observerValueForKeyPath

监听之前

(lldb) po object_getClassName(_p1)
"Person"
(lldb) po object_getClassName(_p2)
"Person”

监听之后

(lldb) po object_getClassName(_p2)
"Person"
(lldb) po object_getClassName(_p1)
"NSKVONotifying_Person"
自定义KVO

思路分析

  • 创建指定前缀CustomKVONotifying_%@的子类并注册
  • 修改isa 指针指向
  • 保存observer:关联属性
  • 保存set、get方法名
  • 自己的方法实现
    • 改变父类的属性值
    • 获取观察者
    • 通知发生改变 objc_msgSend
#import "NSObject+CustomKVO.h"
#import <objc/message.h>
#import <objc/runtime.h>

static const char *custom_observer = "custom_observer";
static const char *custom_getter = "custom_getter";
static const char *custom_setter = "custom_setter";

@implementation NSObject (CustomKVO)
NSString *getValueKey(NSString *setter) {
    NSRange range = NSMakeRange(3, setter.length-4);
    // name
    NSString *key  = [setter substringWithRange:range];
    NSString *letter = [[key substringToIndex:0] lowercaseString];
    key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:letter];
    return key;
}

void setMethod(id self, SEL _cmd, NSString *name) {
    // 改变父类的属性值
    struct objc_super superClass = {
        self,
        class_getSuperclass([self class])
    };
    
    objc_msgSendSuper(&superClass, _cmd, name);
    
    // 获取观察者
    id observer = objc_getAssociatedObject(self, custom_observer);
    
    // 通知发生改变
    NSString *methodName = NSStringFromSelector(_cmd);
    // setName
    NSString *key = getValueKey(methodName);
    objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), observer, self, @{key:name}, nil);
}

- (void)custom_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    
    //创建指定前缀CustomKVONotifying_%@的子类并注册
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"CustomKVONotifying_%@", oldName];
    
    // 动态创建一个类
    Class customClass = objc_getClass(newName.UTF8String);
    if (!customClass) {
        customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
        objc_registerClassPair(customClass);
    }
    
    //set方法首字母大写
    NSString *keyPathChange = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
    NSString *setNameStr = [NSString stringWithFormat:@"set%@", keyPathChange];
    SEL setSEL = NSSelectorFromString([setNameStr stringByAppendingString:@":"]);
    
    //添加set方法
    Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
    const char *types = method_getTypeEncoding(getMethod);
    class_addMethod(customClass, setSEL, (IMP)setMethod, types);
    
    //修改isa 指针指向
    object_setClass(self, customClass);
    
    //保存observer:关联属性
    objc_setAssociatedObject(self, custom_observer, observer, OBJC_ASSOCIATION_ASSIGN);
    
    //保存set、get方法名
    objc_setAssociatedObject(self, custom_setter, setNameStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, custom_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
    
}
@end

使用

[_p1 custom_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

(lldb) po object_getClassName(_p1)
"CustomKVONotifying_Person"

查找所有子类

+ (NSArray *)hk_findSubClass:(Class)class{
    int count = objc_getClassList(NULL, 0);
    NSMutableArray *array = [NSMutableArray arrayWithObject:class];
    Class *classes = (Class *)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i < count; i++) {
        if (class == class_getSuperclass(classes[i])){
            [array addObject:classes[i]];
        }
    }
    free(classes);
    return array;
}
自定义内存泄漏检查工具

原理分析

通常,UIViewController的释放都是在UINavigationController pop和自身的dismiss,而且一定会走viewDidDisappear这个方法,但是不是所有的viewWillDisappear都是这两个时机,所以需要区分标记。

如果没有走,则通过GCD提供的一个延时处理事物的方式,只需要将延时的任务放到block中,设置好时长即可,而我们将一个弱引用对象放到block中,不会影响对象的释放,就有如下的代码:

///延时处理,如果释放,不会走 [strongSelf showMsg:msg];
- (void) willDealloc{
    __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            NSString* msg = [NSString stringWithFormat:@"%@__存在内存泄漏",[weakSelf class]];
            ///泄漏提示
            [strongSelf showAlertWithTitle:@"内存泄露了" message:msg confirmTitle:@"好的" handler:nil];
        }
    });
}

报错汇总

1.objc_msgSend Too many arguments to function call

方法①

项目配置文件 -> Build Settings -> Enable Strict Checking of objc_msgSend Calls 这个字段默认为YES,修改为NO即可


image.png
方法②强制转换

由于objc_msgSend函数本身是无返回值无参数的函数, 所以要给它强制转换类型代码如下:

 ((void (*) (id, SEL)) (void *)objc_msgSend)(callMe, @selector(callMeNow));
 ((void (*) (id, SEL)) (void *)objc_msgSend)(callMe, sel_registerName("callMeNow"));

@selector(callMeNow)等价于sel_registerName("callMeNow"),都是SEL

2.class_copyMethodList导致内存泄漏

runtime中的class_copyMethodList能获取对应类所有方法。它导致内存泄漏的原因是,这class_copyMethodList函数是C层的,没有OC的自动指针管理,需要手动free。

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,145评论 1 32
  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,266评论 2 54
  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,445评论 1 14
  • java 接口的意义-百度 规范、扩展、回调 抽象类的意义-乐视 为其子类提供一个公共的类型封装子类中得重复内容定...
    交流电1582阅读 2,279评论 0 11
  • 整理自:Java面试题全集(上)https://blog.csdn.net/jackfrued/article/d...
    在水之天阅读 3,120评论 1 24