美文网首页IOS 常用技术
iOS 常用技术基础

iOS 常用技术基础

作者: wnido | 来源:发表于2017-08-16 18:12 被阅读13次

    基于 《Effective+Objective-C+2.0++编写高质量iOS与OS+X代码的52个有效方法》,借鉴、搬个砖,然后补个墙

    coffee-heart-i.jpg

    对Objective-C 语言的发展史理解

    1980 年代初,布莱德·考克斯(Brad Cox)在其公司 Stepstone 发明 Objective-C。Brad Cox 一直专注软件工程,软件重用性,组建化,这也是 ObjC 里面的核心思想,Brad 当时想打造一门流行的、可移植的 C 语言与 优雅的Smalltalk 的结合体,而 Smalltalk 是消息型语言的鼻祖。

    ObjC 是根据 C语言 所衍生出来的语言,继承了 C语言 的特性,是扩充 C语言 的面向对象编程语言。
    相较于开始于 1982 年的 C++ ,Obj-C 更是古老。Object-C 和 C++ 在成长过程中,吸收新生语言,同时相互借鉴。
    所以在理解 OC 语言上,应该从 C语言 入手,而不是 C++,尤其要掌握 C语言 中的内存模型指针

    //消息型语言
    Object *obj = [Object new];
    [obj performWith:parameter1 and:parameter2];
     
    //函数型语言
    Object *obj = new Object;
    obj->preform(parameter1,parameter2);
    

    Objective-C 为 C语言 了添加面向对象特性,是其超集。Objective-C 使用了动态绑定的消息结构,在运行时才会检查对象类,所以也叫运行时动态语言


    对xcode的配置理解

    • Architectures(指令集)——设置你想支持的指令集
    • Valid architectures : 指即将编译的指令集
    • arm指令集和i386、x86_64指令集与真机和模拟器的关系
    • Build Active Architecture Only : 是否只编译当前适用的指令集。
    • iPhone OS Deployment Target:指的是编译出的程序将在哪个系统版本上运行
    • bitcode 理解
    • search paths 的理解
    • ...

    在类的头文件中尽量少引用其他头文件

    虽然,由于 OC 中,#import 即使重复添加头,也不会造成编程错误,但重复的头文件,在运行和后期维护中会造成干扰。

    @class MGDeviceModel;
    
    @interface MGLocalManager : NSObject
    
    
    @property (nonatomic,strong)  MGDeviceModel  *m_deviceModel;
    

    一般来说,应在某个类的头文件中使用向前声明(@class语法)来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合

    有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continutation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。

    注:“class-continutation 分类”说的就是“扩展”。

    个人理解:少引入无用头文件的作用:

    • 减少程序编辑时间

    • 降低类之间的耦合,使类更清晰,让类的使用者更容易理解

    • 有效避免相互引用的问题

    • 减少类修改维护代价

    个人觉得:代码要简洁而精炼


    多用字面量语法,少用与之等效的语法

    NSNumber *someNumber = @(1);
    NSArray *animals = @[@"dog",@"cat",@"mouse",@"badger"];
    //取下标操作
    NSString *dog = animal[1];
    NSDictionary *personData = @{@"firstName":@"shi",@"lastName":@"xueqian",age:@(26)};
    NSString *lastName = personData[@"lastName"];
     
    //可变数组和字典
    [mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
    [mutableDictionary setObject:@"xueqian" forKey:@"lastName"];
    //可用字面量语法来替换
    mutableArray[1] = @"dog";
    mutableDictonary[@"lastName"] = @"xueqian";
    

    应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要,而且便于修改。
    应该通过取下标操作来访问数组下标或字典中的所对应的元素。
    用字面量语法创建数组或者字典时,若值中有nil,则会抛出异常。务必确保值里不含 nil。


    多用类型常量,少用#define预处理指令

    //预处理指令
    #define ANIMATION_DURATION 0.3
    //常量定义
    static const NSTimeInterval kAnimationDuration = 0.3;
     
    //全局常量  头文件中  声明
    extern NSString *const EOCStringConstant;
    //全局常量  实现文件中  定义
    NSString *const EOCStringConstant = @"VALUE";
    

    少用预处理指令定义常量。因为这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找和替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
    在实现文件中使用 static const 来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所有无须为其名称加前缀。
    在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所有其名称要加以区隔,通常用与之相关的类名做前缀。

    但要注意的是,#define 还可以用来定义方法,因为 #define 定义的方法或者常量,内存分配在栈上面,另外是在程序运行前,预编译在内存中,所以一定程度上,会加快编译速度,也是用内存空间换时间的一种方式。


    用枚举表示状态、选项、状态码

    //普通枚举
    typedef NS_ENUM(NSUInteger, EOCConnectionState){
          EOCConnectionStateDisconnected,
          EOCConnectionStateConnecting,
          EOCConnectionStateConnected,
    };
    //switch语句最好不要加defalut分支
    switch(_currentState) {
        case:EOCConnectionStateDisconnected:
            //干活
            break;
        case:EOCConnectionStateConnecting:
            //干活
            break;
        case:EOCConnectionStateConnected:
            //干活
            break;
    }
     
    //二进制枚举
    typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection){
        EOCPermittedDirectionUp       = 1 <<  0,
        EOCPermittedDirectionDown  = 1 <<  1,
        EOCPermittedDirectionLeft     = 1 <<  2,
        EOCPermittedDirectionRight   = 1 <<  3,
    }
    //二进制枚举使用
    EOCPermittedDirection direction = EOCPermittedDirectionUp | EOCPermittedDirectionDown;
    if (direction & EOCPermittedDirectionUp){
        //有设置  EOCPermittedDirectionUp
    }
    

    应用枚举来表示状态机的状态、传递给方法的选项及状态码等值,给这些值起个易懂的名字 ,这样会让程序易读性增加,统一逻辑,另外方便 switch 语句的调度。应避免用散乱的方式去对这种场景的逻辑处理。
    另外,如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项的值定义为2的幂,以便通过按位或操作将其组合起来。
    swift中,switch 还可以直接匹配字符串,这让这种方式写法变的更有趣。


    理解“属性”这一概念

    Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中,“获取方法(getter)”用于读取变量值,而“设置方法(setter)”用于写入变量值。

    @synthesize语法:编译器会自动创建 get 和 set 方法

    @implementation EOCPerson
    @synthesize firstName = _myFirstName;
    @end
    

    @dynamic关键字:它会告诉编译器,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。

    @implementation EOCPerson
    @dynamic firstName,lastName;
    @end
    

    原子性:默认atomic属性。可以通过锁定机制来确保 getter 方法操作的原子性。但是并不能保证“线程安全”。由于iOS中使用同步锁开销太大,一般只使用 nonatomic。

    读/写权限:readonly 和 readwrite

    getter=:指定getter的方法名。

    @property (nonatomic, getter=isOn) BOOL on;
    

    可以使用 @property 语法来定义对象中所封装的数据。
    通过“特质”来定义存储数据所需的正确语义。
    在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
    详细请看:iOS属性关键字


    理解“对象等同性”这一概念

    若想检测对象的等同性,请提供“isEqual”hash方法。
    相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
    不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
    编写 hash 方法时,应该使用计算速度快而且哈希码碰撞概率低的算法。


    以“类族方式”隐藏实现细节

    类族模式可以把实现细节隐藏在一套简单的公共接口后面。
    系统框架中经常使用类族。
    从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
    类族,也是面向接口编程的体现,这对一个需要长期维护的大型项目,是非常重要的。


    在既有类中使用关联对象存放自定义数据

    关联类型 等效的 @property 属性

    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC   nonatomic retain
    OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic copy
    OBJC_ASSOCIATION_RETAIN retain
    OBJC_ASSOCIATION_COPY   copy
    
    //次方法以给定的键和策略为某对象设置关联对象值
    void objc_setAssociatedObject(id object, void *key, id value, objc_associationPolicy policy)
    //此方法通过给定的键从某对象中获取关联对象的值
    void objc_getAssociatedObject(id object, void *key)
    //此方法移除某对象的全部关联对象
    void objc_removeAssociatedObject(id object)
    

    可以通过“关联对象”机制把两个对象连起来。
    定义关联对象时可指定内存管理语义,用以模仿定义属性时所有采用的“拥有关系”和“给拥有关系”。
    只有在其他方法不可行时才应选用关联对象,因为这种用法通常会引入难以查找的bug。


    理解 objc_msgSend 的作用

    C语言:C语言使用“静态绑定”,也就是说,在编译期就能决定运行时所调用的函数。
    Objective-C:所要调用的函数直到运行期才能确定。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法完全由运行期决定,甚至可以在程序运行时改变,这些特性使得 Objective-C 成为一门真正的动态语言

    //给对象发送消息
    id returnValue = [someObject messageName:parameter];
    //objc_msgSend原型
    void objc_msgSend(id self, SEL _cmd, ...)
    //给对象发送消息底层
    id returnValue = objc_msgSend(some Object, @selector(messageName:),parameter);
    

    消息由接受者、选择子和参数组成。给某个对象“发送消息”(invoke a message)也就相当于在该对象上“调用方法”(call a method)。
    发送给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。


    理解消息转发机制

    1818095-e7a25f32f80550ba.png

    OC消息传递机制和消息转发机制

    若对象无法响应某个选择子,则进入消息转发流程。
    通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
    对象可以把其无法解读的某些选择子转交给其他对象来处理。
    经过上面两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。


    用“方法调配”技术调试“黑盒方法”

    //方法交换
    void method_exchangeImplementations(Method m1, Method m2)
    //方法实现
    Method class_getInstanceMethod(Class aClass, SEL aSelector)
     
    //demo,交换 lowercaseString 和 uppercaseString 方法
    Method originalMethod = class_getInstanceMethod([NSString class],
    @selector(lowercaseString));
    Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
    method_exchangeImplementations(originalMethod,swappedMethod);
    

    在运行期,可以向类中新增或替换选择子所应用的方法实现。
    使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”(method swizzing),开发者常用此技术向原有实现中添加新功能。
    一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
    这种方式,由于所有的类,都会加载 load 方法,所以,可以实现对类默认属性的修改,比如说修改 UILable 的默认字体、UIView 的默认背景等,这个用处还是非常多的,要挖掘。
    【iOS 不常用技术解密之 hook】 也有提到这方面的应用


    第14条:理解“类对象”的用意

    1818095-16f3c333126d16f0.png

    Class定义

    1818095-08172175a478dd08.png
    • isMemberOfClass:能够判断出对象是否为某个类的实例
    • isKindOfClass:能够判断出对象是否为某类或其派生类的对象

    每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
    如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法探知。
    尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。


    用前缀避免命名空间冲突

    Objective-C 没有其他语言那种内置的命名空间机制。鉴于此,我们在起名时需要避免潜在的命名冲突。

    避免此问题的唯一办法是变相实现命名空间:为所有名称都加上适当前缀。

    选择与你的公司、应用程序或者二者皆有关联之名称作为类名的前缀,并在所有代码中均使用该前缀。
    若自己开发的程序库中用到了第三方库,则应为其中的名称加上前缀。

    这个是非常重要的,特别是在做 framework 和 library 的封装时,尤为重要


    实现description方法

    - (NSString *)description {
        return [NSString stringWithFormat:@"<%@:%p %@",[self class], self, @{@"firstName":_firstName, @"lastName":_lastName}];
    }
     
    - (NSString *)description {
          return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
    }
    - (NSString *)debugDescription {
          return [NSStrig stringWithFormat:@"< %@ ,%p  %@ ,%@",[self class], self, _firstName, _lastName];
    }
    

    实现 description 方法返回一个有意义的字符串,用以描述该实例。
    若想在调试时打印出更详尽的对象描述信息,则应实现 debugDesription 方法。
    这其实应该属于编程习惯,就像写一个 shell 时,会加一个 help 方法


    为私有方法名加前缀

    给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开,有助于代码维护。
    不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。


    理解Objective-C错误模型

    NSError对象里封装了三条信息:

    • Error domain:错误发生的范围,其类型为字符串,通常用一个特有的全局变量来定义。
    • Error code:独有的错误代码,其类型为整数。用以知名在某个返回内具体发生了何种错误,常用 enum 来定义。
    • User info:用户信息,其类型为字典。有关此错误的额外信息,其中或许包含一段“本地化描述”.
    - (BOOL)doSomething:(NSError **)error {
     
    if (/*  there was an error  */){
        if (error) {
            *error = [NSError errorWithDomain:domain code:code userInfo:userInfo];
        }
    return NO;
    else {
    return YES;
         }
    }
     
    NSError *error = nil;
    BOOL ret = [object doSomethig:&error];
    if (error) {
     
    }
    

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


    理解NSCopying协议

    //一个类支持拷贝功能需要实现 NSCopying协议只有这一个方法。其中NSZone目前只有一个默认去,可不管。
    - (id)copyWithZone:(NSZone *)zone
     
    - (id)copyWithZone:(NSZone *)zone {
        EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
        return copy;
    }
    

    若想令自己所写的对象具有拷贝功能,则需事先 NSCopying 协议。
    如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
    复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量 执行浅拷贝。
    如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。


    通过委托与数据源协议进行对象间通信

    对象把应对某个行为的责任委托给另外一个类了。

    常规的委托模式:信息从类流向受委托者(delegate)。

    数据源模式:信息从数据源(Data Source)流向类。

    信息流向

    委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
    将委托对象应该支持的接口定义为协议,在协议中把可能需要处理的事件定义成方法。
    当某对象需要从另外一个对象获取数据时,可以使用委托模式。这种情境下,该模式亦称“数据源协议”。
    若有必要,可实现含有位段的结构体,将委托对象是否能相应相关协议方法这一信息缓存至其中。


    将类的实现代码分散到便于管理的数个分类之中

    使用分类机制把类的实现代码划分多个管理的小块。
    将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。
    以一个分类,实现某个功能,这种方式在 OC 的官方代码中,也是非常常用的方式


    总是为第三方类名称加前缀

    向第三方类中添加分类时,总应给其名称加上你专用的前缀。
    向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。


    勿在分类中声明属性

    把封装数据所用的全部属性都定义在主接口里。
    在“class-continuation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
    若是定义了,会非常不好用,只会自己坑自己;当然也有特殊情况,也是需要这种方式的


    使用“扩展分类”隐藏实现细节

    这里的“class-continuation分类”其实就是我们平常所说的“扩展”。

    通过“class-continuation分类向类中新增实例变量。
    如果某属性在主接口
    中声明为“只读”,而类的内部又要用设置方法修改此属性
    尽可能的暴露少的接口,方便其他人使用你的类


    通过协议提供匿名对象

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


    在dealloc方法中只释放引用并解除监听

    dealloc方法绝不能主动调用。

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


    编写“异常安全代码”时留意内存管理问题

    捕获异常时,一定要注意将 try 块内所创立的对象清理干净。
    在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。


    以弱引用避免保留环

    将某些引用设为 weak,可避免出现“保留环”。
    weak 引用可以自动清空,也可以不自动清空。自动清空是随着 ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。


    以“自动释放池块”降低内存峰值

    iOS系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。

    自动释放池的范围:左括号到右括号({自动释放池范围})。在该范围内的对象,将会在末尾处收到release消息。

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


    用“僵尸对象”调试内存管理问题

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

    理解“ block ”这一概念

    //块的语法结构
    return_type (^block_name)(parameters)
    

    块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。也就是说,那个范围内的全部变量,在块里依然可用。

    如果块所捕获的变量是对象类型,那么就会自动保留它。

    定义块的时候,其所占的内存区域是分配在栈中的。也就是说,块只在定义它的那个范围内有效。

    为解决此问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。

    全局块;不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。

    块是C、C++、Objective-C中的语法闭包。
    块可接受参数,也可返回值。
    块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。

    为常用的块类型创建 typedef

    //块类型的语法结构:
    return_type (^block_name)(parameters)
    //typedef
    typedef return_type(^block_name)(parameters);
    

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


    用 handler 块降低代码分散程度

    委托模式有个缺点:如果累要分别使用多个获取器系在不同数据,那么就得在 delegate 回到方法里根据传入的获取器来切换。

    在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
    在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改用 handler 块来实现,则可直接将块与相关对象放在一起。
    设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
    其实在出现 handler 后, 我基本不再使用 delegate 模式,定义可读性都不好;但如果有多个类,使用到同一个 delegate ,这种情况下,delegate 方式会让程序结构更清晰和完善。


    用块引用其所属对象时不要出现保留环

    如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
    一定要找个适当的时机解除保留环,而不能把责任推给 API 的调用者。
    内存的管理,应当尽量保证在当前类内,虽然这种方式会造成一定程度上的内存开销和使用,但更安全


    多用派发队列,少用同步锁

    派发队列可用来表示同步语义,这种做法要比使用 @synchronized块NSLock对象更简单。
    将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞队列执行异步派发的线程。
    使用同步队列及栅栏块,可以令同步行为更加高效。


    多用GCD,少用 performSelector 系列方法

    //可以在运行时调用方法
    - (id)performSelector:(SEL)selector
    //可带一个参数
    - (id)performSelector:(SEL)selector withObject:(id)object
    //可带两个参数
    - (id)performSelector:(SEL)selector withObject:(id)object withObject:(id)object
    //可延时执行方法
    - (void)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay
    //可放到另一个线程中执行
    - (void)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait
    - (void)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait
     
    //延后执行方法的两种实现方式:
    [self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
     
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^(void){
        [self doSomething];
    });
     
     
    //把任务放在主线程执行的两种方式
    [self performSlectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];
     
    dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
    });
    

    performSelector 系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而 ARC 编译器也就无法插入适当的内存管理方法。
    performSelector 系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
    如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用GCD的相关方法来实现。
    另外,从本身机制上来说,GCD 方式是 纯C 的API,在内存上和调度时间上,都优于 performSelector 的线程调度;所有,在任何时候都应该使用 GCD 实现线程的调度。


    掌握GCD及操作队列的使用时机

    NSOperationQueue 发者可以把操作以 NSOperation 子类的形式放在队列中,而这些操作也能够并发执行。

    GCD是 纯C 的 API,而 NSOperationQueue 则是 Objective-C 的对象

    用 NSOperationQueue 类的 “addOerationWithBlock:” 方法搭配 NSBlockOperation 类来使用操作队列,其语法与纯GCD非常类似。

    NSOperationQueue与NSOperation类的优点

    • 取消某个操作
    • 指定操作间的依赖关系
    • 通过KVO监控NSOperation对象的属性
    • 指定操作的优先级
    • 重用NSOperation对象

    在解决多线程与任务管理问题时,派发队列并非唯一方案。
    操作队列提供了一套高层的 Objective-C API,能实现纯 GCD 所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那么操作若改用GCD来实现,则需另外编写代码。


    通过 Dispatch Group机制,根据系统资源状况来执行任务

    dispatch group 是 GCD 的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。

    //创建dispatch group
    dispatch_group_t dispatch_group_create();
    //把任务编组(普通dispatch_async的变体)
    void dispatch_group_asunc(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    //指定任务所属的dispatch_group
    void dispatch_group_enter(dispatch_group_t group);
    void dispatch_group_leave(dispatch_group_t group);
    //等待dispatch_group执行完毕(timeout可以取常量DISPATCH_TIME_FOREVER,表示函数一致等待dispatch_group执行完,而不会超时)
    long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
    //等待dispatch_group执行完毕之后执行block
    void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
    
    • 一系列任务可归入一个 dispatch group 之中。开发者可以在这组任务执行完毕时获得通知。
    • 通过 dispatch group,可以在并发派发队列里同时执行多项任务。此时 GCD 会根据系统资源状况来调度这些并发执行的任务。 这里若是开发者自己来实现此功能,则需要编写大量代码。

    第45条:使用dispatch_once来执行只需运行一次的线程安全代码

    void dispatch_once (dispatch_once_t *token, dispatch_block_t block);
     
    (id)sharedInstance {
    static EOCClass )sharedInstance = nil;
    static icdispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
    }
    

    使用 dispatch_once 可以简化代码并且彻底保证线程安全,开发者根本无需担心加锁或同步。

    由于每次调用时都必须使用完全相同的标记,所以标记要声明成 static

    把该变量定义在 static 作用域中,可以保证编译器在每次执行 sharedInstance 方法时都会服用这个变量,而不会创建新变量。

    经常需要编写“只需执行一次的线程安全代码”。通过 GCD 所提供的 dispatch_once 函数,很容易就能实现此功能。
    标记应该声明在 static 或 global 作用域中,这样的话,把只需执行一次的块传给 dispatch_once 函数时,传进去的标记也实现相同的。


    不要使用 dispatch_get_current_queue

    dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用
    由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
    dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。

    多用块枚举,少用for循环

    //for循环遍历
        NSArray *arr1 = @[@1,@2,@3,@4,@5];
        for (int i = 0; i < arr1.count; ++i) {
            NSLog(@"arr1[i]=%@",arr1[i]);
        }
     
        //for循环反向遍历
        for (NSInteger i = arr1.count-1; i >= 0; --i) {
            NSLog(@"arr1[i]=%@",arr1[i]);
        }
     
     //NSEnumerator遍历法
        NSArray *arr1 = @[@1,@2,@3,@4,@5];
        NSEnumerator *enumerator = [arr1 objectEnumerator];
        id object;
        while ((object = [enumerator nextObject]) != nil) {
            NSLog(@"object=%@",object);
        }
     
        //NSEnumerator遍历法反向遍历
        NSEnumerator *reverseenu = [arr1 reverseObjectEnumerator];
        id object1;
        while ((object1 = [reverseenu nextObject]) != nil) {
            NSLog(@"object1=%@",object1);
        }
     
     
        //快速遍历法
        NSArray *arr1 = @[@1,@2,@3,@4,@5];
        for (NSObject *obj in arr1) {
            NSLog(@"obj=%@",obj);
        }
     
        //快速遍历法反向遍历
        for (NSObject *obj1 in [arr1 reverseObjectEnumerator]) {
            NSLog(@"obj1=%@",obj1);
        }
        //块枚举法
        NSArray *arr1 = @[@1,@2,@3,@4,@5];
        [arr1 enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"idx=%zd,obj=%@",idx,obj);
        }];
     
        //块枚举法反向遍历
        [arr1 enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"idx=%zd,obj=%@",idx,obj);
        }];
    

    遍历 collection 有四种方式。最基本的办法是 for 循环,其次是 NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
    “块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
    若提前知道待遍历的 collection 含有何种对象,则应修改块签名,指出对象的具体类型。

    构建缓存时选用NSCache而非 NSDictionary 或者其他类

    实现缓存时应选用 NSCache 而非 NSDictionary 对象等。因为 NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
    可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”,他们仅对 NSCache 起指导作用。
    将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
    如果缓存使用得当,那么应用程序的相应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些从网络获取或从磁盘读取的数据。

    相关文章

      网友评论

        本文标题:iOS 常用技术基础

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