OC-Runtime-常用API

OC-Runtime-常用API

image-20210510151310311
image-20210510151322735
image-20210510151337970
image-20210510151352812
image-20210510151410706

一. 类相关API

//动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

//注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)

//销毁一个类
void objc_disposeClassPair(Class cls)

//获取对象的isa指向的Class
Class object_getClass(id obj)

//设置对象的isa指向的Class
Class object_setClass(id obj, Class cls)

//判断一个对象是否为Class
BOOL object_isClass(id obj)

//判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

//获取父类
Class class_getSuperclass(Class cls)
  • Class object_getClass(id _Nullable obj) 获取 isa指向的Class
***********************🕴MJPerson.h 🕴**************************
#import <Foundation/Foundation.h>

@interface MJPerson : NSObject
@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *name;
- (void)run;
@end

***********************🕴MJPerson.m 🕴**************************  
#import "MJPerson.h"

@implementation MJPerson

- (void)print
- (void)run
{
    NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MJPerson *person = [[MJPerson alloc] init];
        class_isMetaClass(object_getClass([MJPerson class]));
        
        Class cls = object_getClass(person);//实例对象的isa指向类对象
        Class metaClass = object_getClass([MJPerson class]);//类对象的isa指向元类对象
        
        NSLog(@"是元类对象 %d", class_isMetaClass(metaClass));
        NSLog(@"%d %d %d",
              object_isClass(person),
              object_isClass([MJPerson class]),
              object_isClass(object_getClass([MJPerson class]))
              );
        
    }
    return 0;
}

RUN🚗🚗🚗🚙🚙🚙

2021-05-10 15:26:50.390907+0800 Interview02-runtime应用[3278:157126] 是元类对象 1
2021-05-10 15:26:50.391387+0800 Interview02-runtime应用[3278:157126] 0 1 1

Class object_setClass(id _Nullable obj, Class _Nonnull cls) 设置 isa的指向的Class

***********************🐱 MJCar.h 🐱**************************
@interface MJCar : NSObject
- (void)run;
@end

***********************🐱 MJCar.m 🐱**************************
@implementation MJCar
- (void)run
{
    NSLog(@"%s", __func__);
}
@end
***********************🐱 main.m 🐱**************************
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MJPerson *person = [[MJPerson alloc] init];
        [person run];
        
        object_setClass(person, [MJCar class]);
        [person run];
        
        NSLog(@"%d %d %d",
              object_isClass(person),
              object_isClass([MJPerson class]),
              object_isClass(object_getClass([MJPerson class]))
              );
        
    }
    return 0;
}

RUN🚗🚗🚗🚕🚕🚕

2021-05-10 15:29:57.963729+0800 Interview02-runtime应用[3304:159036] -[MJPerson run]
2021-05-10 15:29:57.964308+0800 Interview02-runtime应用[3304:159036] -[MJCar run]
2021-05-10 15:29:57.964363+0800 Interview02-runtime应用[3304:159036] 0 1 1
  • objc_allocateClassPair

Class objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
动态创建一个类,(参数:父类,类名,额外的存储空间)

objc_registerClassPair(Class cls)注册一个类 (要在类注册之前添加成员变量)

void run(id self, SEL _cmd)
{
    NSLog(@"_____ %@ - %@", self, NSStringFromSelector(_cmd));
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        // 创建类,传入父类和类名
            Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 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];

            NSLog(@"%@ %@", [dog valueForKey:@"_age"], [dog valueForKey:@"_weight"]);

            MJPerson *person = [[MJPerson alloc] init];
            //修改person对象isa指向
            object_setClass(person, newClass);
            [person run];

            // 在不需要这个类时释放
//            objc_disposeClassPair(newClass);
        
    }
    return 0;
}

RUN>🚗🚗🚗🚙🚙🚙

2021-05-10 15:36:09.336005+0800 Interview02-runtime应用[3430:165339] _____ <MJDog: 0x10067f380> - run
2021-05-10 15:36:09.336861+0800 Interview02-runtime应用[3430:165339] 10 20
2021-05-10 15:36:09.337011+0800 Interview02-runtime应用[3430:165339] _____ <MJDog: 0x100686fd0> - run

在程序运行的时候,动态添加一个类,并且添加成员变量、方法,最后使用类。

  1. 一定要在注册类之前添加成员变量,因为成员变量是在_r_o_t表里面,是只读的,所以要在类的结构确定之前添加成员变量。
  2. 不能使用class_addIvar给已经创建的类添加成员变量,因为已经创建的类的结构在代码写完就已经确定好了,程序运行中就不能给已经创建的类添加成员变量了。
  3. 方法可以在注册类之后添加,因为方法是在_r_w_t表里面,是可读可写的。

