Effective Objective-C 学习总结

2019-10-29  本文已影响0人  就是奶牛君

0. 前言

1. 熟悉OC

1.1 尽量少在头文件中import

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

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

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

1.3 使用常量类型,少用Define

how to use:

xxxxx.h
extern NSString *const kGlobalA;

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

2. 对象 消息 运行时

2.1 消息转发机制

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

  1. msg_send

2.2 Method Swizzling

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

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

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

2.3 理解类对象

  // 尝试分配在栈空间
  NSString str = [NSString xxxxxx];

3. 接口与API设计

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

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

使用前缀的好处:

3.2 重写对象的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时:

3.4 使用合理的方法命名

// better
- (CGFloat)area;

// worse
- (CGFloat)calculateTheArea;
- (BOOL)isEqualToString:
- (BOOL)hasPrefix:

3.5 为私用方法名加上前缀

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

3.6 理解OC的错误模型

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

3.7 理解NSCopying

- (id)copyWithZone:(NSZone *)zone;
- (id)mutableCopyWithZone:(NSZone *)zone;

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

4. 分类与协议

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

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 理解引用计数

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

[object release];
object = nil;

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

[[NSNotificationCenter defaultCenter] removeObserver:self];

5.3 善用Autorelease Pool降低内存峰值

for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        // code here
    }
}

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

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

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;

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

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

6.4 多用GCD,少用同步锁

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

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

6.5 掌握GCD及NSOperationQueue的使用时机

GCD:

NSOperationQueue:

需要注意的是,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后,以下是两种继续执行的方式:

//...
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//code here...
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;
}

6.8 不要使用dispatch_get_current_queue

7. 系统框架

7.1 熟悉系统框架

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

一些其他的框架:

7.2 多用Enumberator,少用for

7.3 toll-free bridging

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

NSArray *anNSArray = @[@"1", @"2", @"3"];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray; // OC -> C

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

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

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

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

7.5 精简initialize与load

+(void)load;

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

+(void)initialize;

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

上一篇 下一篇

猜你喜欢

热点阅读