美文网首页
Objective-C 阶段性总结(1)

Objective-C 阶段性总结(1)

作者: 韩陈昊 | 来源:发表于2015-12-08 23:12 被阅读0次

    1、了解obj-C语言的起源

    OC与C++和java等面向对象语言类似,不过很多方面都有所差别,OC使用消息结构而非函数调用.OC语言由smailtalk演化而来,后者是消息语言的鼻祖.消息与函数调用之前的区别就看起来是使用消息结构语言,其运行时执行的代码由运行环境来决定,而使用函数调用的语言则由编译器决定.如果调用的函数是多态的,那么在运行时就按照虚方法表来查出到底应该执行哪个函数实现.而采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要的执行方法,编译器不比关心接收消息是哪种类型,接收消息的对象问题在运行时处理.

    OC的重要工作都由运行时组件而非编译器来完成,使用OC的面向对象特性所需的全部数据结构都在运行时组件里面.这种机制的好处就是比所有工作都在编译期完成的语言,性能有明显的提升.

    OC语言是C语言的超集,也可以说它是C语言的最精简的面向对象封装,所以C语言中的语法在OC中一样适用,因此同时掌握C语言和OC语言的核心概念,才能编写出高效的代码.比如要理解C语言中的内存模型,才能有助理解OC中的内存管理及其自动引用计数机制(ARC)的工作原理.

    OC语言中的指针是指向对象的,比如:NSString *str = @"hello,world";
    这种语法是照搬C语言的,它声明了一个str的变量,其类型是NSString*,此变量是指向NSString的指针,所有OC语言的对象都要这样声明,因为对象所占的内存是分配在堆空间,str指向堆空间的某块内存,其中含有一个NSString对象,也就是说如果再创建一个变量,其指向同一地址,那么并不会拷贝该对象,而只是这两个变量会同时指向该对象.

    分配在堆中的内存必须直接管理,而分配在栈中的内存会自动清理.OC将堆内存管理抽象出来了,不需要C语言中的malloc和free来管理所占内存,OC运行时环境把这部分工作抽象成一套内存管理架构,叫引用计数.在OC代码中,有时会遇到定义不含*的变量,他们可能会使用栈空间,这些变量保存的通常不是OC对象,而是结构体.比如我们常见的CGRect等等.

    整个系统框架为了提升性能,使用了大量的结构体.因为与创建结构体相比,创建对象需要更多的开销,比如分配内存以及释放内存等等.

    2、OC类的头文件

    **
    与C和C++一样,OC也使用头文件(.h)与实现文件(.m)来区隔代码.用OC语言编写任何一个类几乎都要引入Foundation,其封装了我们常用的各种数据结构以及算法,如果不在该类本身引入的话,那么就要引入与其父类所属框架对应的基本头文件,例如在创建iOS应用程序时,通常会继承UIViewController这个类,而这些子类的头文件需要导入UIKit.
    例如:

    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic , copy) NSString *name;
    @property (nonatomic , assign) int *age;
    
    @end
    
    #import "Person.h"
    
    @implementation Person
    
    @end
    
    

    过段时间,可能你又创建了一个名为Job的类,所以常见的办法是在加上#import "Job.h"进行声明
    这种办法虽然可行,但是整体看并不优雅,因为在编译一个使用了Person类文件时,不需要知道Job类的全部细节,只需要知道有一个类名叫Job就好,所以我推荐用下面这个方案:

    #import <Foundation/Foundation.h>
    //这种办法叫做向前声明该类
    @class Job;
    @interface Person : NSObject
    
    @property (nonatomic , copy) NSString *name;
    @property (nonatomic , assign) int *age;
    @property (nonatomic , strong) Job *job;
    
    @end
    

    在Person类的实现文件中需要引入Job类的头文件,因为如果要使用Job类,必须知道其接口的所有细节.代码如下:

    #import "Person.h"
    #import "Job.h"
    
    @implementation Person
    
    @end
    

    将引入头文件的时机尽量延后,只有在确定有需要时再引入,这样就可以减少类的使用者所需引入头文件的数量,假设把Job.h引入到Person.h中,那么只要引入Person.h就会引入Job.h的所有内容,此过程若持续下去,则要引入许多根本用不到的内容,从而大大增加了编译时间.

    向前声明同时也解决了两个类相互引用的问题,比如说在Job类中加入新增以及删除等Person的方法,那么在类的声明文件中将会这样定义:

    - (void) addPerson:(Person *) pernson;
    - (void) removePerson:(Person *) pernson;
    

    此时若要编译Job这个类,编译器必须知道Person这个类的信息,而要同时要编译Person这个类,编译器也要知道Job这个类.如果在各自的头文件中引用对方的头文件,则会出现循环引用,使用#import虽然可以让程序不导致死循环,可以两个类将会有一个无法正常编译.

    所以除非有必要,否则不要引入头文件,一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些必要使用的头文件,这样做既可以降低类之间的耦合,也可以提高编译效率.

    3、字面量语法

    我们在日常的开发过程中,Foundation可能是我们使用最频繁框架之一了,因为其封装了NSString,NSArray等常用的类,Objective-C最早以语法复杂著称,比如创建一个数组是这样的NSArray *array = [[NSArray alloc] init];从Objective-C 1.0起,有了一种非常简单的方式能创建NSArray对象,这个就是我们常说的字面量语法,其语法如下: NSArray *array = @[@"hello",@"world"];
    使用字面量语法可以缩减代码长度,使其更为易读.
    有时需要把整数,浮点数,布尔值封装在Objective-C对象中,这种情况下可以用NSNumber类,该类可以处理多种类型的数值,如果不用字面量,那么就需要用很原始的方式来创建实例:
    NSNumber *number = [NSNumber numberWithInt:1];
    上面的代码创建了一个数字,将其值设为1,然而使用字面量会使其更加直观简洁:
    NSNumber *number = @1;
    能够以NSNumber实例表示的所有数据都可以用该语法,比如:

        float x = 5;
        int y = 2.0;
        NSNumber *floatNumber = @(x * y);
    

    在数组中常用的创建方式和字面量创建方式分别是这样:

        NSArray *array = [NSArray arrayWithObjects:@"hello",@"world", nil];
        NSArray *array = @[@"hello",@"world"];
        
        NSArray *arr = [array objectAtIndex:1];
        NSArray *arr = array[1];
    

    用字面量语法取下标的时候要注意,如果数组中有元素为空,则会抛出异常,因为字面量的语法实际上只是一种语法糖,其效果是等于先创建一个数组,然后把括号内的对象加到数组当中.

    其实字面量的语法也有一定的局限性,就是除了字符串以外,所创建出来的对象必须属于Foundation框架才行,如果自定义了这些子类,则无法用字面量语法创建其对象,要创建自定义子类实例,必须用飞字面量语法.
    使用字面量创建出来的字符串,字典,数组都是不可变的,所以若要想用可变版本,需要copy一份,比如:
    NSMutableArray *mArray = @[@"hello",@"world"].mutableCopy;这么做的弊端是会多调用一个方法,而且还要创建一个对象,所以使用字面量要考虑好使用场景.

    4、类型常量与#define预处理指令

    编写代码时要定义常量,例如要写一个UI视图类,我们为了代码的简洁和方便,常常会把控件的frame值分别提取为常量,我们常用的写法是这样:
    #define HEIGHT 30;
    上述预处理指令会把HEIGHT字符串替换成30,这个可能正好是我们想要的效果,不过这样定义出来的常量并没有类型信息,HEIGHT这个词看起来应该和高度有关,可是我们并没有明确指出.此外,预处理指令会把所有的HEIGHT一律替换成30,这样的话假设此指令声明在某个头文件中,那么所有引用了此头文件的代码,其HEIGHT都会被替换.

    有一种方式比用预处理指令定义常量更好,比如:
    static const CGFloat kHEIGHT = 30.0;
    用此方法定义的常量包含信息的类型,可以清楚的描述常量的含义,由此可见该常量的类型是CGFloat,这有助于后续编写开发文档,如果要定义很多常量,那么这种方式能让后续阅读代码的人更加容易理解其意图.
    同时还要注意常量的名称,常用的命名法是若常量局限于某编译单元之内,则尽量在前面加一个字母k来规范,如果常量在类之外可见,则通常要以类名为前缀才符合我们常见的命名规范.
    定义常量的位置也很重要,我们总喜欢在头文件里声明预处理指令,这样做其实并不太好,当常量名称有可能冲突的时候更是如此,比如如果HEIGHT引入到头文件中,那么引入这份头文件的其他文件中也会出现这样的名字.
    若不打算公开某个常量,则应该将其定义在使用该常量的实现文件里,比如要实现一个动画效果,其中应该有表示含有播放时间的常量,那么就可以来这样写:

    #import "TestViewController.h"
    @interface TestViewController ()
    
    @end
    
    static const  NSTimeInterval kAnimationDuration = 2;
    @implementation TestViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [UIView animateWithDuration:kAnimationDuration animations:^{
            /**
             *  动画效果实现代码
             */
        }];
    }
    @end
    

    变量用conststatic声明,是因为我想让动画的播放时间为一个固定值,不能随便修改,如果修改,编译器会立刻报错,static修饰表示该变量仅在定义此变量的编译单元中可见,编译器每收到一个单元,就会输出一份目标文件,在OC的语境下,编译单元通常是指每个类的实现文件,所以上述代码中声明的kAnimationDuration变量,其作用域仅限于当前类.
    实际上使用conststatic的作用和#define命令一样,把所以的变量都替换成常值,但是以这种方式定义变量会带有变量的类型信息.

    有时候需要对外公开某个常量,那么此常量应该放在全局符号表中,以便可以在定位该常量的编译单元外使用,因此,其定义方法方式上与conststatic会有所不同,应该这样来定义:

    #import "TestViewController.h"
    @interface TestViewController ()
    extern  NSString *const TestString;
    @end
    
    @implementation TestViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        NSString *const TestString = @"hello world"
    
    }
    @end
    

    这个常量在头文件中声明,且在实现文件中定义,注意const修饰符在常量类型中的位置,常量定义应从右至左解读,所以在本例中TestString就是一个常量,其代表一个常量,而这个常量是个指针,指向NSString对象.这样就不会允许其他人改变这个指针常量,使其指向另外一个NSString对象.
    编译器看到头文件中的extern关键字,就能明白如何在此头文件引用代码中处理该常量了,这个关键字是告诉编译器,全局符号表中,将会有一个名叫TestString的符号,编译器无需查看其定义,即允许代码使用此常量

    相关文章

      网友评论

          本文标题:Objective-C 阶段性总结(1)

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