二. 成员变量相关API

//获取类中指定名称实例成员变量的信息
Ivar class_getInstanceVariable(Class cls, const char *name)

//获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

//设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

//拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

//动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
  • Ivar class_getInstanceVariable 获取一个实例变量的信息
  • object_setIvar(id obj, Ivar ivar, id value)设置实例变量的值
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //获取类中指定名称实例成员变量的信息
        //传入的是一个类对象,所以只能获取成员变量的信息,并不能获取成员变量的值
        Ivar ageIvar = class_getInstanceVariable([MJPerson class], "_age");
        Ivar nameIvar = class_getInstanceVariable([MJPerson class], "_name");

        NSLog(@"%s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));
        //打印:_age i   i代表字符编码int

        MJPerson *person = [[MJPerson alloc] init];
        //设置成员变量的值
        //传入的是一个实例对象,所以可以设置成员变量的值
        object_setIvar(person, nameIvar, @"123");
        object_setIvar(person, ageIvar, (__bridge id)(void *)10);
        //获取成员变量的值
        id name = object_getIvar(person, nameIvar);

        NSLog(@"%@ %d", name, person.age);
        //打印:123 10
        
    }
    return 0;
}

RUN> 🚗🚗🚗🚙🚙🚙🏎🏎🏎

2021-05-10 15:40:12.967391+0800 Interview02-runtime应用[3454:168018] _age i
2021-05-10 15:40:12.967815+0800 Interview02-runtime应用[3454:168018] 123 10

object_setIvar(person, ageIvar, (__bridge id)(void *)10);上面runtimeAPI内部没做转换,所以需要传什么值就传什么值,但是要做一些数据类型转换(先转成指针类型,再转成id类型)。

如果是KVC的value值,可以传NSNumber类型的值,因为KVC内部会做转换:[@10 integerValue]。

  • BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)动态添加成员变量 (已经注册的类是不能添加成员变量的)
  • const char *ivar_getName(Ivar v) 获取成员变量 name
  • const char * ivar_getTypeEncoding(Ivar v) 获取成员变量字符串编码
  • Ivar * class_copyIvarList(Class cls, unsigned int * outCount)拷贝实例变量列表,最后需要调用free释放.

用途一: 获取系统类私有的成员变量 (这种方式在 iOS13 后 已经被禁用了,iOS13 后系统禁止访问一些私有的成员变量)

    unsigned int count;
    Ivar *ivar = class_copyIvarList([UITextField class], &count);
    for (int i = 0; i < count; i ++) {
        Ivar iva = ivar[I];
        NSLog(@"%s",ivar_getName(iva));
    }
    self.nameTF.placeholder = @"请输入姓名";
    [self.nameTF setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
    free(ivar);

用途二:字典转模型

+ (instancetype)json2Model:(NSDictionary *)json{
    id obj = [[self alloc]init];
    unsigned int count;
    Ivar *ivars = class_copyIvarList([self class], &count);
    //遍历所有的成员变量
    for (int i = 0; i < count; i ++) {
        Ivar iva = ivars[I];
        NSMutableString *ivarStr = [NSMutableString stringWithUTF8String:ivar_getName(iva)];
        //去掉成员变量前面的 _
        [ivarStr deleteCharactersInRange:NSMakeRange(0, 1)];
        
        [obj setValue:json[ivarStr] forKey:ivarStr];
    }
    
    return obj;
}

这样写会有很多问题,这只是个思路,仅供参考

用途三:归档,解档

- (instancetype)initWithCoder:(NSCoder *)coder{
    if (self = [super init]) {
        unsigned int count;
        Ivar *ivars = class_copyIvarList([self class], &count);
        //遍历所有的成员变量
        for (int i = 0; i < count; i ++) {
            Ivar iva = ivars[I];
            NSMutableString *ivarStr = [NSMutableString stringWithUTF8String:ivar_getName(iva)];
            //去掉成员变量前面的 _
            [ivarStr deleteCharactersInRange:NSMakeRange(0, 1)];
            //从文件中取出值
            id value = [coder decodeObjectForKey:ivarStr];
            //赋值到对象中
            [self setValue:value forKey:ivarStr];
        }
    }
    return self;
}


- (void)encodeWithCoder:(NSCoder *)coder{
    
    unsigned int count;
    Ivar *ivars = class_copyIvarList([self class], &count);
    //遍历所有的成员变量
    for (int i = 0; i < count; i ++) {
        Ivar iva = ivars[I];
        NSMutableString *ivarStr = [NSMutableString stringWithUTF8String:ivar_getName(iva)];
        //去掉成员变量前面的 _
        [ivarStr deleteCharactersInRange:NSMakeRange(0, 1)];
        //从对象中取出对应的值
        id value = [self valueForKey:ivarStr];
        //归档到文件中
        [coder encodeObject:value forKey:ivarStr];
    }
}

三. 属性相关API

//获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

//拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

//动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                       unsigned int attributeCount)

//动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                           unsigned int attributeCount)

//获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

四. 方法相关API

//获取一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

//根据class和方法名获取方法的imp
IMP class_getMethodImplementation(Class cls, SEL name)
//设置方法的imp
IMP method_setImplementation(Method m, IMP imp)
//交换方法的imp
void method_exchangeImplementations(Method m1, Method m2)
//获取方法名
SEL method_getName(Method m)
//获取imp
IMP method_getImplementation(Method m)
//获取方法返回值类型、参数类型的编码
const char *method_getTypeEncoding(Method m)
//获取参数个数
unsigned int method_getNumberOfArguments(Method m)
//获取返回值类型
char *method_copyReturnType(Method m)
//根据index获取参数
char *method_copyArgumentType(Method m, unsigned int index)

//拷贝方法列表(最后需要调用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)

//根据SEL获取名字
const char *sel_getName(SEL sel)
//根据字符串包装成一个SEL,和@selector("方法名字")方法等效
SEL sel_registerName(const char *str)

//根据block返回一个imp
IMP imp_implementationWithBlock(id block)
//根据imp返回一个block
id imp_getBlock(IMP anImp)
//移除imp对应的block
BOOL imp_removeBlock(IMP anImp)
void myrun()
{
    NSLog(@"---myrun");
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];
        
        class_replaceMethod([MJPerson class], @selector(run), (IMP)myrun, "v");
        [person run];
    }
    return 0;
}

