【iOS】OC底层系列十一 - runtime的应用

runtime在OC中的应用非常广泛,可以动态创建和获取类、成员变量、属性、方法。

一、类

常用API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 动态创建一个类(参数:父类,类名,额外的内存空间),创建完成后一定要注册,否则无法使用
// superclass:创建类的父类对象
// name:类名
// extraBytes:额外的内存空间,默认传0即可
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)

// 判断一个OC对象是否为Class(此处的obj可以是类或元类,因为元类是一种特殊的类对象)
BOOL object_isClass(id obj)

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

// 获取父类
Class class_getSuperclass(Class cls)

注册类一定要放在添加成员变量后,因为类注册后,类的结构就已经固定了。添加方法、属性、协议不受此限制,具体可参考前面章节介绍:类的结构体。成员变量放在class_ro_t里面,方法、属性、协议放在class_rw_t里面。

二、成员变量

常用API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

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

// 拷贝实例变量列表(最后需要调用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)

// 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
// cls:成员变量添加到哪一个类对象中
// name:成员变量名称
// size:占用内存大小
// alignment:内存对齐,默认传1即可
// types:变量类型,可以使用@encode(类型)获取types
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

三、属性

常用API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取一个属性
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

// 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

// 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

// 动态添加方法
// cls:方法要添加到哪个类对象
// name:添加的方法名
// imp:方法的实现(C语言方法,也可以结合class_getInstanceMethod和method_getImplementation函数创建OC方法的实现)
// types:方法的返回值类型和参数类型
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 m, 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的实际应用场景有很多,我们把一些常用的API做一些简单的示例,具体的细节需要根据业务进行调整和变换。

5.1. 动态创建类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void run(id self, SEL _cmd) {
NSLog(@"%@ %@", self, NSStringFromSelector(_cmd));
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 动态创建类:@interface DBStudent : NSOBject
Class stuCls = objc_allocateClassPair([NSObject class], "DBStudent", 0);
// 动态添加方法:- (void)run;
class_addMethod(stuCls, @selector(run), (IMP)run, "v@:");
// 动态添加成员变量:NSString *_name
class_addIvar(stuCls, "_name", 8, 1, @encode(NSString*));
// 动态添加成员变量:int _height
class_addIvar(stuCls, "_height", 4, 1, @encode(int));

// 注册类
objc_registerClassPair(stuCls);

// 创建stuCls类型的实例对象
id stu = [[stuCls alloc] init];
// 为成员变量赋值
[stu setValue:@"idbeny" forKey:@"_name"];
[stu setValue:@20 forKey:@"_height"];

// 执行run方法
[stu run];
// 输出:<DBStudent: 0x100733740> run

// 获取成员变量的值
NSLog(@"name:%@, age:%@", [stu valueForKey:@"_name"], [stu valueForKey:@"_height"]); // 输出:name:idbeny, age:20
}
return 0;
}

5.2. 私有属性赋值/取值

修改UITextField的placeholder颜色和字体,可以通过attributedPlaceholder添加富文本达到目的。也可以通过获取UITextField的成员变量,查看控制placeholder的是哪一个成员变量,然后使用KVC方式修改成员变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
// 获取UITextField的成员变量(目的:查看是哪个属性控制的placeholder)
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([UITextField class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSLog(@"%@", name);
}
free(ivars);

// _placeholderLabel最终继承自UILabel,所以可以当成UILabel使用
[self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];

在实际开发中,不建议直接修改系统控件的成员变量,因为不确定在未来什么时候成员变量不存在或被苹果禁止(也可能会影响APP上架)。所以一般情况下,我们都是自定义一个UI控件或组件。

5.3. 字典转模型

字典转模型我们一般使用的是第三方库(例如:YYModel,MJExtension等)。但里面的核心就是使用的runtime一些API,更多的是处理细节问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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];
NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
// 删除成员变量的下划线前缀
[name deleteCharactersInRange:NSMakeRange(0, 1)];

// 设值(关键字id特殊处理)
id value = json[name];
if ([name isEqualToString:@"ID"]) {
value = json[@"id"];
}
[obj setValue:value forKey:name];
}
free(ivars);

return obj;
}

@end

以上代码仅仅是一个示范,真正的字典转模型需要处理很多细节问题(比如:继承、自定义映射关系、模型嵌套等)。

5.4. 拦截按钮点击事件

UIButton继承自UIControl,所有点击事件触发时都会调用UIControl的方法- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event,因此如果要拦截按钮的点击事件,只需要把这个系统方法和自定义的方法交换实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation UIControl (HookEvent)

+ (void)load {
Method sysMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method mMethod = class_getInstanceMethod(self, @selector(db_sendAction:to:forEvent:));
method_exchangeImplementations(sysMethod, mMethod);
}

- (void)db_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
NSLog(@"%@ - %@ - %@", self, NSStringFromSelector(action), target);
// 调用系统原来的实现:不覆盖系统原来方法的实现
[self db_sendAction:action to:target forEvent:event];
}

@end

方法交换的本质是把class_rw_t -> methods -> method_list_t -> method_t中的IMP交换了,并且使用method_exchangeImplementations后,缓存的方法会清空。所以需要调用交换后的方法才可以调用交换前的方法实现。

一般情况下,我们把方法交换称为hook(钩子)。

5.5. 防止数组添加空值导致崩溃

使用NSMutableArray添加成员时,addObject:不能添加nil,而addObject:的本质是调用insertObject:atIndex:,所以我们只需要hookinsertObject:atIndex方法就可以。

NSMutableArray的真实类型并不是NSMutableArray。NSString、NSArray、NSDictionary等都是类簇(真实类型和暴露的类型不一致),真实类型是其他类型。所以在传入cls时一定要注意,否则方法会交换失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@implementation NSMutableArray (Extension)

+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"__NSArrayM");
Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
Method method2 = class_getInstanceMethod(cls, @selector(db_insertObject:atIndex:));
method_exchangeImplementations(method1, method2);
});
}

- (void)db_insertObject:(id)anObject atIndex:(NSUInteger)index
{
if (anObject == nil) return;

[self db_insertObject:anObject atIndex:index];
}

@end

5.6. 防止字典的key是空值导致崩溃

NSDictionary添加键值对时真实类型是__NSDictionaryM,根据键取值时字典的真实类型有很多种,但都继承自__NSDictionaryI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@implementation NSMutableDictionary (Extension)

+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = NSClassFromString(@"__NSDictionaryM");
Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
Method method2 = class_getInstanceMethod(cls, @selector(db_setObject:forKeyedSubscript:));
method_exchangeImplementations(method1, method2);

Class cls2 = NSClassFromString(@"__NSDictionaryI");
Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
Method method4 = class_getInstanceMethod(cls2, @selector(db_objectForKeyedSubscript:));
method_exchangeImplementations(method3, method4);
});
}

- (void)db_setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
{
if (!key) return;

[self db_setObject:obj forKeyedSubscript:key];
}

- (id)db_objectForKeyedSubscript:(id)key
{
if (!key) return nil;

return [self db_objectForKeyedSubscript:key];
}

@end