iOS - Runtime 中有关类、成员和方法的 API

image.png

有关类的 API

获取 isa 指向的 Class

Class object_getClass(id obj);

设置 isa 指向的 Class

object_setClass(id _Nullable obj, Class _Nonnull cls) 

该方法能修改 isa 的指向,假如现在有两个类 People 和 Car 类,都有 run() 方法,两个 run 方法的实现为:

- (void)run {
    NSLog(@"%s", __func__);
}

执行:

People* p = [[People alloc] init];
[p run];
        
object_setClass(p, [Car class]);
[p run];

结果为:

-[People run]
-[Car run]

判断一个对象是否为类对象

object_isClass(id _Nullable obj)

运行:

NSLog(@"%d, %d, %d",
              object_isClass([[People alloc] init]),
              object_isClass([People class]),
              object_isClass(object_getClass([People class])));

结果为:

0, 1, 1

最后一个,元类对象是特殊的类对象。

动态创建一个类

objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) 

第一个参数为父类,第二个参数为目标类的类名,第三个参数为给目标类新增的额外空间,一般传 0。

假如要创建一个 Valenti 类,则:

Class Valenti = objc_allocateClassPair([NSObject class], "Valenti", 0); // Valenti 继承自 NSObject
id v = [[Valenti alloc] init];
        
objc_registerClassPair(Valenti); // 注册该类,提示系统未来会用到该类
NSLog(@"%@", [v class]);

运行结果:

Valenti

动态注册类

objc_registerClassPair(Class _Nonnull cls) 

该方法为动态注册一个类,注册过后的类可以正常使用,需要注意的是添加成员操作都要在注册类前完成。类注册完后,基本信息会在只读的 class_ro_t 中存储,所以注册过后不能向只读的成员中覆盖新数据。

也就是说,添加成员的动态操作是不能对已存在的 People 和 Car 类使用的。

但是动态添加方法的操作是可以在注册之后进行。因为方法列表是存在 class_rw_t 中的,该结构是可读可写的。

动态销毁类

objc_disposeClassPair(Class _Nonnull cls)

有关成员的 API

动态添加成员

class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, uint8_t alignment, const char * _Nullable types) 

若要给上节动态新建的 Valenti 类中动态添加成员 int 型 age,则:

class_addIvar(Valenti, "age", 4, 1, @encode(int)); // 第三个参数为添加的成员类型字节数, 第四个参数为内存对齐相关,传 1 即可

首先在添加成员之前我们打印:

NSLog(@"%zd", class_getInstanceSize(Valenti));

结果为 8,很简单,因为目前该类只有一个 isa 指针,所以整个结构体占 8 个字节。
在添加成员后打印,结果为 16,说明 _age 添加成功。

我们可通过 KVC 对其进行赋值:

[v setValue: @26 forKey:@"age"];

打印:

NSLog(@"%@", [v valueForKey:@"age"]);

结果为: 26

获取实例变量信息

class_getInstanceVariable(Class _Nullable cls, const char * _Nonnull name)

现对 People 增加如下属性:

@property(assign, nonatomic) int age;
@property(assign, nonatomic) NSInteger weight;
@property(copy, nonatomic) NSString* name;

若要在外部获取实例变量 _age 的信息,则:

Ivar ageIvar = class_getInstanceVariable([People class], "_age");
NSLog(@"%s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));

结果为:

_age i

获取成员变量信息

ivar_getName(Ivar _Nonnull v) // 获得成员变量的名字
ivar_getTypeEncoding(Ivar _Nonnull v) // 获得成员变量的编码类型

使用见上。

设置、获取成员变量的值

object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 
object_getIvar(id _Nullable obj, Ivar _Nonnull ivar) 

运行:

People* p = [[People alloc] init];
Ivar nameIvar = class_getInstanceVariable([People class], "_name");
object_setIvar(p, nameIvar, @"valenti"); 
NSLog(@"%@", object_getIvar(p, nameIvar));

结果为:

valenti

但这种方法对非对象类型的数据行不通,因为 API 的第三个接口需要我们传 id 类型的值,想要调用这个接口对非对象类型设值,则:

Ivar ageIvar = class_getInstanceVariable([People class], "_age");
object_setIvar(p, ageIvar, (__bridge id)(void*)10);
NSLog(@"%d", p.age);

结果:

10

首先将 10 转成指针变量,即 void*,因为指针变量就是存地址值的,然后转成 id 类型,即 __bridge id

获取成员变量数组

class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount) 

运行:

unsigned int count;
Ivar* ivars = class_copyIvarList([People class], &count);
        
for (int i = 0; i < count; i ++) {
    NSLog(@"%s %s", ivar_getName(ivars[i]), ivar_getTypeEncoding(ivars[i]));
}
free(ivars)

结果为:

_age i
_weight q
_name @"NSString"