RUN> 🚗🚗🚗🚕🚕🚕🚕

2021-05-10 15:47:37.466763+0800 Interview04-方法[3531:172517] ---myrun
  • 将block当做方法实现
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];
        
        class_replaceMethod([MJPerson class], @selector(run), imp_implementationWithBlock(^{
            NSLog(@"123123");
        }), "v");
        [person run];
    }
    return 0;
}

RUN>🚗🚗🚗🚓🚓🚓

2021-05-10 15:49:19.473743+0800 Interview04-方法[3570:174628] 123123
  • 交换方法实现
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *person = [[MJPerson alloc] init];

        Method runMethod = class_getInstanceMethod([MJPerson class], @selector(run));
        Method testMethod = class_getInstanceMethod([MJPerson class], @selector(test));
        method_exchangeImplementations(runMethod, testMethod);

        [person run];
    }
    return 0;
}

RUN> 🚕🚕🚕🚙🚙🚙

2021-05-10 15:51:18.409680+0800 Interview04-方法[3593:175998] -[MJPerson test]

交换方法实现的使用

交换方法实现在开发中经常使用,但是实际上我们使用最多的是交换系统或者第三方框架的方法。

拦截所有按钮的点击事件:

UIButton继承于UIControl,UIControl有一个sendAction:to:forEvent:方法,每当触发一个事件就会调用这个方法,所以我们可以给UIControl添加分类,在分类中交换这个方法的实现:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // hook:钩子函数
        Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method method2 = class_getInstanceMethod(self, @selector(mj_sendAction:to:forEvent:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)mj_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));

    // 调用系统原来的实现
    // 因为方法已经交换了,所以其实是调用sendAction:to:forEvent:
    [self mj_sendAction:action to:target forEvent:event];

    //拦截按钮事件
    if ([self isKindOfClass:[UIButton class]]) {
        // 拦截了所有按钮的事件

    }
}

上面交换方法也叫钩子函数,利用钩子函数就实现了拦截所有UIButton的点击事件。

问题1:为什么上面要加个dispatch_once?

按理说load方法只会调用一次,万一别人主动调用了load方法那不就调用两次了吗,这样方法就交换两次了和没交换一样,所以加个dispatch_once。

问题2:交换方法实现的原理是什么?

method_exchangeImplementations方法是传入两个Method,以前我们讲过Method的内部结构,其实交换方法实现就是把Method里面的IMP交换了,如下图:

交换前.png
交换后.png

上面说的交换方法实现,交换的是方法列表(methods数组)里面的method_t(也就是Method),如果这个方法有缓存,怎么办?

问题3:如果这个方法有缓存,怎么办?

其实,调用method_exchangeImplementations函数会清空缓存,这样就保证了交换方法之后调用方法不会出错。

可以在objc4里面搜索到源码:

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);//交换IMP之后就会清空缓存。

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

上面源码很简单,可以发现,交换IMP之后就会清空缓存。

特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

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

推荐阅读更多精彩内容