美文网首页
Effective Objective-C 学习总结

Effective Objective-C 学习总结

作者: 就是奶牛君 | 来源:发表于2019-10-29 16:50 被阅读0次

0. 前言

  • 这是一本传说中不看完不让转正的书。瑟瑟发抖系列。
  • 并不适合作为入门书籍,更像是实战经验总结。

1. 熟悉OC

1.1 尽量少在头文件中import

  • 减少编译时间
  • 避免循环引用,如果A B互相import,虽然不会造成死循环(不是include),但是两个文件里面有一个会编译失败
  • 协议尽量单独拆分,方便别的文件进行引用
  • 可以使用@class进行向前声明

1.2 多用语法糖(字面量语法)

use:     @(1);
instead: [NSNumber numberWithInt:1];

类似的语法有:
NSString *str = @"1";
Person *person = people[0];
NSDictionary *dict = @{@"key":@"value"};

1.3 使用常量类型,少用Define

  • define有被重复覆盖的可能
  • define没有类型检查,即使被更改了类型也不会有警告

how to use:

  • 在头文件中extern const
  • 在.m中定义
  • 使用k开头,与一般变量/常量进行区分
xxxxx.h
extern NSString *const kGlobalA;

xxxxx.m
NSString *const kGlobalA = @"test string";

2. 对象 消息 运行时

2.1 消息转发机制

在OC中,如果向某个对象传递消息,使用的是动态绑定技术

  1. msg_send
  • 实例接受消息
  • 当前不能处理,isa寻找父类
  • 遵循继承链都不能处理,做消息转发(forwarding)【- +方法都可以进行转发】

2.2 Method Swizzling

通过交换IMP,将selector映射到不同的方法实现上。

//取出方法:
class_getInstanceMethod(Class class, SEL selector)

//交换方法:
class_exchangeImplementations(method1, method2)

2.3 理解类对象

  • 对象是分配在堆空间上的,一个“*”代表一个内存地址
  • 对象不能分配在栈空间上,例如以下就是非法的:
  // 尝试分配在栈空间
  NSString str = [NSString xxxxxx];
  • id可以指代任意对象,本身就是一个指针

3. 接口与API设计

3.1 使用前缀来避免命名冲突

  • 重名时,app的链接过程会报错。
  • 重名发生在动态链接时,会引发crash
  • 可以使用适当的前缀来避免库之间、app之间的重名问题。

需要注意的是,Apple保留其使用双字幕前缀的权利,所以我们最好选用三字母作为前缀
eg: 用项目名称的简写来命名,QQXXXViewController QRXXXXXView等

使用前缀的好处:

  • 避免重名引发的编译问题/运行时问题
  • 在回溯问题时,可以比较方便地定位代码块

3.2 重写对象的description方法

  • 直接NSLog打印对象/在断点时po对象,看不到具体的属性信息
  • 重写description方法,格式化输出对象信息
  • 当然,最好保持原有的打印体验
