《Effective Objective-C》iOS开发的教科书

2018-01-10  本文已影响73人  柯浩然

概念篇:

1.运行时
2.属性
@synthesize testString = _testString;
@dynamic testString;
_testString //Use of undeclared identifier '_testString'
@interface TestObject ()
//虽然这个属性已经是只读性质,也要写上具体的语义,以此表明初始化方法在设置这些值时所用方法
@property(copy,readonly) NSString *testString;
@end

@implementation TestObject
- initWithString:(NSString *)string {
    
    self = [super init];
    
    if (self) {
        //用初始化方法设置好属性值之后,就不要再改变了,此时属性应设为“只读”
        _testString = [string copy];
    }
    return  self;
}
@end
3.对象等同性
    NSString *textA = @"textA";
    NSString *textAnother = [NSString stringWithFormat:@"textA"];
    NSLog(@"%d",textA == textAnother);// 0
    NSLog(@"%d",[textA isEqual:textAnother]);// 1
    NSLog(@"%d",[textA isEqualToString:textAnother]);// 1
@interface TestObject : NSObject
@property NSString *testString;
@end

@implementation TestObject
- (BOOL)isEqual:(id)object {
    if (self == object)  return YES;
    if ([self class] != [object class]) return NO;
    
    TestObject *otherObject = (TestObject *)object;
    if (![self.testString isEqualToString:otherObject.testString]) {
        return  NO;
    }
    return YES;
}
-(NSUInteger)hash {
    //在没有性能问题下,hash 方法可以直接返回一个数
    return 1227;
}

@end

在继承体系中判断等同性,还需判断是否是其子类
相同的对象必须具有相同的哈希码,但是相同哈希码的对象却未必相同

特定类型等同性判断
- (BOOL)isEqualToTestObject:(TestObject *)testobject {
    
    if (self == testobject) {
        return YES;
    }
    if (![self.testString isEqualToString:testobject.testString]) {
        return  NO;
    }
    return YES;
}
- (BOOL)isEqual:(id)object {
    
    if ([self class] == [object class]) {
        return [self isEqualToObject:(TestObject *)object];
    }else {
        return [super isEqual:object];
    }
}

比方说一个模型类的实例是根据数据库的数据创建而来,那么其中可能会含有一个唯一标识符(unique identifier),在数据库中用作主键。这时候,我们就可以根据标识符来判定等同性,尤其是此属性声明为 readonly 时更应该如此。只要标识符相等,就可以说明这两个对象是由相同数据源创建,据此断定,其他数据也相等。
当然,只有类的编写者才知道那个关键属性是什么。

要点:不要盲目的逐个检测每条属性,而是应该按照具体需求制定检测方案

4.理解 objc_msgSend

id value = [obj messageName:parameter]

obj 叫做接收者,messageName 叫做 selector,selector 和参数合起来称为消息
编译器看到此消息后,将其转换为一条标准的 C 语言函数调用

void objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

第一个参数代表接收者,第二个代表 selector(SEL是selector类型)
这是个“参数个数可变的函数”,”…“ 代表后续参数,就是消息中的参数

//Sends a message with a simple return value to the superclass of an instance of a class.
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
//Sends a message with a data-structure return value to an instance of a class.
objc_msgSend_stret(id _Nullable self, SEL _Nonnull op, ...)
//Sends a message with a data-structure return value to the superclass of an instance of a class.
objc_msgSendSuper_stret(struct objc_super * _Nonnull super,  SEL _Nonnull op, ...)
<return_type> Class_selector(id self, SEL _cmd, ...)

每个类都有一张表格,selector 的名称就是查表时所用的 key

尾调用技术:编译器会生成跳转至另一函数所需的指令码,而且不会向调用堆栈中推入新的”栈帧“

5.消息转发机制
- 因为在运行期可以继续向类中添加方法,所以编译器在编译期还无法确知类中是否有某个方法的具体实现。
- 当对象接收到无法解读的消息,就会触发“消息转发机制”,程序员可以经由此过程告诉对象如何处理未知消息

消息转发分为两大阶段

//当未知的 selector 是实例方法时的调用
+ (BOOL)resolveInstanceMethod:(SEL)sel ;
//当未知的 selector 是类方法时的调用
+ (BOOL)resolveClassMethod:(SEL)sel;

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行时加入类里面

