《Objective-C学习总结》

本文详细探讨Objective-C的runtime机制,包括类与对象、成员变量与属性、方法与消息、协议与分类等核心概念。同时,深入剖析RunLoop的工作原理、模式以及在iOS系统和开发中的应用。此外,还涵盖了Block的使用、深复制与浅复制的区别以及NSNotification的实践。通过对这些概念的深入理解,提升Objective-C编程能力。
摘要由CSDN通过智能技术生成
学习要点
    • runtime(√√√√√√√√√√√√√√√√√√)
    • runloop(√√√√√√√√√√√√√√√√√√)
    • category(√√√√√√√√√√√√√√√√√√)
    • protocol(√√√√√√√√√√√√√√√√√√√)
    • extension(√√√√√√√√√√√√√√√√√√√)
    • property(√√√√√√√√√√√√√√√√√√√√)
    • 深复制与浅复制(√√√√√√√√√√√√√√√√)
    • NSNotification(√√√√√√√√√√√√√√√√√√)
    • Block(√√√√√√√√√√√√√√√√√√√√√√√√√)
    • KVO&KVC(√√√√√√√√√√√√√√√√√√√√√)
    • 多态性(√√√√√√√√√√√√√√√√√)
    • 继承(√√√√√√√√√√√√√√√√√√√√)
    • 垃圾回收机制(√√√√√√√√√√√√√√√√)
    • 异常处理(√√√√√√√√√√√√√√√√√√√)
    • 多线程()



Runtime:

  • 作用
  • 原理
  • 实战应用
  • 参考文章

(1)作用
  • OC最开始是参考SmartTalk,而且smartTalk是一门动态语言,所以OC可以理解为在C的基础上扩展了runtime库,从而实现具有面向对象性。
  • runtime是如何实现让OC具备面向对象性?
    • 首先面向对象的一大特点就是把方法与属性捆绑在一起形成对象,所以OC就通过runtime,把C语言的结构体与函数捆绑到一起,到OC代码层我们是直观地编码一门面向对象语言,但实际上就是通过runtime在动态地调用C中结构体中的变量与相应的函数指针实现效果。
  • runtime的实际主要做了什么事情?
    • 封装:在runtime的封装下,对象的属性可以用结构体表示,而方法可以用C函数实现,并让程序运行时能实现创建、检查、修改类、对象方法。
    • 消息传递:通过objc_sendmsg(),找到相应的函数指针与数据进行运算并返回结果。


(2)原理

  • 类与对象
  • 成员变量与属性
  • 方法与消息
  • 协议与分类
  • …...



  • 类与对象:
          由于在Runtime中,类与对象的表示实际上就是结构体,因此我们得从结构体的组成的角度去探索类与对象在OC的实现机制。
 
类在runtime中的表示:
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;                                            // isa指针用于指向自身属于的类,在OC中对象-》类对象-》元类,所以isa指针在这里指向原类 

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;  //指向父类的指针
    const char *name                                         OBJC2_UNAVAILABLE;  //类名
    long version                                             OBJC2_UNAVAILABLE;  //类版本
    long info                                                OBJC2_UNAVAILABLE;  //类信息
    long instance_size                                       OBJC2_UNAVAILABLE;  //实例的控件大小
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;  //变量列表指针
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;  //方法列表指针
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;  //缓存指针
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;  //协议指针
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

对象在runtime的表示:
/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object * id ;
关于isa、super_class指针与类对象、元类的概念:

isa指针:存储着对象或者类所属的类型的指针,对象-》类对象,类对象-》元类。
super_class指针:指向父类的指针,若为顶层的跟类则为NULL
类对象与元类:对象-》类对象-》元类,这是在OC中的一种独特的组织方法,但类对象与元类在runtime中,本质上都是 objc_class结构体,区别是类对象管理动态方法,元类管理静态方法。
因此若我们用一个类调用类方法,如[NSDictionary dictionary];,在runtime的表现实际上就是objc_sendmsg(NSDictionary, @selector(dictionary));就是通过NSDictionary类对象的isa指针找到其对应的元类,并找元类的方法列表中找到相应的静态方法指针,并执行相应的代码。
那么一个类的元类的isa指针又会指向哪里?元类的isa指针会直接指向跟类(这里是NSObject的meta_class),而跟类的isa指针会自指形成闭环。
那么一个跟类的super_class的指向又会指向哪里?从下图我们可以发现root class的super指向了nil,而root meta的super指向了root class,那么最终还是指向了nil。


根据上面图,我们就可以很清楚每当发送一个消息,这个消息在对象、类对象、元类中的传递与走向。

关于super指针的调用实现:

关于oc中super指针容易造成一个误解,就是super指针是直接指向父类的,但并不是,如下
struct objc_super { id receiver; Class superClass; };
根据runtime中super指针的结构体,我们可以发现消息的接收者还是对象本身,它是通过自己的superClass指针来访问父类的。

关于instance_size的对象控件创建的实现:

instance_size用于记录创建该类对象的实例需要多大的内存空间,但根据OC的继承规则,这里会有个疑问,到底instance_size是指这个类所需的全部空间,还是需要把对象的继承体系中的所有instance_size相累加才是所需的控件大小?
答案是,每个类对象的instance_size已经是创建对象所需的全部存储空间,那时因为在创建一个子类时,即使我们不用调用父类的构造方法,也能访问从父类中继承回来的属性。

关于cache指针的实现:

OC是门动态语言,那么每调用一次方法则需要一次发送消息的行为,这样相比静态语言的C、C++直接调用函数对应的指针回慢不少,毕竟中间还穿插着一个runtime寻找对象的实现。那么如何能优化这个过程。
OC采用了缓存的策略,chahe指针的定义如下:
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

#define CACHE_BUCKET_NAME(B)  ((B)->method_name)
#define CACHE_BUCKET_IMP(B)   ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>
2 ) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>
3 )) & (mask))
#endif
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;      //
    unsigned int occupied                                    OBJC2_UNAVAILABLE;     //
    Method buckets[
1 ]                                        OBJC2_UNAVAILABLE;    //
};

关于Method的定义:
typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;  //SEL方法子,方法名经过hash后得到的一个字符串,与IMP是一一对应关系
    char *method_types                                       OBJC2_UNAVAILABLE;  //方法的类型,动态方法/静态方法
    IMP method_imp                                           OBJC2_UNAVAILABLE;  //IMP指针,指向具体的C实现函数
}      
每当对一个对象或者类对象发送消息时,就会先访问其cache的方法列表里是否有相同的Method,有则直接调用其IMP实现调用。
PS:iOS中的方法名字是以字符串为准的,在同一个类中,即使字符串同名参数不同也会编译不过,那时因为sel的原理就会把方法名进行hash生成唯一的字符串,所以在比较的时候,实际上只比对指针地址。

关于ivar_list指针的实现:

ivar_list实际上就是指向实例变量的指针。下面是runtime中的定义代码:
struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;   //变量的数量
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;   //数组,数组单元为 objc_ivar结构体
}          
                                                  OBJC2_UNAVAILABLE;
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
struct objc_ivar {
   
char *ivar_name                                          OBJC2_UNAVAILABLE;    //变量名
    char *ivar_type                                          OBJC2_UNAVAILABLE;    //变量类型
    int ivar_offset                                          OBJC2_UNAVAILABLE;    //变量的内存位置偏移量
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}      
首先访问一个对象本质上就是对其内存地址进行读写操作,而对象runtime的表示就是一个isa指针,而在内存的表示就是isa指针指向的地址+各种实例变量的存储。
所以当我们访问一个对象,实际上就是通过 objc_ivar_list获取到相应的 objc_ivar,然后通过其 ivar_offset计算出实际的内存地址,计算方法为isa指针地址+ivar_offset。