// for NSLog
- (NSString *)description {
    return [NSString stringWithFormat:@"<%@: @p, %@>
        [self class], // 类名
        self,         // 地址
        @{@"title" : self.title,
          @"name"  : self.name,
          ...}
    "];
}
// for po in lldb
- (NSString *)debugDescription {
    // detail description
}

3.3 尽量使用不可变对象

声明property时:

  • 在.h中时指定 readonly
  • 在.m中使用 raedwrite

3.4 使用合理的方法命名

  • 清晰
  • 可以稍长,但是不要啰嗦
// better
- (CGFloat)area;

// worse
- (CGFloat)calculateTheArea;
  • 范围值为BOOL时,方法名一般以is或者has开头
- (BOOL)isEqualToString:
- (BOOL)hasPrefix:
  • 方法中尽量不要使用缩略词,除非是默认熟知的词汇(eg,MD5)

3.5 为私用方法名加上前缀

Apple喜欢用单下划线作为私用方法的前缀。因此,我们应该尽量避免以“_”作为方法的开头

  • 便于区分方法是否被外部调用(如果方法实现有调整,被外部使用的方法需要全量过一遍,对减包来说同理)

3.6 理解OC的错误模型

ARC下不是“异常安全的(Exception Safe)”
如果抛出异常,那么本应在作用域末尾释放的对象就不会自动释放了
如果想要异常安全,需要加上编译选项 -fobjc-arc-exceptions

  • 异常应该只作用于极其严重的错误(比如放在某个重要基类的init方法中)
  • OC中处理一般错误,可以返回nil/0,业务层做校验。一些比较重要的路径,可以asset一下。
  • 对于一些自定义类,在处理错误的时候,可以使用delegate将错误信息返回给调用者

3.7 理解NSCopying

  • 如果想让自己实现的类有copy操作,需要实现NSCopying协议。该协议只有一个实现方法:
- (id)copyWithZone:(NSZone *)zone;
  • 当然,也有可变版本的mutableCopy方法:
- (id)mutableCopyWithZone:(NSZone *)zone;
  • 可以根据自己的需要实现不同的协议

需要注意的是,Zone这个概念是个历史遗留问题。在以前的开发时,会将内存分为不同的区域(Zone),而对象会创建在特定的区域中。现在只有一个区(默认区,default zone)的概念。现在仍使用这个协议进行copy的重写,但是无需关心zone的概念了。

4. 分类与协议

4.1 使用委托与数据源/协议进行对象间通信

  • delegate对象需要使用weak修饰(使用strong会造成retain cycle)
  • 类内部调用委托方法进行信息传递的时候,最好使用一下respondsToDelegate:,判断一下delegate是否真的实现了这个协议方法,防止crash

4.2 使用分类来分离臃肿的代码

  • 将类的方法划分成易于管理的小模块
  • 可以覆盖主类的方法实现
  • 不同分类间的方法可以重名,后编译的会被调用

4.3 使用分类来扩展第三方类时加上前缀

4.2 中所述,通过加上自己的前缀,可以避免方法/分类名重复

@interface NSString(ZK_Debug)
// some code
@end

4.4 勿在分类中声明变量

分类中无法声明新的属性,这与分类的实现是息息相关的。

// 分类结构体 源码实现
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。

一些iOS底层的知识,例如对runtime、category、block、runloop等的详细解析,推荐@xx_cc在简书上的这一系列文章
https://www.jianshu.com/notebooks/24110540

  • 不要在分类中添加属性
  • 可以在主类中定义,分类中使用

5. 内存管理

OC的内存管理是基于引用计数机制来实现的。有了ARC之后,几乎所有的内存管理工作都交给了编译器,开发者可以更关注于业务逻辑。
从MacOS 10.8开始,垃圾收集器(garbage collector)被正式废弃;对iOS来说,这个概念是从未被引入的。

5.1 理解引用计数

  • retain 递增引用计数
  • release 递减
  • autorelease 等autorelease pool来进行递减操作

当引用计数为0时,会将对应内存标记为reuse,所有指向对象的引用都无效

  • 计数为0,内存不一定立即被回收。
  • 如果在执行后续代码时,未覆写对象内存,不会crash
  • 一般在release后执行一下置空操作
[object release];
object = nil;

5.2 在dealloc中释放引用&解除监听

[[NSNotificationCenter defaultCenter] removeObserver:self];

5.3 善用Autorelease Pool降低内存峰值

  • App生命周期中会自动持有一个autorelease pool
  • 在遇到for里大量创建临时对象时,可以使用autorelease pool,来降低内存峰值
for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        // code here
    }
}

5.4 使用Zombie Object来调试内存管理问题

启用Zombie Object后,在运行期,系统会将所有已经收回的实例转化为僵尸对象,而不会重新回收它们;这种对象的内存地址不会遭到覆写。在向僵尸对象发送消息时,会抛出异常,App crash。

