大家好,今天小编来为大家解答以下的问题,关于深入剖析iOS RunTime:第四篇学习心得分享,这个很多人还不知道,现在让我们一起来看看吧!
struct objc_method {
SEL 方法名称OBJC2_UNAVAILABLE; //方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; //方法实现
}可以看出主要由三部分组成:SEL、IMP、char *method_types。我们可以分别来看:
2. SEL
SEL,也称为选择器,是一个代表方法选择器的指针。其定义如下:
typedef struct objc_selector *SEL;方法选择器
用于表示运行时方法的名称。 Objective-C编译时会根据每个方法的名称和参数序列生成一个唯一的整数标识符(Int)。
地址类型),该标识符为SEL。如下代码所示:
SEL sel1=@selector(方法1);
NSLog(@"sel : %p", sel1);上面的输出是:
2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel :0x100002d72 两个类之间,无论是父类和子类还是没有这种关系,只要方法名相同,方法的SEL就是相同的。每个方法对应一个SEL。因此,在同一个Objective-C 类(以及类继承系统)中,不能存在两个同名的方法,即使参数类型不同。同一方法只能对应一个SEL。这也导致Objective-C处理方法名相同、参数数量相同但类型不同的方法的能力较差。
当然,不同的类可以有相同的选择器,这没有问题。不同类的实例对象执行同一个选择器时,会根据各自的方法列表中的选择器找到对应的IMP。
项目中的所有SEL 形成一个集合。 Set的特性是唯一的,因此SEL也是唯一的。因此,如果我们想在这个方法集合中找到某个方法,只需要找到这个方法对应的SEL即可。 SEL其实就是根据方法名进行哈希处理的字符串,而字符串的比较只需要比较它们的地址就可以了,可以说速度是无与伦比的!但是有一个问题,就是数量增加会增加哈希冲突,导致性能下降(或者不会有冲突,因为也可能使用完美哈希)。但无论用什么方法加速,如果总量能够减少(多种方法可能对应同一个SEL),那就是最犀利的方法。那么,我们就不难理解为什么SEL只是一个函数名了。
其实说了这么多我总结的就一个SEL就是根据方法名称得到字符串,其已经忽略了所带的参数是什么,就是在OC中代表某个方法,它最后要和IMP(具体执行的C函数,标准的C调用)进行映射绑定,下面我们就来介绍IMP
3. IMP
实际上是一个函数指针,指向方法实现的首地址。其定义如下:
id (*IMP)(id, SEL,) 该函数使用当前CPU 架构实现的标准C 调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(选择器)。接下来是该方法的实际参数列表。
前面介绍的SEL就是找到方法IMP的最终实现。由于每个方法对应一个唯一的SEL,我们可以通过SEL
方便快速准确的获取其对应的IMP。下面将讨论搜索过程。获得IMP后,我们就获得了执行该方法代码的入口点。这时候我们就可以像调用普通的C语言函数一样使用这个函数指针了。
综上所述,IMP实际上是一个C函数指针。该函数必须传入两个参数。第一个是执行函数的对象,第二个是关联的SEL。其余参数是方法的参数。您要关注多少人取决于您!
二,方法的调用流程
1,基本调用
在Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[接收者消息]转换为消息函数调用,即objc_msgSend。该函数以消息接收者和方法名作为其基本参数,如下所示
objc_msgSend(receiver,selector) 如果有参数:
objc_msgSend(receiver, Selector, arg1, arg2,)1,首先找到选择器对应的方法实现。因为同一个方法在不同的类中可能有不同的实现,所以我们需要依赖接收者的类来找到准确的实现。
2.它调用方法实现并向其传递接收者对象和方法的所有参数。
3、最后将返回的值实现为自己的返回值。
通俗地说,我们是使用实例或类来调用方法。该实例或类是接收者。方法的字符串就是对应的选择器。该方法对应的参数按照agr1,arg2.的顺序排列。
2, 消息转发(message forwarding)
这是真正的核心。其实上面提到的基本调用都是正常的转发,但是如果一个对象无法接收到指定的消息会发生什么情况呢?默认情况下,如果一个方法被调用为[object message],那么如果该对象无法响应message消息,编译器就会报错。但如果以perform.的形式调用,则需要等到运行时才能判断该对象是否可以接收到消息message。如果不能,程序就会崩溃。通常,当我们不确定一个对象是否可以接收消息时,我们会首先调用respondsToSelector:
我们来判断一下。当一个对象无法接收到某个消息时,所谓的“消息转发(message forwarding)”机制就会被激活。通过这种机制,我们可以告诉对象如何处理未知消息。默认情况下,当对象接收到未知消息时,会导致程序崩溃,如下代码所示:
无法识别的选择器发送到实例0x100111940
*** 由于未捕获的异常“NSInvalidArgumentException”而终止应用程序,reason:“-[SUTRuntimeMethod 方法]: 无法识别的选择器发送到实例0x100111940”此异常消息实际上是由NSObject 的“doesNotRecognizeSelector”方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,避免程序崩溃。
消息转发机制基本上分为三个步骤:
1.动态方法分析-------- 2.替代接收者-------- 3.完成消息转发
1动态方法解析
当对象收到未知消息时,会首先调用所属类的类方法+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法)。在这个方法中,我们有机会为未知消息添加一个“处理方法”。不过使用该方法的前提是我们已经实现了“处理方法”,只需要在运行时通过class_addMethod函数动态添加到类中即可。进去吧。
例如,我有另一个Person 类。我在外面通过performSelector调用了它的一个实例方法testMethod1,并传入了两个参数。
/*****方法调用************/
人*testPerson=[[人分配]init];
[testPerson PerformSelector:@selector(testMethod1) withObject:@"小HU" withObject:@"酒鬼"];我的Person 类中没有testMethod1 方法。我应该怎么办?
//C函数中的IMP实际上是标准C函数调用
void IMPTestMethod1Funcation(id self,SEL _cmd,NSString *name,NSString *nick){
NSLog(@"C 函数中实现的IMP: %@, %p", self, _cmd);
NSLog(@"我传入的参数是name:%@ nick:%@",name,nick);
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selString=NSStringFromSelector(sel);
NSLog(@":%@调用的方法是什么", selString);
if ([selString isEqualToString:@"testMethod1"]) {
class_addMethod(self.class, sel, (IMP)IMPTestMethod1Function, "@:@@");
}
返回[超级resolveInstanceMethod:sel];
首先,调用方法testMethod1在Person中查找不对应的方法时,会进入+(BOOL)resolveInstanceMethod:(SEL)sel方法。这里可以通过判断方法的名称(字符串)来判断要处理的方法。然后使用class_addMethod动态绑定一个实现方法到这个方法上。该实现方法是标准的C 调用方法,如示例: void IMPTestMethod1Funcation(id self, SEL _cmd, NSString *name, NSString *nick) 其中前两个参数是必需的:
第一个id self代表执行该方法的对象,
第二个SEL _cmd 指示该C 方法应替换该OC 方法的SEL。
下面是替换后的OC方法的根参数(我的OC方法有两个参数,所以后面跟着两个参数)
打印结果:
2016-10-31 15:30:06.205 ObRunTime[35215:6061873] :testMethod1 的方法是什么
2016-10-31 15:30:06.205 ObRunTime[35215:6061873] :_dynamicContextEvaluation:patternString: 的方法是什么
2016-10-31 15:30:06.206 ObRunTime[35215:6061873] :descriptionWithLocale: 的方法是什么
2016-10-31 15:30:06.206 ObRunTime[35215:6061873] 在IMP C 函数中实现: testMethod1
2016-10-31 15:30:06.206 ObRunTime[35215:6061873] 我传入的参数是name: 小胡nick: Drunkard About
class_addMethod(Class cls, SEL name, IMP imp, const char *types)的第四个参数*types,请解释一下,比较混乱。我将在这里探索后解释它:
我这里用的是@:@@。其实完整的写法是v@:@@,其中v@:表示返回值为void(无返回值),接下来的两个表示这个方法传入了两个参数。如果只传递一个参数,则使用v@:@即可。还有i@:表示可以有int返回值,后面的参数也一样。
当然,如果你觉得class_addMethod中添加的IMP是C函数,不太习惯,可以这样写:
class_addMethod(self.class, sel, class_getMethodImplementation(self, @selector(your oc method)), "v@:@@");//替换C函数的OC函数
-(void)test:(NSString *)agr1 agr2:(NSString *)agr2{
NSLog(@"我传入的参数是name: %@ nick:%@ ", agr1, agr2);
NSLog(@"TODO 做什么");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selString=NSStringFromSelector(sel);
NSLog(@":%@调用的方法是什么", selString);
if ([selString isEqualToString:@"testMethod1"]) {
//class_addMethod(self.class, sel, (IMP)IMPTestMethod1Function, "@:@@");
class_addMethod(self.class, sel, class_getMethodImplementation(self, @selector(test:agr2:)), "v@:@@");
}
返回[超级resolveInstanceMethod:sel];
}打印结果:
2016-10-31 16:03:46.543 ObRunTime[35350:6168967] :testMethod1 的方法是什么
2016-10-31 16:03:46.543 ObRunTime[35350:6168967] 我传入的参数是name: 小胡nick: Drunkard
2016-10-31 16:03:46.544 ObRunTime[35350:6168967] 做什么TODO
2,备用接收者
如果上一步无法处理该消息,Runtime 将继续调用以下方法:
-(id)forwardingTargetForSelector:(SEL)aSelector 如果一个对象实现了该方法并返回非零结果,则该对象将作为消息的新接收者,消息将被分发到该对象。当然,这个对象不能是self本身,否则会出现死循环。当然,如果我们没有指定对应的对象来处理aSelector,我们应该调用父类的实现来返回结果。
该方法通常在对象内部使用。可能有一系列其他对象可以处理该消息。我们可以使用这些对象来处理消息并返回它。这样,从对象的外部,对象本身处理消息。
如下我声明一个ObjectHelper类
#import "ObjectHelper.h"
@implementationObjectHelper
-(void)testMethodHelp:(NSString *)字符串{
NSLog(@"方法转移到该类来实现参数:%@", string);
}
@end 现在我们直接调用Person的这个方法,因为Person没有这个方法,我转发给ObjectHelper
人*testPerson=[[人分配]init];
[testPerson PerformSelector:@selector(testMethodHelp:) withObject:@"forward test"];/********************将消息转发给其他类处理******** *****/
//如果resolveInstanceMethod无法处理该消息,它会给你一个机会处理该方法分发的其他对象。
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSString *selString=NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"testMethodHelp:"]) {
返回[[ObjectHelper分配]init];
}
return [超级转发TargetForSelector:aSelector];
}控制台打印:
2016-10-31 17:43:20.447 ObRunTime[35692:6278785] :testMethodHelp: 的方法是什么
2016-10-31 17:43:20.447 ObRunTime[35692:6278785] 不是当前正在研究的方法
2016-10-31 17:43:20.447 ObRunTime[35692:6278785] 方法转移到该类来实现参数:转发测试
3,完整消息转发
如果上一步无法处理未知消息,唯一能做的就是启用完整的消息转发机制。此时会调用以下方法:
- (void)forwardIn Vocation:(NSInvocau *)Invoking运行时系统会在这一步给消息接收者最后一次机会将消息转发给其他对象(多个对象,可以是多个)。该对象会创建一个代表消息的NSInitation 对象,将与未处理消息相关的所有细节封装在anInvocation 中,包括选择器、目标(target)和参数。我们可以在forwardIncation方法中选择将消息转发给其他对象。
forwardInvocation:方法的实现有两个任务:定位一个可以响应封装在anInitation 中的消息的对象。该对象不需要能够处理所有未知消息。使用anIncation 作为参数向选定的对象发送消息。 anIncation将保留调用的结果,运行时系统将提取该结果并将其发送给消息的原始发送者。不过,在这个方法中我们可以实现一些更复杂的功能。我们可以修改消息的内容,比如恢复一个参数等,然后触发消息。另外,如果发现某个消息不应该由该类处理,则应该调用父类的同名方法,以便继承系统中的每个类都有机会处理这个调用请求。
还有一个很重要的问题,我们必须重写下面的方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 消息转发机制使用从此方法获得的信息来创建NSInitation 对象。因此,我们必须重写此方法以为给定选择器提供适当的方法签名。
其实签名可以理解为找到一个实现该方法的类,然后将该方法的名称、参数、返回值封装到一个包中!
这是一个例子。它仍然是我的Person 类。我在ViewController 中调用了一个方法testMethodInvork,但它没有实现。
/********消息已完全转发*************/
人*testPerson=[[人分配]init];
[testPerson PerformSelector:@selector(testMethodInvork)];我们将消息分发给ObjectHelper进行处理,ObjectHelper实现了testMethodInvork方法
-(void)testMethodInvork{
NSLog(@"天啊,消息转发了");
}亲自实施:
/************处理未知消息的步骤************/
//C函数中的IMP实际上是标准C函数调用
void IMPTestMethod1Funcation(id self,SEL _cmd,NSString *name,NSString *nick){
NSLog(@"C 函数中实现的IMP: %@, %@", self, NSStringFromSelector(_cmd));
NSLog(@"我传入的参数是name:%@ nick:%@",name,nick);
}
//替换C函数的OC函数
-(void)test:(NSString *)agr1 agr2:(NSString *)agr2{
NSLog(@"我传入的参数是name: %@ nick:%@ ", agr1, agr2);
NSLog(@"TODO 做什么");
}
/************1、动态方法分析(运行时添加实现方法)************/
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"第1步:处理未知消息");
NSString *selString=NSStringFromSelector(sel);
NSLog(@":%@调用的方法是什么", selString);
if ([selString isEqualToString:@"testMethod1"]) {
//class_addMethod(self.class, sel, (IMP)IMPTestMethod1Function, "@:@@");
class_addMethod(self.class, sel, class_getMethodImplementation(self, @selector(test:agr2:)), "v@:@@");
}别的{
NSLog(@"第一步不是目前要研究的方法");
}
返回[超级resolveInstanceMethod:sel];
}
/************2、将消息转发给其他类处理(如果1中途不处理该方法)************/
//如果resolveInstanceMethod无法处理该消息,它会给你一个机会处理该方法分发的其他对象。
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"第二步:将消息转发给其他类执行");
NSString *selString=NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"testMethodHelp:"]) {
返回[[ObjectHelper分配]init];
}别的{
NSLog(@"第二步消息未转发");
}
return [超级转发TargetForSelector:aSelector];
}
/************3、完成消息转发(1和2都不处理此消息,只是对消息进行封装即可实现完整转发)**************** ** */
-(void)forwardIn Vocation:(NSInitation *)anInitation{
NSLog(@"第三步:最终完成转发");
if([ObjectHelper 实例RespondToSelector:anInspiration.selector]){
ObjectHelper *helper=[[ObjectHelper alloc]init];
[一个调用invokeWithTarget:helper];
}
}
//对这个方法进行签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelec
tor{ NSMethodSignature *signature = [super methodSignatureForSelector:aSelector]; if (!signature) { if([ObjectHelper instancesRespondToSelector:aSelector]){ signature = [ObjectHelper instanceMethodSignatureForSelector:aSelector]; } } return signature; }运行结果: 2016-11-02 11:26:47.638 ObRunTime[41985:8436765] 第一步:处理未知消息 2016-11-02 11:26:47.638 ObRunTime[41985:8436765] 调用的方法是什么:testMethodInvork 2016-11-02 11:26:47.638 ObRunTime[41985:8436765] 第一步中不是当前要研究的方法 2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 第二步:转发消息给别的类实现 2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 第二步没有转发消息 2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 第一步:处理未知消息 2016-11-02 11:26:47.639 ObRunTime[41985:8436765] 调用的方法是什么:testMethodInvork 2016-11-02 11:26:47.640 ObRunTime[41985:8436765] 第一步中不是当前要研究的方法 2016-11-02 11:26:47.640 ObRunTime[41985:8436765] 第三步:最后的完全转发 2016-11-02 11:26:47.640 ObRunTime[41985:8436765] 天啊,消息转发了上面这个例子能很清楚的看到,消息转发的三个步骤,是不是很爽,哈哈! 下面举一个实际的开发场景,大家可以看到这篇博客:http://kittenyang.com/forwardinvocation/ 问题描述,看博客就行,简单的一句话就是,UIScorllView的委托Delegate,能不能让两个或更多的实例同时相应! 我们就可以用到消息转发机制,让多个对象同时响应Delegate ,为此我们创建一个Delegate分发类DelegateRouter.h,让它响应目标delegate,因为它没有实现delegate,所以通过消息转发 分发给目标1:ViewController和 目标2:SecondDelegate DelegateRouter.h文件如下: #import#import#import "ViewController.h" #import "SecondDelegate.h" @interface DelegateRouter : NSObject @property (weak,nonatomic) ViewController *vcDelegate; @property (strong,nonatomic) SecondDelegate *secDelegate; @endDelegateRouter.m文件如下: #import "DelegateRouter.h" @implementation DelegateRouter -(BOOL)respondsToSelector:(SEL)aSelector{ // NSString *test = NSStringFromSelector(aSelector); // NSLog(@"响应方法:%@",test); if ([self.vcDelegate respondsToSelector:aSelector] ||[self.secDelegate respondsToSelector:aSelector]) { return YES; }else{ return NO; } } //+(BOOL)resolveInstanceMethod:(SEL)sel{ // return [super resolveInstanceMethod:sel]; //} -(void)forwardInvocation:(NSInvocation *)anInvocation{ if ([self.vcDelegate respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:self.vcDelegate]; } if ([self.secDelegate respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:self.secDelegate]; } } //方法签名就是 配置一段方法字符串和参数的唯一性,不是和类进行绑定 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; if (!methodSignature) { NSMethodSignature *firstMethodSignature = [self.vcDelegate methodSignatureForSelector:aSelector]; NSMethodSignature *secondMethodSignature = [self.secDelegate methodSignatureForSelector:aSelector]; if (firstMethodSignature) { NSLog(@"注册1"); methodSignature = firstMethodSignature; }else if (secondMethodSignature){ NSLog(@"注册2"); methodSignature = secondMethodSignature; } return methodSignature; } return methodSignature; } @end这个需要强调: 我需要路由类DelegateRouter.h响应我需要委托的方法,所以加上判断-(BOOL)respondsToSelector:(SEL)aSelector,只有这个返回YES 这个对象才响应这个方法,才会调用【object sendmsg】,如果没有实现方法,才会走消息的转发流程。这里我只需要处理ViewController和SecondDelegate里的指定委托,所以我加了判断,别的我直接不让它响应,他就无法转发。-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector签名方法,不针对类,只要你这个类实现了你要执行的方法,都可以签名这个方法,这里实现加个if~else其实只会执行注册1我们的两个响应委托的地方,一个是ViewController中 #pragma mark - UIScrollViewDelegate -(void)scrollViewDidScroll:(UIScrollView *)scrollView{ NSLog(@"我在第一出响应delegate"); }一个是SecondDelegate类中 @implementation SecondDelegate -(void)scrollViewDidScroll:(UIScrollView *)scrollView{ NSLog(@"我在第二次响应Delegate"); }我们再给ScrollViewDelegate 赋值的时候就选择赋给"DelegateRouter" delegateRouter = [[DelegateRouter alloc]init]; delegateRouter.vcDelegate = self; delegateRouter.secDelegate = [[SecondDelegate alloc]init]; UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:self.view.bounds]; scrollView.delegate = delegateRouter; scrollView.backgroundColor = [UIColor orangeColor]; scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.frame), 700); [self.view addSubview:scrollView];这里需要注意,你响应的Delegate必须是各全局或者静态变量,不然无法执行Delegate。 执行控制台输出: 2016-11-02 16:08:55.445 ObRunTime[42837:8844820] 注册1 2016-11-02 16:08:55.445 ObRunTime[42837:8844820] 我在第一出响应delegate 2016-11-02 16:08:55.446 ObRunTime[42837:8844820] 我在第二次响应Delegate 2016-11-02 16:08:55.465 ObRunTime[42837:8844820] 注册1 2016-11-02 16:08:55.465 ObRunTime[42837:8844820] 我在第一出响应delegate 2016-11-02 16:08:55.465 ObRunTime[42837:8844820] 我在第二次响应Delegate 2016-11-02 16:08:55.496 ObRunTime[42837:8844820] 注册1 2016-11-02 16:08:55.496 ObRunTime[42837:8844820] 我在第一出响应delegate 2016-11-02 16:08:55.497 ObRunTime[42837:8844820] 我在第二次响应Delegate 2016-11-02 16:08:55.514 ObRunTime[42837:8844820] 注册1两个类同时响应多个,同理,你也可以弄更多的对象!好了,文章到此结束,希望可以帮助到大家。
【深入剖析iOS RunTime:第四篇学习心得分享】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
终于开始学习 iOS Runtime 了!感觉这方面的东西一直都蛮难懂的。
有15位网友表示赞同!
最近也对 iOS 内存管理越来越感兴趣,RTTI 是个好东西,可以帮我更好地理解它。
有6位网友表示赞同!
iOS 开发笔记真棒,希望你的笔记系统化一点,让我能更好地吸收知识
有9位网友表示赞同!
学习 Runtime 的路上难免会遇到瓶颈,记录下来学习过程真的很重要!
有20位网友表示赞同!
我之前也有尝试过 Runtime ,感觉很难上手啊,看看你的学习记录能给我一些帮助!
有14位网友表示赞同!
这篇文章标题让人很期待内容,希望能够详细讲解一下 Runtime 的部分概念。
有16位网友表示赞同!
iOS 运行时机制真是一个黑盒,终于有人出来分享学习记录了!
有8位网友表示赞同!
Runtime 学习是提升开发能力的关键,谢谢你愿意和我们分享你的宝贵经验!
有19位网友表示赞同!
期待能够看到你的 Runtime 学习总结,希望能更加深入地理解这个领域。
有18位网友表示赞同!
iOS 开发确实有很多需要注意的地方,Runtime 也是其中之一,谢谢你的详细记录。
有6位网友表示赞同!
我还没接触过 Runtime,这篇学习记录或许能引领我入门?
有8位网友表示赞同!
Runtime 的应用场景这么多,真是太强大!期待看看你的笔记里有哪些实际案例。
有6位网友表示赞同!
iOS 开发是一个不断挑战的过程,Runtime 也是其中一个难啃的骨头!
有17位网友表示赞同!
学习 Runtime 需要耐心和毅力,相信你的学习记录能够带给我们很多启发!
有7位网友表示赞同!
代码的精炼程度和效率很大程度上取决于对 Runtime 的掌握,感谢你的分享!
有11位网友表示赞同!
Runtime 的概念确实抽象,希望你的笔记能用通俗易懂的方式来讲解。
有6位网友表示赞同!
学习 iOS 必须了解 Runtime,期待你的总结能够帮助我填补知识的空白!
有11位网友表示赞同!
苹果产品的底层原理确实很吸引人,希望能通过你的 学习记录进一步探索Runtime。
有18位网友表示赞同!
iOS 开发者的学习之旅真的很有趣!Runtime 是其中的必修课,感谢你的分享!
有20位网友表示赞同!