//第一步:询问能不能把未知的消息转给其他接收者处理
- (id)forwardingTargetForSelector:(SEL)aSelector ;

若当前接收者能找到备援接收者,则将其返回,若找不到,则返回 nil
-如果返回一个对象,则运行期系统把消息转给那个对象,于是消息转发结束
如果返回 nil,执行第二步👇

//第二步:把消息相关的细节封装在 NSInvocation对象中,再给接收者最后一次机会
- (void)forwardInvocation:(NSInvocation *)anInvocation 

可以在触发消息前,先以某种方式改变消息内容,比如追加一个参数,或改换 selector,等等
如果实现此方法时,发现某调用操作不应由本类处理,则调用超类的调用方法。如果继承体系的类都不处理此调用请求,那就最后调用 NSObject 类的方法,那么该方法会执行👇方法

//抛出异常,表明 selector 未能处理
- (void)doesNotRecognizeSelector:(SEL)aSelector;
图片来自:《Effective Objective-C 》
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        //最后一个参数表示待添加方法的类型编码(type encoding)
        class_addMethod([self class], sel, (IMP)autoDictionarySetter, "v@:@");
    }else {
        class_addMethod([self class], sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return [super resolveInstanceMethod:sel];
}


SEL 是方法编号,SEL类型通过 @selector() 提取
IMP 是一个函数指针,保存了方法的地址
type encoding:
v 代表 void
: 代表 (method selector)SEL
@ 代表 object(whether statically typed or typed id);

在 setter 实现的时候给一个字典添加键值对,getter 从字典获取值,这样的实现就在 iOS 的 CoreAnimation 框架中的 CALayer 类里面。CALayer 是一种“兼容于键值编码的”容器类,能向其中随意添加属性,然后以键值对的方式访问。

6.类对象的用意
    NSString *pointerVariable = @"Some string";

pointerVariable 是存放内存地址的变量,而 NSString 自身的数据就存于那个地址中

    //对于通用的对象类型 id ,其本身已经是指针了,可以这样写
    id genericTypeString = "id String"

上面两种不同的定义方式的区别在于,如果声明时指定了类型,在实例上调用没有的方法时,编译器会发出警告。而 id 类型,编译器默认它能响应所有本项目中存在的方法。

自此,我们知道的了对象就是一个含有指向 Class 类的 isa 指针的结构体,Class 也是一个对象,它其中也有一个 isa 指针。那么类对象的 isa 指针指向哪里,类对象的父类又是什么呢?看下图

这样 OC 中类和对象的关系就清楚了。对象的 isa 指针指向类对象,类对象的 isa 指针指向 metaClass,译为元类。元类的 isa 指针指向根元类
由此可以看出来 Apple 设计类对象的用意就是为了存储对象的信息,比如对象方法,对象属性,遵守的协议,对象的类的父类,等等,而类对象的相关信息被存储在元类中。

7.理解 Objective-C 错误类型
- initWithString:(NSString *)string {
    
    if (self = [super init]) {

        if (string == nil) {
            self = nil;
        }else {
            //Initialize instance
        }
    }
    return  self;
}

NSError 的用法更加灵活,因为通过此对象,我们可以获知错误的具体信息。NSError 对象里封装了三条消息:

@{NSLocalizedDescriptionKey : @"Image data is nil"}
/* Sent as the last message related to a specific task.  Error may be
 * nil, which implies that no error occurred and this task is complete. 
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                           didCompleteWithError:(nullable NSError *)error;

这个委托方法未必非得实现不可,是不是必须处理此错误,可交由用户来处理。
NSError 的另一种常见用法:经由方法的“输出参数”返回给调用者。比如在 MJExtension 中:

/**
 *  通过字典来创建一个模型
 *  @param keyValues 字典
 *  @return 新建的对象
 */
+ (instancetype)objectWithKeyValues:(id)keyValues;
+ (instancetype)objectWithKeyValues:(id)keyValues error:(NSError **)error;

入参是一个指针,指向另一个指针,那个指针指向 NSError 对象。使用如下方式获取错误信息。

 NSError *error = nil;
 MyModel *model = [MyModel objectWithKeyValues:responseObject error:&error];
        
if (error) {
  //获取 error 信息
}

实际上,在使用 ARC 时,编译器会把方法签名中的 NSError ** 转换成 NSError *__autoreleasing *,也就是说,指针所指的对象会在方法执行完毕后自动释放。
MJ 通过以下代码把 NSError 对象传递到“输出参数”中:

// 构建错误
#define MJBuildError(error, msg) \
if (error) *error = [NSError errorWithDomain:msg code:250 userInfo:nil];

-这段代码以 *error 语法为 error 参数”解引用“,也就是说,error 所指的那个指针现在要指向一个新的 NSError 对象了。在解引用前,必须确保 error 不是 nil,因为空指针解引用会导致“段错误”并使程序奔溃。

8.理解引用计数
引用计数工作原理
- (void)retain;//使递增
- (void)release;//使递减
- (void)autorelease;//稍后清理“自动释放池”时再进行递减

查看引用计数的方法是 retainCount,不过这个方法不是很准确,苹果官方也不推荐
应用程序在生命期内会创建很多对象,这些对象相互联系着。例如表示个人信息的对象会引用表示名字的字符串对象。对象如果持有其他对象的强引用,那么前者就拥有后者。也就是说,对象想让它所引用的对象继续存活,可将其“保留”。等用完了之后再释放。
下图表示了一个对象从保留到释放的过程

图片来自Effective Objective-C

按图可以想象,有一些其他对象想要保持 B 或 C 对象存活,而引用程序中又会有另外一些对象想让这些对象存活。如果按“引用树”回溯,那么最终会发现有一个“根对象”。在 iOS 中,就是 UIApplication 对象。是在应用程序启动时创建的对象。

当对象的引用计数为 0 ,对象所占内存”解除分配“之后,就被放回”可用内存池“。此时再去调用该对象,可能会有不同情况发生:如果此时内存对象已经做了他用,就会引起程序奔溃;如果此时对象内存未被复写,就可能正常运行。由此可见,由过早释放对象而导致的 bug 很难调试。

属性存取方法中的内存管理
- (void)setFoo:(id)foo {
    [foo retain];
    [_foo release];
    _foo = foo;
}

此方法保留新值,释放旧值,然后更新变量指向新值。顺序很重要。如果先释放旧值,那此对象就被回收。后续操作就都没有意义了。

自动释放池
保留环
9.以 ARC 简化引用计数
此处没有彻底看懂,暂时先放一放
10.理解“块”这一概念

块是一种可在 C 、C++、OC 代码中使用的“词法闭包”,它非常有用,可以把一段代码像对象一样传递。在定义“块”的范围内,它可以访问到其中所有的变量。

块的基础知识
int adder = 8;
//定义一个变量名为 myBlock 的块
//语法结构:return_type (^blockName) (parameters)
int (^addBlock)(int num) = ^(int num){
        //在块内部可以使用外部变量
        return num+ adder;
 };

int addEight = addBlock(5);
块的内部结构
图片来自Effective Objective-C

块本身是对象,所以他也有内存空间。

全局块 栈块 堆块
 void (^myBlock)();
    if (/* some condition*/) {
        myBlock = ^{};
    }else {
        myBlock = ^{};
    }
    myBlock();

定义在 if 和 else 里面的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,但是等离开了相应的范围之后,编译器就可能把分配给块的内存覆写掉。这样写出来的代码时而正确时而错误。

为解决此问题,可给块对象发送 copy 消息以拷贝之。这样就把块对象从栈内存移到了堆内存中,拷贝后的块,就可以在定义它的范围之外使用了。而且,一旦复制到堆上,块对象就成了具有引用计数的对象了。后续的复制操作就只是递增引用计数了。

明白了这一点,我们只要给代码加上两个 copy 调用就安全了。

 void (^myBlock)();
    if (/* some condition*/) {
        myBlock = [^{} copy];
    }else {
        myBlock = [^{} copy];
    }
    myBlock();
void (^myBlock)() = ^{
        NSLog(@"This is a block");
    };

由于运行该块的所有信息都能在编译的时候确定,所以可把他做成全局块。
这种块不会捕捉任何状态(比如外围的变量等),可以声明在全局内存中,不需要在每次用到的时候于栈中创建。这种块实际上相当于单例。

10.熟悉系统框架
上一篇 下一篇

猜你喜欢

热点阅读