实现方式:

  • 通过对dealloc方法进行method swizzle。
  • 目的是为了保留原类名(LOG的时候能区分出来具体是哪个类)

原理:

  • 类似KVO,系统会自动创建以_NSZombie_开头的类,通过修改isa来指向新类
  • 新类会将整个类结构copy一份
  • 新类会响应原有类的所有selector,但是会打印出消息内容,app crash

6. Block & GCD

Block是可在C、C++、OC中使用的语法闭包,开发者可以将代码像对象一样传递。

GCD是一种与Block有关的技术,基于dispatch queue,提供对线程的抽象

6.1 理解block

// block的定义
return_type(^block_name)(paramters) {
    //code here
}

以下是一个实例:

// define
int (^addBlock)(int a, int b) = ^(int a, int b) {
    return a+b;
}

// imp
int sum = addBlock(a, b);
  • 在block声明范围内的所有变量,均可以被block捕获;但是如果需要在block内改变它的值,则需要加上__block前缀。
  • 要注意避免retain cycle,对于self来说,可以定义__weak
  • block可以分配在栈或堆上, 也可以是全局的
  • 栈上的block可以通过copy,移至堆中。

6.2 为常用block进行typedef

如果我们有网络框架,回包数据通过block传递给上层,我们可能会这么写:

- (void)requestWithCompletion:(void(^)(NSData *data, NSError *error));

如果封装了若干个方法,回包结构又是一致的,我们可以typedef一下block,使方法看起来可读性更强。

typedef void(^completeBlock)(NSData *data, NSError *error);

- (void)requestWithCompletion:(completeBlock)completeBlock;
  • 增强代码可读性
  • 后续如果需要扩展block,直接修改定义的地方,可以通过触发编译错误的方式,快速定位所有需要修改的方法实现。

6.3 使用block来降低代码分散程度

对比的对象是delegate
如我们熟悉的tableView和早期的UIAlertView组件,都是使用delegate来完成一些相关逻辑的。
缺点在于,组件创建、展示和业务处理的代码,是分离开的。且同一种类型对象的业务逻辑,需要在delegate中使用if-else进行区分,久而久之代码比较臃肿。

  • 开发者可以主动选择在什么线程上执行代码
  • 在创建的时候声明后续业务逻辑,更完整也更简洁

6.4 多用GCD,少用同步锁

在多线程问题的时候,往往需要对一些数据进行“加锁”。

  • @synchronize()
  • NSLock
  • NSRecursiveLock
  • ...

可以使用GCD来进行优化。(对synchronize来说,不滥用即可)

  • 使用串行同步队列
  • 混合使用同步和异步GCD
  • 加入队列和barrier,可以掌控同步时序,提升效率

6.5 掌握GCD及NSOperationQueue的使用时机

GCD:

  • GCD并不是永远都是最佳解决方案
  • GCD是基于C的API

NSOperationQueue:

  • NSOperationQueue是比较重量级的封装,是OC对象。
  • 面向对象封装,使用起来更直观简单
  • 底层使用GCD实现
  • 在各种设置项上更简单(线程池,并发数,线程优先级)

需要注意的是,NSNotificationCenter内部是使用NSOperationQueue的。

6.6 使用dispatch group

// 创建
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// mission1
dispatch_group_enter(group); //group引用计数+1
dispatch_async(queue, ^{
    //code here
    dispatch_group_leave(group); //group引用计数-1
});

// mission2
dispatch_group_enter(group);
dispatch_async(queue, ^{
    //code here
    dispatch_group_leave(group);
});

使用dispatch_group后,以下是两种继续执行的方式:

  • 当前线程等待,直到group全部执行完再往下执行
//...
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//code here...
  • 当前线程继续往下执行;当group全部执行完成后,执行block
dispatch_group_notify(group, queue, ^{
    // code here...
});

6.7 dispatch_once 只执行一次的线程安全代码