关于method_list指针的实现:

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;   //废弃方法列表

   
int method_count                                         OBJC2_UNAVAILABLE;   //方法总数
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;   //方法列表数组
}                                                            OBJC2_UNAVAILABLE;

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
   
SEL method_name                                          OBJC2_UNAVAILABLE;
   
char *method_types                                       OBJC2_UNAVAILABLE;
   
IMP method_imp                                           OBJC2_UNAVAILABLE;
}       
当在类对象或者元类中查询方法,实际上就是对 objc_method_list进行操作,通过遍历 objc_method_list方法列表通过SEL找到对应IMP执行调用,以IMP的返回作为返回结果,同事把对应的 objc_method缓存在cache中。
其中 struct objc_method_list *obsolete负责管理与维护准备废弃的方法。

关于protocol指针的实现:

struct objc_protocol_list {
   
struct objc_protocol_list *next;
   
long count;
    Protocol *list[
1 ];
};

#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif
protocol的本质也是一个方法列表,实现与动态方法与静态方法是类似的,不同的是在类中它专门由 objc_protocol_list指针管理,这样就可以加快方法查询的效率。

关于property属性的实现:

typedef struct objc_property *objc_property_t; 

typedef struct { 
    const char *name;           // 特性名 
    const char *value;          // 特性值 
} objc_property_attribute_t; 
property的本质就是实例变量+getter&setter,通过设置修饰词 objc_property_attribute_t,赋予getter&setter不同的实现效果。

类型编码(Type Encoding):
每一个函数的参数与返回的类型,都可以通过 @encode返回其编码,这个编码为类型编码,系统就是通过这些类型编码来识别变量的类型的。
如OC不支持long double,但我们依然可以使用long double进行变量的定义,但在底层实现中其 @encode返回结构与double是一致的,所以分配的存储空间大小也与double是一致的。

关联对象:
实现思路:通过
objc_getAssociatedObject
objc_setAssociatedObject
方法,把静态字符串(keyName)与相应的实例变量联系到一起。
经典应用:为category动态添加属性。

关于category的实现:

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;   //category名
    char *class_name                                         OBJC2_UNAVAILABLE;   //类名
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;   //实例方法列表
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;   //类方法列表
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;   //协议方法列表
}                                                            OBJC2_UNAVAILABLE;
从category的runtime表示中可以大概知道,在编译期间会把category中的实例方法、类方法、协议方法写入相应类对象或元类的实例方法、类方法、协议方法,根据OC的继承体系与消息发送规则,可知,这样若有与父类同名的方法,就会被子类拦截掉,从而实现重写。

消息转发的机制:

当一条消息在其对象的继承体系中找不到相应的方法调用,这会启动消息转发机制,若消息转发机制都无法处理,则会触发sigforbid的错误,程序奔溃。

消息转发机制的三个步骤:
  1. 动态方法的解析:(允许动态添加实例方法/类方法,处理未知消息)
  2. 备用接收者:(把消息转发到具备处理该消息的别的对象里)
  3. 完整转发:()

动态方法的解析:

类通过实现 +resolveInstanceMethod:(实例方法)或 者+resolveClassMethod:(类方法)。方法可以为未知消息添加相应的处理方法。

备用接收者:

若动态方法解析失败,则会调用以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
同时通过该方法可以实现类似多继承的效果。

完整转发:

如果上两个步骤都无法处理未知消息,则启动完成的消息转发机制。有以下两个关键方法需要重写:

- ( void )forwardInvocation:( NSInvocation *)anInvocation;     
- ( NSMethodSignature *)methodSignatureForSelector:( SEL )aSelector;
对象会创建 NSInvocation对象,里面包含消息处理的所有参数,并且我们能修改相应的参数,通过其实现相应的消息转发,并且消息转发的处理结果会作为原始发送者的处理结果。
其中 methodSignatureForSelector一定要重写,因为 aninvocaaion中有个关键参数signature,需要重写该方法来定义,才能正常地生成 aninvocaaion。

(3)实践应用

因为根据runtime给OC带来的动态性,以及其的调用规则,我们能灵活地添加、删除、改变方法的实现,能实现很多特殊的功能与效果。
首先可以看看runtime提供的函数类别:

即可得知它大概能实现什么功能,灵活的runtime function能让我们实现功能:

Method swizzling:
通过改变SEL所对应的IMP,从而实现方法的统一修改。
JSPatch:
通过消息转发机制,实现通过JS往OC中动态添加、修改方法的实现。
多继承:
通过消息转发机制,实现类似多继承的效果
为Category添加属性:
在编译期,category不允许自己创建属性,但通过runtime可以实现。
插件:
不少的xcode插件都是通过利用runtime在运行时注入代码实现的。
万能控制器跳转:
自动归档与解档:
本来写归档与接档程序,常规写法就是一个个属性手动写入,但这样会写入大量无聊的重复代码,通过runtime的函数可以大大简化这个过程。
通过class_copyIvarList函数取出相应对象的属性,从而获得ivar_name&ivar_value从而实现归档,通过这种方式能实现像遍历NSDictionary与NSArray一样地去遍历一个对象。
对象转model
实现方法同上。
未完待续。。。

(4)参考文章


RunLoop:

  • 作用
  • 原理
  • iOS系统用例
  • 程序开发用例
  • 参考文章

(1)作用:
Runloop本质上是一个对象,用于实现线程保活,让线程在需要处理任务的时候激活并处理任务,在无任务时进行休眠状态。
所以RunLoop是为了更好地管理线程的运作,优化硬件资源的使用。

(2)原理:

一般来说线程只能接受一个任务,在任务完成后就会退出,但通过下面的代码,即可让线程一直存在。
function loop ( ) {
     initialize ( ) ;
     do {
         var message = get_next_message ( ) ;
         process_message ( message ) ;
     } while ( message != quit ) ;
}
所以Runloop的原理也与上一致。

在iOS中Runloop在与线程的关系是一一对应关系,但除了主线程是默认开启Runloop之外,其他的线程都是处于不开启的状态,但已经创建,需要用户去主动获取并运行。

RunLoop的相关类:

其中这里有以下重要的类,RunLoop、CFRunLoop、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopOberverRef 。
  • RunLoop
    • 是由CFRunLoop的基础上封装得来的。
  • CFRunLoopModeRef
    • 用于描述与封装事件源的类
    • 同一时间Runloop只能配置一个mode,若需要切换mode,需要退出在重新设置
    • mode items(事件源的种类)
      • CFRunLoopSourceRef:通用事件源(包括硬件信息、线程之间的交流等等)
        • source0:(只包含回调指针)
        • source1:(包含mash port 与回调指针)
      • CFRunLoopTimerRef:基于时间的触发器事件源
      • CFRunLoopOberverRef:监听Runloop的节点事件而触发的时间源


关于source0与source1的区别:

source0需要手动触发,触发过程如下:
  1. CFRunLoopSourceSignal(source),标记为待处理
  2. CFRunLoopWakeUp(runloop),唤醒相应线程已处理source0事件源
  3. runloop调用source0的callback IMP

source1基于mash port,是自动触发的
mash port用于实现线程或进程之间的交流,一旦某个注册了mash port的runloop线程会自动active来处理相应的事件源头。
具体的mash port原理下面再阐述。

关于CFRunLoopTimerRef事件源头的补充说明:

但timer事件源被注册成功后,其中就是注册了相应的时间节点,timer事件源能主动触发runloop,但却不是保证事件源能在准确的注册时间点上运行,而且错过时间点的事件源会被忽略,直接执行下一个timer事件源,具体原理看下面的runloop运行逻辑阐述。

