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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

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