Runtime应用

Runtime API01 - 类
  • 获取isa指向的Class(类对象):Class object_getClass(id obj)
  • 设置isa指向的Class : Class object_setClass(id obj, Class cls)
  • 判断一个OC对象是否为Class:Bool object_isClass(Class cls)
  • 判断一个Class是否为元类:Bool class_isMetaClass(Class cls)
  • 获取父类:Class class_getSuperclass(Class cls)
  • 动态创建一个类(参数:父类,类名,额外的内存空间):Class objc_allocateClassPair(Class superclass,const char *name,size_t extraBytes)
  • 注册一个类(要在类注册之前添加成员变量):void objc_registerClassPair(Class cls)
  • 销毁一个类:void objc_disposeClassPair(Class cls)
Person.h文件
@interface Person : NSObject
- (void)run;
@end

Person.m文件
#import "Person.h"
@implementation Person
- (void)run{
    NSLog(@"%s",__func__);
}
@end
Car.h文件
#import <Foundation/Foundation.h>
@interface Car : NSObject
- (void)run;
@end

Car.m文件
#import "Car.h"
@implementation Car
- (void)run{
    NSLog(@"%s",__func__);
}
@end
main文件
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person = [[Person alloc] init];
        [person run];//打印结果:-[Person run]
        NSLog(@"%p  %p",object_getClass(person),[Person class]);//打印分别为类对象地址,类对象地址:0x100008230  0x100008230
        NSLog(@"%p  %p",object_getClass([Person class]),[Person class]);//打印分别为元类对象地址,类对象地址:0x100008208  0x100008230
        object_setClass(person, [Car class]);//设置person的isa指向Car
        [person run];//打印结果为-[Car run]
        
        NSLog(@"%d %d %d",object_isClass(person),object_isClass([Person class]),object_isClass(object_getClass([Person class])));//object_isClass是否为类对象,元类对象是特殊的类对象,所以打印结果为:0 1 1   
    }
    return 0;
}
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
#import <objc/runtime.h>
void run(id self,SEL _cmd){
    NSLog(@"%@ %@",self,NSStringFromSelector(_cmd));
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //动态创建类
       Class newClass =  objc_allocateClassPair([NSObject class], "Dog", 0);
        //添加成员变量
        class_addIvar(newClass, "_age", 4, 1, @encode(int));
        class_addIvar(newClass, "_weight", 4, 1, @encode(int));
        class_addMethod(newClass, @selector(run), (IMP)run, "v@:");
        //注册类:类一旦注册完毕,类对象和元类对象里边的结构就已经创建好,所以添加成员变量要放在注册类前边。方法是可以放在注册类之后的。
        objc_registerClassPair(newClass);
        id dog = [[newClass alloc] init];
        [dog setValue:@10 forKey:@"_age"];
        [dog setValue:@20 forKey:@"_weight"];
        [dog run];//打印结果为:<Dog: 0x108f2f070> run
        
        NSLog(@"%zd",class_getInstanceSize(newClass));//打印结果为:16
        NSLog(@"%@ %@",[dog valueForKey:@"_age"],[dog valueForKey:@"_weight"]);//打印结果为:10 20
        
        Person *person = [[Person alloc] init];
        object_setClass(person, newClass);
        [person run];//打印结果为:<Dog: 0x108f0f050> run
        
        //在不需要这个类时释放
        objc_disposeClassPair(newClass);
    }
    return 0;
}
Runtime API02 - 成员变量
  • 获取一个实例变量信息:Ivar class_getInstanceVariable(Class cls,const char *name)
  • 拷贝实例变量列表(最后需要调用free释放):Ivar *class_copyIvarList(Class cls,unsigned int *outCount)
  • 设置和获取成员变量的值:
    void object_setIvar(id obj, Ivar ivar, id value)
    id object_getIvar(id obj, Ivar ivar)
  • 动态添加成员变量(已经注册的类是不能动态添加成员变量的):BOOL class_addIvar(Class cls,const char *name,size_t size,uint8 alignment,const char *types)
  • 获取成员变量的相关信息
    const char *ivar_getName(Ivar v)
    const char *ivar_getTypeEncoding(Ivar v)
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Car.h"
#import <objc/runtime.h>
void run(id self,SEL _cmd){
    NSLog(@"%@ %@",self,NSStringFromSelector(_cmd));
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //获取成员变量信息
        Ivar ageIvar =  class_getInstanceVariable([Person class], "_age");
        NSLog(@"%s  %s",ivar_getName(ageIvar),ivar_getTypeEncoding(ageIvar));//打印结果为:_age  i
        //设置和获取成员变量的值
        Ivar nameIvar =  class_getInstanceVariable([Person class], "_name");
        Person *person = [[Person alloc] init];
        object_setIvar(person, nameIvar, @"123");
        object_getIvar(person, nameIvar);
        NSLog(@"name = %@",person.name);//打印结果为:name = 123
        //成员变量的数量
        unsigned int count;
        Ivar *ivars =  class_copyIvarList([Person class], &count);
        for (int i=0; i<count; i++) {
            //取出i位置的成员变量
            Ivar ivar = ivars[i];
            NSLog(@"%s  %s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
        }
        //打印结果:
        //_age  i
        //_name  @"NSString"
        free(ivars);
    }
    return 0;
}
Runtime API04 - 方法
  • 获得一个实例方法、类方法
    Method class_getInstanceMethod(Class cls, SEL name)
    Method class_getClassMethod(Class cls,SEL name)
  • 方法实现相关操作
    IMP class_getMethodImplementation(Class cls,SEL name)
    IMP method_setImplemention(Method m,IMP imp)
    void method_exchangeImplementations(Method m1,Method m2)
  • 拷贝方法列表(最后需要调用free释放)
    Method *class_copyMethodList(Class cls, unsigned int *outCount)
  • 动态添加方法
    BOOL class_addMethod(Class cls,SEL name,IMP imp,const char *types)
  • 动态替换方法
    IMP class_replaceMethod(Class cls, SEL name, IMP imp,const char *types)
  • 获取方法的相关信息(带有copy的需要调用free去释放)
    SEL method_getName(Method m)
    IMP method_getImplementation(Method m)
    const char *method_getTypeEncoding(Method m)
    unsigned int method_getNumberOfArguments(Method m)
    char *method_copyReturnType(Method m)
    char *method_copyArgumentType(Method unsigned int index)
  • 选择器相关
    const char *sel_getName(SEL sel)
    SEL sel_registerName(const char *str)
  • 用block作为方法实现
    IMP imp_implementationWithBlock(id block)
    id imp_getBlock(IMP anImp)
    BOOL imp_removeBlock(IMP anImp)