关于CFRunLoopOberverRef事件源头的补充说明:

CFRunLoopOberverRef,每一个Observer都包含一个回调IMP,主要监听以下事件。
typedef CF_OPTIONS ( CFOptionFlags , CFRunLoopActivity ) {
     kCFRunLoopEntry          = ( 1UL << 0 ) , // 即将进入Loop
     kCFRunLoopBeforeTimers    = ( 1UL << 1 ) , // 即将处理 Timer
     kCFRunLoopBeforeSources = ( 1UL << 2 ) , // 即将处理 Source
     kCFRunLoopBeforeWaiting = ( 1UL << 5 ) , // 即将进入休眠
     kCFRunLoopAfterWaiting    = ( 1UL << 6 ) , // 刚从休眠中唤醒
     kCFRunLoopExit            = ( 1UL << 7 ) , // 即将退出Loop
} ;


RunLoop的mode设置
struct __CFRunLoopMode {
     CFStringRef _name ;              // Mode Name, 例如 @"kCFRunLoopDefaultMode"
     CFMutableSetRef _sources0 ;      // Set
     CFMutableSetRef _sources1 ;      // Set
     CFMutableArrayRef _observers ; // Array
     CFMutableArrayRef _timers ;      // Array
     . . .
} ;
 
struct __CFRunLoop {
     CFMutableSetRef _commonModes ;      // Set
     CFMutableSetRef _commonModeItems ; // Set<Source/Observer/Timer>
     CFRunLoopModeRef _currentMode ;      // Current Runloop Mode
     CFMutableSetRef _modes ;            // Set
     . . .
} ;
从上图可以看出,三种输入源就是mode items,通过它们组成mode。
而CFRunloop中有以下参数:
  1. commonModes:想成为commonModes的modes,需要把name写入
  2. commonModeItems:设置相应的事件源,那么配置了commonModes的mode就会被统一设置相应的commonModelItems
  3. currentMode:当前Mode
  4. modes:runLoop所有的modes
Result:通过设置commonModeItems与commonModes能让相应的model能处理共同的事件源。
sample:iOS中mainRunloop默认设置 UITrackingRunLoopMode、 kCFRunLoopDefaultMode两种模式,其中default是默认模式,而当界面中的scrollView发生滚动就会切换成tracking模式,若你还有一个与timer相关的功能,那么timer就会停止计数,若想timer继续运行,通过以上配置即可实现统一的配置。

RunLoop的内部逻辑:

Note:
  1. 图中的step2、step3只是标记任务将要处理,并不是立即处理。
  2. source0是在step4中执行,而timer、source1在step9中执行。
  3. Observer在线程的各种状态中被调用。
  4. 只有source0手动触发、source1 mash port、timer、外部手动的方式来唤醒线程。
  5. mach_msg()是实现整个runLoop的核心方法,具体原理下面会阐述。


RunLoop的底层实现:

从runLoop的开源代码可以知道,其核心是mach port,其进行休眠调用的函数为mach_msg()。

OSX、iOS的系统架构:


可以从图中得知Darwin层是iOS最底层,在硬件的上面的三个组成部分:Mach、BSD、IOKit共同组成了XNU内核。
XNU内核的内环是Mach,外环是BSD,而IOKit层是为了设备驱动提供了一个面向对象(c++)的框架。
Mach中负责通过自己的对象(进程、线程、虚拟内存等)发送相应的消息以实现相应的功能。其发送消息的实质就是在两个port中发送二进制数据包。

typedef struct {
   mach_msg_header_t header ;
   mach_msg_body_t body ;
} mach_msg_base_t ;
 
typedef struct {
   mach_msg_bits_t msgh_bits ;
   mach_msg_size_t msgh_size ;
   mach_port_t msgh_remote_port ;
   mach_port_t msgh_local_port ;
   mach_port_name_t msgh_voucher_port ;
   mach_msg_id_t msgh_id ;
} mach_msg_header_t ;

mach_msg_return_t mach_msg (
mach_msg_header_t * msg ,
mach_msg_option_t option ,
mach_msg_size_t send_size ,
mach_msg_size_t rcv_size ,
mach_port_name_t rcv_name ,
mach_msg_timeout_t timeout ,
mach_port_name_t notify ) ;

从其的定义可以得知,mach_msg是数据包的实现细节。
所以发送消息必然会有接受消息,发送消息是通过Mach_sendMsg()实现,那接受消息就由mach_msg_tarp(),让一个有runloop运行的程序到了没有消息源而进行休眠的状态时,就会触发mach_msg_tarp(),让内核设置线程休眠,并监听mach port已随时准备把线程wake up。


(3)iOS系统中的用例:


主线程与mainRunloop启动:

但启动app main函数时,iOS系统就会默认创建一个mainRunloop,其具备配置可看文档。

系统会默认注册了5个Mode:
1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
所以本质上有用的就只有 kCFRunLoopDefaultMode、 UITrackingRunLoopMode。
autoReleasePool:

苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

first observe:监听entry,监听是否进入Loop,并调用_objc_autoReleasePoolPush(),创建自动释放池,优先级别最高。

second observe:
  1. BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池
  2. Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

事件响应:

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当有硬件事件发生时:
硬件时间-》IOKit-》IOHIDEvent-》springBoard-》通过mach port转发到相应进程的app->source1触发回调-》
_UIApplicationHandleEventQueue()进行内部分发。
手势识别:

_UIApplicationHandleEventQueue() 接收到一个手势-》cancel掉旧的手势-》 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

界面更新:
通过监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,将之前在上一轮runLoop处理中的界面处理执行相应的绘制与调整。
定时器与PerformSelector:
NSTimer其实就是CFRunloopTimerRef,把时间触发时间注册到runloop中等待执行。
而performSelector:After实际上就是生成一个NSTimer完成runloop的事件源注册。
所以以上实现都是基于runloop,若在非主线程以外调用,一定注意该线程是否开启了runloop。
网络请求:

  • iOS的网络请求框架如下
    • CFSocket:完成底层的网络连接任务
    • CFNetwork:基于CFSocket的封装。
    • NSURLConnection:基于CFNetwork的封装。
    • NSURLSession:底层使用了部分NSURLConnection的。

     NSURLConnection的工作过程:

  1. NSURLConnection执行start
  2. 通过currentRunLoop获取当前线程并添加4个source0,其中两个为CFMultiplexerSource(负责触发各种delegate回调)、CFHTTPCookieStorage(负责处理各种socket事务)。
  3. create com.apple.cfsocket.private thread and  com.apple.NSURLConnectionLoader thread,其中com.apple.cfsocket.private负责处理底层socket连接并完成接受数据,runloop负责通过source1源监听socket的完成,然后通过手动触发source0事件完成delegate线程的回调处理。
   
GCD:

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。


(4)程序开发中的用例:


AFNetworking:

AFNetworking想是想可以在后台线程中接收回调,所以他通过创建一个独立的线程并为其添加runloop,注册了mach port,mach port的存在,让runloop不会被销毁,那么该线程就具备能监听网络回调的基础了。

AsyncDisplayKit:

UI线程一旦出现繁重的任务而导致界面的卡顿,这三类任务分别是排版、绘制、UI对象操作。
其中UI对象操作必须得在主线程中操作,而排版与绘制可以在后台中进行,所以该框架就是通过runloop建立私有线程,把排版与绘制的操作都放到后台线程去执行。



(5)参考文章:
  1. 深入理解Runloop
  2. CFRunLoopRef源码