调用 runtime 的 API 中有 copy 操作都需要 free 释放掉。

有关方法的 API

动态添加方法

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

若要给上节动态新建的 Valenti 类中动态添加方法,则:

class_addMethod(Valenti, @selector(run), (IMP)run, "v@:");

run 方法实现为:

void run(id self, SEL _cmd) {
    NSLog(@"%@ %@", self, NSStringFromSelector(_cmd));
}

调用 [v run];
打印结果为:

<Valenti: 0x102032170> run

替换方法实现

class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

上面的 People 类增加了 funcAfunB 方法,在外部增加了 funcC 函数:

- (void)funcA {
    NSLog(@"This is function A");
}

- (void)funcB {
    NSLog(@"This is function B");
}

funcC 函数的实现为:

void funcC() {
    NSLog(@"This is function C");
}

那么,要替换 funcA 的实现为 funcC,则:

People* p = [[People alloc] init];
        
class_replaceMethod([People class], @selector(funcA), (IMP)funcC, @encode(void));
[p funcA];

结果为:

This is function C

获得方法列表

class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount)

和获取成员列表数组一样,最后也需要 free
运行:

unsigned int count;
        Method* methods = class_copyMethodList([People class], &count);
        for (int i = 0; i < count; i ++) {
            Method method = methods[i];
            
            NSLog(@"%@", NSStringFromSelector(method_getName(method)));
            NSLog(@"%p", method_getImplementation(method));
            NSLog(@"%s", method_getTypeEncoding(method));
            NSLog(@"%d", method_getNumberOfArguments(method));
            NSLog(@"%s", method_copyReturnType(method));
            NSLog(@"%s", method_copyArgumentType(method, 0));
}
free(methods);

结果为:

funcA
0x100001b30
v16@0:8
2
v
@
funcB
0x100001920
v16@0:8
2
v
@
.cxx_destruct // dealloc 
0x100001a60
v16@0:8
2
v
@
name
0x1000019f0
@16@0:8
2
@
@
setName:
0x100001a20
v24@0:8@16
3
v
@
run
0x1000018c0
v16@0:8
... // 下面是其他属性的 setter/getter 方法

获取方法相关信息

method_getName(Method _Nonnull m) // 返回方法名
method_getImplementation(Method _Nonnull m) // 返回方法实现
method_getTypeEncoding(Method _Nonnull m) // 返回方法编码类型
method_getNumberOfArguments(Method _Nonnull m) // 返回参数数量 
method_copyReturnType(Method _Nonnull m) // 返回方法返回值的编码 void -> v
method_copyArgumentType(Method _Nonnull m, unsigned int index) // 返回目标 index 下参数的编码类型

用法见上。

获取一个实例/类方法

class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)

Objective-C 在转成机器码之前会通过 LLVM 先转成中间码,生成中间代码命令

clang -emit-llvm -S 目标文件

生成的代码为全新的语法,具体语法可参考官方文档

用 block 作为方法实现

imp_implementationWithBlock(id _Nonnull block)

运行:

People* p = [[People alloc] init];
        
class_replaceMethod([People class], @selector(funcA), imp_implementationWithBlock(^{
    NSLog(@"This is a block");
}), @encode(void));
[p funcA];

结果为:

This is a block

交换两个方法的实现

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

倘若要交换 funcA 和 funcB 的实现,则:

People* p = [[People alloc] init];
Method aMethod = class_getInstanceMethod([People class], @selector(funcA));
Method bMethod = class_getInstanceMethod([People class], @selector(funcB));
        
method_exchangeImplementations(aMethod, bMethod);
[p funcA];
[p funcB];

结果为:

This is function B
This is function A

那么它真正交换的实现是什么?
首先,方法列表在 class_rw_t 结构体的 method_array_t 类型的二位数组 methods 中,methods 的每个子元素都是 method_list_t 类型的一维数组,method_list_t 每个子元素为 method_t 类型,那么 method_t 的结构为:

struct method_t {
    SEL name;
    const char* types;
    IMP imp;
}

那么,交换 API 中交换的就是这个结构体的 imp。

并且,一旦调用交换 API,就会清空缓存,所有的方法缓存都需要重新走一遍。源码中有体现:

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

    mutex_locker_t lock(runtimeLock);

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

    flushCaches(nil); // 清除方法缓存

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

交换方法的 API 在开发中应用极其广泛,Hook 技术中经常能用到该 API,但是对于 NSString、NSArray 和 NSDictionary 进行 hook 的时候需要注意,这三个的真实类型是 __NSArrayI、__NSStringI 和 __NSDictionaryI。对应可变类型的真实类型为:__NSArrayM、__NSStringM 和 __NSDictionaryM,I 为 immutable,不可变,M 为 mutable,可变。

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

推荐阅读更多精彩内容