Objective-C 基本知识点

1. Cocoa 和 Cocoa Touch 的区别。

  • 相同点:都包含运行时的核心框架;
  • Cocoa:包含 Foundation 和 AppKit 框架,可用于开发 Mac OS X 系统的应用程序。
  • Cocoa Touch:包含 Foundation 和 UIKit 框架,可用于开发 iOS 系统的应用程序。

2. 直接访问实例变量和用点语法访问属性的优劣。

  • 直接访问实例变量的优势:不需要通过“方法派发系统”,所以直接访问实例变量速度更快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
  • 直接访问实例变量的劣势:①无法触发 KVO“键值观测”。 ②无法通过重写set 或者 get 方法来断点定位错误。③绕过了为相关属性所定义的“内存管理语义”。④懒加载的属性,将无法得到加载。
  • 结论:①类内部读取数据的时候,直接访问实例变量,来提高速度 ②类内部设置属性值的时候,使用点语法来写。③在需要惰性初始化的时候,使用点语法来读取属性数据。

3. 字符串存储位置

  • 如果是通过字符串常量创建,那么字符串对象存储在常量区中。
  • 如果是通过 alloc initWithFormat / stringWithFormat 创建,那么字符串对象存储在堆区中。(stringWithFormat,不加变量,小于 9 个字符,还是在常量区)