Category:

  • 作用:
          category又叫类别或范畴,category本来就是smallTalk概念,被用于将多个方法按照相互关系、用途等特征分类,以便最快速地找到自己想要的方法,非常适合用来横向切割类的方法,提高代码的可维护性。

  • 原理:
struct objc_category {
   
char *category_name                                      OBJC2_UNAVAILABLE;
   
char *class_name                                         OBJC2_UNAVAILABLE;
   
struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;
   
struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;
   
struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
从category的runtime表示中可以大概知道,在编译期间会把category中的实例方法、类方法、协议方法写入相应类对象或元类的实例方法、类方法、协议方法,根据OC的继承体系与消息发送规则,可知,这样若有与父类同名的方法,就会被子类拦截掉,从而实现重写。

  • 实践:


通过将实例方法或者类方法,按照相应分类从类文件中提取出来,横向切割类的组成,方便需要相应的实现部分,提高代码可维护性。
通过objc_setAssociatedObject或objc_getAssociatedObject的关联引用,实现在runtime中添加属性。
通过category把方法添加到类的规则,可以实现对系统库方法的重写。
category可以实现相应的协议方法,为类的主文件减负。

参考文章:
Objective-C编程全解第十章


Protocol:

  • 作用:
          表示对象与对象之间的通信规范,能用于实现多继承的效果、回调、类的自定义部分。

  • 原理:
Protocol于runtime中的定义:
struct objc_protocol_list {
    struct objc_protocol_list *next;
   
long count;
    Protocol *list[
1 ];
};

#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif

所以本质上,protocol的底层表示就是一个方法列表,当对象A遵守了对象B的协议,那么对象B就能安全地通过对象A调用相应的方法来完成一定的功能,而在内部实现却好像是自己实现的,利用这个特性能实现类似多继承的效果。
所以protocol可以理解把,把适合别的对象处理的事情委托给别的对象处理的一种机制。

关于协议的继承:
在声明协议的时候,可以通过以下的方式实现协议的继承效果:
@protocol A<B,C,D>
      //coding
@end
本质上就是把B,C,D的protocol list方法列表中的IMP指针copy到当前协议的结构体中的protocol。

关于在protocol中添加属性的作用:


@required & @optional
@required用于标记必须实现的方法(省缺/默认)
@optional用于标记需要实现的方法(OC2.0出的,使用这个即可实现类似非正式协议的效果)

非正式协议:

非正式协议的相关概念
  1. 非正式协议被声明为NSObject类的分类
  2. 非正式协议中的生命方法不一定需要实现
  3. 编译时,不能检查类对非正式协议的适用性
  4. 运行时,不能检查类对非正式协议的适用性,只能确认是否实现了某个方法。
Note:
1、以NSObject类的分类作为非正式协议的声明,那么程序中的任何一个实例发送消息,编译也不会出现问题。
2、非正式协议只需要声明,但不一定要实现,所以无论在编译期还是运行时,都无法检查适用性,就是检测到这个协议的所有方法都已经实现了,但可以在运行期通过respond来检测某个方法是否实现了。
3、本质上,NSObject分类实现的非正式协议,是OC1.0时代的实现,在OC2.0中protocol提供了@optional后,使用@optional更加易懂好理解。

  • 实践作用:

委托者模式:delegate的实现
多继承的效果的实现:



  • 参考文章:

Objective-C编程全解 第十二章 协议



Extension:

  • 作用:
          用于在类中定义属性与实例变量。

  • 原理:
          extension在iOS中可以看做是一个匿名的Category,原理也与之类似。

  • 实践应用:
可以用来定义只读属性:
在头文件中定义属性为readonly,而在extension在定义为readwrite,即可实现外部只可读,内部可读可写。

  • 参考文章:
          略。


Property:

  • 作用
          生成自动实例变量,并为其通过修饰词添加定制的getter&setter方法,方法实例通过点语法进行链式访问,所以本质上,属性就是把实例变量与相应的访问方法封装起来的一种策略。

  • 原理:
Property属性声明规则总结:
  1. 自动生成访问方法
  2. 自动生成实例变量
  3. 更简单地调用访问方法
  4. 属性的内省

ARC/MRC下的property修饰词:
ARC:assign、weak、strong、copy
MRC:assign、retain、copy、nonatomic、atomic

  1. assign:(ARC/MRC)
    1. 直接赋值,指向对象时不会引用计数+1,本质上就是直接把目标地址copy到自身的指针。
  2. strong(ARC)
    1. 直接赋值并引用计数+1。因为在ARC环境下,当对象被检测到引用计数器为0时会被自动释放,所以不需要想retain中手动释放当前指向的对象
    2. 在ARC里替代retain
  3. weak(ARC)
    1. 引用计数器不会+1,直接赋值,但在所指向的对象销毁(dealloc)后,会被赋值为nil,从而确保访问的安全。
    2. weak自动置nil原理:每个对象都会维护一个weak指针的hash_table,key为所指向对象的地址的hash值,value是weak指针地址,当对象被销毁后,会遍历该table,把所有的weak指针的指向nil对象。
  4. retain(MRC)
    1. 在MRC环境下,会先检测当前指针是否有指向对象,有着release,然后再将新对象retain并指向,在ARC环境下,这些对象生命周期的操作会由编译器实现。
  5. copy(ARC/MRC)
    1. 在MRC环境下的实现与与retain类似,得先检测当前指针是否有指向对象,有则释放,然后再执行copy操作。
    2. 在ARC环境下,则直接执行copy操作
    3. 这里需要注意的是执行的是copy操作,所以该修饰词不能用于mutable对象。
  6. nonatomic(ARC/MRC)
    1. 不对setter method添加互斥锁
    2. 性能快但不确保线程安全(添加了atomic同样线程不安全,详见多线程)
  7. atomic(ARC/MRC)
    1. 本质上就是通过互斥锁实现,需要消耗资源
    2. 本质上就是利用线程同步实现,确保同一时间只有一个setter在执行,但因为只是确保同一时间只有一个setter执行,若在setter执行期间进行访问,有可能出现问题。
  8. readwrite(默认)/readonly

  • 实践应用:
          略

