简介
runtime
,简称运行时,是一套比较底层的纯C语言API
, 属于1个C语言库, 包含了很多底层的C语言API
。在OC中"函数调用"称之为消息的分发,编译的时候不需要查找要执行的函数,必须要等到真正运行的时候,程序才查找要执行的函数,甚至在程序运行时这些函数可以改变,这些特性使得OC成为一门真正动态的语言.
准备工作
- 类的本质:
struct objc_class {
Class isa; //指向父类或者元类
Class super_class ; //父类
const char *name ; //类名
long version ; //版本
long info ; //信息
long instance_size ; //实例变量的大小
struct objc_ivar_list *ivars ; //成员变量列表
struct objc_method_list **methodLists ; //方法列表,存储的是Method类型的方法
struct objc_cache *cache ; //调用过得方法的缓存
struct objc_protocol_list *protocols ; //要遵守的协议列表
} ;
由此可见,类的本质是一个结构体, Class
是指向类结构体的指针,该类结构体含有一个指向其父类类结构的指针;同时,在objc_class
结构体中:ivars
是objc_ivar_list
指针;methodLists
是指向objc_method_list
指针的指针。也就是说可以动态修改*methodLists
的值来添加成员方法,这也是Category
实现的原理,同样解释了Category
不能添加属性的原因。
#####另解:
我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。但是 方法和属性并不“属于”类实例,所以可以在分类中定义, 而成员变量恰恰“属于”类实例。
- 消息发送函数
objc_msgSend:
id objc_msgSend ( id self, SEL op, ... );
如果有参数,应该为:
id objc_msgSend(receiver, selector, arg1, arg2, ...)
- 第一个参数
id
,它是一个指向类实例的指针:
typedef struct objc_object *id;
类实例是一个包含isa
指针的结构体,根据isa
指针就可以顺藤摸瓜找到对象所属的类.
注意:
isa
指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。例如KVO
的实现机理就是动态创建一个子类,将被观察对象的isa指针指向子类,而不是真实的类,而且把创建出来的子类的isa
指针指向了父类,从而才能回调到原来的方法中,这是一种叫做isa-swizzling
的技术,详见官方文档.
- 第二个参数
SEL
,它是selector
在Objc
中的表示类型(Swift中是Selector
类)。selector
是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL
:
typedef struct objc_selector *SEL;
不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc
中方法命名有时会带上参数类型,但是不同类中方法的实现是不一样的哦!
3.Method
,是一种代表类中的某个方法的类型。
typedef struct objc_method *Method;
而objc_method
存储了方法名,方法类型和方法实现:
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
- 方法名类型为
SEL
,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。 - 方法类型
method_types
是个char
指针,其实存储着方法的参数类型和返回值类型。 -
method_imp
指向了方法的实现,本质上是一个函数指针.
4.Ivar
,是一种代表类中实例变量的类型。
typedef struct objc_ivar *Ivar;
objc_ivar
存储了变量名称,变量类型,变量大小和变量偏移等信息.
struct objc_ivar {
char *ivar_name
char *ivar_type
int ivar_offset
#ifdef __LP64__
int space
#endif
}
可以用ivar_getName()
根据实例查找其在类中的名字,还可以用class_copyIvarList
来获取类中的实例变量和属性.
5.IMP
在objc.h
中的定义是:
typedef id (*IMP)(id, SEL, ...);
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC
消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP
这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到, 一般用于循环执行一个确定的方法, 这样可以提升性能.
6.Cache
,在runtime.h
中Cache
的定义如下:
typedef struct objc_cache *Cache
objc_cache
的实现是:
struct objc_cache {
unsigned int mask /* total = mask + 1 */
unsigned int occupied
Method buckets[1]
};
Cache
为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa
指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache
中查找。Runtime
系统会把被调用的方法存到Cache
中,下次查找的时候效率更高。
7.Property
,它是一个指向objc_property
结构体的指针:
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList
和 protocol_copyPropertyList
方法来获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回类型为指向指针的指针, 因为属性列表是个数组,每个元素内容都是一个objc_property_t
指针,而这两个函数返回的值是指向这个数组的指针。
你可以用property_getName
函数来查找属性名称:
const char *property_getName(objc_property_t property)
对比下 class_copyIvarList
函数,使用class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
把上面的代码放一起,你就能从一个类中获取它的属性啦:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
消息
objc_msgSend
函数执行步骤:
- 查找本类的
IMP
,先从cache
里面找,完了找得到就跳到对应的函数去执行。--------每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就会很快了;当然了,这种快速执行路径还是不如"静态绑定函数调用操作"(statically bound function call)那样迅速,不过只要把选择子缓存起来,下次执行也就不会慢很多了. - 如果
cache
找不到就找一下Class
中的方法列表 (它将方法选择器和方法实现地址联系起来)。 - 如果方法列表找不到就到超类的方法列表中去找,一直找,直到找到
NSObject
类为止。 - 如果最后还是找不到相符的方法,那就执行"消息转发"(message forwarding)操作. 我们在编译期向类发送了其无法解读的消息并不会报错,因为在运行期还可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法的实现.当对象接收到无法解读的消息后,就会启动"消息转发"机制,程序员可由此告诉对象应该如何处理未知消息.
- 消息转发分为两个阶段. 第一阶段先征询接收者所属的类,看其是否能通过
resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现, 以处理这个"未知的选择子"(unknown selector), 这叫做"动态方法解析"(dynamic method resolution). 第二阶段涉及"完整的消息转发机制"(full forwarding mechanism). 如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了.此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用.这又分为两小步.首先,请接收者看看有没有其他对象能处理这条消息.若有,则运行期系统会把消息转给那个对象, 通过重载- (id)forwardingTargetForSelector:(SEL)aSelector
方法替换消息的接受者为其他对象, 于是消息转发过程结束,一切如常.若没有"备援接收者"(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封闭到NSInvocation
对象中, 重写forwardInvocation:
来重写定义转发逻辑, 再给接收者最后一次机会,令其设法解决当前还未处理的这条消息.
注意:
前面讲的这部分内容只描述了部分消息的调用过程, 其他"边界情况"(edge case) 则需要交由Objective-C
运行环境中的中的另一些函数来处理:
-
objc_msgSend_stret
.如果消息返回的是结构体,那么可交由此函数处理.只有当CPU寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息. 若是返回值无法容纳于CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发. 此时, 那个函数会通过分配在栈上的某个变量来处理消息返回的结构体. -
objc_msgSend_fpret
. 如果消息返回的是浮点数, 那么可交由此函数处理.值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret
函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend
不再适用,于是objc_msgSend_fpret
被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。 -
objc_msgSendSuper
. 如果要给超类发消息.那么交由此函数来处理.也有另外两个与objc_msgSend_stret
和objc_msgSend_fpret
等效的函数,用于处理发给super的相应消息. - 当
objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数: 接收消息的对象(也就是self
指向的内容) 和 方法选择器(_cmd
指向的内容);之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的,所以我们可以在方法中使用self关键字来引用实例本身,实际上,self
是在方法实现中访问消息接收者对象的实例变量的途径, 而_cmd
则应用很少.
详细讲解执行过程
一. 获取方法地址
可以用methodForSelector:
实例方法来获取某个方法选择器对应的IMP
,避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
注意:
-
methodForSelector:
方法是由Cocoa
的Runtime
系统提供的,而不是Objc
自身的特性。 - 此时一般用于大批量数据的存储, 此外还可以使用事务来执行批量数据的存储操作, 其效率可以提升几十倍哦!
二. 消息转发第一阶段----动态方法解析
(1) 你可以动态地提供一个方法的实现。例如我们可以用@dynamic
关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:
和propertyName
方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现。因为当Runtime
系统在Cache
和方法分发表中(包括超类)找不到要执行的方法时,在继续往下执行转发机制之前, Runtime
会调用resolveInstanceMethod:
或resolveClassMethod:
来给程序员一次动态添加方法实现的机会。
使用这种办法的前提是: 相关的实现代码已经写好, 只等着运行的时候动态插在类里面就可以了.
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
上面的例子为resolveThisMethodDynamically
方法添加了实现内容,也就是dynamicMethodIMP
方法中的代码。其中` “v@:” ``表示返回值和参数,这个符号涉及 TypeEncoding.
注意:
动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:
或instancesRespondToSelector:
方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:
返回NO。
(2) 我们需要用class_addMethod
函数完成向特定类添加特定方法实现的操作, resolveClassMethod:
解析类方法, 可以把实例方法和类方法的动态方法解析作对比:
.h文件:
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end
.m 文件:
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(goToSchool:)) {
class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}
- (void)myInstanceMethod:(NSString *)string {
NSLog(@"myInstanceMethod = %@", string);
}
@end
三. 消息转发第二阶段----备援接收者
在"动态方法解析"失败后,Runtime
系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector
方法替换消息的接受者为其他对象:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
若当前接收者能找到备援对象, 则将其返回,若找不到,就返回nil. 不过千万不要返回self
,因为那样会死循环,如果想替换类方法的备援接收者,也要重写此方法,并返回类对象:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if(aSelector == @selector(xxx)) {
return NSClassFromString(@"Class name");
}
return [super forwardingTargetForSelector:aSelector];
}
注意:
通过此方案,我们可以用"组合"(compositon)来模拟出"多重继承"(multiple inheritance)的某些特性.在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的.
请注意,我们无法操作经由这一步所转发的消息.若是想在发送给备援接收者之前先修改消息内容,那就得通过的消息转发机制来做了.
四. 消息转发第二阶段----完整的消息转发
如果转发算法已经来到这一步的话, 那么唯一能做的就是启用完整的消息转发机制了.首先创建NOInvocation
对象, 把与尚未处理的那条消息有关的全部细节都封于其中.此对象包含选择子, 目标(target
)及参数.在触发NSInvocation
对象时, "消息派发系统"(message-dispatch system)将亲自出马,把消息指派给目标对象.
我们可以重写forwardInvocation:
来重写定义转发逻辑:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
这里需要注意的是参数anInvocation
是从哪的来的呢?其实在forwardInvocation:
消息发送前,Runtime
系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation
对象。所以我们在重写forwardInvocation:
的同时也要重写methodSignatureForSelector:
方法,否则会抛异常。
这个方法可以实现得很简单: 只需改变调用目标,使消息在新目标上得以调用即可. 然而这样实现出来的方法与"备援接收者"方案所实现的方法等效,所以很少有人采用这么简单的实现方式.比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容, 比如追加另外一个参数,或是改变选择子,等等.
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法.这样继承体系中的每个类都有机会处理此请求,直至NSObject.
然而,NSObject
中的方法实现只是简单地调用了doesNotRecognizeSelector:
, 以抛出异常, 此异常表明选择子最终未能得到处理.
forwardInvocation:
方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:
方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意:
forwardInvocation:
方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate
消息转发给其它对象,则这个对象不能有negotiate
方法。否则,forwardInvocation:
将不可能会被调用。
消息转发流程全图:
运行时的一般应用
除了以上运行时的动态处理选择子外,还能有好多的用法:
一. AssociatedObject----关联对象
在 OS X 10.6 之后,Runtime
系统让Objc
支持向对象动态添加变量, 即某个OC对象通过一个唯一的key连接到一个类的实例上(或者给一个类动态地添加属性), 涉及到的函数有以下三个:
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );
这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量 (这些常量对应着引用关联值的政策,也就是 Objc
内存管理的引用计数机制) :
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
实例
给按钮关联一个添加点击事件的block:
.h文件:
@interface UIButton()
@property (nonatomic, copy) void(^callBackBlock)(UIButton *button);
@end
.m文件:
@implementation UIButton (CallBackButton)
- (void)setCallBackBlock:(void (^)(UIButton *))callBackBlock {
objc_setAssociatedObject(self, @selector(callBackBlock), callBackBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (void (^)(UIButton *))callBackBlock {
return objc_getAssociatedObject(self, @selector(callBackBlock));
}
//以上为给分类添加了一个回调属性,紧接着实现对外方法,给按钮添加一个事件,事件中回调block
- (instancetype)initWithFrame:(CGRect)frame callBlock:(void (^)(UIButton *))callBackBlock {
self = [super initWithFrame:frame];
if (self) {
self.callBackBlock = callBackBlock;
[self addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (void)btnClick:(UIButton *)button {
self.callBackBlock(button);
}
@end
二. Method Swizzling----黑魔法(方法交换)
此时一般用于: 当我们使用系统自带的方法或者框架方法时,发现该方法的功能不能满足我们的需求,这时候需要我们在执行对应的方法之前添加额外的功能.
实例
给凡是继承自UIView
的类创建出来的View
,指定背景色:
.h文件:
@interface UIView (BlackView)
@end
.m文件:
@implementation UIView (BlackView)
+ (void)load {
Method originalM = class_getInstanceMethod([self class], @selector(setBackgroundColor:));
Method exchangeM = class_getInstanceMethod([self class], @selector(pb_setBackGroundColor:));
method_exchangeImplementations(originalM, exchangeM);
}
- (void)pb_setBackGroundColor:(UIColor *)color {
//交换方法可以在系统方法或者第三方框架方法之前做事情
NSLog(@"自定义方法先打印哦");
//交换完方法后,凡是继承自系统UIView创建出来的View,其背景色都是
[self pb_setBackGroundColor:[UIColor greenColor]];
}
@end
注意:
通过类似处理,开发者可以为那些"完全不知道其具体实现的" (completely opaque, "完全不透明的") 墨盒方法增加日志记录功能, 这非常有助于程序调试.然而,此做法只在高度程序时有用.很少有人在调试程序之外的场合用上述"方法调配技术"来永久改动某个类的功能.不能仅仅因为Objctive-C语言里有这个特性就一定要用它.若是滥用,反而会令代码变得不易读懂且难于维护,一旦出现bug很难解诀.
三. 私有成员变量的获得
由于之前已经有过类似的说明,所以这里不再赘述.
实例
点击屏幕获得相应类的全部成员变量(包括私有成员变量),以便对其进行赋值操作:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
unsigned int count = 0;
注意:Class为类名称
Ivar *ivars = class_copyIvarList([Class class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *cname = ivar_getName(ivar);
NSLog(@"%@",[[NSString alloc] initWithCString:cname encoding:NSUTF8StringEncoding]);
}
free(ivars);
}
比如UIPageControl
中的_currentPageImage
和_pageImage
的赋值操作.
四. 动态地获得类的属性列表----字典转模型核心算法
与第三种用法非常相似,所以也不作太多解释了.
const char *kPropertyListKey = "YFPropertyListKey";
+ (NSArray *)yf_objcProperties
{
/* 获取关联对象 */
NSArray *ptyList = objc_getAssociatedObject(self, kPropertyListKey);
/* 如果 ptyList 有值,直接返回 */
if (ptyList) {
return ptyList;
}
/* 调用运行时方法, 取得类的属性列表 */
/* 成员变量:
* class_copyIvarList(__unsafe_unretained Class cls, unsigned int *outCount)
* 方法:
* class_copyMethodList(__unsafe_unretained Class cls, unsigned int *outCount)
* 属性:
* class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount)
* 协议:
* class_copyProtocolList(__unsafe_unretained Class cls, unsigned int *outCount)
*/
unsigned int outCount = 0;
/**
* 参数1: 要获取得类
* 参数2: 雷属性的个数指针
* 返回值: 所有属性的数组, C 语言中,数组的名字,就是指向第一个元素的地址
*/
/* retain, creat, copy 需要release */
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
NSMutableArray *mtArray = [NSMutableArray array];
/* 遍历所有属性 */
for (unsigned int i = 0; i < outCount; i++) {
/* 从数组中取得属性 */
objc_property_t property = propertyList[i];
/* 从 property 中获得属性名称 */
const char *propertyName_C = property_getName(property);
/* 将 C 字符串转化成 OC 字符串 */
NSString *propertyName_OC = [NSString stringWithCString:propertyName_C encoding:NSUTF8StringEncoding];
//在这里可以对对象的每一个属性通过KVC来赋值,从而达到字典转模型的目的
//赋值操作*************************
[mtArray addObject:propertyName_OC];
}
/* 设置关联对象 */
/**
* 参数1 : 对象self
* 参数2 : 动态添加属性的 key
* 参数3 : 动态添加属性值
* 参数4 : 对象的引用关系
*/
objc_setAssociatedObject(self, kPropertyListKey, mtArray.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/* 释放 */
free(propertyList);
return mtArray.copy;
}
引用:
Objective-C Runtime
Effective Objective-C 2.0 --52 Specific Ways to Improve Your iOS and OS X Programs
网友评论