iOS之runtime详解api(一)

什么是runtime?

runtime在iOS中是“运行时”的含义,是一套用c语言写的api,很多人会用但是也仅仅用过最最常用的几个函数,这次,我将详细的带着大家探索下runtime的API,这一章就说下<objc/runtime.h>这个文件里的API,并且我会把不适用于ARC和不支持64位的API剔除掉。

1.Class相关

首先,我们先看一个简单的函数:

const char * _Nonnull
class_getName(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

这个函数是通过传入Class类型的cls来得到Class的名字。那我们测试下这个函数:

-(void)getName {
    const char* name = class_getName([Person class]);
    NSLog(@"name = %s",name);
}

其中[Person class]OC中获得Class的方法,当然,你也可以用runtime里面的objc_getClass等函数,后面我也会讲到。
运行结果:

name = Person

我们可以看到打印出来的结果就是类的名字。
上面既然用到了[Person class],那我们就看下在runtime[Person class]的替代函数,都是通过名字来获得Class

Class _Nullable
objc_getClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

Class _Nullable
objc_lookUpClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

Class _Nonnull
objc_getRequiredClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

那这三个有什么区别,从结论上讲,objc_getClassobjc_lookUpClass的效果是一致的,在最新的源码里面,这两个方法调用的底层也是一致的,当你要找的类不存在的话,就返回nil,而objc_getRequiredClass里你要找的类不存在的话,就会崩溃。下面我们来测试下,我们创建一个Person类。

-(void)getClass {
    const char* name = "Person1";
    const char* name_exist = "Person";
    Class class1_exist = objc_getClass(name_exist );
    NSLog(@"class1_exist = %@",class1_exist);

    Class class1 = objc_getClass(name);
    NSLog(@"class1 = %@",class1);
    Class class2_exist = objc_lookUpClass(name_exist );
    NSLog(@"class2_exist = %@",class2_exist);
    Class class2 = objc_lookUpClass(name );
    NSLog(@"class2 = %@",class2);

    Class class3_exist = objc_getRequiredClass(name_exist );
    NSLog(@"class3_exist = %@",class3_exist);
    Class class3 = objc_getRequiredClass(name );
    NSLog(@"class3 = %@",class3);
}

运行结果:

2019-02-21 16:58:39.173892+0800 Runtime-Demo[91840:2890084] class1_exist = Person
2019-02-21 16:58:39.173939+0800 Runtime-Demo[91840:2890084] class1 = (null)
2019-02-21 16:58:39.173951+0800 Runtime-Demo[91840:2890084] class2_exist = Person
2019-02-21 16:58:39.173960+0800 Runtime-Demo[91840:2890084] class2 = (null)
2019-02-21 16:58:39.173969+0800 Runtime-Demo[91840:2890084] class3_exist = Person
objc[91840]: link error: class 'Person1' not found.

最后也确实崩溃了,所以大家使用objc_getRequiredClass这个函数时候要慎重小心。
除了用名字获得类对象以外,还可以用实例对象来获取:

Class _Nullable
object_getClass(id _Nullable obj)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

我们测试下:

-(void)getClassWithObjc {
    Person* person = [Person new];
    Class class = object_getClass(person);
    NSLog(@"class = %@",class);
}

运行结果:

class = Person

完全没问题。
Class不仅可以代表类对象,也可以代表元类对象,下面这个函数就是通过名字获取元类对象。

Class _Nullable
objc_getMetaClass(const char * _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

如果你读过源码的话,你就会清楚元类对象储存的是类方法,类对象储存的是实例方法,在后面讲到Method相关的API的时候,我们在具体讲他们之间的区别。

讲到元类对象,我们还要关注下这个函数,

BOOL
class_isMetaClass(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

这个函数是用来判断是否是元类对象。

-(void)isMetaClass {
    const char* name = "Person";
    BOOL isMetaClass1 = class_isMetaClass(objc_getMetaClass(name ));
    BOOL isMetaClass2 = class_isMetaClass(objc_getClass(name));
    NSLog(@"objc_getMetaClass = %d,objc_getClass = %d",isMetaClass1,isMetaClass2);
}

运行结果:

objc_getMetaClass = 1,objc_getClass = 0

我们可以看到objc_getMetaClass生成才是元类对象,objc_getClass生成的只是类对象。
那么有没有函数区分类(元类)对象和实例对象呢?当然有:

BOOL
object_isClass(id _Nullable obj)
OBJC_AVAILABLE(10.10, 8.0, 9.0, 1.0, 2.0);

这个方法只要是类对象或者元类对象都会返回YES:

-(void)isClass {
    Person* person = [Person new];
    BOOL isClass_objc = object_isClass(person);
    BOOL isClass_class = object_isClass(objc_getClass("Person"));
    BOOL isClass_metaClass = object_isClass(objc_getMetaClass("Person"));

    NSLog(@"isClass_objc = %d  isClass_class = %d  isClass_metaClass = %d",isClass_objc,isClass_class,isClass_metaClass);
}

运行结果:

isClass_objc = 0  isClass_class = 1  isClass_metaClass = 1

当然也可以获得父类对象。

Class _Nullable
class_getSuperclass(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

我们新建一个继承Person的类Student,然后我们通过Student类来获得Person类。

-(void)getSuperclass {
    Class class = class_getSuperclass(objc_getClass("Student"));
    NSLog(@"class = %@",class);
}

运行结果:

class = Person

Student的父类确实是Person

我们知道OC里面可以强转类型,当然,runtime里面也有相关方法

Class _Nullable
object_setClass(id _Nullable obj, Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

这个方法的意思是给一个实例对象设置新的类,返回旧的类

-(void)setClass {
    Student* student = [Student new];
    Class class = object_setClass(student, objc_getClass("Person"));
    NSLog(@"oldClass = %@",class);
    NSLog(@"newStudent = %@",student);
}

运行结果:

2019-02-21 17:38:17.388341+0800 Runtime-Demo[92493:2904857] oldClass = Student
2019-02-21 17:38:17.388413+0800 Runtime-Demo[92493:2904857] newStudent = <Person: 0x282dd8b50>

我们可以看出开始的时候student的类是Student,用了object_setClass后就是Person类了。
runtime的动态性还可以动态新增类,下面四个函数分别表示为一个类分配内存,注册一个类,复制一个类,销毁一个类

Class _Nullable
objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name,
                       size_t extraBytes)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

创建一个新类,superclass是新类所继承的类,如果为nilsuperclass就默认为根类,也就是NSObjectextraBytes是在类和元类对象的末尾为索引ivars分配的字节数。这一般是0,name是新类的名字。

void
objc_registerClassPair(Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

注册类,如果这个类objc_allocateClassPair好了,就必须objc_registerClassPair才能使用。

Class _Nonnull
objc_duplicateClass(Class _Nonnull original, const char * _Nonnull name,
                    size_t extraBytes)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

这个方法在系统KVO的底层用过,系统不推荐我们自己用。

 void
objc_disposeClassPair(Class _Nonnull cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

objc_disposeClassPair只能销毁通过objc_allocateClassPair创建的类。
我们写个demo来测试这些方法,objc_duplicateClass官方不建议使用,那么我们就不测试这函数。

-(void)classLifeCycle {
    Class class = objc_allocateClassPair(objc_getClass("Person"), "Teacher" , 0);
    const char* name = class_getName(class);
    Class allocateClass = objc_getClass(name);
    NSLog(@"allocateClass = %@",allocateClass);
    
    objc_registerClassPair(class);
    Class registerClass = objc_getClass(name);
    NSLog(@"registerClass = %@",registerClass);
    
    objc_disposeClassPair(class);
    Class disposeClass = objc_getClass(name);
    NSLog(@"disposeClass = %@",disposeClass);
}

运行结果:

2019-02-22 09:37:52.705001+0800 Runtime-Demo[99587:3143177] allocateClass = (null)
2019-02-22 09:37:52.705049+0800 Runtime-Demo[99587:3143177] registerClass = Teacher
2019-02-22 09:37:52.705071+0800 Runtime-Demo[99587:3143177] disposeClass = (null)

我们可以知道如果仅仅只是objc_allocateClassPair的话,你是找不到这个类的,必须再objc_registerClassPair才可以找到,objc_disposeClassPair则是把类销毁掉,所以再实际开发中,如果我们不再使用自建类的时候,就要及时销毁,节省内存。

下面两个函数是关于整个工程的类列表的函数:

Class _Nonnull * _Nullable
objc_copyClassList(unsigned int * _Nullable outCount)
OBJC_AVAILABLE(10.7, 3.1, 9.0, 1.0, 2.0);

这个函数是获得所有注册类的列表,我们试用下:

-(void)copyClassList {
    unsigned int outCount;
    Class *classes = objc_copyClassList(&outCount);
    NSLog(@"outCount = %d",outCount);
    for (int i = 0; i < outCount; i++) {
        NSLog(@"%s", class_getName(classes[i]));
    }
    free(classes);
}

运行结果:

2019-02-22 09:52:12.218871+0800 Runtime-Demo[99840:3149922] outCount = 15765
2019-02-22 09:52:12.218939+0800 Runtime-Demo[99840:3149922] _CNZombie_
2019-02-22 09:52:12.218953+0800 Runtime-Demo[99840:3149922] JSExport
2019-02-22 09:52:12.218963+0800 Runtime-Demo[99840:3149922] NSLeafProxy
......
......

我们看到注册的类有15765个。
objc_getClassList也是获取注册类的方法.

int
objc_getClassList(Class _Nonnull * _Nullable buffer, int bufferCount)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

第一个参数buffer已分配好内存空间的数组指针,bufferCount是数组的个数,如果bufferCount的数量小于实际的数组数量,那么buffer返回的是所有数组集合的任意一个子类。如果buffer为NULL,那么bufferCount为0。无论那种情况,返回结果都是当前注册类的总数。

-(void)getClassList {
    int bufferCount = 4;
    Class* buffer = (Class*)malloc(sizeof(Class)* bufferCount);
    int count1 = objc_getClassList(buffer, bufferCount);
    for (unsigned int i =0; i <bufferCount; i++) {
        NSLog(@"name = %s",class_getName(buffer[i]));
    }
    NSLog(@"count1 = %d",count1);

    int count2 = objc_getClassList(NULL, 0);
    NSLog(@"count2 = %d",count2);
}

运行结果:

2019-02-22 10:14:34.487051+0800 Runtime-Demo[354:3159864] name = _CNZombie_
2019-02-22 10:14:34.487145+0800 Runtime-Demo[354:3159864] name = JSExport
2019-02-22 10:14:34.487158+0800 Runtime-Demo[354:3159864] name = NSLeafProxy
2019-02-22 10:14:34.487173+0800 Runtime-Demo[354:3159864] name = NSProxy
2019-02-22 10:14:34.487186+0800 Runtime-Demo[354:3159864] count1 = 15765
2019-02-22 10:14:34.493662+0800 Runtime-Demo[354:3159864] count2 = 15765
size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

返回类实例的大小。

-(void)getInstanceSize {
    size_t size = class_getInstanceSize(objc_getClass("Person"));
    NSLog(@"size = %zu",size);
}

运行结果

size = 8

一个没有变量或属性的继承于NSObject的类占有8个字节。
还有个方法是:

id _Nullable
class_createInstance(Class _Nullable cls, size_t extraBytes)
OBJC_RETURNS_RETAINED
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这是一个创建实例的方法,cls是要创建的类,extraBytes是额外的字节内存,用来存储类定义中的实例变量之外的其他实例变量。在源码中alloc方法底层就是用的这个函数。那么,我们用这个函数来初始化Person类:

-(void)createInstance {
    Person* person = class_createInstance(objc_getClass("Person"), 0);
    NSLog(@"%@",person);
}

运行结果:

<Person: 0x60000343d2f0>

确实能够成功创建出来。
最后剩下两个方法:

int
class_getVersion(Class _Nullable cls)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

void
class_setVersion(Class _Nullable cls, int version)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

这两个方法都和version有关,这个version在实际中我也没发现用处,可能是在改变类的变量或者方法时给定一个标识.

-(void)version {
    int verson = class_getVersion(objc_getClass("Person"));
    NSLog(@"version = %d",verson);
    class_setVersion(objc_getClass("Person"), 10);
    int newVersion = class_getVersion(objc_getClass("Person"));
    NSLog(@"newVersion = %d",newVersion);
}

运行结果

2019-02-22 11:29:57.325309+0800 Runtime-Demo[526:167322] version = 0
2019-02-22 11:29:57.325349+0800 Runtime-Demo[526:167322] newVersion = 10

2.objc_category or Category

下面我们将使用runtime里面最最常用的api,也就是给分类绑定对象,这里,我们先了解下,一个枚举:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

objc_AssociationPolicy是一个枚举,里面的枚举值分别代表要添加的属性的修饰类型。
OBJC_ASSOCIATION_ASSIGN相当于weak
OBJC_ASSOCIATION_RETAIN_NONATOMIC相当于strongnonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC相当于copynonatomic
OBJC_ASSOCIATION_RETAIN相当于strongatomic
OBJC_ASSOCIATION_COPY相当于copyatomic
关于分类的runtime函数,主要有下面3个:

void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

void
objc_removeAssociatedObjects(id _Nonnull object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

含义分别为设置关联对象,获得关联对象,删除关联对象。
我们知道如果在分类的.h文件设置属性并没有用,调用的时候会发生闪退,这是因为系统并没有自动为属性生成SetGet方法,所以,我们用上面三个方法来手动关联对象。
我们创建一个 Person的分类Person+Actor.h,在.h文件里新建一个新属性@property(nonatomic, assign)float actingSkill而不做其他任何处理,这时候,.m文件就会有警告。

屏幕快照 2019-02-23 下午12.41.52.png

这就告诉我们需要手动实现setActingSkill:actingSkill方法:
.m文件

#import "Person+Actor.h"
#import <objc/runtime.h>
static const char* key = "actingSkill";
@implementation Person (Actor)

-(void)setActingSkill:(float)actingSkill {
    NSNumber *actingSkillObjc = [NSNumber numberWithFloat:actingSkill];
    objc_setAssociatedObject(self, key, actingSkillObjc, OBJC_ASSOCIATION_RETAIN);
}

-(float)actingSkill {
    NSNumber *actingSkillObjc = objc_getAssociatedObject(self, key);
    return [actingSkillObjc floatValue];
}

@end

这时候就绑定好了。
ViewController里面去使用下这个属性

-(void)testCategory {
    _person = [Person new];
    _person.actingSkill = 0.1;
    NSLog(@"actingSkill = %f",_person.actingSkill);
}

运行结果:

actingSkill = 0.100000

说明set和get方法都成功了。
那么还有一个objc_removeAssociatedObjects方法还没用,这个方法是解除绑定,为了测试这个效果,我们在ViewController里面touchesBegan里面去调用这个方法。

-(void)testCategory {
    _person = [Person new];
    _person.actingSkill = 0.1;
    NSLog(@"actingSkill = %f",_person.actingSkill);
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    if (_person) {
        objc_removeAssociatedObjects(_person);
        NSLog(@"actingSkill2 = %f",_person.actingSkill);
    }
}

运行结果:

2019-02-23 13:21:13.090961+0800 Runtime-Demo[2964:201009] actingSkill = 0.100000
2019-02-23 13:24:24.585347+0800 Runtime-Demo[2964:201009] actingSkill2 = 0.000000

之前绑定的结果被移除了。
今天我们这一篇就讲到这,runtime还有很多其他的用法我们下一篇见。
对了,这个是demo,喜欢的可以点个星。

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

推荐阅读更多精彩内容

  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,129评论 0 9
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9
  • 简介 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 O...
    专业男神经阅读 901评论 0 2
  • 其实4.1号就该写这篇的,因为这个题目就是为了一个人:“哥哥 张国荣” 我从小就是一个孤陋寡闻的人,第一次对哥哥有...
    佛右扯阅读 422评论 0 0
  • 再次感觉到无比的疲惫!这就是我现在的状态! 都说,你不坚强,没有人替你坚强。而我想说,我真的没那么...
    听风舞者阅读 174评论 0 0