OC-Runtime-常用API
一. 类相关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
在程序运行的时候,动态添加一个类,并且添加成员变量、方法,最后使用类。
- 一定要在注册类之前添加成员变量,因为成员变量是在_r_o_t表里面,是只读的,所以要在类的结构确定之前添加成员变量。
- 不能使用class_addIvar给已经创建的类添加成员变量,因为已经创建的类的结构在代码写完就已经确定好了,程序运行中就不能给已经创建的类添加成员变量了。
- 方法可以在注册类之后添加,因为方法是在_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交换了,如下图:
上面说的交换方法实现,交换的是方法列表(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对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!