一、isKindOfClass和isMemberOfClass
题目:
有一道经典面试题关于isKindOfClass和isMemberOfClass
代码:
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; // 1
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; // 0
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; // 0
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; // 0
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; // 1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; // 1
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; // 1
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]; // 1
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
输出结果:
2019-12-31 14:49:22.734091+0800 LGTest[35237:2807868]
re1 :1
re2 :0
re3 :0
re4 :0
2019-12-31 14:49:22.735580+0800 LGTest[35237:2807868]
re5 :1
re6 :1
re7 :1
re8 :1
为什么结果是这样呢?
答案:
我们先放一个isa的指针图:
再打开一份objc的源码,来看一下对应方法里面的实现
- 1、我们先看一下类的class方法
+ (Class)class {
return self;
}
- 2、看一下类的isKindOfClass的实现
+ (BOOL)isKindOfClass:(Class)cls {
//
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
再看一下object_getClass的源码
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
我们发现isKindOfClass是循环不断获取self的isa指针以及父类的isa指针指向和cls做对比,通过上面isa的指向图,我们对上面判断一一解释下:
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; // 1
因为NSObject的isa指向NSObject的元类,先拿到NSObject的元类跟NSObject比,不通过,而NSObject元类的isa指向的是NSObject,然后跟NSObject对比,所以结果是YES
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; // 0
而LGPerson的isa指向依次是LGPerson的元类 ---> NSObject的元类 ---> NSObject --- > nil,然后和LGPerson进行对比,没有匹配的,所以结果是NO
- 3、再看一下类的isMemberOfClass的实现
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
我们发现isMemberOfClass仅仅是拿到当前self的isa指针指向和cls对比,然后我们分析测试代码逻辑:
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; // 0
NSObject的元类和NSObject不匹配,所以不成立
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; // 0
LGPerson的元类和LGPerson不匹配,所以不成立
- 4、再看一下实例方法的isKindOfClass方法和isMemberOfClass的实现
- (BOOL)isKindOfClass:(Class)cls {
// 类 - NSObject 类 vs 父类 nil
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
- (Class)class {
return object_getClass(self);
}
这里有一个iOS交流圈:891 488 181 分享BAT,阿里面试题、面试经验,讨论技术,有兴趣的可以进来了解。
我们可以发现对于对象方法,只是拿到对象的isa指向和相应的类对比,而对象的isa指向的都是相应的类,所以下面四个输出结果都是YES。
二、[super class]和[self class]
题目:
我们创建一个集成LGPerson的类LGStudent的类,然后在LGStudent的实例方法里面写下面代码,然后调用该对象方法:
-(void)testSuperClass{
NSLog(@"%@",NSStringFromClass([self class]));
NSLog(@"%@",NSStringFromClass([super class]));
}
输出:
2020-01-16 10:36:23.651909+0800 LGTest[18422:366866] LGStudent
2020-01-16 10:36:23.652760+0800 LGTest[18422:366866] LGStudent
这是为什么呢,[self class]我们都能理解是LGStudent,但是[super class]为什么也是LGStudent呢,不应该是LGPerson吗,下面我们来探索下:
答案:
1、汇编分析法
我们发现[super class]是通过objc_msgSendSuper2进行发送消息的,而不是通过objc_msgSend发送消息的,我们再到objc源码中去找一下objc_msgSendSuper2的实现
/********************************************************************
* id objc_msgSendSuper2(struct objc_super *super, SEL op, ...)
*
* struct objc_super {
* id receiver;
* Class cls; // SUBCLASS of the class to search
* }
********************************************************************/
ENTRY _objc_msgSendSuper2
ldr r9, [r0, #CLASS] // class = struct super->class
ldr r9, [r9, #SUPERCLASS] // class = class->superclass
CacheLookup NORMAL
// cache hit, IMP in r12, eq already set for nonstret forwarding
ldr r0, [r0, #RECEIVER] // load real receiver
bx r12 // call imp
CacheLookup2 NORMAL
// cache miss
ldr r9, [r0, #CLASS] // class = struct super->class
ldr r9, [r9, #SUPERCLASS] // class = class->superclass
ldr r0, [r0, #RECEIVER] // load real receiver
b __objc_msgSend_uncached
END_ENTRY _objc_msgSendSuper2
我们最终在汇编地方找到了实现,并且发现_objc_msgSendSuper2的参数分别为objc_super、SEL等等,其中objc_super是消息接受者,并且它是一个结构体:
* struct objc_super {
* id receiver;
* Class cls; // SUBCLASS of the class to search
* }
我们知道receiver是self,cls是self的父类,_objc_msgSendSuper2其实就从self的父类开始查找方法,但是消息接受者还是self本身,也就类似是让self去调父类的class方法,所以返回的都是LGStudent
2、hook分析法:
我们创建一个NSObject的分类,然后在里面hook一下class方法
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(class) swizzledSEL:@selector(lg_class)];
});
}
- (Class)lg_class{
NSLog(@"来了,老弟");
return [self lg_class]; // sel -> imp(class)
}
我们在lg_class里面打一个断点,通过lldb来看一下调进来的self是什么: 打印结果:
2020-01-16 11:04:30.876482+0800 LGTest[19658:395083] 来了,老弟
(lldb) p self
(LGStudent *) $0 = 0x00000001022327c0
2020-01-16 11:04:54.903791+0800 LGTest[19658:395083] 来了,老弟
(lldb) p self
(NSTaggedPointerString *) $1 = 0xd76f961d90151cc3 @"LGStudent"
2020-01-16 11:05:07.057101+0800 LGTest[19658:395083] LGStudent
我们发现调进来的self都是LGStudent,所以也验证了[super class]的调用者还是self本身
三、weak和strong底层原理
问题:
__weak我们在项目中经常用于打破循环引用,但为什么weak可以打破循环引用呢?strong又是怎么回事呢?
答案:
weak
我们在objc源码中的main方法中写上下面这句代码,打上断点并打开汇编调试:
LGPerson __weak *objc = object;
然后我们发现在此处调用了objc_initWeak方法,我们再点击进去:
/**
* Initialize a fresh weak pointer to some object location.
* It would be used for code like:
*
* (The nil case)
* __weak id weakPtr;
* (The non-nil case)
* NSObject *o = ...;
* __weak id weakPtr = o;
*
* This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
*
* @param location Address of __weak ptr.
* @param newObj Object ptr.
*/
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak
(location, (objc_object*)newObj);
}
根据注释我们知道weak的使用方法,并且介绍了该方法是用来初始化对象弱指针的,并且是线程不安全的,根据代码进入到了storeWeak函数,我们再进入到storeWeak里面看看。
源码太长就先不放了,根据源码分析,前部分都是对表进行判断的,并且我们知道弱引用指针是存在一个叫SideTable的表中,再往下我们发现如果没表就走weak_register_no_lock函数,看名字知道应该是注册弱引用指针的方法,如果有就走weak_unregister_no_lock方法
我们再进入到weak_register_no_lock方法里:
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
if (!referent || referent->isTaggedPointer()) return referent_id;
// ensure that the referenced object is viable
bool deallocating;//判断该对象是否在dealloc
if (!referent->ISA()->hasCustomRR()) {
deallocating = referent->rootIsDeallocating();
}else {
BOOL (*allowsWeakReference)(objc_object *, SEL) =
(BOOL(*)(objc_object *, SEL))
object_getMethodImplementation((id)referent,
SEL_allowsWeakReference);
if ((IMP)allowsWeakReference == _objc_msgForward) {
return nil;
}
deallocating =
! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
}
if (deallocating) {
if (crashIfDeallocating) {
_objc_fatal("Cannot form weak reference to instance (%p) of "
"class %s. It is possible that this object was "
"over-released, or is in the process of deallocation.",
(void*)referent, object_getClassName((id)referent));
} else {
return nil;
}
}
// now remember it and where it is being stored
weak_entry_t *entry;//判断表里有没有这个对象的子表,如果有就从weak_table中取出weak_entry_t然后将弱指针插入到weak_entry_t中
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else { //如果没有就创建一个weak_entry_t,再将这个weak_entry_t插入到weak_table中去
// 创建了这个weak_entry_t 再插入到weak_table
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
// Do not set *referrer. objc_storeWeak() requires that the
// value not change.
return referent_id;
}
从上我们会发现weak指针在创建的时候并没有调用retain操作,并且会将weak指针存储在SideTable的weak_table中,然后每个对象在weak_table里面都有一个对应的weak_entry_t,每个weak_entry_t里面可以放多个弱指针
strong
有了weak我们再看看strong是什么情况呢? 我们依然打开汇编调试,然后将__weak改成__strong然后运行
LGPerson __strong *objc = object;
我们发现此处调用的是objc_retain,command+点击,进不去,我们就在OBJC源码里面搜,也搜不到,怎么办呢,考虑到汇编一般会在函数前面添加,我们去掉_再次搜索,然后我们找到了objc_retain函数
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
然后进入到retaun函数
inline id
objc_object::retain()
{
assert(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
发现是通过objc_msgSend发送了SEL_retain消息,然后让引用计数器+1
四、runtime的应用以及注意点
问题:
我们经常在项目中用的runtime,并且用的最多的是交换方法,在交换方法中有哪些注意事项呢:
答案:
1、NSArray 类簇,
类簇实际上是Foundation framework框架下的一种设计模式,它管理了一组隐藏在公共接口下的私有类,
所以涉及到类簇的类,NSDictionary、NSArray、,本身类并不是NSArray等,这个需要去确定该类是否是类簇,然后在确定真正的类是什么,然后对真正的类进行交换才行
2、交换的方法是父类的方法
如果交换的方法是父类的方法,就会导致当父类调用该方法时候报错,因为父类没有子类的方法。
解决方法就是:先尝试给交换的类添加要交换的方法,如果添加成功,说明自己没有这个方法,那么就对该类做替换操作,如果添加失败说明自己有这个方法,那么就直接做交换操作。 代码:
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
//方式一:
//给cls添加oriSEL方法,确保cls有oriSEL方法
class_addMethod(cls, oriSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
//取到cls的oriSEL的Method,因为上面oriMethod可能是cls父类的,交换的话可能会导致父类找不到swizzledSEL方法
Method oriMethod1 = class_getInstanceMethod(cls, oriSEL);
//交换方法
method_exchangeImplementations(oriMethod1, swiMethod);
/**************************************************************/
//方式二:
//尝试添加,如果添加成功,说明自己没有这个方法,那么就对该类做替换操作,因为此处给oriSEL方法添加的方法指针是swiMethod的方法指针,那么swizzledSEL的指针就要改成oriSEL的指针
//如果添加失败说明自己有这个方法,那么就直接做交换操作
// BOOL isSuccess = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
//
// if (isSuccess) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
// class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
//
// }else{ // 自己有就做交换操作
// method_exchangeImplementations(oriMethod, swiMethod);
// }
}
本质就是先给cls添加oriSEL方法,确保cls有了oriSEL方法后再交换,这样就不会交换到cls父类的方法
3、交换的方法不存在
假如交换的方法不存在,就会导致交换失败,那么就要在上面代码中单独处理下单独处理下:
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
当方法不存在时候就需要单独给cls添加一个方法实现
五、内存偏移面试题
问题:
我们创建一个有saySomething实例方法的LGPerson的类,问下面代码能执行吗?
id pcls = [LGPerson class];
void *pp= &pcls;
[(__bridge id)pp saySomething];
//p -> LGPerson 实例对象
LGPerson *p = [LGPerson alloc];
[p saySomething];
答案:
答案是可以执行的。为什么呢?我们来分析下:
- 1、首先对象的本质是个结构体,并且第一个元素是isa,isa指向的是对象的类对象
- 2、我们知道指针指向的对象的地址其实就是对象的首地址,所以p指向的是LGPerson对象的首地址isa
- 3、而isa指向的是LGPerson的类对象,那么对象指针就形成了这样的指向关系:p ---> 对象 ---> LGPerson类对象
- 4、再看上面的pcls:pcls指向的是LGPerson的类对象,pp又指向的是pcls,这样也形成了一个指向关系:pp ---> pcls ---> LGPerson类对象
- 5、两者对比起来,p和pp性质就是一样的了,所以上面两个都能执行
疑问:但是对象是个结构体可以执行方法,&pcls只是个地址为啥也能执行方法
我们再扩展一下: 我们将saySomething重写一下,并且给LGPerson增加一个NSString属性name
- (void)saySomething{
NSLog(@"NB %s - %@",__func__,self.name);
}
在执行一下,看一下打印:
2020-01-16 21:07:16.278767+0800 LGTest[50715:802279] NB -[LGPerson saySomething] -
这又是为啥呢,我们猜测一下这个地方是野指针,正好ViewController对象在那一块,但是我们多次运行测试后结果却一样,我们来分析一下:
我们做一个测试,在方法中写下如下代码:
int a = 1;
int b = 2;
int c = 3;
int d = 4;
NSLog(@"a = %p\nb = %p\nc = %p\nd = %p\n",&a,&b,&c,&d);
得到打印结果
a = 0x7ffee837d19c
b = 0x7ffee837d198
c = 0x7ffee837d194
d = 0x7ffee837d190
我们发现a、b、c、d地址是连续的,且abcd都是临时变量,变量是以栈的形式存储,
- 1、我们知道OC对象本质是结构体,里面第一个元素是isa,然后下面元素依次是对象的属性
- 2、当我们只有一个属性时候,对象访问属性其实就是将对象的指针下移属性大小的位置
- 3、那么上面那只方式其实访问的就是pp下移8个字节的位置的数据
- 4、每个方法都有两个隐藏参数super和self,所以在方法中临时变量顺序是super、self、pcls、pp,pp指向的是pcls
- 5、当我们用pp获取name的时候,本质上就是得到pcls指针然后往下移name的大小的位置,然后读取值,name大小是8字节,pcls大小也是8字节,所以移完之后正好指向了self
这就是为啥打印出
我们可以再做一个实验,在代码前加上一个NSString临时变量
NSString *tem = @"KC";
id pcls = [LGPerson class];
void *pp= &pcls;
[(__bridge id)pp saySomething];
再执行,打印:
2020-01-16 21:43:05.084478+0800 LGTest[52497:844500] NB -[LGPerson saySomething] - KC
更印证了我们的结果
五、关联对象的原理
问题:
分类中如何创建属性?
答案:
在分类中创建属性,我们一般会写上一个属性,然后实现该属性的set和get方法,再关联对象,这是为什么呢,我们来一步步分析:
1、创建属性
在分类中创建了一个属性,会在rw中属性列表中有数据,然后有了set和get方法,但是该属性没有成员变量,需要重写该属性的set/get方法来保存属性值。
2、重写set/get方法,关联对象
首先我们来看下set方法
-(void)setCate_name:(NSString *)cate_name{
/**
参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
*/
objc_setAssociatedObject(self, @"name",cate_name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
然后我们再看看objc_setAssociatedObject方法:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
这里我们会发现,苹果对外接口,一般都有一个加_的对内接口与之对应,这是苹果为了解耦合,即使底层内部实现了也不会影响到对外接口,我们再看一下_object_set_associative_reference的实现:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// retain the new value (if any) outside the lock.
// 在锁之外保留新值(如果有)。
ObjcAssociation old_association(0, nil);
// acquireValue会对retain和copy进行操作,
id new_value = value ? acquireValue(value, policy) : nil;
{
// 关联对象的管理类
AssociationsManager manager;
// 获取关联的 HashMap -> 存储当前关联对象
AssociationsHashMap &associations(manager.associations());
// 对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
// 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 根据key去获取关联属性的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// 替换设置新值
j->second = ObjcAssociation(policy, new_value);
} else {
// 到最后了 - 直接设置新值
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// 如果AssociationsHashMap从没有对象的关联信息表,
// 那么就创建一个map并通过传入的key把value存进去
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
// 如果传入的value是nil,并且之前使用相同的key存储过关联对象,
// 那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
// 最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
if (old_association.hasValue()) ReleaseValue()(old_association);
}
从上面我们梳屡一下逻辑:
- 1、先对传进来的值做下retain或者copy处理得到new_value
- 2、再获取到管理所有关联对象的哈希map总表的管理者AssociationsManager,然后拿到哈希map总表AssociationsHashMap
- 3、对关联对象的地址进行取反操作得到哈希表对应的下标index(其实disguised_ptr_t就是一个long类型的)
- 4、如果得到的new_value不为空的话,就拿到总表的迭代器通过拿到的下标index进行遍历查找
- 5、如果找到管理对象的关联属性哈希map表,然后再通过key去遍历取值,
- 如果取到了,就先把新值设置到key上,再将旧值释放掉
- 如果没取到,就直接将新值设置在key上
- 6、如果没找到关联对象的关联属性哈希map表,就创建一个表,然后将新值设置在key上
- 7、如果得到的new_value为空的话,就尝试取值,取到了的话就将key对应的值置为nil,如果取不到就不做处理
我们再看一下objc_getAssociatedObject:
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 关联对象的管理类
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 生成伪装地址。处理参数 object 地址
disguised_ptr_t disguised_object = DISGUISE(object);
// 所有对象的额迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
// 内部对象的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 找到 - 把值和策略读取出来
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
发现流程跟objc_setAssociatedObject反过来而已~
作者:海浪宝宝
链接:https://juejin.cn/post/6844904049481875470