欲诚其意者,先致其知;致知在格物。物格然后知至,知至然后意诚。现代汉语词典中将格物致知解释为: "推究事物的原理,从而得到知识"。html
在编程中咱们接触最多的也是最基本的就是类和对象,当咱们在建立类或者实例化对象时,是否考虑过类和对象究竟是什么?理解其本质才能真正掌握一门语言。本文将从结构类型角度并结合实际应用探讨下Objective-C的类和对象。git
在Objective-C中,对象是广义的概念,类也是对象,因此严谨的说法应该是类对象和实例对象。既然实例对象所属的类称为类对象,那类对象有所属的类吗?有,称之为元类(Metaclass)。程序员
类对象(Class)是由程序员定义并在运行时由编译器建立的,它没有本身的实例变量,这里须要注意的是类的成员变量和实例方法列表是属于实例对象的,但其存储于类对象当中的。咱们在/usr/include/objc/objc.h
下看看Class
的定义:github
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
复制代码
能够看到类是由Class
类型来表示的,它是一个objc_class
结构类型的指针。咱们接着来看objc_class
结构体的定义:面试
struct objc_class {
Class isa; // 指向所属类的指针(_Nonnull)
Class super_class; // 父类
const char *name; // 类名(_Nonnull)
long version; // 类的版本信息(默认为0)
long info; // 类信息(供运行期使用的一些位标识)
long instance_size; // 该类的实例变量大小
struct objc_ivar_list *ivars; // 该类的成员变量链表
struct objc_method_list * *methodLists; // 方法定义的链表
struct objc_cache *cache; // 方法缓存
struct objc_protocol_list *protocols; // 协议链表
};
复制代码
isa指针是和Class
同类型的objc_class
结构指针,类对象的指针指向其所属的类,即元类。元类中存储着类对象的类方法,当访问某个类的类方法时会经过该isa指针从元类中寻找方法对应的函数指针objective-c
super_class为该类所继承的父类对象,若是该类已是最顶层的根类(如NSObject
或NSProxy
), 则 super_class为NULL
编程
ivars是一个指向objc_ivar_list
类型的指针,用来存储每个实例变量的地址缓存
info为运行期使用的一些位标识,好比: CLS_CLASS (0x1L)
表示该类为普通类, CLS_META (0x2L)
则表示该类为元类bash
methodLists用来存放方法列表,根据info中的标识信息,当该类为普通类时,存储的方法为实例方法;若是是元类则存储的类方法数据结构
cache用于缓存最近使用的方法。系统在调用方法时会先去cache中查找,在没有查找到时才会去methodLists中遍历获取须要的方法
实例对象是咱们对类对象alloc
或者new
操做时所建立的,在这个过程当中会拷贝实例所属的类的成员变量,但并不拷贝类定义的方法。调用实例方法时,系统会根据实例的isa指针去类的方法列表及父类的方法列表中寻找与消息对应的selector指向的方法。一样的,咱们也来看下其定义:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
复制代码
能够看到,这个结构体只有一个isa变量,指向实例对象所属的类。任何带有以指针开始并指向类结构的结构均可以被视做objc_object
, 对象最重要的特色是能够给其发送消息. NSObject类的alloc
和allocWithZone:
方法使用函数class_createInstance
来建立objc_object
数据结构。
另外咱们常见的id类型,它是一个objc_object
结构类型的指针。该类型的对象能够转换为任何一种对象,相似于C语言中void *
指针类型的做用。其定义以下所示:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
复制代码
元类(Metaclass)就是类对象的类,每一个类都有本身的元类,也就是objc_class
结构体里面isa指针所指向的类. Objective-C的类方法是使用元类的根本缘由,由于其中存储着对应的类对象调用的方法即类方法。
因此由上图能够看到,在给实例对象或类对象发送消息时,寻找方法列表的规则为:
元类,就像以前的类同样,它也是一个对象,也能够调用它的方法。因此这就意味着它必须也有一个类。全部的元类都使用根元类做为他们的类。好比全部NSObject的子类的元类都会以NSObject的元类做为他们的类。
根据这个规则,全部的元类使用根元类做为他们的类,根元类的元类则就是它本身。也就是说基类的元类的isa指针指向他本身。
咱们能够经过代码来实际验证下, Runtime提供了object_getClass
函数:
Class _Nullable object_getClass(id _Nullable obj)
复制代码
来获取对象所属的类,看到这个函数你也许会好奇这个和咱们日常接触的NSObject的[obj class]
有什么区别?
// NSObject.h
- (Class)class;
+ (Class)class;
复制代码
咱们继续从runtime的源码里面寻找答案:
Class object_getClass(id obj) {
return _object_getClass(obj);
}
复制代码
object_getClass
实际调用的是_object_getClass
函数,咱们接着看其实现:
static inline Class _object_getClass(id obj) {
#if SUPPORT_TAGGED_POINTERS
if (OBJ_IS_TAGGED_PTR(obj)){
uint8_t slotNumber = ((uint8_t)(uint64_t) obj) & 0x0F;
Class isa = _objc_tagged_isa_table[slotNumber];
return isa;
}
#endif
if (obj) return obj->isa;
else return Nil;
}
复制代码
显然_object_getClass
函数就是返回对象的isa指针,也就是返回该对象所指向的所属类。咱们接着看[obj class]
的具体实现(包括类方法和实例方法两种):
+ (Class)class {
return self; // 返回自身指针
}
- (Class)class {
return object_getClass(self); // 调用'object_getClass'返回isa指针
}
复制代码
从代码中能够看出+ (Class)class
返回的是其自己,而- (Class)class
则等价于object_getClass
函数。
咱们来写个测试代码,看看这些函数的实际返回值是否和上面的所述保持一致,好比咱们有个RJObject继承自NSObject:
RJObject *obj = [RJObject new];
Class clsClass0 = [RJObject class]; // 返回RJObject类对象的自己的地址
Class objClass0 = [obj class]; // isa指向的RJObject类对象的地址
Class ogcClass0 = object_getClass(obj); // isa指向的RJObject类对象的地址
NSLog(@"clsClass0 -> %p", clsClass0); // -> 0x10fb22068
NSLog(@"objClass0 -> %p", objClass0); // -> 0x10fb22068
NSLog(@"ogcClass0 -> %p", ogcClass0); // -> 0x10fb22068
复制代码
打印结果能够看出,当obj为实例变量时, object_getClass(obj)
与[obj class]
输出结果一致,均返回该对象的isa指针,即指向RJObject类对象的指针。而[RJObject class]
则直接返回RJObject类对象自己的地址,因此与前面二者返回的地址相同。
// 'objClass0'为RJObject类对象(RJObject Class)
Class objClass1 = [objClass0 class]; // 返回RJObject类对象自己的地址
Class ogcClass1 = object_getClass(objClass0); // isa指向的RJObject元类的地址
NSLog(@"objClass1 -> %p", objClass1); // -> 0x10fb22068
NSLog(@"ogcClass1 -> %p", ogcClass1); // -> 0x10fb22040
复制代码
此时objClass0
为RJObject的类对象,因此类方法[objClass0 class]
返回的objClass1
为self
, 即RJObject类对象自己的地址,故结果与上面的地址相同。而ogcClass1
返回的为RJObject元类的地址。
// 'ogcClass1'为RJObject的元类(RJObject metaClass)
Class objClass2 = [ogcClass1 class]; // 返回RJObject元类对象的自己的地址
Class ogcClass2 = object_getClass(ogcClass1); // isa指向的RJObject元类的元类地址
NSLog(@"objClass2 -> %p", objClass2); // -> 0x10fb22040
NSLog(@"ogcClass2 -> %p", ogcClass2); // -> 0x110ad9e58
复制代码
同理,这边ogcClass2
为RJObject元类的元类的地址,那问题来了,某个类它的元类的元类的是什么类呢?这样下去岂不是元类无穷尽了?擒贼先擒王,咱们先来看看根类NSObject的元类和它元类的元类分别是什么:
Class rootMetaCls0 = object_getClass([NSObject class]); // 返回NSObject元类(根元类)的地址
Class rootMetaCls1 = object_getClass(rootMetaCls0); // 返回NSObject元类(根元类)的元类地址
NSLog(@"rootMetaCls0 -> %p", rootMetaCls0); // -> 0x110ad9e58
NSLog(@"rootMetaCls1 -> %p", rootMetaCls1); // -> 0x110ad9e58
复制代码
看到结果就一目了然了,根元类的isa指针指向本身,也就是根元类的元类即其自己。另外,能够发现ogcClass2
的地址和根元类isa的地址相同,说明任意元类的isa指针都指向根元类,这样就构成一个封闭的循环。
另外,咱们能够经过class_isMetaClass
函数来判断某个类是不是元类,好比:
NSLog(@"ogcClass0 is metaClass: %@", class_isMetaClass(objClass0) ? @"YES" : @"NO");
NSLog(@"ogcClass1 is metaClass: %@", class_isMetaClass(ogcClass1) ? @"YES" : @"NO");
复制代码
输出结果为:
LearningClass[58516:3424874] ogcClass0 is metaClass: NO
LearningClass[58516:3424874] ogcClass1 is metaClass: YES
复制代码
日志代表ogcClass0
为类对象,而ogcClass1
则为元类对象,这与咱们上面的分析是一致的。
类和元类的父类指向状况也能够参照上面的步骤,经过
class_getSuperclass
或者[obj superClass]
函数来获取分析,这边就再也不赘述了。
除了isa声明了实例与所属类的关系,还有superClass代表了类和元类的继承关系,类对象和元类对象都有父类。一样,为了造成一个闭环,根类的父类为nil
, 根元类的父类则指向其根类。咱们能够经过一张示意图来看下三种对象之间的链接关系:
总结一下实例对象,类对象以及元类对象之间的isa指向和继承关系的规则为:
规则一: 实例对象的isa指向该类,类的isa指向元类(metaClass)
规则二: 类的superClass指向其父类,若是该类为根类则值为nil
规则三: 元类的isa指向根元类,若是该元类是根元类则指向自身
规则四: 元类的superClass指向父元类,若根元类则指向该根类
Objective-C做为动态语言的优点在于它能在运行时建立类和对象,并向类中增长方法和实例变量。具体示例以下:
Class newClass = objc_allocateClassPair([NSObject class], "RJInfo", 0);
if (!class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:")) {
NSLog(@"Add method 'report' failed!");
}
if (!class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *))) {
NSLog(@"Add ivar '_name' failed!");
}
objc_registerClassPair(newClass);
复制代码
上面代码建立了一个RJInfo
的类,并分别添加了_name
成员变量和report
实例方法。须要注意的是,方法和变量必须在objc_allocateClassPair
和objc_registerClassPair
之间进行添加。因此,在运行时建立一个类只须要3个步骤:
首先是调用objc_allocateClassPair
为新建的类分配内存,三个参数依次为newClass的父类,newClass的名称,第三个参数一般为0, 从这个函数名字能够看到新建的类是一个pair, 也就是成对的类,那为何新建一个类会出现一对类呢?是的,元类!类和元类是成对出现的,每一个类都有本身所属的元类,因此新建一个类须要同时建立类以及它的元类。
而后就能够向newClass中添加变量及方法了,注意若要添加类方法,需用objc_getClass(newClass)
获取元类,而后向元类中添加类方法。由于示例方法是存储在类中的,而类方法则是存储在元类中。最后必须把newClass注册到运行时系统,不然系统是不能识别这个类的。
上面的代码中添加了一个成员变量_name
, 咱们来看下实际应用中如何获取和使用这个变量:
unsigned int varCount;
Ivar *varList = class_copyIvarList(newClass, &varCount);
for (int i = 0; i < varCount; i++) {
NSLog(@"var name: %s", ivar_getName(varList[i]));
}
free(varList);
id infoInstance = [[newClass alloc] init];
Ivar nameIvar = class_getInstanceVariable(newClass, "_name");
object_setIvar(infoInstance, nameIvar, @"Ryan Jin");
NSLog(@"var value: %@",object_getIvar(infoInstance, nameIvar));
复制代码
咱们能够经过class_copyIvarList
来查看实例变量列表,注意获取的varList
列表须要调用free()
函数释放。当前只添加了一个变量,因此varCount
为1
, 在调用ivar_getName
打印出变量的名字。如若对_name
赋值,则须要先实例化newClass对象,并取出对象的该变量后调用object_setIvar
进行赋值操做。示例代码的输出结果为:
LearningClass[58516:3424874] var name: _name
LearningClass[58516:3424874] var value: Ryan Jin
复制代码
好了,验证完变量的添加,继续看方法的添加和使用。上文的示例中添加了report
方法,但仅仅是作了SEL
方法名的声明,咱们来接着完成其IMP
所指向函数ReportFunction
的具体实现:
void ReportFunction(id self, SEL _cmd) {
Class currentClass = [self class];
Class metaClass = objc_getMetaClass(class_getName(currentClass));
NSLog(@"Class is %@, and super - %@.", currentClass, [self superclass]);
NSLog(@"%@'s meta class is %p.", NSStringFromClass(currentClass), metaClass);
}
复制代码
在函数实现中咱们打印了类,父类以及元类的相关信息,为了运行ReportFunction
, 咱们须要建立一个动态实例来建立类的实例对象并调用report
方法:
id instanceOfNewClass = [[newClass alloc] init];
[instanceOfNewClass performSelector:@selector(report)];
复制代码
输出结果:
LearningClass[58516:3424874] Class is RJInfo, and super - NSObject.
LearningClass[58516:3424874] RJInfo's meta class is 0x600000253920.
复制代码
除了给类添加方法,咱们一样也能够动态修改已存在方法的实现,好比:
class_replaceMethod(newClass, @selector(report), (IMP)ReportReplacedFunction, "v@:");
复制代码
这样就将report
这个SEL
所指向的IMP
实现换成了ReportReplacedFunction
. 若是类中不存在name
指定的方法, class_replaceMethod
则相似于class_addMethod函数同样会添加方法;若是类中已存在name
指定的方法,则相似于method_setImplementation
同样替代原方法的实现。
看到
class_replaceMethod
的解释,相信你已经发现了,这不就是Method Swizzling吗?没错,所谓的黑魔法,其实就是底层原理的应用而已!
知其然亦知其因此然才是获取知识的正确方式,理解了类和对象的本质后,咱们来看看格物致知后的理论能够引导出哪些应用和认识:
在Objective-C中,属性(property)和成员变量是不一样的。那么,属性的本质是什么?它和成员变量之间有什么区别?简单来讲属性是添加了存取方法的成员变量,也就是:
@property = ivar + getter + setter;
复制代码
所以,咱们每定义一个@property
都会添加对应的ivar
, getter
和setter
到类结构体objc_class
中。具体来讲,系统会在objc_ivar_list
中添加一个成员变量的描述,而后在methodLists
中分别添加setter
和getter
方法的描述。
如上文所述,方法调用是经过查询对象的isa指针所指向归属类中的methodLists
来完成。这里咱们经过孙源在runtime分享会上的一道题目来理解下。假设咱们有一个类RJSark
定义以下:
@interface RJSark : NSObject
- (void)speak;
@end
复制代码
而后经过以下方式调用speak
方法:
@implementation RJViewController
- (void)viewDidLoad
{
[super viewDidLoad];
id cls = [RJSark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
复制代码
这里会正常完成调用,并不会致使程序crash. 这又是为何呢?咱们先来看下cls
. 显然,它是RJSark
的类对象,通过void *obj = &cls
赋值后obj
为指向cls
的指针,再经过(__bridge id)
将其转换为id
对象。上文中咱们提到id
实际上是一个objc_object
结构体,里面存放了指向所属类的isa指针,因此调用[obj speak]
可以找到它的isa所指向的类对象(也就是RJSark
类)的方法列表并完成调用,但其实obj
并非RJSark
的实例对象,它仅仅拥有和RJSark
实例对象同样的isa指针而已。
空说无凭,咱们将上面的代码稍微修改后验证下:
id cls = [RJSark class];
RJSark *sark = [[cls alloc] init];
void *obj = &cls;
NSLog(@"cls = %p", cls);
NSLog(@"sark = %p", objc_getClass(object_getClassName(sark)));
NSLog(@"obj = %p", objc_getClass(object_getClassName((__bridge id)obj)));
复制代码
输出结果为:
LearningClass[58516:3424874] cls = 0x10fbd02d0
LearningClass[58516:3424874] sark = 0x10fbd02d0
LearningClass[58516:3424874] obj = 0x10fbd02d0
复制代码
能够发现obj
和sark
的isa指针所指向的地址相同且与cls
的地址一致,也就是它们都指向cls
类对象。
注意这边用的是
objc_getClass
方法,该方法只是单纯的返回本类的地址,上文用到的object_getClass
方法返回的才是isa
指针所指向的(元)类对象地址
咱们仍是直接来看一个面试题, Father
继承与NSObject
, Son
则继承于Father
类,分别调用[self class]
和[super class]
, 输出结果是?
@implementation Son : Father
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
复制代码
输出结果都为Son
, 为何[super class]
的结果不是Father
? 咱们简单分析下就明白了。实例对象的方法列表是存放在isa所指向的类对象中的,因此调用[self class]
的时候会去self
的isa所指向的Son
类对象中寻找该方法,在没有重载[obj class]
的状况下, Son
类对象是没有这个方法的,此时会接着在父类对象的方法列表中查找,最终会发现NSObject
存储了该方法,因此[self class]
会返回实例对象(self)所属的Son
这个类对象。
而[super class]
则指定从父类Father
的方法列表开始去查找- (Class)class
这个方法,显然Father
没有这个方法,最终仍是要查找到NSObject
类对象的方法列表中,须要注意的是不论是[self class]
仍是[super class]
, 它们都是调用的实例对象的- (Class)class
方法,虽然其指向的类对象不一样,但实例对象都是self
自己,再强调下区分开实例对象和类对象!于是返回的也是当前self
的isa所指向的Son
类。
其实
super
是objc_super
类型的结构体,它包含了当前的实例对象self
以及父类的类对象。更详细的解答能够参考@iOS程序犭袁的博文。
除了用super
来指向父类外,咱们还能够用isKindOfClass
和isMemberOfClass
来判断对象的继承关系。这两个函数有什么区别呢?一样,先来看一个测试题:
BOOL r1 = [[NSObject class] isKindOfClass:[NSObject class]]; // -> YES
BOOL r2 = [[RJObject class] isKindOfClass:[RJObject class]]; // -> NO
BOOL r3 = [[NSObject class] isMemberOfClass:[NSObject class]]; // -> NO
BOOL r4 = [[RJObject class] isMemberOfClass:[RJObject class]]; // -> NO
复制代码
为何只有r1
是YES
? 实际上isKindOfClass
是判断对象是否为Class
的实例或子类,而isMemberOfClass
则是判断对象是否为Class
的实例。仍是不明白?不要紧,咱们直接来看看这两个函数的源码实现,看看它们本质上是以什么做为判断标准的:
+ (BOOL)isKindOfClass:(Class)cls
{
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls
{
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
复制代码
注意上面的题目是调用的类方法,因此咱们分析下类方法的实现,至于实例方法也是相似的。能够看到isMemberOfClass
的判断是先调用object_getClass
获取isa所指向的归属类,也就是元类,而后直接判断cls
是否就是被比较的对象的元类。而[NSObject class]
的元类是根元类,显然不等于[NSObject class]
自己,因此r3
返回NO
, r4
也是同理。
而isKindOfClass
也是先获取当前对象的元类,可是会循环获取其isa所指向类的父类进行比较,只要该元类或者元类的父类与cls
相对则返回YES
. RJObject
的元类,以及父元类(最终指向根元类)都不等于RJObject
对象,因此r2
返回NO
. 那为何r1
返回YES
呢?还记得上文所说的闭环吗?根元类的父类指向根类自己!显然, r1
符合了isKindOfClass
的判断标准。
到这里理论部分就结束了。那么,问题来了,理解了类和对象的本质原理有什么实际应用价值吗?可让咱们更优雅的解决项目中遇到的问题和需求吗?Talk is cheap, show me the code:
好比App常见的记录用户行为的数据统计需求,俗称埋点。具体来讲假设咱们须要记录用户对按钮的点击。一般状况下,咱们会在按钮的点击事件里面直接加上数据统计的代码,但这样作的问题在于会对业务代码进行侵入,且统计的代码散落各处,难以维护。
固然,咱们还能够建立一个UIButton的子类,在子类中重载点击事件的响应函数,并在其中加上统计数据部分的代码:
-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
复制代码
这样作是能够的,可是现有工程中全部须要支持数据统计的按钮都必须替换成该子类,并且若是哪天不须要支持埋点功能了并须要迁移复用业务代码,那还得一个个再改回去。因此,咱们须要一个更优雅的实现。
咱们能够利用动态建立类并添加方法的思路来实现这个需求,这边只是以埋点做为示例,你也能够利用该思路扩展任意须要处理的需求和功能。简单来讲就是咱们建立一个UIButton的Category, 而后在须要埋点的状况下动态生成一个新的UIButton子类,并给其添加一个能够记录数据的事件响应方法来替代默认的方法,以下所示:
//
// UIButton+Tracking.m
// LearningClass
//
// Created by Ryan Jin on 07/03/2018.
// Copyright © 2018 ArcSoft. All rights reserved.
//
#import "UIButton+Tracking.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation UIButton (Tracking)
- (void)enableEventTracking
{
NSString *className = [NSString stringWithFormat:@"EventTracking_%@",self.class];
Class kClass = objc_getClass([className UTF8String]);
if (!kClass) {
kClass = objc_allocateClassPair([self class], [className UTF8String], 0);
}
SEL setterSelector = NSSelectorFromString(@"sendAction:to:forEvent:");
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
object_setClass(self, kClass); // 转换当前类从UIButton到新建的EventTracking_UIButton类
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(kClass, setterSelector, (IMP)eventTracking_SendAction, types);
objc_registerClassPair(kClass);
}
static void eventTracking_SendAction(id self, SEL _cmd, SEL action ,id target , UIEvent *event) {
struct objc_super superclass = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
void (*objc_msgSendSuperCasted)(const void *, SEL, SEL, id, UIEvent *) = (void *)objc_msgSendSuper;
// to do event tracking...
NSLog(@"Click event record: target = %@, action = %@, event = %ld", target, NSStringFromSelector(action), (long)event.type);
objc_msgSendSuperCasted(&superclass, _cmd, action, target, event);
}
@end
复制代码
而后在添加按钮的地方,若是须要数据统计功能,则调用enableEventTracking
函数来内嵌打点功能。使用示例以下:
- (void)viewDidLoad
{
[super viewDidLoad];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 50, 30)];
button.layer.borderColor = [[UIColor redColor] CGColor];
button.layer.borderWidth = 1.0f;
button.layer.cornerRadius = 4.0f;
button.layer.masksToBounds = YES;
[button addTarget:self action:@selector(trackingButtonAction:)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
[button enableEventTracking];
}
- (void)trackingButtonAction:(UIButton *)sender
{
// to do whatever you want...
NSLog(@"%s", __func__);
}
复制代码
打印输出信息为:
LearningClass[58516:3424874] Click event record: target = <ViewController: 0x7f97a5d0cb80>, action = trackingButtonAction:, event = 0
LearningClass[58516:3424874] -[ViewController trackingButtonAction:]
复制代码
浮于表面探究问题不失为一种方法,可是弄清楚本质才是真正意义上的解决疑惑。