1. 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
首先,关于NSObject,objc_class 和 objc_object 三者之间的关系,我们可以用下面的图来更清晰的了解:
一 NSObject
NSObject是OC 中的基类,除了NSProxy其他都继承自NSObject
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
二 对象结构体 objc_object
在运行时,类的对象
被定义为objc_object
类型,就是对象结构体,在OC 中每一个对象都是一个结构体,结构体都包含了一个isa
成员变量。根据isa的定义可以知道,类型为isa_t
类型的
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
// 省略其余方法
...
}
isa_t 的定义是什么?
是一个union
的结构对象,类似于C++结构体,其内部可以定义为成员变量和函数,是一个联合类型
,其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。
三 类结构体 objc_class
类也是一个对象,类的结构体objc_class 是继承自objc_object的,具备对象的所有特征
iOS中不管类对象
还是元类对象
类型都是Class
,而Class的结构则是objc_class
结构体
typedef struct objc_class *Class;
typedef struct objc_object *id;
//注意,有些人看到的objc_class结构体定义不一样,有OBJC2_UNAVAILABLE 的注释,在OC 2.0中,
//这种关于之前objc_class的定义已经废弃掉了,可以在 objc-runtime-new.h 看OC 2.0之后的,如下:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // 方法缓存 formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
...//此处省略
}
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
...
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
assert(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
...
cache_t cache
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
根据源码,我们可以知道 cache_t 中存储了一个 bucket_t 的结构体,和两个unsigned int的变量。
mask:分配用来缓存bucket的总数。
occupied:表明目前实际占用的缓存bucket的个数。
bucket_t 的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。
cache_t 中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。
Cache 的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。
class_data_bits_t bits
存储类的方法、属性、遵循的协议等信息的地方
class_data_bits_t bits
有一个成员uintptr_t bits
, 可以理解为一个‘复合指针’
。什么意思呢,就是bits不仅包含了指针,同时包含了Class的各种异或flag,来说明Class的属性。把这些信息复合在一起,仅用一个uint指针bits来表示。当需要取出这些信息时,需要用对应的以FAST_
前缀开头的flag掩码
对bits
做按位与
操作。
例如,我们需要取出Classs的核心信息class_rw_t,
则需要调用方法:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
2. 为什么要设计metaclass
类对象(class object)
中包含了类的实例变量,实例方法
的定义,
元类对象(metaclass object)
中包括了类对象的方法,也就是类方法
(也就是C++中的静态方法
)的定义。
那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?
__class_lookupMethodAndLoadCache3
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
`lookUpImpOrForward`用于查找方法
1、首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
2、从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
3、从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
4、如果到基类依然没有找到该方法则触发动态方法解析流程。=
5、还是找不到就触发消息转发流程
答:行是肯定可行的,但是在lookUpImpOrForward
执行的时候就得标注上传入的cls
到底是实例对象
还是类对象
,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。
从OC的消息机制
分析了元类存在的意义
,元类的存在巧妙的简化了实例方法和类方法的调用流程
,大大提升了消息发送的效率
。
metaclass代表的是类对象的对象,它存储了类的类方法
,它的目的是将实例和类的相关方法列表以及构建信息区分开来
,方便各司其职
,符合单一职责
设计原则。
具体可以参考这篇文章
3. class_copyIvarList & class_copyPropertyList区别
class_copyIvarList
获取 类对象 中的所有实例变量信息,从 class_ro_t 中获取
class_copyPropertyList
获取 类对象 中的属性信息, class_rw_t 的 properties,先后输出了 category / extension/ baseClass 的属性,而且仅输出 当前的类 的属性信息,而不会向上去找 superClass 中定义的属性。
Q1: class_ro_t 中的 baseProperties 呢?
Q2: class_rw_t 中的 properties 包含了所有属性,那何时注入进去的呢?
An: methodizeClass 方法,会把类里面的属性,协议,方法都加载进来
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
4. class_rw_t 和 class_ro_t 的区别
class_rw_t 表示read write,class_ro_t 表示 read only
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
每个类都对应有一个class_ro_t
结构体和一个class_rw_t
结构体。在编译期间
,class_ro_t
结构体就已经确定,objc_class
中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass
方法时,会生成class_rw_t
结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t
结构体的地址
区别在于:class_ro_t
存放的是编译期间
就确定的属性、方法和遵守协议;而class_rw_t
是在runtime
时才确定,它会先将class_ro_t的内容拷贝
过去,然后再将当前类的分类
的这些属性、方法
等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容
OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过runtime动态修改类的方法时,是修改在class_rw_t
区域中存储的方法列表。
参考这篇文章
更加详细的分析,请看@Draveness 的这篇文章深入解析 ObjC 中方法的结构
5. category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序
objc_init ->... -> realizeClass -> methodizeClass(用于Attach categories)-> attachCategories -> attachLists
在运行时调用 realizeClass
方法,会做以下3件事情:
- 从
class_data_bits_t
调用data
方法,将结果从class_rw_t
强制转换为class_ro_t
指针 - 初始化一个
class_rw_t
结构体 - 设置结构体 ro的值以及 flag
最后调用methodizeClass
方法,把分类里面的属性,协议,方法都加载进来。
关键就是在methodizeClass
方法实现中
category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。
category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。
category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面
,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category
里的对应方法
6、category & extension区别,能给NSObject添加Extension吗,结果如何?
category:分类
- 给类添加新的方法
- 不能给类添加成员变量
- 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和* setter方法,同时也不能生成带下划线的成员属性
- 是运行期决定的
注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。
extension:扩展
- 可以给类添加成员变量,但是是私有的
- 可以給类添加方法,但是是私有的
- 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
- 伴随着类的产生而产生,也随着类的消失而消失
- 必须有类的源码才可以给类添加extension,所以对于系统一些类,如nsstring,就无法添加类扩展
不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。
8. 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么
OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)
方法进行调用,这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa
指针找到这个对象的类对象,从类对象中的cache
中查找是否存在SEL
对应的IMP
,若不存在,则会在 method_list
中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)
进行消息转发。
9. IMP、SEL、Method的区别和使用场景
typedef struct objc_selector *SEL
SEL
: 方法选择器,虽然 SEL
是 objc_selector
结构体指针,但实际上它只是一个 C 字符串
,方法的名称
typedef id (*IMP)(id, SEL, …)
IMP
-函数指针,指向实际执行函数体
typedef struct objc_method *Method
/// Method
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};
方法名 method_name
类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
方法类型 method_types
是个 char 指针,其实存储着方法的参数类型
和返回值类型
,即是 Type Encoding 编码。
method_imp
指向方法的实现
,本质上是一个函数的指针
,就是前面讲到的 Implementation。
使用场景:
实现类的swizzle
的时候会用到,通过class_getInstanceMethod(class, SEL)
来获取类的方法Method
,其中用到了SEL
作为方法名
调用method_exchangeImplementations(Method1, Method2)
进行方法交换
我们还可以给类动态添加方法
,此时我们需要调用class_addMethod(Class, SEL, IMP, types)
,该方法需要我们传递一个方法的实现函数IMP
,例如:
static void funcName(id receiver, SEL cmd, 方法参数...) {
// 方法具体的实现
}
函数第一个参数:方法接收者
,第二个参数:调用的方法名SEL
,方法对应的参数
,这个顺序是固定的。
10、load、initialize方法的区别什么?在继承关系中他们有什么区别
load:当类被装载的时候被调用,只调用一次
- 调用方式并不是采用runtime的objc_msgSend方式调用的,而是直接采用
函数的内存地址
直接调用的 - 多个类的load调用顺序,是依赖于
compile sources中的文件顺序
决定的,根据文件从上到下的顺序调用 - 子类和父类同时实现load的方法时,父类的方法先被调用
- 本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)
- 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照
compile sources中的顺序
调用的 - load是被动调用的,在类装载时调用的,不需要手动触发调用
initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次
- 调用方式是通过runtime的
objc_msgSend
的方式调用的,此时所有的类都已经装载完毕 - 子类和父类同时实现initialize,
父类的先被调用
,然后调用子类
的 - 本类与category同时实现initialize,
category
会覆盖
本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的) - initialize是主动调用的,只有当类第一次被用到的时候才会触发
参考这篇文章
11. _objc_msgForward函数是做什么的,直接调用它将会发生什么?
_objc_msgForward
是一个函数指针(和 IMP 的类型一样),是用于消息转发的:
当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发
。
也就是说_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:
resolveInstanceMethod:方法 (或 resolveClassMethod:)。
forwardingTargetForSelector:方法
methodSignatureForSelector:方法
forwardInvocation:方法
doesNotRecognizeSelector: 方法
12. 通过runtime动态创建一个类
要创建一个新类,
- 首先调用
objc_allocateClassPair
。 - 然后使用
class_addMethod
和class_addIvar
等函数设置类的属性。 - 完成构建类后调用
objc_registerClassPair
。
/**
* 创建一个新类和元类.
*
* @param superclass 这个类是新创建的类的父类,可以传入Nil去创建一个新根类.
* @param name 这个字符串是类的名字(例:"NSObject")
* @param extraBytes 一般传入0
* @return 新的类,如果返回的是Nil,那么就是这个类创建失败了(例:创建的是"NSObject"类,然而这个类已经存在了)
*/
objc_allocateClassPair(
Class _Nullable superclass,
const char * _Nonnull name,
size_t extraBytes
)
objc_allocateClassPair只返回一个值:Class
。那么pair的另一半在哪里呢?
是的,估计你已经猜到了这个另一半就是meta-class
/**
* 注册使用`objc_allocateClassPair`方法创建的类
*
* @param cls 需要注册的类(不能为Nil)
*/
objc_registerClassPair(Class _Nonnull cls)
代码样例
/// 创建一个元类
Class class = objc_allocateClassPair([NSObject class], "Person", 0);
/// 添加方法
//class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
/// 添加属性
//class_addIvar(<#Class _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
/// 注册元类
objc_registerClassPair(class);
13. object_getClass 和 object_setClass
1. Class object_getClass(id obj)
//返回对象的isa指针
eg:参数为实例对象返回类对象
eg:参数为类对象返回元类对象
eg:参数为元类对象返回根元类对象
eg:参数为根元类对象返回自身
Class object_getClass(id obj)
// 根据字符串返回类对象
Class _Nullable objc_getClass(const char * _Nonnull name)
// 创建一个NSObject对象obj,然后获取obj的类
NSObject *obj = [[NSObject alloc] init]; // 1
Class objClass = object_getClass(obj); // 2
NSLog(@"%@", NSStringFromClass(objClass)); // 3
Class nsobjectClass = object_getClass([NSObject class]); // 4
NSLog(@"%@", NSStringFromClass(nsobjectClass)); // 5
BOOL isMeta1 = class_isMetaClass(objClass); // 6
BOOL isMeta2 = class_isMetaClass(nsobjectClass); // 7
NSLog(@"isMeta1:%i, isMeta2:%i", isMeta1, isMeta2); // 8
//打印结果
2020-04-03 18:08:48.875921+0800 ImplementKVO[81240:4259330] NSObject
2020-04-03 18:08:48.876057+0800 ImplementKVO[81240:4259330] NSObject
2020-04-03 18:08:48.876155+0800 ImplementKVO[81240:4259330] isMeta1:0, isMeta2:1
说明:class_isMetaClass
函数的作用是判断传入的类是不是元类,经过判断是不是元类可看出区别,因此可得出结论:
object_getClass
函数的参数传一个类的实例
时,返回的是该实例的类对象
;
参数传类
时,返回的是该类的元类
。
Class class1 = [obj class];
Class class2 = [[NSObject class] class];
NSLog(@"%@", NSStringFromClass(class1));
NSLog(@"%@", NSStringFromClass(class2));
NSLog(@"%i, %i", class_isMetaClass(class1), class_isMetaClass(class2));
2020-04-03 18:17:05.175123+0800 ImplementKVO[81544:4270627] NSObject
2020-04-03 18:17:05.175261+0800 ImplementKVO[81544:4270627] NSObject
2020-04-03 18:17:05.175363+0800 ImplementKVO[81544:4270627] 0, 0
通过打印结果可以看出,class
方法的调用者是一个实例
时,获取到的是该实例的类,此时和object_getClass函数作用相同;而调用者是一个类
(比如[NSObject class])时,获取到的并不是该类的元类
,此时和object_getClass函数的作用不同.
//[xxx class]方法内部实现
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
2. Class object_setClass(id obj, Class cls)
/**
将一个对象设置为别的类类型,返回原来的Class ,将一个对象的isa指针指向设置的Class
* Sets the class of an object.
*
* @param obj The object to modify.
* @param cls A class object.
*
* @return The previous value of \e object's class, or \c Nil if \e object is \c nil.
*/
Class _Nullable object_setClass(id _Nullable obj, Class _Nonnull cls)
// 分别创建一个可变数组对象mutArray和不可变数组对象array
NSMutableArray *mutArray = [NSMutableArray arrayWithObjects:@"a", @"b", nil]; // 1
NSArray *array = @[@"c", @"d"]; // 2
// 获取这两个对象mutArray和array的类并打印
Class mutArrayClassBefore = object_getClass(mutArray); // 3
Class arrayClassBefore = object_getClass(array); // 4
NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassBefore), NSStringFromClass(arrayClassBefore)); // 5
Class setclass = object_setClass(mutArray, arrayClassBefore); // 6
NSLog(@"%@", NSStringFromClass(setclass)); // 7
Class mutArrayClassNow = object_getClass(mutArray); // 8
Class arrayClassNow = object_getClass(array); // 9
NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassNow), NSStringFromClass(arrayClassNow)); // 10
2020-04-03 18:27:43.569594+0800 ImplementKVO[81775:4277728] __NSArrayM -- __NSArrayI
2020-04-03 18:27:43.569724+0800 ImplementKVO[81775:4277728] __NSArrayM
2020-04-03 18:27:43.569822+0800 ImplementKVO[81775:4277728] __NSArrayI -- __NSArrayI
从打印结果可以看出用object_setClass函数将可变数组对象mutArray的类设置为它的父类是可以的,此时再用mutArray调用NSMutableArray的方法会导致程序crash,如
[mutArray addObject:@"e"]; // 11
crash信息为经典的:
2020-04-03 18:31:04.101045+0800 ImplementKVO[81860:4280357] -[__NSArrayI addObject:]: unrecognized selector sent to instance 0x6000000fec40
总结
- 使用object_setClass后会使对象的
isa
的值指向新的Class
。 - 使用object_setClass对象的内存布局不会发生变化。
- 使用object_setClass
不能访问超过
原对象申请的内存区域,否则程序会crash
。
14. Method Swizzle注意事项
- 对于一般的Class.推荐在load方法中交换, 系统的类,可以通过Category添加方法交换
- 避免交换父类方法(先
class_addMethod
,判断是否成功) - 交换方法应在
+load
方法 - 交换方法应该放到
dispatch_once
中执行 - 交换的分类方法应该添加
自定义前缀
,避免冲突 - 交换的分类方法应
调用原实现
注意:如果是交换不同类的方法,并且在方法中访问了类的属性,会造成Crash,更安全的做法,Runtime 还提供了另一种 Swizzle 函数 method_setImplementation。
method_setImplementation 可以让我们提供一个新的函数来代替我们要替换的方法。 而不是将两个方法的实现做交换。 这样就不会造成 method_exchangeImplementations 的潜在对已有实现的副作用了。
method_setImplementation 接受两个参数,第一个还是我们要替换的方法, 而第二个参数是一个 IMP 类型的。
Objc 黑科技 - Method Swizzle 的一些注意事项
例如:统计VC加载次数并打印
- UIViewController+Logging.m
#import "UIViewController+Logging.h"
#import <objc/runtime.h>
@implementation UIViewController (Logging)
+ (void)load
{
swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
- (void)swizzled_viewDidAppear:(BOOL)animated
{
//交换前需要先调用系统的方法初始化调用[self xxx]的系统方法的时候.需要把xxx改成xx_xxx(你交换的方法)
//这里是因为你的方法已经和系统的方法交换了,调用你的方法其实是调用的系统方法,调用系统方法的话就调用的是你的方法,
//然后就会产生循环调用.
// call original implementation
[self swizzled_viewDidAppear:animated];
// Logging
NSLog(@"%@", NSStringFromClass([self class]));
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
15. iOS 中内省的几个方法有哪些?内部实现原理是什么
反射机制
16. 怎么防止UI控件短时间多次激活事件?
需求
当前项目写好的按钮,还没有全局地控制他们短时间内不可连续点击(也许有过零星地在某些网络请求接口之前做过一些控制)。现在来了新需求:本APP所有的按钮1秒内不可连续点击。你怎么做?一个个改?这种低效率低维护度肯定是不妥的。
方案
给按钮添加分类
,并添加一个点击事件间隔
的属性,执行点击事件的时候判断一下是否时间到了,如果时间不到,那么拦截
点击事件。
怎么拦截点击事件呢?其实点击事件在runtime里面是发送消息,我们可以把要发送的消息的SEL
和自己写的SEL
交换一下,然后在自己写的SEL里面判断是否执行点击事件。
实践
UIButton是UIControl的子类,因而根据UIControl新建一个分类即可
- UIControl+Limit.m
#import "UIControl+Limit.h"
#import <objc/runtime.h>
static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";
@implementation UIControl (Limit)
#pragma mark - acceptEventInterval
- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
{
objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)acceptEventInterval {
return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
}
#pragma mark - ignoreEvent
-(void)setIgnoreEvent:(BOOL)ignoreEvent{
objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}
-(BOOL)ignoreEvent{
return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
}
#pragma mark - Swizzling
+(void)load {
Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));
method_exchangeImplementations(a, b);//交换方法
}
- (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
{
if(self.ignoreEvent){
NSLog(@"btnAction is intercepted");
return;
}
if(self.acceptEventInterval>0){
self.ignoreEvent=YES;
//延迟执行
[self performSelector:@selector(setIgnoreEventWithNo) withObject:nil afterDelay:self.acceptEventInterval];
}
//调用原方法实现
[self swizzled_sendAction:action to:target forEvent:event];
}
-(void)setIgnoreEventWithNo{
self.ignoreEvent=NO;
}
@end
- ViewController.m
-(void)setupSubViews{
UIButton *btn = [UIButton new];
btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
[btn setTitle:@"btnTest"forState:UIControlStateNormal];
[btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
btn.acceptEventInterval = 3;
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction{
NSLog(@"btnAction is executed");
}
//日志打印
2020-04-06 21:07:44.244251+0800 Runtime[5230:326947] btnAction is executed
2020-04-06 21:07:45.077352+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:45.871633+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:46.844650+0800 Runtime[5230:326947] btnAction is intercepted
2020-04-06 21:07:47.719857+0800 Runtime[5230:326947] btnAction is executed