读Effective Objective-C 2.0有感,在读这边书之前OC的基础并不是很好,可以说一窍不通,所以不乏一些很简单的东西。
Accustoming Yourself to Objective-C
Item 4: Typed Constants
主要介绍的是#define、static、const、extern。
- const修饰常量。
- static修饰静态变量
这里OC和Swift是有区别的,OC的static修饰的变量不会创建外部符号,仅在自己的实现文件中进行类似于#define的替换。在Swift中,static表示类变量或者是类方法。
典型使用🌰
1 | /// xx.m |
这样就不会在两个无关的实现文件导入相关头文件。在这里不能添加static修饰符,否则会导致extern失败。
Objects, Messaging, and the Runtime
Item 6: Understand Properties
这里介绍了属性的相关知识,当添加一个实例变量之后,存储实例变量的内存结构发生变化(实例变量在类中offset发生变化)。为了这样的情况发生,OC采用运行时动态查找实例变量的offset来实现稳定的ABI
You can even add instance variables to classes at runtime.这句话让我百思不得其解,不知道如何实现。(objc_setAssociatedObject也不是向类中添加实例变量啊)
Item 27中有这样一段话,Therefore, instance variables do not have to be defined in the public interface, since consumers of the class do not have to know their layout.说的是Extension,可能作者想说的是Extension可以添加实例变量,但是Extension不是运行时。这里不知道是不是作者的错误。
Item 9: Class Cluster
在看Effective Objective-C的时候看到类簇这部分,让我对Objective-C中的工厂模式有了一定的认识,也解开了以前的疑惑(比如UIButton、NSArray等)。
比如在lldb调试的过程中看到的NSArray类型并不是NSArray,可能是一个__NSSingleObjectArrayI
也可能是__NSArrayI
,都是Apple封装在内部的私有类,他们都是NSArray子类,意味着NSArray仅仅是一个壳而已。
1 | id idArray = @[@1]; |
推荐从NSArray看类簇这篇文章。
Item 12: Message Forwarding
- 首先调用
resolveInstanceMethod
或者resolveClassMethod
,表明是否能够新增一个方法来处理。Dynamically provides an implementation for a given selector for an instance method.
Dynamically provides an implementation for a given selector for a class method. - 当上一步遍历到NSObject的时候还是返回NO,就会调用
forwardingTargetForSelector
,在这里可以将消息转发给另一个对象。Returns the object to which unrecognized messages should first be directed.
⚠️ 这里不要返回self,否则会陷入死循环。常用的操作是将消息转发给内部的隐藏的对象。 - 当上一步继续返回nil的时候,会调用
forwardInvocation
进行完整的消息转发。Overridden by subclasses to forward messages to other objects.
resolveInstanceMethod || resolveClassMethod
1 |
|
- 第一次点击:
由于没有
failedDymamic
的实现,导致无法在method list中找到对应实现,所以调用resolveInstanceMethod
向method list中加入新的dynamicMethod
。
- 第二次点击:
不经过
resolveInstanceMethod
,因为method list中已经有对应的方法,所以直接调用。⚠️ 但是method list中对应的sel依旧是
faile淡定ymamic
,但是实际调用的是dynamicMethod
,相当于相应的方法被替换。
forwardingTargetForSelector
1 | /// 假设我们有一个隐藏的属性不想让外部看到,但是外部需要接口访问,其中装逼的方法可以进行消息转发。 |
⚠️ 当一次
forwardingTargetForSelector
成功之后,再次发送该消息的时候并不会重新从resolveInstanceMethod
开始,而是直接forwardingTargetForSelector
。猜测应该是做了缓存。
forwardInvocation
以上都没有结果,就会调用forwardInvocation
进行完整的消息转发过程。
⚠️ 此时必须要重写
methodSignatureForSelector
返回对应方法签名,如果方法签名返回为nil,则不会调用forwardInvocation
。
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation { |
上面仅仅是消息的简单转发,在这里其实我们可以做更多,比如修改参数、修改返回值等。
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation { |
由于执行到该消息转发的时候并没有缓存,所以每次都会执行。
methodSignatureForSelector
To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one.
在上面class_addMethod的时候出现了"v@:"
东西。
- @encode(BOOL) (c) for the return type
- @encode(id) (@) for the receiver (self)
- @encode(SEL) (:) for the selector (_cmd)
- @encode(NSString *) (@) for the first explicit argument
每个方法都有两个默认的参数,一个是self,一个是_cmd。而那个字符串表示的就是类型编码(Type Encodings)。
"v@:"
表示的就是返回值为void,第一个参数是self,第二个参数为_cmd。
doesNotRecognizeSelector
当上面步骤都走完了,还是没有找到相关的消息接受者,就会调用NSObject的doesNotRecognizeSelector
来抛出异常。既”Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason:”
Item 13: Method Swizzling
在了解了消息转发之后,就很容易理解方法交换。就是修改Methos Lists。
在这里我们需要注意的几点:
- 为什么要在load中实现方法交换。
- load和initialize调用时机。
1 | /// main.m |
1 | int main(int argc, const char * argv[]) { |
load调用:
- Load在镜像加载的时候调用。
- Load先于main调用,这个时候Framework已经加载了(C++静态库没有)。
- 父类先于子类调用。
- 类先于carthage调用。
initlizate调用:
- 第一次初始化类,或者是调用类方法等(个人理解就是创建元类的时候,因为元类也可以看成单例)。
- 如果没有调用则不会调用initlizate。
Item 14: Objective-C Object
1 | NSString *string = @"This is NSString"; |
这个是一个很简单的字符串
Type | string | @”This is NSString” |
---|---|---|
内存分配 | 栈 | 堆 |
类型 | objc_object | objc_class |
1 | /// An opaque type that represents an Objective-C class. |
Class中的isa指向其元类,也是类方法定义的地方,只有一个实例关联元类(可以认为类方法的调用类似于单例调用实例方法)。所以下面的判断方法是正确的。
1
2
3
4 id object = /*...*/;
if ([object class] == [AClass class]) {
// 'object' is an instance of AClass
}
⚠️ 这种方式仅对NSObject子类有效,如果类是继承于NSProxy,则不适用。
Interface and API Design
Item 18: Prefer Immutable Objects
Swift中我觉得最好的是权限控制,由于Swift以文件为单位进行权限控制。例如private
、fileprivate
等。内部读写,外部读取我们可以使用private(set)
、fileprivate(set)
这样的方式。刚接触OC的时候很是困惑,直到我明白OC的动态性和Runtime等相关知识。
1 | /// A.h |
下面提供个人的理解:
- Runtime的存在使得OC具有动态性,直到运行时才知道如何进行消息派发,在编译的时候是并不知道的。
- 头文件的作用就是告诉编译器我有这个方法,其他人可以尝试调用,但是有没有实现我并不告诉别人,直到运行时才告诉你。
- Extension其实是在编译的时候进行的,目的就是隐藏具体的细节。
实现文件用来编译,头文件用来链接其他文件。
我们再来分析上面的代码,在头文件声明属性,编译器会自动生成getString方法,其他人看到也就只有这个。在实现文件中声明相同属性,编译器会声明getString和setString两个方法,只有自己知道。所以也就实现了类似于Swift的内部读写,外部读取。但是这个setString方法在Runtime的是可以在方法表中找到的,意味着我们向其发送setString消息,是能够响应的(或者通过KVC)。
1 | ObjectA *a = [[ObjectA alloc] init]; |
Advances
objc_msgSend
1 | /* Basic Messaging Primitives |
从objc_msgSend定义看出,其并不是一个具体的函数,而是在调用的时候需要给出指定的函数指针类型。
下面我们看看编译器是如何为我们解决代码到消息的转换。
1 | /// main.h |
1 | clang -rewrite-objc main.m |
1 | /// main.cpp |
函数指针类型是(void (*)(id, SEL, NSString *)
,sel_registerName
通过给定的"setA:"
字符串生成一个SEL,之后是传递的参数。这样就实现了消息发送。
Extension(Class-continuation Category) && Category
- Extension(编译期间),Category(运行时期间)。
所以Extension可以添加实例变量而Category不能。
- Extension和Category都可以添加属性。
但是只有Extension可以自动生成对应的实例变量和存储方法,而Category不会生成实例变量和存储方法,只有一个存储方法的声明,需要自己去手动实现。
如果没有手动实现就会出现警告Property ‘privateString’ requires method ‘privateString’ to be defined - use @dynamic or provide a method implementation in this category - 如果Category添加的方法和原类中的方法是同名的,那么会覆盖(最后在运行时加载的时候Category的方法表会在原方法表前面,导致被覆盖的现象,其实原方法还是存在的)原有类中方法的实现。
我们再来看看这个结构,则objc_ivar_list
指向的是实例变量表,objc_method_list
指向的实例方法表指针。两者的区别导致了我们只能在运行时动态修改实例方法表,而不能修改实例变量表。
1 | /// An opaque type that represents an Objective-C class. |
在运行期,对象内存布局已经确定。
从category中可以看出并不能添加实例变量。
1 | struct category_t { |
Memory Management
Item 30 ARC
当方法名字是以下面名字为开头,则返回的对象交由调用者管理。如果不是,则返回的对象为autorelease对象。
- alloc
- new
- copy
- mutableCopy
举个🌰
1 | /// ObjectA.m |
1 | # 用clang将ObjectA.m文件编译成LLVM中间文件。 |
1 | define internal i8* @"\01+[ObjectA newCreate]"(i8*, i8*) #0 { |
1 | # 用clang将main.m文件编译成LLVM中间文件。 |
1 | define i32 @main(i32, i8**) #0 { |
这里我们可以看出同一个实例仅仅是命名不同,编译的结果是不一样的。
new开头的方法是直接返回对象的,意味着由调用者管理返回对象的内存,对象为strong类型,所以返回的对象引用计数为1。
非new开头的方法返回的是一个autorelease对象(引用计数为0),但是由于create对象为strong类型,所以做了一次objc_retainAutoreleasedReturnValue操作。
相同点是在函数结束的时候调用objc_storeStrong将对象释放。
接下来我们看看这两个方法的实现,再结合书中的代码分析
1 | // Same as objc_retainAutorelease but suitable for tail-calling |
在这里我们可以看出编译器对autorelease进行了优化。
- 如果该对象返回的对象是__strong类型,则直接返回该对象。
- 如果该对象返回的对象是__weak类型,则返回的对象为autorelease对象。
weak
首先我们证实一下上面的问题。
1 | /// main.m |
1 | define i32 @main(i32, i8**) #0 personality i8* bitcast (i32 (...)* @__objc_personality_v0 to i8*) { |
1 | define i32 @main(i32, i8**) #0 personality i8* bitcast (i32 (...)* @__objc_personality_v0 to i8*) { |
两个同样的实例一个有警告无任何的输出结果,一个没有警告,正常输出。
原因就在于第二个创建的实例是一个autorelease对象,而第一个创建的对象由于引用计数为0,所以一创建就被释放掉了。