目录
动态性
Objective-C
是一门动态性比较强的编程语言,跟C
、C++
等语言有着很大的不同,其动态性是由Runtime API
来支撑的Runtime API
提供的接口基本都是C语言
的,源码由CC++汇编语言
编写
什么是Runtime?
OC是一门动态性很强的编程语言,不像C/C++等编译型语言,程序运行结果就是编译后的结果,OC允许程序在运行时动态地去修改一些东西,比如动态添加方法、动态替换方法的实现、动态地去修改实例对象的类型,也就是允许这些操作推迟到运行时,这种动态性就是由Runtime来支撑和提供的
Runtime就是一套C语言API,封装了很多动态性相关的函数,平时编写的OC中动态相关的代码底层都会转换成Runtime API进行调用
底层相关数据结构
isa结构
在ARM64
架构之前,isa
只是一个普通的指针,存储着class
、meta-class
对象的地址;之后,对isa
进行了优化,其结构采用了union
联合体,将一个64位
的内存数据bits
分开存储了许多数据,其中的33位
才是存储class或meta-class的地址值
优化过的isa
其实就是位域,进一步了解isa
结构见这篇文章:【iOS】OC类与对象的本质分析
Class结构
class_rw_t
在Class
对象的底层结构objc_class
中,我们知道通过bits & FAST_DATA_MASK
就可以得到class_rw_t
类型的表结构
class_rw_t
里面的methods
、properties
、protocols
都是二维数组,是可读可写
的,包含了类的初始内容、分类的内容
以method_array_t
举例,里面的元素都是method_list_t
类型的二维数组,每一个二维数组又是method_t
类型的元素,表示每一个方法类型
class_ro_t
class_ro_t
里面的baseMethodList、baseProtocols、ivars、baseProperties
是一维数组,是只读的,包含了类的初始内容
在objc
源码里的头文件objc-runtime-new.mm
,通过查看函数realizeClassWithoutSwift
的实现,程序运行时会将class_ro_t
里面的数据和分类里面的数据信息全部合并到一起放到class_rw_t
里面来
method_t
method_t
是对函数的封装,里面包含了函数名
、编码信息
以及函数地址
IMP
代表函数的具体实现,指向着该函数的地址
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
SEL
代表函数名,一般叫做选择器,底层结构跟char *
类似,可以通过@selector()
和sel_registerName()
获得
typedef struct objc_selector *SEL;
// 不同类中相同名字的方法,所对应的方法选择器是相同的,可以通过sel_getName()和NSStringFromSelector()转成字符串
types
包含了函数返回值、参数编码的字符串,排列顺序如下
iOS中提供了一个叫做@encode
的指令,可以将具体的类型表示成字符串编码
一个函数默认会带有两个参数id _Nonnull
和SEL _Nonnull
,之后才是写入的参数
下面举例说明,函数的types是多少
- (int)test:(int)age height:(float)height
{
NSLog(@"%s", __func__);
return 0;
}
// 该函数types为i24@0:8i16f20
// i 返回值int类型
// 24 几个返回值类型占据的大小总和(8 + 8 + 4 + 4)
// @ id类型
// 0 表示从第0位开始
// : SEL类型
// 8 从第8位开始
// i 参数int类型
// 16 从第16位开始
// f 参数float类型
// 20 从第20位开始
// 可以通过输出日志验证
NSLog(@"%s %s %s", @encode(unsigned short), @encode(Class), @encode(SEL));
方法缓存cache_t
Class
内部结构中有个方法缓存cache_t
,用散列表(哈希表) 来缓存曾经调用过的方法,可以提高方法的查找速度
在objc-cache.mm
文件里可以查看cache_t::insert
函数,是通过一套哈希算法计算出索引,然后根据索引在散列表数组里直接插入数据进行缓存
void cache_t::insert(SEL sel, IMP imp, id receiver) {
....
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// ....
else {
// 如果空间已满,那么就进行扩容,乘以2倍
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 将旧的缓存释放,清空缓存,然后设置最新的mask值
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
// 通过 sel&mask 计算出索引(哈希算法)
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
// 通过索引找到的该SEL为空,那么就插入bucket_t
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].setAtomic, Encoded>(b, sel, imp, cls());
return;
}
// 用索引从bucket里面取sel和传进来的sel做比较,如果一样证明已经存有,直接返回
if (b[i].sel() == sel) {
return;
}
// 从散列表里查找,如果上述条件不成立(索引冲突),那么通过cache_next计算出新的索引再查找插入
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
清空缓存cache_t::reallocate
当存储空间已满时,会进行扩容,并且将旧的缓存全部释放清空,然后设置最新的mask
值,mask
值是散列表的存储容量-1
,也正好对应散列表的索引值
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) {
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
// 将旧的缓存和mask释放
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
哈希算法cache_hash
static inline mask_t cache_hash(SEL sel, mask_t mask) {
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
// 与mask按位与,有时也可对mask取余%
}
如果计算出的索引在散列表中已经有了缓存数据,那么就通过cache_next
更新下索引值,再去对应的位置插入缓存数据
更新索引值cache_next
通过源码可以看到计算方式如下:
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__
// arm64架构下如果索引非0,就是i-1,索引为0返回mask
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
有冲突的索引如果不为0就直接索引值减1
,然后再根据新的索引值去散列表中对应插入
如果冲突的索引为0,那么直接就将mask赋值给新的索引值
,再去对应查找插入
方法调用底层结构
比如一个实例或类去调用方法
Person* person = [[Person alloc] init];
[person test];
[Person test];
// 将他们转换成Cpp代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("test"));
发现函数调用最后都是转化为objc_msgSend
,尤其我们可以推断出方法的调用本质就是objc_msgSend消息发送
简化后得到以下方法
//由于sel_registerName和@selector返回值都是SEL,我们通过打印两个方法的地址是一样的,可以确定两个方法是可以等同的
NSLog(@"%p, %p", @selector(test), sel_registerName("test")); // 打印结果是一样的
objc_msgSend(person, @selector(test));
// 消息接收者(receiver):person
// 消息名称:test
objc_msgSend([Person class], @selector(test));
// 消息接收者(receiver):[Person class]
// 消息名称:test
方法调用执行流程
objc_msgSend
的执行流程可以分为3大阶段:消息发送、动态方法解析、消息转发
消息发送
我们在objc源码里全局搜索关键字objc_msgSend
可以发现,消息发送的入口一开始是在objc-msg-arm64.s
中通过汇编来实现第一步的
【第一步】 这里主要就是判断receiver
是否有值,然后对应的跳转,isa指针
和ISA_MASK
进行位运算获取到Class数据
(GetClassFromIsa_p16),并且要先去查找是否有缓存方法,在CacheLookup
里进行缓存查找的过程,主要就是SEL & MASK
得出一个索引,根据索引去buckets散列表
中取对应的方法数据,如果没有则要去更深的Class数据
中查找了
【第二步】 上述一系列操作如果没有取到方法缓存,那么就会进到__objc_msgSend_uncached
中,再进一步跳转到MethodTableLookup
,发现最终会调用到C语言函数lookUpImpOrForward
中
知识点: C的函数名称对应到汇编中都会在其函数名之前再加上一个_
,作为函数名称
【第三步】 跳转到objc-rumtime-new.mm
的lookUpImpOrForward
函数来看,会到当前类的方法列表
里查找,如果没有再去父类的方法缓存以及方法列表
中查找,直到找到调用为止;如果都没有找到,那么就会进入到动态方法解析的阶段
第三步的方法查找详细步骤:
(1)根据传进来的类遍历查找,而且每次都要先去_cache_getImp
中查找是否有方法缓存,进而调用回CacheLookup
进一步查找
(2)在getMethodNoSuper_nolock
里会找到class_rw_t
的methods
方法列表里进行遍历查找,在search_method_list_inline
里会根据是否排序来选择是采用二分查找还是线性查找
(3)在log_and_fill_cache
里将查找到的方法插入到缓存中,调用到了objc-cache.mm
中的cache_t::insert
函数
动态方法解析
如果消息发送阶段没有找到方法实现,那么就会进入到动态方法解析阶段
【第一步】 从objc源码中找到函数resolveMethod_locked
,分别对类对象和元类对象做了不同的调用处理
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 不是元类对象
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else { // 是元类对象
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
// 这句印证了在元类对象里找类方法找不到,会去类对象里找同名的对象方法
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
注意: 能调用到这里,说明已经找到了基类的元类对象,如果还是没有,那么就会去基类的类对象里找同名的对象方法,正好印证了之前分析的元类对象的superclass指针
指向类对象的原理
【第二步】 如果是类对象则会进入resolveInstanceMethod
函数中,会走消息发送的流程去查找是否有实现resolveInstanceMethod
方法,如果没有实现则返回;如果有实现就发送消息调用resolveInstanceMethod
方法,并且再次走消息发送流程查找是否有实现对应的实例方法
如果是元类对象则会进入resolveClassMethod
函数中,同类对象的动态方法解析大体相似;先会走消息发送的流程去查找是否有实现resolveClassMethod
方法,如果没有实现则返回;如果有实现就发送消息调用
知识点:Method
的底层实现struct method_t
类型,所以可以理解为等价于struct method_t *
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
// 获取其他方法
Method method = class_getInstanceMethod(self, @selector(other));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod: sel];
}
消息转发
如果没有进行动态分析的代码实现,就会进入到消息转发阶段
消息转发的源码是不开源的,所以我们只能通过一些其他的方法来分析其内部的实现
【第一步】 首先会查看是否实现forwardingTargetForSelector
方法,如果实现了该方法并且返回值不为nil
,那么会调用objc_msgSend
进行消息发送流程;如果该方法未实现或者返回值为nil
,就会调用方法签名methodSignatureForSelector
【第二步】 如果methodSignatureForSelector
的返回值不为nil
,那么就会调用forwardInvocation
,如果该方法未实现或者返回值为nil
,那么就会调用doesNotRecognizeSelector
进行崩溃报错
【第三步】 如果forwardInvocation
未实现,也会进行崩溃报错
#pragma mark 消息转发
// 当前类无法处理这个消息,就可以讲这个消息转发给别的类,进行消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
// aSelector == @selector(test)
// 针对test进行验证
// objc_msgSend([[Cat alloc] init], aSelector)
if (aSelector == @selector(test)) {
// return [[NSObject alloc] init];
return nil;
// 返回nil,就不会转发给备援接收者,就调用以下方法
}
return [super forwardingTargetForSelector: aSelector];
}
// 方法签名:包括返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
// 返回nil,就报错识别不出该方法
}
return [super methodSignatureForSelector: aSelector];
}
// NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
// anInvocation.target 方法调用者
// anInvocation.selector 方法名
// [anInvocation getArgument: NULL atIndex: 0];
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s", __func__);
// anInvocation.target = [[Cat alloc] init];
// anInvocation.selector = @selector(other);
// [anInvocation invoke];
// 开发者可以在此方法中自定义任何逻辑
// 随便打印个日志
NSLog(@"La la land....Do whatever I want....");
// 更改方法调用者
[anInvocation invokeWithTarget: [[Cat alloc] init]];
// 更改方法调用者forwardingTargetForSelector方法就可以解决
// 所以该方法里还可以有以下操作:
// 拿到参数信息,传递的是地址值
int age; // 3个参数:reciever、SEl、age,加了个age参数,字符串编码也要改变
[anInvocation getArgument: &age atIndex: 2]; // 返回值为空,通过传递地址获取值
// 拿到返回值信息
int returnVal;
[anInvocation getReturnValue: &returnVal];
}
super关键字
super本质
运行以下代码:
NSLog(@"[self class] = %@", [self class]); // Student
NSLog(@"[self superclass] = %@", [self superclass]); // Person
NSLog(@"[super class] = %@", [super class]); // Student
NSLog(@"[super superclass] = %@", [super superclass]); // Person
super
调用class
方法也会返回当前类,这是为什么呢?
通过clang
命令将OC代码[super run];
转换成C++代码,会发现super
调用方法时底层调用的是objc_msgSendSuper函数
:
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};
// struct objc_super arg = {self, [Person class]};
// objc_msgSendSuper(arg, @selector(run));
struct objc_super arg = {self, class_getSuperclass(objc_getClass("Student")};
objc_msgSendSuper(arg, sel_registerName("run"));
平时生成的C++代码仅为一个参考,实际实现还是得窥探中间代码的实现
查objc_msgSendSuper函数
的实现,发现最终会调用到_objc_msgSendSuper2函数
,传进来的第一个结构体变量的真实类型是objc_super2
,虽然第二个参数变成了当前类,但函数实现逻辑也有变化,其实现原理会根据当前类型的superclass指针
去查找父类方法来调用
所以效果跟objc_superclass函数
是一样的
struct objc_super2 {
id receiver; // 消息接收者
Class current_class; // 当前类
};
也就是说,self
调用方法会从当前类开始向上查找方法实现,super
调用方法会从父类开始向上查找方法实现
class
和superclass
方法都声明在NSObject
类,所以不论是self
还是super
调用都会找到NSObject类,而这两个方法的返回类型取决于消息接收者
class
中传进去的self
就是当前调用者,所以不论是[self class]
还是[super class]
得到的都是当前调用者的类型,也就是Student
类型superclass
中获取的是当前调用者的父类,所以不论是[self superclass]
还是[super superclass]
得到的都是当前调用者的父类类型,也就是Person
类型
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
+ (Class)superclass {
return self->getSuperclass();
}
- (Class)superclass {
return [self class]->getSuperclass();
}
而superclass
方法的实现就是获取当前调用者类型的父类类型,所以[self superclass]
还是[super superclass]
得到的都是Person
类型
经典问题
@interface Person : NSObject
@property (nonatomic, copy)NSString *name;
@property (nonatomic, strong)NSNumber* age;
- (void)print;
@end
@implementation Person
- (void)print {
NSLog(@"my name is %@", self->_name, self->_age);
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 栈区的最高地址的局部变量其实是[UIViewControlle class]的地址
// 下一个是self
// struct objc_super arg = {self, [UIViewController class]};
// objc_msgSendSuper(arg, sel_registerName("viewDidLoad"));
__unused NSString* test = @"21yr";
// __unused NSString* test2 = @"19yr";
// __unused NSObject* obj2 = [[NSObject alloc] init];
// 打印结果只认比cls地址高的地址值
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
// my name is 21yr, my age is
@end
OC方法print
为什么能通过一个obj
指针调用成功?
将上述代码用下图来表示关系可以看出,创建一个Person
对象Person *person = [[Person alloc] init]
:
person
指针指向isa
指针的地址,isa
又指向[Person class]
类对象,- 等同于
obj
指向cls
指针的地址,cls
指向[Person class]
类对象,
所以obj
可以直接调用print
函数
为什么会打印test
字符串和ViewController
对象?
super
调用viewDidLoad
方法本质是调用objc_msgSendSuper2函数
struct objc_super2 arg = {self, [ViewController class]};
objc_msgSendSuper2(arg, sel_registerName("viewDidLoad"));
函数调用栈(栈区)中存储局部变量的内存地址都是从小到大排列的
类比于person
对象访问成员变量,self->_name
就是在结构体里跳过前8个字节isa指针
找到_name成员变量
,等同于obj
跳过cls
找到test
字符串,再跳就是结构体变量里的self
,那么取得值就是ViewController
的内存地址
struct Person_IMPL {
Class isa;
NSString *_name;
};
以后解决什么问题的话,都是跟我们的知识储备有关的,如果知识储备不够,你会发现很多问题是解决不了的,而且虽然我们现在学的是iOS的东西,但是底层学好了,你会发现许多东西是相通的,比如不管是C、C++还是OC或是其他语言,栈空间的内存分配都是从高地址向低地址分配的,像这种底层内存分配相关许多都是相通的,所以学好底层我觉得对我整个技术生涯是很有帮助的
LVVM的中间代码
LVVM是iOS编译器,OC -> 中间代码(.ll) -> 汇编、机器代码
中间代码有着LLVM特有的语法和跨平台的特性
Runtime常用API
类
创建一个Person类
@interface Person : NSObject {
int _age;
int _name;
}
- (void)run;
@end
@implementation Person
- (void)run {
NSLog(@"%s", __func__);
}
@end
Person* person = [[Person alloc] init];
[person run]; // -[Person run]
获取类对象(isa所指)
NSLog(@"类对象:%p %p 元类对象:%p", [Person class], object_getClass(person), object_getClass([Person class]));
设置类对象(isa所指)
object_setClass(person, [Car class]);
[person run]; // -[Car run]
运行时动态创建一个类
void run(id self, SEL _cmd) {
NSLog(@"%@ --- %@", self, NSStringFromSelector(_cmd));
}
Class dogClass = objc_allocateClassPair([NSObject class], "Dog", 0);
// 添加成员变量
class_addIvar(dogClass, "_age", 4, 1, @encode(int));
class_addIvar(dogClass, "_weight", 4, 1, @encode(int));
// 添加方法
class_addMethod(dogClass, @selector(run), (IMP)run, "v@:");
// class_addProperty(, , , );
// class_addProtocol(, );
// 注册一个类(要在类注册前添加成员变量)
objc_registerClassPair(dogClass);
id dog = [[dogClass alloc] init];
// 使用KVC访问成员变量
[dog setValue: @10 forKey: @"_age"];
[dog setValue: @20 forKey: @"_weight"];
NSLog(@"%@ %@ %@", dog, [dog valueForKey: @"_age"], [dog valueForKey: @"_weight"]); // 10 20
// 调用方法
[dog run]; // --- run
NSLog(@"%zd", class_getInstanceSize(dogClass)); // 8 + 4 + 4
// 在不需要类销毁释放
objc_disposeClassPair(dogClass);
类一旦注册完毕,类的结构就确定了,因为成员变量是只读ro
的,所以成员变量在类的结构确定后就不能添加或修改,而方法是可读写rw
的,所以方法可以在类的结构确定后动态地添加
为了规范,属性、成员变量、方法都最好在注册类之前添加好
判断是否为类对象、元类对象
// 判断对象是否为类对象
NSLog(@"%d %d %d",
object_isClass(person), // 实例对象
object_isClass([Person class]),
object_isClass(object_getClass([Person class]))
); // 0 1 1
// 判断类是否为一个元类
NSLog(@"%d %d", class_isMetaClass([Person class]), class_isMetaClass(object_getClass([Person
class]))); // 0 1
获取类的父类(superclass所指)
// 获取类的父类(superclass所指)
NSLog(@"%@", class_getSuperclass([Person class]));
// 设置类的父类(superclass所指)
// class_setSuperclass([Car class], [Person class]); // deprecated
// NSLog(@"%@", class_getSuperclass([Car class])); // Person
成员变量
获取成员变量信息
Ivar ageIvar = class_getInstanceVariable([Person class], "_age");
NSLog(@"111 %s %s", ivar_getName(ageIvar), ivar_getTypeEncoding(ageIvar));
// 111 _age i
设置和获取成员变量的值
Ivar nameIvar = class_getInstanceVariable([Person class], "_name");
Person* p = [Person new];
object_setIvar(p, nameIvar, @"21");
// 不能直接传基本数据类型,所以先转成指针类型,再变为id类型
// object_setIvar(p, nameIvar, (__bridge id)(void *)21);
id ivar = object_getIvar(p, nameIvar);
NSLog(@"%@", ivar); // 21
拷贝实例变量列表(最后要用free释放)
unsigned int count;
Ivar* ivars = class_copyIvarList([Person class], &count);
NSLog(@"%u", count); // 获取成员变量的数量 2
for (int i = 0 ; i count; ++i) {
Ivar ivar = ivars[i];
NSLog(@"222 %s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
// 222 _age i
// 222 _name i
}
free(ivars);
属性
方法
Runtime具体应用
动态地去修改内存中的一些东西(比如isa、类)
-
利用关联对象给分类添加属性
-
利用消息转发机制解决方法找不到的异常问题,进行一个兜底操作
-
动态地去创建类
// 写一个接口去创建类,@"aaa"为参数 NSString* className = [NSString stringWithFormat: @"allocClass%@", @"aaa"]; Class newClass = objc_allocateClassPair([NSObject class], className.UTF8String, 0); objc_registerClassPair(newClass); id newClassInstance = [[newClass alloc] init]; NSLog(@"%@", newClassInstance);
-
遍历类的所有成员变量(访问到私有成员变量,字典转模型、自动归档解档)
-
用于访问系统类内部的一些私有成员变量
unsigned int count; Ivar* ivars = class_copyIvarList([UITextField class], &count); for (int i = 0; i count; ++i) { // 取出i位置的成员变量 Ivar ivar = ivars[i]; NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar)); }
-
字典(JSON)转模型
// 写个分类,封装到NSObject里面去,注意命名规范 + (instancetype)xy_objcetWithJson:(NSDictionary *)json { id obj = [[self alloc] init]; unsigned int count; Ivar* ivars = class_copyIvarList(self, &count); for (int i = 0; i count; ++i) { Ivar ivar = ivars[i]; NSMutableString* name = [NSMutableString stringWithUTF8String: ivar_getName(ivar)]; // 去掉成员变量的下划线 [name deleteCharactersInRange: NSMakeRange(0, 1)]; // 设值 id value = json[name]; if ([name isEqualToString: @"ID"]) { value = json[@"id"]; } [obj setValue: value forKey: name]; } free(ivars); return obj; }
写一个完整的字典转模型SDK,需要考虑的方面很多,比如,继承体系、JSON数据与Model不一致、键值对格式问题、模型嵌套模型等等,比较著名的第三方字典转模型框架有
JSONModel
、YYModel
、MJExtension
-
-
方法交换
一般用于交换系统类内部方法的实现IMP、第三方框架中的方法,方法交换的原理就是交换类对象rw结构体
的methods
中的method_list_t
中的方法method _t结构体
中的方法实现IMP指针(使用中间变量的交换方法),还会提前清空cache_t
缓存flushCaches(nil)
-
利用方法交换进行容错,对NSArray、NSDictionary和NSString进行兜底操作,在分类中重写
load
方法(分类文件一旦参与编译,都会执行load
方法)@implementation NSMutableDictionary (InsertNilProtect) + (void)load { // 类簇:NSString、NSArray、NSDictionary,真实类型是其他类型 // 真实类型和实际调用方法可以在报错信息中查看 Class cls = NSClassFromString(@"__NSDictionaryM"); Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:)); Method method2 = class_getInstanceMethod(cls, @selector(xy_setObject:forKeyedSubscript:)); method_exchangeImplementations(method1, method2); } // NSMutableDictionary中nil对象作为键值兜底 - (void)xy_setObject:(id)obj forKeyedSubscript:(idNSCopying>)key { if (!key) { NSLog(@"可变字典传入键值为空-xy_%@ %@", obj, key); } else { NSLog(@"xy_%@ %@", obj, key); [self xy_setObject: obj forKeyedSubscript: key]; } } // NSMutableArray插入nil对象兜底 - (void)xy_insertObject:(id)anObject atIndex:(NSUInteger)index { if (!anObject) { NSLog(@"可变数组传入对象为空-xy_%@ %zd", anObject, index); } else { NSLog(@"xy_%@ %zd", anObject, index); [self xy_insertObject: anObject atIndex: index]; } } @end
-
拦截UIButton点击事件
给UIButton调用addTarget
方法后,点击button响应方法实际是UIControl中的sendAction:to:forEvent:
,这个方法会调用给button添加的事件函数,通过交换此方法可以拦截所有button的点击事件@implementation UIControl (HookUIButtonClick) + (void)load { Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method method2 = class_getInstanceMethod(self, @selector(xy_sendAction:to:forEvent:)); method_exchangeImplementations(method1, method2); } // hook函数 - (void)xy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { // NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action)); // 回到默认实现 // [target performSelector: action]; if ([self isKindOfClass: [UIButton class]]) { // 拦截所有按钮的事件 NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action)); } else { // 系统方法原来的实现 [self xy_sendAction: action to: target forEvent: event]; } } @end
-
评论(0)