那么这些东西在实际项目中有什么作用呢?

Runtime的应用01 - 查看私有成员变量
  • 用runtime方法获取并打印UITextField的成员变量,知道内部的很多细节
unsigned int count;
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i=0; i<count; i++) {
      //取出i位置的成员变量
      Ivar ivar = ivars[i];
      NSLog(@"%s %s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
}
free(ivars);
Runtime的应用02 - 字典转模型
  • 利用Runtime遍历所有的属性或者成员变量
  • 利用KVC设值
Person.h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic,assign) int age;
@property(nonatomic,copy) NSString *name;
@property (nonatomic,assign) int weight;
@end

Person.m文件
#import "Person.h"
@implementation Person
@end
main文件
//字典转模型
NSDictionary *json = @{
      @"age" : @20,
      @"weight" : @60,
      @"name" : @"Jack"
};
Person *person = [[Person alloc] init];
person.age = [json[@"age"] intValue];
person.weight = [json[@"weight"] intValue];
person.name = json[@"name"];
        
NSLog(@"----------");

但是如果模型里边有很多属性,就要写很多设置的代码,这里就可以给NSObject写一个分类,处理字典转模型的问题,用runtime的方法实现

NSObject+Json.h文件
#import <Foundation/Foundation.h>

@interface NSObject (Json)
+ (instancetype)ld_objectWithJson:(NSDictionary *)json;
@end

NSObject+Json.m文件
#import "NSObject+Json.h"
#import <objc/runtime.h>
@implementation NSObject (Json)
+(instancetype)mj_objectWithJson:(NSDictionary *)json{
    id obj = [[self alloc] init];
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i=0; i<count; i++) {
        //取出i位置的成员变量
        Ivar ivar = ivars[i];
//        NSLog(@"%s %s",ivar_getName(ivar),ivar_getTypeEncoding(ivar));
        //带有下划线的成员变量的名字
        NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
        //删除最开始的下划线,就可以去字典取东西了
        [name deleteCharactersInRange:NSMakeRange(0, 1)];
        //设值
        [obj setValue:json[name] forKey:name];
    }
    free(ivars);
    return obj;
}
@end
这里只是一个很简单的字典转模型,没有考虑所有情况。不是完整的字典转模型的代码。只是举一个简单runtime的例子用
- (void)encodeWithCoder:(NSCoder *)coder{
    [coder encodeObject:self.name forKey:@"name"];
    
}
- (instancetype)initWithCoder:(NSCoder *)coder{
    if (self = [super init]) {
        self.name = [coder decodeObjectForKey:@"name"];
    }
    return self;
}
这里也可以用字典转模型的思路实现归档、解档
Runtime的应用03 - 替换方法实现
  • class_replaceMethod
  • method_exchangeImplementations