  • 参考文章:
Objective-C编程全解 第七章



Block:

(1)作用
          Block可以形容为带有自动变量(局部变量)的匿名函数。可以用于封装代码作为参数或者回调使用。与C语言中的函数的指针类型。
    • 与C函数指针类似的点
      • block可以作为方法/函数的参数或返回值
      • block可以接收参数与返回值
    • 与C函数指针不同的点
      • block是inline,并且对局部变量是readonly      

(2)原理
Block是“带有自动变量值的匿名函数”,其实带有自动变量值的意思就是Block具备捕获自动变量的能力,而且Block的实现其实就是通过C语言处理实现的,是对C语言语法的一种拓展,是非标注的,下面从几个方面来剖析Block的原理。
  • Block如何捕获变量
  • __blcok的作用与原理
  • block的生命周期
  • block捕获的自动变量的生命周期
1、Block如何捕获变量

1.1、Block捕获的变量类型
C与OC中变量的类型与其存储的区域如下:
(自动)局部变量  ——》栈(stack)
(自动)全局变量  ——》堆(malloc)
(全局)静态变量 ——》静态存储区(.data)
(局部)静态变量 ——》静态存储区(.data)
其中静态存储区的变量的生命周期是属于程序的,所以在程序的整个生命周期也可以访问,因此block也一样可以直接访问。所以block需要捕获的仅仅全局/局部的自动变量。

1.2、block实现自动变量捕获的原理
block通过copy的方法把自动变量保存在block对象的存储区域中(block在OC中是以对象来处理的),有值捕获与指针捕获两种形式。
  • 值捕获:直接把数值回复过来,然后只能访问数值,不能进行修改。
  • 指针捕获:直接把指针值复制过来,能修改指针访问的对象,但不能修改指针指向的值。
2、__block的作用与原理
当block通过值捕获获得自动变量时,在block内是无法对其进行修改的,但可以通过__block的修饰符来让block内对值进行修改。

2.1、__block的原理
通过__block修饰符号,修饰的对象或值被捕获后,会自动生成以下的结构体:
struct __Block_byref_val_0 {
      void * isa;
      __Block_byref_val_0 * __forwarding;
     int __flags;
     int __size;
     int val; //也可能是 objc *val  / int *val 
}
从源代码可以得出,其中__forwarding指针用于自指,主要用于实现copy上stack与malloc上的数据兼容,而val的值就是保存原自动变量。
就是block内还是block外都是通过__Block_byref_val_0或对象来访问val,从而实现block外与block内在合理的生命周期内进行访问,而且通过对象封装,就可以通过引用计数的规则管理其生命周期了。
所以__block的本质就是把数值封装成对象,然后block捕获其时,就是指针捕获,从而实现在block内进行读写的需求。
3、block的生命周期
根据block的内存存储区域,block的生命周期呈现也不一致,其所捕获的自动变量的生命周期同样会跟随着不一致。

3.1、Block的类型
NSConcreteStackBlock: 
NSConcreteGlobalBlock:
NSConcreteMallocBlock:
顾名思义,可以得知哪种类型的Block就会存储在相对应的存储区。

3.2、NSConcreteGlobalBlock
满足以下两个条件即为NSConcreteGlobalBlock:
  1. 在记述全部变量的地方使用Block语法时,会生成NSConcreteGlobalBlock
  2. 只要不捕获自动变量的情况下,生成NSConcreteGlobalBlock
所以决定是否生成NSConcreteStackBlock的关键不是说定义在全局作用域,而是是否捕获了自动变量。

3.3、NSConcreteStackBlock
除了3.2所说的情况,都是生成NSConcreteStackBlock。NSConcreteStackBlock是NSConcreteMallocBlock的基础,只有发生了block_copy的操作才会产生NSConcreteMallocBlock。

3.4、NSConcreteMallocBlock
只有发生block_copy的操作才会把NSConcreteStackBlock转换成NSConcreteMallocBlock,也就是从栈copy道堆中。
在ARC环境下,以下情况会默认调用copy:
  1. 向方法或者函数传递block
  2. block作为返回值
  3. block被id strong类型的指针指向
  4. 使用了带usingBlock的cocoa框架方法或者GCD API的调用。
MRC环境下需要手动调用,而且重复手动调用copy是没有影响的,因为在第二次调用copy时会采用引用的方式实现。

4、block捕获的自动变量的生命周期

4.1、值捕获的自动变量
block本身就是一个对象,其生命周期由引用计数来决定,而值捕获后的数值是直接在对象的存储空间后添加的,所以值的生命周期与block的生命周期一致。

4.2、指针捕获的自动变量
指针捕获的自动变量包括指向对象与指向类型变量的两种情况

4.2.1、指向对象
  1. 对于stackBlokc的情况:无论是局部变量还是全局变量,都会让对像的引用计数加一,然后在作用域结束,释放block的时候,release对象。
  2. 对于mallocBlock(stackBlock转mallocBlock)的情况:
    1. 对于全局变量的情况:持有对象,引用计数加一
    2. 对于局部变量的情况:将对象从栈区复制到堆区,再引用计数加一。
    NOTE:其中__block是一种特殊的对象引用。


4.2.2、指向类型变量
这要视所指向类型变量是存储在stack还是malloc而论。
  1. 指向stack变量:变量会随作用域的结束而释放,会引发野指针错误。
  2. 指向malloc变量:变量不会随着作用域的结束而释放,但得有释放变量的合理时机与操作。


(3)应用实例
作为函数的参数
作为函数的返回


(4)参考文章
Objective-C高级编程 iOS与OS X多线程与内存管理



深复制与浅复制:

  • 作用
          用于实现类的复制的方法,其中分为深复制与浅复制,两种复制实现。

  • 原理
 
          基本概念:
          浅复制:   只是通过copy对象的指针,从而具备访问对象内存区域的能力。
          深复制:实现内存的拷贝,在深复制后,会产生一个数据完全相同,但存储地址不一的内存区域。
          
          关于集合的复制:
          OC主要提供以下几种容器类,NSArray、NSDictionary、NSSet及其的可变容易。    
          
    • 浅复制(shallow copy):在浅复制操作时,对于被复制对象的每一层都是指针复制。
    • 深复制(one-level-deep copy):在深复制操作时,对于被复制对象,至少有一层是深复制。
    • 完全复制(real-deep copy):在完全复制操作时,对于被复制对象的每一层都是对象复制。
          对于容器类会多出一个完全深复制的概念,实际上这个完全复制才是严格意义上的深复制实现,而上面的深复制是单层复制的意义。
          使用copy与mutableCopy能实现浅复制(shallow copy)与深复制(one level deep copy),其中copy返回immutable对象,而mutableCopy返回mutable对象,调用这些方法会根据上下保护的规则决定实现深复制还是浅复制。
          
          容器类的完全深复制的实现方法
    1. 通过归档解档,将对象归档然后再接档可以实现深复制的效果。
    2. 通过系统方法实现:
                     通过以下方法,并可copyItems设置为YES,即可实现深复制
//    [[NSSet alloc] initWithSet:<#(nonnull NSSet *)#> copyItems:<#(BOOL)#>]
//    [[NSArray alloc] initWithArray:<#(nonnull NSArray *)#> copyItems:<#(BOOL)#>]
//    [[NSDictionary alloc] initWithDictionary:<#(nonnull NSDictionary *)#> copyItems:<#(BOOL)#>]

          关于系统对象的复制:
          同样通过copy与mutableCopy方法实现复制,规则也与上面相同,但没有单层深复制的情况。

          关于自定义的对象复制:
          自定义对象需要实现NSCopying、NSMutableCopying协议,并实现以下方法:

- ( id )copyWithZone:( nullable NSZone *)zone{
   
//coding...
}

- (
id )mutableCopyWithZone:( nullable NSZone *)zone{
   
//coding...
}
在该方法内实现相应的复制操作即可。所以本质上,系统对象都是在内部实现了该协议,然后在调用从NSObject类继承回来的copy与mutableCopy就会以delegate的形式调用 copyWithZone、 mutableCopyWithZone 从而实现复制的效果,这也是为什么容器内调用copy、mutableCopy会有单层深复制的效果的原因。
          

  • 实践应用
          略