最典型的代表:【单例】

+ (instancetype)sharedInstance {
    static QRAccountManager *__QRAccountManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        __QRAccountManager = [QRAccountManager new];
    });
    return __QRAccountManager;
}
  • dispatch_once_t类似token,可以唯一标识block
  • 使用dispatch_once的效率是@synchronize的两倍

6.8 不要使用dispatch_get_current_queue

  • iOS6.0起已经废弃
  • 该函数的行为往往与预期不一致
  • 可以做调试,但是尽量避免使用。

7. 系统框架

7.1 熟悉系统框架

  • Foundation
    • 为什么是NS开头?将OC用作是NeXTSTEP操作系统(乔老板被赶出去建立的公司的产物,OSX的前身)的编程语言时确定的
  • CoreFoundation:在里面,很多Foundation的功能可以找到对应的C语言API
  • CFNetwork:对BSD socket的封装,提供网络接口
  • CoreAudio
  • AVFoundation
  • CoreText:文字渲染和排版

在使用OC时,会经常用到C语言级别的API。好处是可以绕过OC的运行时,提升执行速度;缺点是,使用C语言级别API,需要自己管理内存。

一些其他的框架:

  • Appkit
  • UIKit
  • CoreAnimation
  • CoreGraphics
  • QuartzCore
  • MapKit
  • SceneKit
  • CoreML
  • ...

7.2 多用Enumberator,少用for

  • 几乎所有的collection都可以用这个套API
  • 有一些快速遍历的接口(FastEnumberator)
  • 本身就可以通过GCD来进行并发操作

7.3 toll-free bridging

toll-free bridging,无缝桥接,是Foundation和CoreFoundation之间的粘合剂。使用这个技术,可以将两个框架之间的对象进行转换

NSArray *anNSArray = @[@"1", @"2", @"3"];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray; // OC -> C
  • __bridge表示ARC仍具有OC对象的使用权
  • __bridge_retain则表示ARC需要交出OC对象的控制权,开发者自己使用CFRelease()来手动释放
  • __bridge_transfer,CFArrayRef --> NSArray

7.4 构建缓存的时候选用NSCache而非NSDictionary

  • NSCache在系统资源紧张的时候,会自动删除缓存(Dict则需要在内存警告中进行重写)
  • NSCache会优先删除最久未使用的对象
  • NSCache本身是线程安全的

开发者是可以手动控制缓存删除内容的时机:

  • 缓存中的对象总数
  • 所有对象的总开销

想通过调整“开销”来迫使缓存优先删除某对象,不是个好主意。
不想要这种不确定性,还是用dict,手动管理好内存峰值比较合适

_cache = [NSCache new];
_cache.countLimit = 100; // 对象上限
_cache.totalCostLimit = 5 * 1024 * 1024; // 开销上限,5M

7.5 精简initialize与load

+(void)load;

  • App启动时,对于运行时的每个class和category,必定会调用此方法
  • 仅调用一次
  • 执行子类的 +load 前,必定会执行父类的 +load
  • 原类的优先于分类执行
  • 并不完全遵从继承规则(子类没有实现 +load ,各级超类的 +load 不会执行)

实现的时候,应该精简一些:

  • 会在App启动的时候执行
  • 尽量不要使用锁
  • 不要在里面使用别的类,因为有可能还没有被加载

+(void)initialize;

  • 程序首次使用某个类前调用(惰性调用,使用类时才调用)
  • 仅调用一次
  • 该方法是线程安全的
  • 如果子类没有实现,会自动调用父类的方法(如果父类实现了)
  • 一般在initialize里面加上if (self == [XXXClass class]),来特定

实现的时候,也应该精简一些:

  • 不好掌控触发时机
  • 不宜使用锁,避免阻塞线程

相关文章

网友评论

      本文标题:Effective Objective-C 学习总结

      本文链接:https://www.haomeiwen.com/subject/ppsgvctx.html