4. 协议与委托代理(https://juejin.im/post/5b69e74cf265da0f9b4e0ce3)

  • 协议(Protocol):一般被用来定义一套公用的方法接口,而不实现这些方法。

  • 委托代理 Delegate:代理实际上去一种设计模式,在 A 类中定义委托代理属性,在实例化后让 B 类对象成为其委托代理。这样就可以通过 A.delegate 来调用 B 中的方法了。

5. 分类(Category)和类扩展(Extension)(https://www.jianshu.com/p/9e827a1708c6)

分类的情况如下:

  • 分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。所以< 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性> ;
  • 分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量(编译时会报警告);
  • 可以在分类中访问原有类中.h中的属性;
  • 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类。因此在开发中尽量不要覆盖原有类;
  • 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。

类扩展的情况如下:

  • 为一个类添加额外的原来没有变量,方法和属性
  • 一般的类扩展写到.m文件中
  • 一般的私有属性写到.m文件中的类扩展中

两者的区别和联系如下:

  • 类别中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已);
  • 类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(用范围只能在自身类,而不是子类或其他地方);
  • 类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
  • 类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
  • 定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

6. block (https://www.jianshu.com/p/b74598269140)

  • 声明一个 Block:
int (^block)(int, int) = (^)(int x, int y){
  return x + y;
}
  • block 和外部局部变量的关系 正常情况下,block 可以读取但是无法改变外部局部变量的值。如果需要修改需要加上 __block 来修饰。
  • block 和全局变量的关系: block 可以在内部直接改全局变量的值。
  • 在xcode里面,可以输入inlineBlock获取Block的基本写法。
// person.h
@property(nonatomic, copy) void(^eat)(NSString *);

- (void)eatWithBlock:(void(^)(NSString *))block;

- (BlockPerson *(^)(NSString *))eatCallBack;

// person.m
- (void)eatWithBlock:(void (^)(NSString *name))block {
    block(@"哈哈哈");
}

- (BlockPerson *(^)(NSString *name))eatCallBack {
    return ^(NSString *name){
        NSLog(@"block 作为返回值");
        return self;
    };
}

//  外部调用
BlockPerson *person = [BlockPerson new];
    person.eat = ^(NSString *name){
        NSLog(@"这个是 block 作为属性");
    };
    person.eat(@"ddd");
    
    [person eatWithBlock:^(NSString *name){
        NSLog(@"这个是 block 作为参数");
    }];
    
    // block 作为返回值(返回值是 block 且 返回值block的返回值是对象本身 且 方法没有参数::::这种情况可以实现链式调用编程语法)
    [person eatCallBack](@"ddd");
    person.eatCallBack(@"ddd").eatCallBack(@"xx");

6. 栈block 堆block 全局block(https://www.jianshu.com/p/4076fc8005c7)

  • 这三种 block 说的是存储区域不同,需要注意的是栈 block 存在栈空间,创建的对象随时可能被销毁,再次调用将会造成程序崩溃。
  • ARC 模式下,系统会自动对栈 block 进行 copy,让它变成堆 block。所以在 ARC 模式下,block 用 strong 或者 copy 修饰,效果是一样的。
    void (^DemoBlock)(void) = ^{
        NSLog(@"DemoBlock"); // 内部没调用外部变量时(或者调用全局区变量),存在在全局区
    };
    NSLog(@"%@",DemoBlock); // <__NSGlobalBlock__: 0x10359d178>
    
    int a =  100;
    void (^DemoBlock2)(void) = ^{
        NSLog(@"%i",a); // 调用了外部变量,存在堆区
    };
    NSLog(@"%@",DemoBlock2); // <__NSMallocBlock__: 0x6000003bf900>
    
    void (^DemoBlock3)(void)  = [DemoBlock2 copy];
    
    NSLog(@"%@",DemoBlock3); // <__NSMallocBlock__: 0x6000003bf900>

7. 什么是 Runtime

  • 因为 Objective-C 是一门动态语言,它的会把一些工作从编译期推迟到运行期决定。也就是说只有编译器是不够的,还需要一个运行期系统(Runtime system)来执行编译后的代码,这就是 Objective-C Runtime 系统存在的意义,它是整个 Objc 运行框架的一块基石。

8. objc_msgSend 消息发送过程

  1. 拿着选IMP 去类的 cache 列表里面找,找到了就执行函数。
  2. 如果 cache 里面没找到,就去方法分发列表里面找,没找到就去父类里面找,找到就执行,并且存储到 cache 里面,方便下次调用。
  3. 如果父类 NSObject 都没有找到,就要方法动态解析阶段(重定向,转发,动态绑定)

9. 用前缀避免命名空间冲突

  1. Objective-C 里面没有内置的命名空间概念,所以我们只能变相使用前缀来尽量减少命名冲突(naming clash)。
  2. 如果自己开发的库使用到了第三方库,应该为使用的第三方库都加上自己的命名前缀。
  3. 默认情况下,苹果保留了 2 个字符的前缀,所以我们开发尽量使用三个字符的前缀。

10. 尽量使用不可变对象

  1. 尽量创建不可变对象。
  2. 如果对象外部只读,内部可修改,可以在类扩展上重新把 readonly 属性定义为 readwrite 属性。
  3. 对象中如果有可变的 collection ,尽量不要直接把 collection 暴露出去,而是添加额外的修改方法来供给别人调用。

11.为私有方法添加前缀

  1. 尽量给私有方法添加前缀,使得和共有方法区分开来。
  2. 不要使用单个_(下划线)来作为私有方法前缀,因为这是苹果的默认做法,容易造成覆盖一些苹果自带的方法,导致不可预期的 bug。

12.异常处理

  1. 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
  2. 在错误不那么严重的情况下,可以指派“委托方法”(delegate method)来处理,也可以把错误信息放在 NSError 对象里,经由“输出参数”返回给调用者。

13.分类 Category

  1. 把封装数据所用的全部属性都定义在主接口里。
  2. 在 ”class-continuation分类“之外的其他分类中,可以定义存取方法,但是尽量不要定义属性。

14. ”class-continuation 分类“

  1. 通过 ”class-continuation 分类“ 向类中新增实例变量。
  2. 如果某属性在主接口中声明为”只读“,而类的内部又要用设置方法修改此属性,那么就在”class-continuation 分类“中将其扩展为”可读写“。
  3. 把私有方法的原型声明在”class-continuation 分类“里面。
  4. 若想使类所遵循的协议不为人所知,则可于”class-continuation 分类“中声明。

15. 协议 Protocol

  1. 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的 id 类型,协议里规定了对象所应实现的方法。
  2. 使用匿名对象来隐藏类型名称(或类名)
  3. 如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特定方法,那么可以使用匿名对象来表示。

16. 内存管理

  1. 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为 1 。 若保留计数为正,则对象继续存活。当保留计数为0 时,对象就被销毁了。
  2. 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

17. ARC的好处

  1. 有了 ARC 之后,程序猿就无须担心内存管理问题了。使用 ARC 编程,可省去类中许多”样板代码“。
  2. ARC 管理对象生命周期的办法基本上就是:在合适的地方插入”保留“ 以及 ”释放“ 操作。在 ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来需要手工执行”保留“以及”释放“操作。
  3. 由方法所返回的对象,其内存管理语义总是通过方法名来提醒。ARC 将此确定为开发者必须遵守的规则。
  4. ARC只负责管理 Objective-C 对象的内存。尤其要注意:CoreFoundation 对象不归 ARC 管理,开发者必须适时调用 CFRetain、CFRelease。

18. dealloc 方法里面可以做哪些事情,不该做哪些事情。

  1. 在 dealloc 方法里面,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的”键值观测“KVO 或者 NSNotificationCenter 等通知,不要做其他事情。
  2. 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其他使用者约定:用完资源后必须调用 close 方法。
  3. 执行异步任务的方法不应在 dealloc 里调用;只能在正常状态下执行哪些方法也不应在 dealloc 里调用,因为此时对象已经处于正在回收的状态了。

19. weak 的由来和作用

  1. 将某些引用设置为 weak,可以避免出现”保留环“。
  2. weak 引用可以自动清空,也可以不自动清空,自动清空(autonilling)是随着 ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
  3. 对比 unsafe_unretained 属性,这个属性就容易造成访问到已经释放的地址,而导致程序崩溃。

20. 自动释放池

  1. 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池子里。
  2. 合理运用自动释放池,可以降低应用程序的内存峰值。
  3. @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。

21. 僵尸对象(NSZombieEnable)

  1. 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量 NSZombieEnable 可开启此功能。
  2. 系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够相应所有的选择子,相应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

22. 使用 typedef 重新定义块类型

  1. 以 typedef 重新定义块类型,可令块变量用起来更加简单。
  2. 定义新类型时应遵从现有的命名习惯,勿使得其名称与别的类型相冲突。
  3. 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应的 typedef 中的块签名即可,无需修改其他 typedef。

23. 编程思想:块 handler 的使用

  1. 在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
  2. 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入对象来切换,而若改用 handler 块来实现,则可以直接将块与相关对象放在一起。
  3. 设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可以通过此参数来决定应该把块安排在哪个队列上执行。

24. 编程思想:块容易造成保留环

  1. 如果块所捕获的对象直接或者间接地保留了块本身,那么就得当心保留环的问题。
  2. 一定要找个适当的时机解除保留环,而不能把责任推给 API 的调用者。

25. NSOperation 优于 GCD 的地方

  1. 具有取消操作。
  2. 需要指定多个操作之间的依赖关系。
  3. 需要通过键值观测机智监控 NSOperation 对象的属性。
  4. 需要指定操作的优先级。
  5. 重用 NSOperation 对象。

26. GCD 相关

dispatch_group_wait : 如果想令数组中的每个对象都执行某项任务,并不想等待所有任务执行完毕,那么就可以使用这个 GCD 特性来实现。

dispatch_group_notify : 若当前线程不应阻塞,则可以用 notify 函数来取代 wait ;

27. GCD 相关

经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过 GCD 所提供的 dispatch 函数,就很容易实现此功能。

标记应该声明在 static 或 global 作用域中,这样的话,在只需执行一次的块传给 dispatch_once 函数时,传进去的标记也是相同的。

28. GCD 相关

你可能感兴趣的:(Objective-C 基本知识点)