  • 参考:


KVC&KVO

概念:
KVC(key-value-coding)    :键值编码,将表示对象的字符串作为key,来间接访问该信息的机制。
KVO (key-value-oberving):键值观察,即某个对象的属性改变时通知其他对象的机制。

原理:
KVC原理:
但使用KVC的方式来访问一个对象时,的流程如下:
1、检查是否有对应的getter&setter访问器,有则使用其读写实例变量
2、没有访问器时,使用accessInstanceVariablesDirectly来检查是否存在相应的实例变量,有则访问
3、若没有相应的实例变量,将触发setValue:forUndefineKey:,valueForUndefineKey:,该改法是通过NSObject的类别(非正式协议)来实现,其默认实现是抛出异常,可以通过重写实现相应的自定义处理。
4、若有相应的实例变量,会观察值是否对象,不是则使用相应的对象包装值。


KVC的小众使用规则:

(1)根据键路径进行访问
          可以通过valueForKeyPath或setValue:forKeyPath,通过键路径的方式进行数据的访问

(2)一对一关系与一对多关系
         在访问的对象是一个数组的时候,会有一对多的情况发生。
#import "Luozhiwei.h"

@implementation Luozhiwei

- (
instancetype )init{
   
if ( self = [ super init ]) {
       
_a = @"a" ;
       
_array = @[@{ @"name" : @"luo" , @"age" : @"12" } , @{ @"name" : @"zhi" , @"age" : @"13" } , @{ @"name" : @"wei" , @"age" : @"14" }] . mutableCopy ;
       
_dictionary = @{ @"name" : @"luo" , @"age" : @"12" } . mutableCopy ;
    }
   
return self ;
}
@end

//在别的方法中调用以下方法
  NSLog ( @"%@" , [ self valueForKeyPath : @"luozhiwei.dictionary.name" ]);
会得到array中的所有name值。


KVO原理:
要实现KVO键值本质上就是在对象的实例变量被修改的时候,实现相应方法的回调。
在OC语言的实现中,是通过继承重写的方式来实现KVO的。
当我们通过符合KVC标准的方式访问实例变量时,则通过
  1. 访问器
  2. setValue:forKey
  3. setValue:forKeyPath
三种方法访问实例变量时,则会触发KVO回调。
当我们通过 [objcA   addObserver : @"observe" forKeyPath : @"keyPath" options : NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context : nil ]方法后,就会把立即生成一个继承于objc的类,类名NSKVONfying_objcA,然后把objcA的isa指针指向 NSKVONfying_objcA, NSKVONfying_objcA重写了setter方法与accessInstanceVariablesDirectly相应的变量查询方法,在这两个方法中添加入检查到相应修改后就会触发回调方法
- ( void )observeValueForKeyPath:( NSString *)keyPath ofObject:( id )object change:( NSDictionary < NSString *, id > *)change context:( void *)context。
回调方法使用非正式协议实现,但只声明不实现,所以若注册了KVO的类没有实现回调方法,会引发奔溃。

应用实践:

参考文章:

Objective-C编程全解



NSNotification:

(1)作用:
iOS中的NSNotification是以观察者模式为原理设计出来的一个通知中心,能实现一对多消息发送的效果。

(2)原理:


通知中心的结构大致如上图,其中NotificationCenter负责维护一个名为注册对象列表,里面包括注册通知指针、通知调用方法、通知Key等关键信息。
每当有一个对象发起通知时,通知中心就会根据通知Key,然后到注册对象列表中查找出注册了这个通知的所有对象,然后通过对象指针、调用方法、参数,发起objc_sendmsg(),执行相应操作。
根据以上原理,我们可以得知,对象注册通知,实际上就是往NoticationCenter中的注册对象列表中添加自身的指针、调用的方法、注册的通知消息。
需要注意的是,这个注册对象列表并没有去重的效果,所以有可能因为程序逻辑的不当而造成重复注册,造成收到多余的通知,所以一定要遵守对象不需要的时候要即使注销通知。

(3)实践作用

通过NoticationCenter很容易实现跨层的消息发送。
实现一对多的消息发送。


(4)参考文章


多态性

(1)定义:
在面向对象程序设计的理论中,多态是指,同一操作作用于不同的类的实例中会有不同的效果。就是同一消息发送到不同的类中,会得到不一样的响应,这就是多态。

多态的使用总结:
  1. 没有继承就没有多态
  2. 代码的体现:父类指针指向子类的对象
  3. 好处:能通过统一的父类指针,调用不同的子类的方法。
  4. 局限性:父类的变量不能直接调用子类的方法,若要调用,需要转换成子类。

(2)实践应用:

工厂模式
类族


(3)参考文章:
Objective-C编程大全 第四章



继承

  • 概念:
          使用继承可以方便地在已有的类的基础上进行扩展,定义一个具有父类全部功能的新类,其中OC只支持单继承,但可以通过protocol、消息转发机制的方式实现类似多继承的效果。

  • 原理:


iOS中典型的继承用例:
由上图可以看出,UIKit框架的基类是NSObject,所有的类都是直接或者间接地从NSObject继承回来的。
其中NSObject是runtime的入口,由它负责封装runtime库,从而继承它的类就能具备消息发送的能力,这就是为什么所有的类都得继承与NSObject的原因。
其中UIResponder实现了事件响应的相关功能,只有继承了UIResponder的类就能加入响应者链条并事件进行响应处理,所以从图可以看出大部分的图形界面都是继承与UIRespond,所以通过合理的继承设计可以轻松地实现相应功能的扩展,但也不建议滥用继承,因为用得不好很容易产生个各种依赖问题,而且代码的学习周期容易边长,代码不好维护。
还是那句话,该出手时就出手。

OC的继承实现:
OC的继承首先靠super_class指针实现,在我们定义一个类继承与什么类的时候,就会确定super_class指针的指向,从而决定了在objc_sendmsg()消息发送的走向。
从runtime的类的定义我们可以看出无论是属性的定义还是方法的定义是都公开的,那么OC是怎么实现共有方法与私有方法?
OC是通过文件的组织架构来实现的,正因为如此OC把声明与定义分开,也就分成了.h & .m文件,而能在别的类中导入的只要.h文件,也就是在编译器的辅助下,我们只能调用.h文件所声明的属性与方法,从而实现了公有方法与私有方法。

关于OC的方法继承的具体表现:
原理如上《OC的继承实现》

关于OC的属性与实例变量的继承的具体表现:

实例变量的继承类型:
  1. @private:私有成员,不能被外部函数访问,也不能被子类继承
  2. @protected:保护成员,不能被外部函数访问,可以被子类继承。
  3. @public:共有成员,可以被外部函数访问,也可以被子类继承
外部访问类的对象可以通过KVC与属性方法来访问,其中KVC能直接绕过私有方法的保护而直接读取相应的内存,而getter与setter方法又只能通过修饰词来决定属性的是否可读,所以上面的词只能控制继承关系的效果。

属性的继承:
原理如上《OC的继承实现》

多继承的实现讨论:
OC只支持单一继承,但若有实现多继承的效果可以通过protocol、消息转发、组合的方法来实现。

- protocol
通过实现多个protocol限定的方法,可以实现多继承的效果。
优势:轻量,依赖少,灵活定制,只需要实现需要的部分即可。
局限性: 不可重用,每次都得重新实现。
- 消息转发
利用消息转发机制的第二层、第三层,将无法处理的消息转发给相应能处理的对象,然后将别的对象的处理结果作为原方法的结果返回。这种方法与组合的实现原理类似,但有消息转发的壳,让其更加接近多继承。
- 组合

          通过将实现相应功能的对象都以变量持有,将相应的处理丢给它们。

  • 实践应用:
          略。

  • 参考文章:

Objective-C编程大全


垃圾回收机制

  • 作用
          用于确保内存资源的高效使用。          

  • 原理
          OC语言支持垃圾回收机制,而OSX系统具备垃圾回收机制,而iOS并不支持垃圾回收机制。
 
           垃圾回收机制本质上就是在一定的时间内启动,并针对内存中没被使用但没被释放的内存进行释放回收操作,例如引用环。但这个机制比较消耗资源,相对于iOS只使用引用计数的方式来管理内存稍微复杂点。       

  • 实践应用
          略

  • 参考文章
          略


异常处理

  • 概要:
          大部分语言都有其对于异常情况的捕获与处理,以确保程序能将一些异常情况过滤或者调整,以致于程序还能正常运行或者不至于引发奔溃。
          但OC的编程规范并不太支持使用异常处理的方法来处理异常,觉得出现异常就得让异常合理的奔溃从而让程序退出。