void myrun(){
    NSLog(@"-----myrun");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        //替换方法
        class_replaceMethod([Person class], @selector(run), (IMP)myrun, "v");
        
        [person run];
       
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Method runMethod = class_getInstanceMethod([Person class], @selector(run));
        Method testMethod = class_getInstanceMethod([Person class], @selector(test));
        method_exchangeImplementations(runMethod, testMethod);
        [person run];//打印结果为:test ----
        [person test];//打印结果为:run ----
    }
    return 0;
}

实现一个功能:拦截项目中所有按钮的点击事件

UIControl的分类
UIControl+ Extension.h文件
#import <UIKit/UIKit.h>
@interface UIControl (Extension)

@end
UIControl+ Extension.m文件
#import "UIControl+Extension.h"
#import <objc/runtime.h>
@implementation UIControl (Extension)

+(void)load{
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(ld_sendAction:to:forEvent:));
    method_exchangeImplementations(method1, method2);
}

- (void)ld_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    NSLog(@"%@ - %@ - %@ ---",self,target,NSStringFromSelector(action));
    //调用回系统原来的实现
    [self ld_sendAction:action to:target forEvent:event];//这里为什么调用的是自己写的而不是系统之前的方法名呢?因为在上边的代码里已经交换了这两个方法。所以这里调用自己写的就是在调用系统自带的方法
    
    if ([self isKindOfClass:[UIButton class]]) {
        //拦截了所有按钮的事件
    }
}
@end
#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (IBAction)click1 {
    NSLog(@"%s",__func__);
}

- (IBAction)click2 {
    NSLog(@"%s",__func__);
}

- (IBAction)click3 {
    NSLog(@"%s",__func__);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    /*
    UIButton继承自UIControl,UIControl中有如下一个方法,每个button点击的时候都会先走下边的方法。
     - (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
     那么想拦截所有按钮的点击,我们用runtime替换一下上边系统自带的方法就可以了
     */  
}
@end

再举一个数组的例子,如下:

NSString *str = nil;
NSMutableArray *array = [NSMutableArray array];
[array addObject:@"jack"];
[array addObject:str];
//报错:-[__NSArrayM insertObject:atIndex:]: object cannot be nil'
NSLog(@"%zd",array.count);

如果我们不想因为数组添加了nil报错崩掉,那么我们就需要每次添加数据之前都要进行判断是否等于nil,太麻烦了,这个时候我们就可以用runtime的方法,hook 住insertObject:atInde这个函数,代码如下:

NSString *str = nil;
NSMutableArray *array = [NSMutableArray array];
[array addObject:@"jack"];
[array addObject:str];
//报错:-[__NSArrayM insertObject:atIndex:]: object cannot be nil'
NSLog(@"%@",array);
NSMutableArray+ Extensions.h文件
#import <Foundation/Foundation.h>
@interface NSMutableArray (Extensions)

@end

NSMutableArray+ Extensions.m文件
#import "NSMutableArray+Extensions.h"
#import <objc/runtime.h>

@implementation NSMutableArray (Extensions)
+(void)load{
    //类簇:NSString、NSArray、NSDictionary
    //也就是说在写交换的方法时,类对象那里一点要传对,虽然这里是要给NSMutableArray交换,但是他的底层依然是NSArray,所以直接写self的话,是交换不成功的。所以这里的类对象需要放__NSArrayM,从崩溃的提示能看出来:-[__NSArrayM insertObject:atIndex:]: object cannot be nil'
    Class cls = NSClassFromString(@"__NSArrayM");
    Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
    Method method2 = class_getInstanceMethod(cls, @selector(ld_insertObject:atIndex:));
    
    method_exchangeImplementations(method1, method2);
}

- (void)ld_insertObject:(id)anObject atIndex:(NSUInteger)index{
    if (anObject == nil) {
        return;
    }
    [self ld_insertObject:anObject atIndex:index];
}
@end

这样就不会崩溃报错了。

这里需要注意一点的是:
交换方法放在dispatch_once里边,为了以防万一交换之后又交换一遍,

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

推荐阅读更多精彩内容