          异常发生时,为执行异常处理而准备的流程称为异常句柄(exception handler),该异常句柄处理的对象的代码部分称为异常处理域(exception handling domain)。当在异常处理域中发生了异常,异常处理域的代码执行会立即终止,然后调用异常句柄处理异常。

  • 要点:

OC异常处理机制:

标准的异常处理语法:
    @try {
        [self performSelector:@selector(luozhiwei)];  //异常处理域:在此记录正常的处理
    } @catch (NSException *exception) {
        NSLog(@"%@", exception);                      //异常句柄:异常处理过程。
    } @finally {
        NSLog(@"finish”);                             //后处理:无论异常是否发送,都会执行的代码块
    }
以上就是一个OC标准的异常处理语句通过@try来实现代码逻辑,通过@catch来捕获@try的异常,无论@catch处理结果如何,最后都会走finally。

关于异常的发生与传播:
若都按照上面的语法实现异常处理,规则是简单的,但一旦引入了自身触发异常的方法后,异常的传播就稍微复杂一些。
[ NSException raise : @"jhasdf" format : @"fjsdkjf” ];   //通过NSException引发异常
@throw [ NSString stringWithFormat : @"luozhiwei” ];   //@throw抛出异常,可以写入任何对象

其中@catch的参数是可以自己定义的,如@catch(MyClass A),@catch (MyClass B),结合@throw object 语法,即可手动触发相应的catch,进行相应的处理。
但我们需要捕获未知类型的异常,可以通过@catch (…)实现。

在异常处理嵌套的情况下,只需要注意是否有@throw语句控制异常信息传播,其余的与标准异常处理的规则一致。

断言:
出于调试的目的,有时需要查看程序是否满足约束的条件,可是可以通过断言宏来实现,当相应的条件触发时,就会触发异常的结构称为断言。

断言宏的实现:
#define _NSAssertBody(condition, desc, arg1, arg2, arg3, arg4, arg5) \
    do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) { \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @
"<Unknown File>" ; \
    [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd object:self file:__assert_file__ \
    lineNumber:__LINE__ description:(desc), (arg1), (arg2), (arg3), (arg4), (arg5)]; \
} \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(
0 )
#endif
#if !defined(_NSCAssertBody)
#define _NSCAssertBody(condition, desc, arg1, arg2, arg3, arg4, arg5) \
    do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) { \
            NSString *__assert_fn__ = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \
            __assert_fn__ = __assert_fn__ ? __assert_fn__ : @
"<Unknown Function>" ; \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @
"<Unknown File>" ; \
    [[NSAssertionHandler currentHandler] handleFailureInFunction:__assert_fn__ file:__assert_file__ \
    lineNumber:__LINE__ description:(desc), (arg1), (arg2), (arg3), (arg4), (arg5)]; \
} \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(
0 )
#endif
从上面的代码可以得出断言宏是分两种的一种面向方法,另一种面向函数,而从结构可以看出,里面的本质都是一个do()while(0)结构,为什么要使用这种结构,应该只是为了能构成一个代码域罢了,而且从上代码可以得出,其实现的核心方法如下:
//对应方法的断言宏
//    [[NSAssertionHandler currentHandler] handleFailureInMethod:<#(nonnull SEL)#> object:<#(nonnull id)#> file:<#(nonnull NSString *)#> lineNumber:<#(NSInteger)#> description:<#(nullable NSString *), ...#>]
//对应函数的断言宏
//    [[NSAssertionHandler currentHandler] handleFailureInFunction:<#(nonnull NSString *)#> file:<#(nonnull NSString *)#> lineNumber:<#(NSInteger)#> description:<#(nullable NSString *), ...#>]

  • 参考文章:

Objective-C编程大全 第18章


多线程

概念:
在iOS中,一个进程就是一个应用,在应用中只允许进行多线程开发。而多线程的本质就是实现更高效的程序,而在iOS中利用多线程技术提高效率的思路也是一样的,尽量利用后台线程实现任务,从而减轻主线程的负担,让主线在执行渲染、排版等界面相应任务更加流畅。

知识点介绍:
多线程编程本质上就是解决两个问题:并行计算、异步计算。
应用多线程技术,本质上就是为了实现更高效率的程序,提供更快的响应给用户,下面着重以以下四个方面来谈谈iOS的多线程开发。
  1. 多线程的基本概念与原理
  2. iOS同步锁的原理与作用
  3. GCD的原理与作用
  4. NSOperration的原理与作用

多线程的基本概念与原理
  1. 进程与线程
  2. 并发与并行
  3. 线程死锁的原理
  4. iOS中实现多线程编程的方案
1、进程与线程
进程与线程的关系是密不可分的,在iOS系统中,一个app程序就有且仅有一个进程,但一个进程可以拥有多个线程来并发地执行任务。

1.1、进程概念与作用

1.1.1进程的狭义定义与广义定义

狭义定义:
进程就是正在运行的程序的实例。

广义定义:
进程的严格定义是“计算机中的程序关于某个数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础”
操作系统运行的是各种各样的程序,而一个程序至少有一个进程(主进程),而一个进程至少有一个线程(主线程),所以这个结构关系也说明了为什么进程是操作系统的一个重要的组成。

1.1.2、进程的主要组成及其特点
进程的组成:
  • 进程有自己的地址空间,包括如下
    • 文本区域:用于存储处理器执行的代码或指令
    • 数据区域
    • 堆栈区域
  • 进程可拥有多个线程,线程可以把任务切分成多个子任务,并发处理挺高效率。
  • 进程控制块,负责线程的切换、执行资源分配得任务。

进程的特点:
  • 动态性:进程实际上就是程序的一次执行过程,它是动态产生,也动态消亡。
  • 并发性:允许多个进程同时执行
  • 独立性:每个进程都是一个独立的个体,拥有自己的内存资源和线程管理
  • 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
  • 结构特征:进程由程序、数据、进程控制器三部分所组成

1.1.3、进程的运作原理

进程的状态:
  • 就绪状态(Ready):已获取执行所需的所有资源,正等待分配处理器使用权限
  • 运行状态(Running):正占用处理器资源,执行任务
  • 阻塞状态(Blocked):等待某种资源或者条件,直到进行就绪状态否则无法运行。

进程的切换:
线程的切换实际上就是Ready-》running或者running-》Ready/Blocked的过程。
说白了就是获取与放弃处理器使用的权限的过程。
当一个进程要使用处理器,首先把上下文环境配置处理器上,上下文环境就是设置一大堆的临时数据的寄存器和PC(程序指针)用于指明程序运行到哪里,然后处理器才继续往下执行。
在进程运行阶段,所有计算的过程中产生的数据会先保存在处理器的临时存储区中,并根据相应的状态的修改而修改相应的寄存器。
当一个进程要让出处理器的使用权限时,会先把所有的处理结果保存会自己的私有堆栈中,并把当前的上下文环境保存好,再回到非激活状态。


1.2、线程概念与作用

线程是进程内假想的持有CPU使用权的执行单位。多线程就是进程允许多个线程以并发的形式执行任务,多个线程是共享进程的私有堆栈的,所以在多线程编程中常见的挑战就是如何安全地读写数据。

线程的本质就是为了完成一个特定的任务的命令集合,进程把线程的命令集合发送到处理器进行执行实现计算。
所以线程自身的没有存储区域的。

2、并发与并行
  • 并发:多个进程或线程在同“一个时间段”执行,可以是按时间来分配各种的执行时间的。
  • 并行:多个进程或线程在同”一个时间点“执行,是真正意义上的同时执行。
3、线程死锁的原理
形成死锁的原理就是两个在业务上相关的线程形成了一种互相等待的状态,最终这两条线程会在程序的整个运行生命周期内都处于block状态,从而形成死锁,通常是由于同步锁使用不惬当而导致了,遇到这种问题,注意一下同步锁的使用是否恰当即可。
4、iOS中实现多线程编程的方案
  1. pthread
  2. NSThread
  3. Grand Centrel Dispatch(GCD)
  4. NSOperation

     

NSOperation是线程安全的,而GCD是线程不安全的。

iOS同步锁的原理与作用
  1. 同步锁的原理
  2. iOS同步锁的类型
    1. NSLock()
    2. NSConditionLock(条件锁)
    3. NSRecursiveLock(递归锁)
    4. NSCondition
    5. @synchrnize
    6. OSSponLock
    7. pthread_mutex
    8. dispatch_semaphore

1、同步锁的原理

在程序中同步锁会呈现成一个对象或者一个语法,但其本质就是令牌算法,如一个NSLock对象就是一个令牌,哪个线程通过竞争上岗获得令牌则获得处理器的使用权,而其余未获取令牌的线程则会在线程队列里等待进入下一轮的令牌竞争,而运行完的线程则释放令牌并释放线程资源,从而保证对相同资源的读写安全。具体流程如图:
同步锁的作用原理图:


线程的状态和进程的状态也是类似的,如下图所示。
线程的状态图:

  • ready-to-run:已经准备好一切资源准备运行,就差处理器的使用权限
  • running:正在处理器上进行计算
  • sleeping:休眠,任务完成,没有新的任务进入,或其他情况
  • waiting:线程切换
  • blocked:线程因同步代码或者IO阻塞的一种状态
当线程进入等待队列时,实际上就会进入blocked的状态,但blocked又有两种获得锁的状态:
  1. 空转阶段(主动获取锁):会每隔一小段时间就会轮询同步锁的状态(tryLock),过了一段时间就会进入阶段二线程挂起。
  2. 线程挂起(被动获取锁):通过收到notify的方式唤醒线程并获取同步锁。
2、iOS同步锁的类型与特点
  1. NSLock:
  2. NSConditionLock:条件锁。
  3. NSRecursiveLock:处理递归的情况。
  4. NSCondition:特点是在等待队列时,不会有空转阶段,会直接挂起线程等待通知快速响应。
  5. @synchrnize(objc):使用对象作为唯一标识,只有标识相同才满足互斥
  6. OSSponLock:OSSpinLock 是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法,当在等待队列中时,只会一直轮询,不会进入休眠状态,比较消耗CPU资源
  7. pthread_mutex:C语言下的互斥锁
  8. dispatch_semaphore:GCD提供的令牌数可调的同步锁,比较灵活。

Grand Central Dispatch(GCD)的原理与作用

无论是pthread还是NSThread都是直接用线程的概念去管理线程的,就是说它们所创建的对象或者结构体就是与相应的线程是对应的关系。
但iOS4.0后,apple推出GCD,以队列的概念来管理线程,而具体的线程操作(创建、释放、调用等)则由系统根据具体的情况而实现。
通过GCD,编程时只需要关注往相应的队列派遣block封装的任务即可,而无需关心接下来具体的线程操作。

Grand Central Dispatch的头文件结构:
#include <os/object.h>
#include <dispatch/base.h>   //相关的基础宏定义
#include <dispatch/time.h>   //定义dispatch的时间单位与创建方法
#include <dispatch/object.h> //将线程以队列的概念管理,涵盖了线程生命周期的所有方法,如release、retain、active、resume、suspend等等。
#include <dispatch/queue.h>  //队列的创建与调用的相关方法,提供主动调用派遣block的方法。
#include <dispatch/block.h>  //定义block任务块
#include <dispatch/source.h> //定义source,根据事件源被动触发block任务
#include <dispatch/group.h>  //用组的概念来管理队列,能控制对象的集体执行又或者优先级控制
#include <dispatch/semaphore.h> //信号量,信号令牌
#include <dispatch/once.h>      //once,线程安全的基础下保证任务在程序周期中只执行一次
#include <dispatch/data.h>      //数据模型
#include <dispatch/io.h>        //IO,input&output
关于线程与队列:
无论是pthread或者NSThread都是直接以线程的概念去实现多线程编程的,但这样会提高编程的难度,因为要考虑的东西比较多需要的知识也比较底层,如你必须得精准得知道线程的状态,并且通过各种任务的执行与判断,进行状态的切换之类的操作。
所以Apple基于这点,设计出基于队列的概念来实现多线程编程的GCD(low-level),然后再以GCD定制重写了NSOperation(high-level)。
这样设计的好处是,开发者主需要关注在什么类型的队列上以什么方式派遣了什么任务即可,剩余的关于线程状态的调度与判断处理均交给系统进行优化处理,也正因为如此,线程与队列也不是一一对应的,会根据系统当前的资源情况进行派发。
线程block任务的派遣方式:
  • 主动派遣
    • 同步派遣
    • 异步派遣
  • 被动派遣
    • DISPATCH_SOURCE_TYPE_DATA_ADD
    • DISPATCH_SOURCE_TYPE_DATA_OR
    • DISPATCH_SOURCE_TYPE_MACH_SEND
    • DISPATCH_SOURCE_TYPE_MACH_RECV
    • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE
    • DISPATCH_SOURCE_TYPE_PROC
    • DISPATCH_SOURCE_TYPE_READ
    • DISPATCH_SOURCE_TYPE_SIGNAL
    • DISPATCH_SOURCE_TYPE_TIMER
    • DISPATCH_SOURCE_TYPE_VNODE
    • DISPATCH_SOURCE_TYPE_WRITE

被动派遣的信号源的分类如下:
  • Timer dispatch:根据设定的时间间隔触发
    • DISPATCH_SOURCE_TYPE_TIMER
  • Signal dispatch:根据UNXI系统信号的触发
    • DISPATCH_SOURCE_TYPE_SIGNAL
  • Descriptor source dispatch:文件读写修改触发
    • DISPATCH_SOURCE_TYPE_READ
    • DISPATCH_SOURCE_TYPE_WRITE
    • DISPATCH_SOURCE_TYPE_VNODE
  • process dispatch:进程时间触发
    • DISPATCH_SOURCE_TYPE_PROC
    • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE
  • mach port dispatch:mach port 读写触发(实现进程与进程之间的通信!!!)
    • DISPATCH_SOURCE_TYPE_MACH_SEND
    • DISPATCH_SOURCE_TYPE_MACH_RECV
  • custom dispatch:自定义触发
    • DISPATCH_SOURCE_TYPE_DATA_ADD
    • DISPATCH_SOURCE_TYPE_DATA_OR

  • 关于派遣与执行的讨论:
    这里所说的派遣是指直接把以block封装好的业务逻辑派遣到目标队列里,然后等待系统进行任务执行,所以派遣不等于执行。
任务优先级控制:
通过group与semaphore可以实现调用优先级的调整处理。

NSOperration的原理与作用

NSOperation在iOS2.0已经推出,当时是基于NSThread实现的,在iOS4.0推出GCD后,则用GCD重写。所以NSOperation可以视为基于GCD的更高一层的定制,对线程操作的添加了不少限制,从而确保线程安全。
若需要自己定制自己的多线程架构则可以实现GCD,否则推荐使用NSOperation。


实践应用:

faceBook asyncDispaly:
将主界面的渲染、排版、UI对象操作中的,渲染与排版大量放置到后台线程实现,提高主线程的响应效率。


参考文章:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值